From 3165c4acf86e61a4c5cd90e271a81f5f3ab5857a Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:49:10 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .github/.codedev.yml | 31 + .github/.commit-rules.json | 41 + .github/ISSUE_TEMPLATE/bug_report.md | 40 + .github/ISSUE_TEMPLATE/feature_request.md | 24 + .github/PULL_REQUEST_TEMPLATE.md | 49 + .github/static/img/eino/chain.png | Bin 0 -> 87237 bytes .../static/img/eino/chain_append_branch.png | Bin 0 -> 311210 bytes .../static/img/eino/chain_append_parallel.png | Bin 0 -> 247833 bytes .../img/eino/eino_architecture_overview.png | Bin 0 -> 802559 bytes .../eino_project_structure_and_modules.png | Bin 0 -> 456147 bytes .github/static/img/eino/graph.gif | Bin 0 -> 190311 bytes .github/static/img/eino/graph.png | Bin 0 -> 39537 bytes .github/static/img/eino/graph_add_branch.png | Bin 0 -> 262216 bytes .github/static/img/eino/graph_add_edge.png | Bin 0 -> 153045 bytes .github/static/img/eino/graph_parallel.png | Bin 0 -> 318903 bytes .github/static/img/eino/lark_group_en.png | Bin 0 -> 174845 bytes .github/static/img/eino/lark_group_zh.png | Bin 0 -> 174817 bytes .github/static/img/eino/react.png | Bin 0 -> 83594 bytes .github/static/img/eino/simple_chain.png | Bin 0 -> 62136 bytes .github/static/img/eino/simple_workflow.png | Bin 0 -> 231593 bytes .github/static/img/eino/stream.png | Bin 0 -> 493369 bytes .github/static/img/eino/tool_call_graph.png | Bin 0 -> 192317 bytes .github/workflows/pr-check.yml | 156 + .github/workflows/tag-notification.yml | 120 + .github/workflows/tests.yml | 262 + .gitignore | 66 + .golangci.yaml | 148 + .licenserc.yaml | 27 + CODE_OF_CONDUCT.md | 128 + CONTRIBUTING.md | 51 + LICENSE-APACHE | 177 + README.md | 209 + README.wehub.md | 7 + README.zh_CN.md | 210 + _typos.toml | 14 + adk/agent_tool.go | 417 ++ adk/agent_tool_test.go | 1241 ++++ adk/agentic_callback_integration_test.go | 268 + adk/agentic_integration_test.go | 665 ++ adk/agentic_react_test.go | 1288 ++++ adk/agentic_test.go | 1745 +++++ adk/call_option.go | 211 + adk/callback.go | 202 + adk/callback_integration_test.go | 1062 +++ adk/callback_test.go | 388 ++ adk/cancel.go | 975 +++ adk/cancel_edge_test.go | 2085 ++++++ adk/cancel_multicall_test.go | 125 + adk/cancel_recursive_test.go | 387 ++ adk/cancel_test.go | 3794 +++++++++++ adk/chatmodel.go | 1714 +++++ adk/chatmodel_retry_test.go | 3208 +++++++++ adk/chatmodel_test.go | 2417 +++++++ adk/config.go | 35 + adk/deterministic_transfer.go | 301 + adk/deterministic_transfer_test.go | 731 ++ adk/failover_chatmodel.go | 490 ++ adk/failover_chatmodel_test.go | 742 ++ adk/filesystem/backend.go | 304 + adk/filesystem/backend_inmemory.go | 774 +++ adk/filesystem/backend_inmemory_test.go | 2441 +++++++ adk/flow.go | 809 +++ adk/flow_test.go | 389 ++ adk/handler.go | 451 ++ adk/handler_test.go | 2571 +++++++ adk/instruction.go | 64 + adk/interface.go | 519 ++ adk/internal/config.go | 76 + adk/internal/message_id.go | 58 + adk/internal/message_id_test.go | 87 + adk/interrupt.go | 412 ++ adk/interrupt_test.go | 2149 ++++++ adk/message_id_test.go | 1094 +++ adk/middlewares/agentsmd/agentsmd.go | 213 + .../agentsmd/agentsmd_generic_test.go | 351 + adk/middlewares/agentsmd/agentsmd_test.go | 1367 ++++ adk/middlewares/agentsmd/loader.go | 299 + .../dynamictool/toolsearch/prompt.go | 162 + .../dynamictool/toolsearch/toolsearch.go | 647 ++ .../toolsearch/toolsearch_generic_test.go | 404 ++ .../dynamictool/toolsearch/toolsearch_test.go | 1042 +++ adk/middlewares/filesystem/backend.go | 33 + adk/middlewares/filesystem/filesystem.go | 1245 ++++ adk/middlewares/filesystem/filesystem_test.go | 2647 ++++++++ .../filesystem/large_tool_result.go | 177 + .../filesystem/large_tool_result_test.go | 611 ++ adk/middlewares/filesystem/prompt.go | 270 + .../patchtoolcalls/patchtoolcalls.go | 263 + .../patchtoolcalls/patchtoolcalls_test.go | 394 ++ adk/middlewares/plantask/backend_test.go | 80 + adk/middlewares/plantask/plantask.go | 81 + adk/middlewares/plantask/plantask_test.go | 137 + adk/middlewares/plantask/task.go | 123 + adk/middlewares/plantask/task_create.go | 265 + adk/middlewares/plantask/task_create_test.go | 91 + adk/middlewares/plantask/task_get.go | 176 + adk/middlewares/plantask/task_get_test.go | 80 + adk/middlewares/plantask/task_list.go | 194 + adk/middlewares/plantask/task_list_test.go | 60 + adk/middlewares/plantask/task_update.go | 565 ++ adk/middlewares/plantask/task_update_test.go | 739 ++ adk/middlewares/reduction/consts.go | 122 + .../reduction/internal/clear_tool_result.go | 165 + .../internal/clear_tool_result_test.go | 302 + .../reduction/internal/large_tool_result.go | 210 + .../internal/large_tool_result_test.go | 568 ++ .../reduction/internal/tool_result.go | 121 + adk/middlewares/reduction/legacy.go | 69 + adk/middlewares/reduction/reduction.go | 2084 ++++++ .../reduction/reduction_generic_test.go | 955 +++ adk/middlewares/reduction/reduction_test.go | 3566 ++++++++++ adk/middlewares/skill/doc.go | 47 + adk/middlewares/skill/filesystem_backend.go | 186 + .../skill/filesystem_backend_test.go | 500 ++ adk/middlewares/skill/prompt.go | 154 + adk/middlewares/skill/skill.go | 711 ++ adk/middlewares/skill/skill_generic_test.go | 466 ++ adk/middlewares/skill/skill_test.go | 1353 ++++ adk/middlewares/summarization/consts.go | 39 + .../summarization/customized_action.go | 101 + .../summarization/finalizer_builder.go | 442 ++ .../summarization/finalizer_builder_test.go | 528 ++ adk/middlewares/summarization/prompt.go | 340 + .../summarization/summarization.go | 1289 ++++ .../summarization/summarization_test.go | 2245 ++++++ .../deep/checkpoint_compat_resume_test.go | 203 + adk/prebuilt/deep/deep.go | 267 + adk/prebuilt/deep/deep_test.go | 700 ++ adk/prebuilt/deep/prompt.go | 685 ++ adk/prebuilt/deep/task_tool.go | 191 + adk/prebuilt/deep/task_tool_test.go | 78 + .../deep/testdata/_gen/generate_test.go | 176 + .../deep/testdata/checkpoint_data_v0.7.37.bin | Bin 0 -> 179670 bytes .../deep/testdata/checkpoint_data_v0.8.2.bin | Bin 0 -> 289355 bytes .../deep/testdata/checkpoint_data_v0.8.3.bin | Bin 0 -> 286562 bytes .../deep/testdata/checkpoint_data_v0.8.4.bin | Bin 0 -> 212884 bytes adk/prebuilt/deep/types.go | 65 + adk/prebuilt/integration_test.go | 455 ++ adk/prebuilt/planexecute/plan_execute.go | 880 +++ adk/prebuilt/planexecute/plan_execute_test.go | 1201 ++++ adk/prebuilt/planexecute/utils_test.go | 58 + adk/prebuilt/supervisor/supervisor.go | 120 + adk/prebuilt/supervisor/supervisor_test.go | 1159 ++++ adk/react.go | 823 +++ adk/react_test.go | 672 ++ adk/retry_chatmodel.go | 759 +++ adk/runctx.go | 567 ++ adk/runctx_test.go | 634 ++ adk/runner.go | 342 + adk/runner_test.go | 311 + adk/turn_buffer.go | 134 + adk/turn_loop.go | 2056 ++++++ adk/turn_loop_test.go | 6024 +++++++++++++++++ adk/utils.go | 313 + adk/utils_test.go | 455 ++ adk/workflow.go | 706 ++ adk/workflow_test.go | 1361 ++++ adk/wrappers.go | 1448 ++++ adk/wrappers_failover_test.go | 215 + adk/wrappers_retry_failover_test.go | 613 ++ adk/wrappers_test.go | 2188 ++++++ callbacks/aspect_inject.go | 174 + callbacks/aspect_inject_test.go | 330 + callbacks/doc.go | 129 + callbacks/handler_builder.go | 163 + callbacks/interface.go | 145 + callbacks/interface_test.go | 56 + components/document/callback_extra_loader.go | 71 + .../document/callback_extra_transformer.go | 68 + components/document/doc.go | 41 + components/document/interface.go | 55 + components/document/option.go | 167 + components/document/option_test.go | 78 + components/document/parser/doc.go | 48 + components/document/parser/ext_parser.go | 130 + components/document/parser/interface.go | 36 + components/document/parser/option.go | 116 + components/document/parser/option_test.go | 54 + components/document/parser/parser_test.go | 118 + components/document/parser/testdata/test.md | 2 + components/document/parser/text_parser.go | 60 + components/embedding/callback_extra.go | 97 + components/embedding/callback_extra_test.go | 33 + components/embedding/doc.go | 44 + components/embedding/interface.go | 39 + components/embedding/option.go | 102 + components/embedding/option_test.go | 30 + components/indexer/callback_extra.go | 66 + components/indexer/callback_extra_test.go | 35 + components/indexer/doc.go | 39 + components/indexer/interface.go | 41 + components/indexer/option.go | 113 + components/indexer/option_test.go | 48 + components/model/agentic_callback_extra.go | 94 + .../model/agentic_callback_extra_test.go | 35 + components/model/callback_extra.go | 117 + components/model/callback_extra_test.go | 35 + components/model/doc.go | 59 + components/model/interface.go | 109 + components/model/option.go | 255 + components/model/option_test.go | 136 + components/prompt/agentic_callback_extra.go | 70 + .../prompt/agentic_callback_extra_test.go | 46 + components/prompt/agentic_chat_template.go | 84 + .../prompt/agentic_chat_template_test.go | 125 + components/prompt/callback_extra.go | 70 + components/prompt/callback_extra_test.go | 45 + components/prompt/chat_template.go | 89 + components/prompt/chat_template_test.go | 201 + components/prompt/doc.go | 56 + components/prompt/interface.go | 50 + components/prompt/option.go | 52 + components/prompt/option_test.go | 51 + components/retriever/callback_extra.go | 74 + components/retriever/callback_extra_test.go | 35 + components/retriever/doc.go | 46 + components/retriever/interface.go | 50 + components/retriever/option.go | 147 + components/retriever/option_test.go | 60 + components/tool/callback_extra.go | 68 + components/tool/callback_extra_test.go | 37 + components/tool/doc.go | 53 + components/tool/interface.go | 79 + components/tool/interrupt.go | 185 + components/tool/interrupt_test.go | 134 + components/tool/option.go | 78 + components/tool/option_test.go | 54 + components/tool/utils/common.go | 28 + components/tool/utils/common_test.go | 187 + components/tool/utils/create_options.go | 82 + components/tool/utils/doc.go | 57 + components/tool/utils/error_handler.go | 166 + components/tool/utils/error_handler_test.go | 81 + components/tool/utils/invokable_func.go | 318 + components/tool/utils/invokable_func_test.go | 405 ++ components/tool/utils/streamable_func.go | 259 + components/tool/utils/streamable_func_test.go | 329 + components/types.go | 87 + compose/agentic_tools_node.go | 260 + compose/agentic_tools_node_test.go | 467 ++ compose/branch.go | 176 + compose/branch_test.go | 132 + compose/chain.go | 600 ++ compose/chain_branch.go | 343 + compose/chain_branch_test.go | 348 + compose/chain_parallel.go | 263 + compose/chain_test.go | 873 +++ compose/checkpoint.go | 404 ++ compose/checkpoint_migrate_test.go | 116 + compose/checkpoint_test.go | 2059 ++++++ compose/component_to_graph_node.go | 212 + compose/dag.go | 195 + compose/dag_test.go | 223 + compose/doc.go | 19 + compose/error.go | 111 + compose/error_test.go | 137 + compose/field_mapping.go | 774 +++ compose/generic_graph.go | 158 + compose/generic_helper.go | 256 + compose/graph.go | 1219 ++++ compose/graph_add_node_options.go | 168 + compose/graph_call_options.go | 264 + compose/graph_call_options_test.go | 496 ++ compose/graph_compile_options.go | 133 + compose/graph_manager.go | 556 ++ compose/graph_node.go | 177 + compose/graph_run.go | 1055 +++ compose/graph_test.go | 2071 ++++++ compose/interrupt.go | 363 + compose/introspect.go | 57 + compose/pregel.go | 95 + compose/resume.go | 156 + compose/resume_test.go | 1208 ++++ compose/runnable.go | 539 ++ compose/runnable_test.go | 210 + compose/state.go | 196 + compose/state_test.go | 765 +++ compose/stream_concat.go | 88 + compose/stream_concat_test.go | 218 + compose/stream_reader.go | 129 + compose/stream_reader_test.go | 102 + compose/tool_alias_test.go | 1178 ++++ compose/tool_node.go | 1310 ++++ compose/tool_node_test.go | 1310 ++++ compose/types.go | 47 + compose/types_composable.go | 31 + compose/types_lambda.go | 265 + compose/types_lambda_test.go | 212 + compose/utils.go | 324 + compose/utils_test.go | 114 + compose/values_merge.go | 82 + compose/values_merge_test.go | 256 + compose/workflow.go | 545 ++ compose/workflow_test.go | 1603 +++++ doc.go | 19 + flow/agent/agent_option.go | 71 + flow/agent/multiagent/host/callback.go | 100 + flow/agent/multiagent/host/compose.go | 335 + flow/agent/multiagent/host/compose_test.go | 691 ++ flow/agent/multiagent/host/doc.go | 17 + flow/agent/multiagent/host/options.go | 30 + flow/agent/multiagent/host/types.go | 204 + flow/agent/react/callback.go | 33 + flow/agent/react/doc.go | 17 + flow/agent/react/option.go | 401 ++ flow/agent/react/option_test.go | 1388 ++++ flow/agent/react/react.go | 492 ++ flow/agent/react/react_test.go | 822 +++ flow/agent/utils.go | 52 + flow/indexer/parent/parent.go | 162 + flow/indexer/parent/parent_test.go | 121 + flow/retriever/multiquery/multi_query.go | 211 + flow/retriever/multiquery/multi_query_test.go | 118 + flow/retriever/parent/doc.go | 19 + flow/retriever/parent/parent.go | 113 + flow/retriever/parent/parent_test.go | 88 + flow/retriever/router/router.go | 195 + flow/retriever/router/router_test.go | 134 + flow/retriever/utils/utils.go | 86 + go.mod | 47 + go.sum | 133 + internal/callbacks/inject.go | 203 + internal/callbacks/interface.go | 54 + internal/callbacks/manager.go | 70 + internal/channel.go | 94 + internal/channel_test.go | 221 + internal/concat.go | 224 + internal/concat_test.go | 52 + internal/core/address.go | 338 + internal/core/interrupt.go | 349 + internal/core/interrupt_test.go | 1128 +++ internal/core/resume.go | 109 + internal/generic/generic.go | 90 + internal/generic/generic_test.go | 104 + internal/generic/type_name.go | 71 + internal/generic/type_name_test.go | 86 + internal/gmap/gmap.go | 121 + internal/gmap/gmap_test.go | 89 + internal/gslice/gslice.go | 39 + internal/gslice/gslice_test.go | 36 + internal/merge.go | 81 + internal/mock/adk/Agent_mock.go | 171 + .../mock/components/document/document_mock.go | 116 + .../components/embedding/Embedding_mock.go | 77 + .../mock/components/indexer/indexer_mock.go | 78 + .../mock/components/model/ChatModel_mock.go | 256 + .../components/retriever/retriever_mock.go | 78 + internal/mock/doc.go | 51 + internal/safe/panic.go | 40 + internal/safe/panic_test.go | 28 + internal/serialization/serialization.go | 697 ++ internal/serialization/serialization_test.go | 351 + schema/agentic_message.go | 2284 +++++++ schema/agentic_message_test.go | 1729 +++++ schema/claude/consts.go | 27 + schema/claude/extension.go | 134 + schema/claude/extension_test.go | 190 + schema/doc.go | 86 + schema/document.go | 220 + schema/document_test.go | 53 + schema/gemini/extension.go | 115 + schema/gemini/extension_test.go | 79 + schema/message.go | 1937 ++++++ schema/message_parser.go | 141 + schema/message_parser_test.go | 179 + schema/message_test.go | 2189 ++++++ schema/openai/consts.go | 95 + schema/openai/extension.go | 283 + schema/openai/extension_test.go | 268 + schema/select.go | 73 + schema/serialization.go | 148 + schema/serialization_test.go | 191 + schema/stream.go | 1024 +++ schema/stream_copy_external_test.go | 155 + schema/stream_oneof_test.go | 324 + schema/stream_test.go | 905 +++ schema/tool.go | 580 ++ schema/tool_test.go | 235 + scripts/dev_setup.sh | 122 + scripts/eino_setup.sh | 180 + utils/callbacks/template.go | 793 +++ utils/callbacks/template_test.go | 807 +++ 382 files changed, 154008 insertions(+) create mode 100644 .github/.codedev.yml create mode 100644 .github/.commit-rules.json create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/static/img/eino/chain.png create mode 100644 .github/static/img/eino/chain_append_branch.png create mode 100644 .github/static/img/eino/chain_append_parallel.png create mode 100644 .github/static/img/eino/eino_architecture_overview.png create mode 100644 .github/static/img/eino/eino_project_structure_and_modules.png create mode 100644 .github/static/img/eino/graph.gif create mode 100644 .github/static/img/eino/graph.png create mode 100644 .github/static/img/eino/graph_add_branch.png create mode 100644 .github/static/img/eino/graph_add_edge.png create mode 100644 .github/static/img/eino/graph_parallel.png create mode 100644 .github/static/img/eino/lark_group_en.png create mode 100644 .github/static/img/eino/lark_group_zh.png create mode 100644 .github/static/img/eino/react.png create mode 100644 .github/static/img/eino/simple_chain.png create mode 100644 .github/static/img/eino/simple_workflow.png create mode 100644 .github/static/img/eino/stream.png create mode 100644 .github/static/img/eino/tool_call_graph.png create mode 100644 .github/workflows/pr-check.yml create mode 100644 .github/workflows/tag-notification.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 .licenserc.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE-APACHE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README.zh_CN.md create mode 100644 _typos.toml create mode 100644 adk/agent_tool.go create mode 100644 adk/agent_tool_test.go create mode 100644 adk/agentic_callback_integration_test.go create mode 100644 adk/agentic_integration_test.go create mode 100644 adk/agentic_react_test.go create mode 100644 adk/agentic_test.go create mode 100644 adk/call_option.go create mode 100644 adk/callback.go create mode 100644 adk/callback_integration_test.go create mode 100644 adk/callback_test.go create mode 100644 adk/cancel.go create mode 100644 adk/cancel_edge_test.go create mode 100644 adk/cancel_multicall_test.go create mode 100644 adk/cancel_recursive_test.go create mode 100644 adk/cancel_test.go create mode 100644 adk/chatmodel.go create mode 100644 adk/chatmodel_retry_test.go create mode 100644 adk/chatmodel_test.go create mode 100644 adk/config.go create mode 100644 adk/deterministic_transfer.go create mode 100644 adk/deterministic_transfer_test.go create mode 100644 adk/failover_chatmodel.go create mode 100644 adk/failover_chatmodel_test.go create mode 100644 adk/filesystem/backend.go create mode 100644 adk/filesystem/backend_inmemory.go create mode 100644 adk/filesystem/backend_inmemory_test.go create mode 100644 adk/flow.go create mode 100644 adk/flow_test.go create mode 100644 adk/handler.go create mode 100644 adk/handler_test.go create mode 100644 adk/instruction.go create mode 100644 adk/interface.go create mode 100644 adk/internal/config.go create mode 100644 adk/internal/message_id.go create mode 100644 adk/internal/message_id_test.go create mode 100644 adk/interrupt.go create mode 100644 adk/interrupt_test.go create mode 100644 adk/message_id_test.go create mode 100644 adk/middlewares/agentsmd/agentsmd.go create mode 100644 adk/middlewares/agentsmd/agentsmd_generic_test.go create mode 100644 adk/middlewares/agentsmd/agentsmd_test.go create mode 100644 adk/middlewares/agentsmd/loader.go create mode 100644 adk/middlewares/dynamictool/toolsearch/prompt.go create mode 100644 adk/middlewares/dynamictool/toolsearch/toolsearch.go create mode 100644 adk/middlewares/dynamictool/toolsearch/toolsearch_generic_test.go create mode 100644 adk/middlewares/dynamictool/toolsearch/toolsearch_test.go create mode 100644 adk/middlewares/filesystem/backend.go create mode 100644 adk/middlewares/filesystem/filesystem.go create mode 100644 adk/middlewares/filesystem/filesystem_test.go create mode 100644 adk/middlewares/filesystem/large_tool_result.go create mode 100644 adk/middlewares/filesystem/large_tool_result_test.go create mode 100644 adk/middlewares/filesystem/prompt.go create mode 100644 adk/middlewares/patchtoolcalls/patchtoolcalls.go create mode 100644 adk/middlewares/patchtoolcalls/patchtoolcalls_test.go create mode 100644 adk/middlewares/plantask/backend_test.go create mode 100644 adk/middlewares/plantask/plantask.go create mode 100644 adk/middlewares/plantask/plantask_test.go create mode 100644 adk/middlewares/plantask/task.go create mode 100644 adk/middlewares/plantask/task_create.go create mode 100644 adk/middlewares/plantask/task_create_test.go create mode 100644 adk/middlewares/plantask/task_get.go create mode 100644 adk/middlewares/plantask/task_get_test.go create mode 100644 adk/middlewares/plantask/task_list.go create mode 100644 adk/middlewares/plantask/task_list_test.go create mode 100644 adk/middlewares/plantask/task_update.go create mode 100644 adk/middlewares/plantask/task_update_test.go create mode 100644 adk/middlewares/reduction/consts.go create mode 100644 adk/middlewares/reduction/internal/clear_tool_result.go create mode 100644 adk/middlewares/reduction/internal/clear_tool_result_test.go create mode 100644 adk/middlewares/reduction/internal/large_tool_result.go create mode 100644 adk/middlewares/reduction/internal/large_tool_result_test.go create mode 100644 adk/middlewares/reduction/internal/tool_result.go create mode 100644 adk/middlewares/reduction/legacy.go create mode 100644 adk/middlewares/reduction/reduction.go create mode 100644 adk/middlewares/reduction/reduction_generic_test.go create mode 100644 adk/middlewares/reduction/reduction_test.go create mode 100644 adk/middlewares/skill/doc.go create mode 100644 adk/middlewares/skill/filesystem_backend.go create mode 100644 adk/middlewares/skill/filesystem_backend_test.go create mode 100644 adk/middlewares/skill/prompt.go create mode 100644 adk/middlewares/skill/skill.go create mode 100644 adk/middlewares/skill/skill_generic_test.go create mode 100644 adk/middlewares/skill/skill_test.go create mode 100644 adk/middlewares/summarization/consts.go create mode 100644 adk/middlewares/summarization/customized_action.go create mode 100644 adk/middlewares/summarization/finalizer_builder.go create mode 100644 adk/middlewares/summarization/finalizer_builder_test.go create mode 100644 adk/middlewares/summarization/prompt.go create mode 100644 adk/middlewares/summarization/summarization.go create mode 100644 adk/middlewares/summarization/summarization_test.go create mode 100644 adk/prebuilt/deep/checkpoint_compat_resume_test.go create mode 100644 adk/prebuilt/deep/deep.go create mode 100644 adk/prebuilt/deep/deep_test.go create mode 100644 adk/prebuilt/deep/prompt.go create mode 100644 adk/prebuilt/deep/task_tool.go create mode 100644 adk/prebuilt/deep/task_tool_test.go create mode 100644 adk/prebuilt/deep/testdata/_gen/generate_test.go create mode 100644 adk/prebuilt/deep/testdata/checkpoint_data_v0.7.37.bin create mode 100644 adk/prebuilt/deep/testdata/checkpoint_data_v0.8.2.bin create mode 100644 adk/prebuilt/deep/testdata/checkpoint_data_v0.8.3.bin create mode 100644 adk/prebuilt/deep/testdata/checkpoint_data_v0.8.4.bin create mode 100644 adk/prebuilt/deep/types.go create mode 100644 adk/prebuilt/integration_test.go create mode 100644 adk/prebuilt/planexecute/plan_execute.go create mode 100644 adk/prebuilt/planexecute/plan_execute_test.go create mode 100644 adk/prebuilt/planexecute/utils_test.go create mode 100644 adk/prebuilt/supervisor/supervisor.go create mode 100644 adk/prebuilt/supervisor/supervisor_test.go create mode 100644 adk/react.go create mode 100644 adk/react_test.go create mode 100644 adk/retry_chatmodel.go create mode 100644 adk/runctx.go create mode 100644 adk/runctx_test.go create mode 100644 adk/runner.go create mode 100644 adk/runner_test.go create mode 100644 adk/turn_buffer.go create mode 100644 adk/turn_loop.go create mode 100644 adk/turn_loop_test.go create mode 100644 adk/utils.go create mode 100644 adk/utils_test.go create mode 100644 adk/workflow.go create mode 100644 adk/workflow_test.go create mode 100644 adk/wrappers.go create mode 100644 adk/wrappers_failover_test.go create mode 100644 adk/wrappers_retry_failover_test.go create mode 100644 adk/wrappers_test.go create mode 100644 callbacks/aspect_inject.go create mode 100644 callbacks/aspect_inject_test.go create mode 100644 callbacks/doc.go create mode 100644 callbacks/handler_builder.go create mode 100644 callbacks/interface.go create mode 100644 callbacks/interface_test.go create mode 100644 components/document/callback_extra_loader.go create mode 100644 components/document/callback_extra_transformer.go create mode 100644 components/document/doc.go create mode 100644 components/document/interface.go create mode 100644 components/document/option.go create mode 100644 components/document/option_test.go create mode 100644 components/document/parser/doc.go create mode 100644 components/document/parser/ext_parser.go create mode 100644 components/document/parser/interface.go create mode 100644 components/document/parser/option.go create mode 100644 components/document/parser/option_test.go create mode 100644 components/document/parser/parser_test.go create mode 100644 components/document/parser/testdata/test.md create mode 100644 components/document/parser/text_parser.go create mode 100644 components/embedding/callback_extra.go create mode 100644 components/embedding/callback_extra_test.go create mode 100644 components/embedding/doc.go create mode 100644 components/embedding/interface.go create mode 100644 components/embedding/option.go create mode 100644 components/embedding/option_test.go create mode 100644 components/indexer/callback_extra.go create mode 100644 components/indexer/callback_extra_test.go create mode 100644 components/indexer/doc.go create mode 100644 components/indexer/interface.go create mode 100644 components/indexer/option.go create mode 100644 components/indexer/option_test.go create mode 100644 components/model/agentic_callback_extra.go create mode 100644 components/model/agentic_callback_extra_test.go create mode 100644 components/model/callback_extra.go create mode 100644 components/model/callback_extra_test.go create mode 100644 components/model/doc.go create mode 100644 components/model/interface.go create mode 100644 components/model/option.go create mode 100644 components/model/option_test.go create mode 100644 components/prompt/agentic_callback_extra.go create mode 100644 components/prompt/agentic_callback_extra_test.go create mode 100644 components/prompt/agentic_chat_template.go create mode 100644 components/prompt/agentic_chat_template_test.go create mode 100644 components/prompt/callback_extra.go create mode 100644 components/prompt/callback_extra_test.go create mode 100644 components/prompt/chat_template.go create mode 100644 components/prompt/chat_template_test.go create mode 100644 components/prompt/doc.go create mode 100644 components/prompt/interface.go create mode 100644 components/prompt/option.go create mode 100644 components/prompt/option_test.go create mode 100644 components/retriever/callback_extra.go create mode 100644 components/retriever/callback_extra_test.go create mode 100644 components/retriever/doc.go create mode 100644 components/retriever/interface.go create mode 100644 components/retriever/option.go create mode 100644 components/retriever/option_test.go create mode 100644 components/tool/callback_extra.go create mode 100644 components/tool/callback_extra_test.go create mode 100644 components/tool/doc.go create mode 100644 components/tool/interface.go create mode 100644 components/tool/interrupt.go create mode 100644 components/tool/interrupt_test.go create mode 100644 components/tool/option.go create mode 100644 components/tool/option_test.go create mode 100644 components/tool/utils/common.go create mode 100644 components/tool/utils/common_test.go create mode 100644 components/tool/utils/create_options.go create mode 100644 components/tool/utils/doc.go create mode 100644 components/tool/utils/error_handler.go create mode 100644 components/tool/utils/error_handler_test.go create mode 100644 components/tool/utils/invokable_func.go create mode 100644 components/tool/utils/invokable_func_test.go create mode 100644 components/tool/utils/streamable_func.go create mode 100644 components/tool/utils/streamable_func_test.go create mode 100644 components/types.go create mode 100644 compose/agentic_tools_node.go create mode 100644 compose/agentic_tools_node_test.go create mode 100644 compose/branch.go create mode 100644 compose/branch_test.go create mode 100644 compose/chain.go create mode 100644 compose/chain_branch.go create mode 100644 compose/chain_branch_test.go create mode 100644 compose/chain_parallel.go create mode 100644 compose/chain_test.go create mode 100644 compose/checkpoint.go create mode 100644 compose/checkpoint_migrate_test.go create mode 100644 compose/checkpoint_test.go create mode 100644 compose/component_to_graph_node.go create mode 100644 compose/dag.go create mode 100644 compose/dag_test.go create mode 100644 compose/doc.go create mode 100644 compose/error.go create mode 100644 compose/error_test.go create mode 100644 compose/field_mapping.go create mode 100644 compose/generic_graph.go create mode 100644 compose/generic_helper.go create mode 100644 compose/graph.go create mode 100644 compose/graph_add_node_options.go create mode 100644 compose/graph_call_options.go create mode 100644 compose/graph_call_options_test.go create mode 100644 compose/graph_compile_options.go create mode 100644 compose/graph_manager.go create mode 100644 compose/graph_node.go create mode 100644 compose/graph_run.go create mode 100644 compose/graph_test.go create mode 100644 compose/interrupt.go create mode 100644 compose/introspect.go create mode 100644 compose/pregel.go create mode 100644 compose/resume.go create mode 100644 compose/resume_test.go create mode 100644 compose/runnable.go create mode 100644 compose/runnable_test.go create mode 100644 compose/state.go create mode 100644 compose/state_test.go create mode 100644 compose/stream_concat.go create mode 100644 compose/stream_concat_test.go create mode 100644 compose/stream_reader.go create mode 100644 compose/stream_reader_test.go create mode 100644 compose/tool_alias_test.go create mode 100644 compose/tool_node.go create mode 100644 compose/tool_node_test.go create mode 100644 compose/types.go create mode 100644 compose/types_composable.go create mode 100644 compose/types_lambda.go create mode 100644 compose/types_lambda_test.go create mode 100644 compose/utils.go create mode 100644 compose/utils_test.go create mode 100644 compose/values_merge.go create mode 100644 compose/values_merge_test.go create mode 100644 compose/workflow.go create mode 100644 compose/workflow_test.go create mode 100644 doc.go create mode 100644 flow/agent/agent_option.go create mode 100644 flow/agent/multiagent/host/callback.go create mode 100644 flow/agent/multiagent/host/compose.go create mode 100644 flow/agent/multiagent/host/compose_test.go create mode 100644 flow/agent/multiagent/host/doc.go create mode 100644 flow/agent/multiagent/host/options.go create mode 100644 flow/agent/multiagent/host/types.go create mode 100644 flow/agent/react/callback.go create mode 100644 flow/agent/react/doc.go create mode 100644 flow/agent/react/option.go create mode 100644 flow/agent/react/option_test.go create mode 100644 flow/agent/react/react.go create mode 100644 flow/agent/react/react_test.go create mode 100644 flow/agent/utils.go create mode 100644 flow/indexer/parent/parent.go create mode 100644 flow/indexer/parent/parent_test.go create mode 100644 flow/retriever/multiquery/multi_query.go create mode 100644 flow/retriever/multiquery/multi_query_test.go create mode 100644 flow/retriever/parent/doc.go create mode 100644 flow/retriever/parent/parent.go create mode 100644 flow/retriever/parent/parent_test.go create mode 100644 flow/retriever/router/router.go create mode 100644 flow/retriever/router/router_test.go create mode 100644 flow/retriever/utils/utils.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/callbacks/inject.go create mode 100644 internal/callbacks/interface.go create mode 100644 internal/callbacks/manager.go create mode 100644 internal/channel.go create mode 100644 internal/channel_test.go create mode 100644 internal/concat.go create mode 100644 internal/concat_test.go create mode 100644 internal/core/address.go create mode 100644 internal/core/interrupt.go create mode 100644 internal/core/interrupt_test.go create mode 100644 internal/core/resume.go create mode 100644 internal/generic/generic.go create mode 100644 internal/generic/generic_test.go create mode 100644 internal/generic/type_name.go create mode 100644 internal/generic/type_name_test.go create mode 100644 internal/gmap/gmap.go create mode 100644 internal/gmap/gmap_test.go create mode 100644 internal/gslice/gslice.go create mode 100644 internal/gslice/gslice_test.go create mode 100644 internal/merge.go create mode 100644 internal/mock/adk/Agent_mock.go create mode 100644 internal/mock/components/document/document_mock.go create mode 100644 internal/mock/components/embedding/Embedding_mock.go create mode 100644 internal/mock/components/indexer/indexer_mock.go create mode 100644 internal/mock/components/model/ChatModel_mock.go create mode 100644 internal/mock/components/retriever/retriever_mock.go create mode 100644 internal/mock/doc.go create mode 100644 internal/safe/panic.go create mode 100644 internal/safe/panic_test.go create mode 100644 internal/serialization/serialization.go create mode 100644 internal/serialization/serialization_test.go create mode 100644 schema/agentic_message.go create mode 100644 schema/agentic_message_test.go create mode 100644 schema/claude/consts.go create mode 100644 schema/claude/extension.go create mode 100644 schema/claude/extension_test.go create mode 100644 schema/doc.go create mode 100644 schema/document.go create mode 100644 schema/document_test.go create mode 100644 schema/gemini/extension.go create mode 100644 schema/gemini/extension_test.go create mode 100644 schema/message.go create mode 100644 schema/message_parser.go create mode 100644 schema/message_parser_test.go create mode 100644 schema/message_test.go create mode 100644 schema/openai/consts.go create mode 100644 schema/openai/extension.go create mode 100644 schema/openai/extension_test.go create mode 100644 schema/select.go create mode 100644 schema/serialization.go create mode 100644 schema/serialization_test.go create mode 100644 schema/stream.go create mode 100644 schema/stream_copy_external_test.go create mode 100644 schema/stream_oneof_test.go create mode 100644 schema/stream_test.go create mode 100644 schema/tool.go create mode 100644 schema/tool_test.go create mode 100755 scripts/dev_setup.sh create mode 100755 scripts/eino_setup.sh create mode 100644 utils/callbacks/template.go create mode 100644 utils/callbacks/template_test.go diff --git a/.github/.codedev.yml b/.github/.codedev.yml new file mode 100644 index 0000000..ccbcfbd --- /dev/null +++ b/.github/.codedev.yml @@ -0,0 +1,31 @@ +coverage: + status: + project: #add everything under here, more options at https://docs.codecov.com/docs/commit-status + default: # default is the status check's name, not default settings + target: auto #default + threshold: 1% #allow coverage to drop by 1% + base: auto + if_ci_failed: error #success, failure, error, ignore + patch: + default: + target: 82% #default + threshold: 1% #allow coverage to drop by 1% + base: auto + if_ci_failed: error #success, failure, error, ignore + +comment: #this is a top-level key + layout: " diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes + require_base: false # [true :: must have a base report to post] + require_head: true # [true :: must have a head report to post] + hide_project_coverage: false # [true :: only show coverage on the git diff aka patch coverage] + +# sample regex patterns +ignore: + - "tests" + - "examples/" + - "mock/" + - "callbacks/interface.go" + - "utils/safe" + - "components/tool/utils/create_options.go" \ No newline at end of file diff --git a/.github/.commit-rules.json b/.github/.commit-rules.json new file mode 100644 index 0000000..3bc6176 --- /dev/null +++ b/.github/.commit-rules.json @@ -0,0 +1,41 @@ +{ + "allowedTypes": [ + "feat", + "fix", + "docs", + "style", + "refactor", + "perf", + "test", + "build", + "ci", + "chore", + "revert" + ], + "allowedScopes": [ + "adk", + "adk/filesystem", + "callbacks", + "components", + "compose", + "deep", + "dynamictool", + "filesystem", + "flow", + "internal", + "middlewares", + "planexecute", + "plantask", + "prebuilt", + "reduction", + "schema", + "skill", + "summarization", + "supervisor", + "toolsearch", + "utils", + "docs", + "ci", + "serialization" + ] +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..28bb067 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Version:** + +Please provide the version of {project_name} you are using. + +**Environment:** + +The output of `go env`. + +**Additional context** + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f0291e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** + +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** + +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3f6e529 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,49 @@ +#### What type of PR is this? + + +#### Check the PR title. + +- [ ] This PR title match the format: \(optional scope): \ +- [ ] The description of this PR title is user-oriented and clear enough for others to understand. +- [ ] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io) + + +#### (Optional) Translate the PR title into Chinese. + + +#### (Optional) More detailed description for this PR(en: English/zh: Chinese). + +en: +zh(optional): + + +#### (Optional) Which issue(s) this PR fixes: + + +#### (optional) The PR that updates user documentation: + diff --git a/.github/static/img/eino/chain.png b/.github/static/img/eino/chain.png new file mode 100644 index 0000000000000000000000000000000000000000..91632f3b9173bc0fd5114599b49c652627e35862 GIT binary patch literal 87237 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 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/chain_append_branch.png b/.github/static/img/eino/chain_append_branch.png new file mode 100644 index 0000000000000000000000000000000000000000..070ab145593e6f56249ccdd61dbc3d31399a9462 GIT binary patch literal 311210 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

%2PJ~f8Rq;> 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ξ%9IfoPkO^ep^Z= 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

(vC6^EjoQ=n9J;jbEU8}CI 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

7r@8yuYGMeV74=ZWgeXh48Z2#uyIHH_q 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

GDd5$m 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 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/chain_append_parallel.png b/.github/static/img/eino/chain_append_parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..0405f01eef7955421d203a624a71ff9d966b9fcc GIT binary patch literal 247833 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

_=n8FJ(RJ5B*ssh|WTo6cI~E`)GHi!!`~?XyQd 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

c0NlZB%ENox}CRmvJTAoIQ?jUW3ubK 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
YZP|pA;biqtdtL(yySJ|ocGNupgNgiOlZ^g? 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

=A0QG>j0f#=L&w2Tq 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[QwO!0E|_2xAH9KAT}=13DBs%e>*v+C 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 I=akR{0F+d+umAu6 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/eino_project_structure_and_modules.png b/.github/static/img/eino/eino_project_structure_and_modules.png new file mode 100644 index 0000000000000000000000000000000000000000..78a94cd8523e1a1a236df5363a38e867a40fe4f5 GIT binary patch literal 456147 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 z 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


aC-m6bhy5Vb4fNV2pG$IsfOgb9I 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

WC~3xE(S;VTjTFwn7z2TDEt>YTTomK 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 ls~9 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�`Yk)g7Zu?5MYSxEwi|i=Es4!GIx0QB$zT4- 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


v&crg{z_(?ss0_e-=<7H~YjpYaSNd)# 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%0ARye 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

sa94O?^35R5x@?7D)AP| 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

Q>Ml&9yl6rHCMB?m@rFlggWT^f0QO8l?Ug1JzL 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 BO{4$- literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/graph.png b/.github/static/img/eino/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2f05c6de814b331263e64ac05c65bd63822674 GIT binary patch literal 39537 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 E0a!_u{{R30 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/graph_add_branch.png b/.github/static/img/eino/graph_add_branch.png new file mode 100644 index 0000000000000000000000000000000000000000..cd21a45deff2efa72de128dabe99c9acc55f3048 GIT binary patch literal 262216 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%0AaNpM^(VDrx%e^( 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 Dqv<{x literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/graph_add_edge.png b/.github/static/img/eino/graph_add_edge.png new file mode 100644 index 0000000000000000000000000000000000000000..3c5666136dfac635ca93a78211dedb307c8813d8 GIT binary patch literal 153045 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

hRQ~I= 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 z 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≠*C1dh>`?R(3u^lAB`br0OrY_QrGZYtwi8&$4$?Hg*HiZDU5 zRt>5kIiDRY#C;E}1aktX8g6F1DH2gF(EOU2DV>&JOx|xPoihSp)beTlc<_MGedJ_5 zP^*w`7#KRLv>KA-+W-k_@!2NNR%;hOgr>ZygZ zS^2E(Vk`MQl==G*OK}+L!kZDo$RIl~b&fyK-A8s%TT%#grcG+ni|>t@mBnsY{7^3d ztCBK*_So{`)Z3vrTD6F`c65B6ro+>br&-`^C};W+Z^-Fm6_2YyN0Adq&b?_B0xi(e zmmy+PetWNn6#nUxlD!)&8Ky;hmovYdr>C7yC2KSY&z;}XXkgtPD$+MtV(7WMQV3fS z_jQB;^{?X34vix`A)2E2c zGSyKSI!23mk1q(VRU9VsV;swU%cHBfN{;T9!<|T3x4h(_QzcKe)x1;BUy_|{y$blm z_hS|kYIfLQHkz*z8x5>I-iZa>HxVX+FIc?YgV&>fK)OJlzMnRX+w)>4p zn^VE}ub<#KyDCF1LE#mfCqY7(9zK^r1yn~6wZ0*PCOI!R=W-FsMsS-SdukwXc zC5qKNt8dpZY(59yl9Qd^w7BSNo{=grSu@feNwZsSe4-g8ww0IWX^(i>R^V~ic2k1) zC3HvwGhpdGY|EwVoE^_)TvGF9vCGAdFXXG&+Pw46xHrzeJsbD$TprGe>i-m_M|I5& zz89I*X=|}qpR?u&s?yVS)22%?;r5*5khR4upS$b1&Af@y7Tx(6EGHN4)`|V=TON7k zLLx&=)kNWwH&7(2*ja+m;%d3eXnI;UK#|J7A~CQv#wB(FdRpmsS9br5G z+|~-`1w~OHiO2H2oU;QbqDwU=?&~4kMoTFLqnDj?!Q%&T`XPZih&&CIv_dpgSS3!e zw|-h(7x#0>_<~ib`8W%NLuvycIA4pAq$WPyeppI-it`Bu7y>D0dNRM|^U!-G>3PM?=Kt7#}-SL$63YURb;p z^Q5gT8k&fg05#Z7HjDaP`~@ncNPpP*M+m6R@|(4DDAR+6dy@E+3+KKhnPLjc$2i3abwX)la20sw}ZC^HWiaF{Nd zX*kojb4Q=|yzA--8a@#;E{AZeCxo-*%3zkH%PsmwEfS3Kf$Td#B21j0n=1!5PYK8G z3N!6^jD7w?OZvhkG-=+y+Bt7)_Li+;eq;LM*NVJnxLRJRI+0?4W~q@V?c%{?ew&^7 zaIQxRU&9vJZ*KEj(V}=@-3z(B(1paXU%b!ZukPMAqtg6+{fnVU`n~gEp=X`^c*kzkfuXxtKp1rIlr;kerwysAVl_KGV$}W9)IrT&%y(etM7@qMT80 zwav2qu2P-9!&<Nhvn)dSHnzPK zZa-Dv(x{iqn(m>9LBf#1gftbqk;OTB;}y?Ugd|^e+iO+H{`ubgP3xrfdyE1OAbQME z8uZC;%HGeyIu@x;!2q*chWqw!D-)B6M5L^fNz3aY6|WxL8*6I}orUS!@uSwgag)-< z0o)B2Yat@|>p@B`K-+J*)QcRH>%Di<)hZ~n>GM`r1HL=nyfI*X7HYfSXv0<0r;S^j?_UuUAFSKY63{Y+1cV#kt^nghLxFwy(m7tj#^{jR1WMx37q>tT5A0 zE=lg~fJ^N*N%;TO@@^U5Fn5nWxo6tYpLY`ly!^_=Ln2BX>?*(=npssLrpt*Vj>?pz zB5K4EtQ0y<^#~e=*Jf^F+oEcdg?w2L?k%huQR6YX(V4x82T zWM=VT{Dnta;VNg0^a`jQ^0Px!SbK4wecVYI^CovY3(Mc^LTGpbSAU-(pOGe+d=rs@ zW=Y4#;YF^wwyq70X<8Kqy-n6DJ{7`7Z$V0IC}8Z5%!GfoZky#EgUB|FEF33&icWQ- zh+Vo#YHS6kpU4i5Ilfyxs9fwgTjffn)9+Qe^dhrO0!dfTeDRytuJtc0r2r_U|Na?5 z|KAU5L09qo)u(TtTrKeG-IDW_j<4SJFZ?yb%eNWbS0~BUXHOFzTX0kFISLJkuH%m@&7-QJDdgp literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/graph_parallel.png b/.github/static/img/eino/graph_parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..0dae772451f2d0625574781ef754b3e69c949012 GIT binary patch literal 318903 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�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%0DT&kkX-L&~Jl|#I70)MBxt$L|KvKKJ( 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 z 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 z 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�O>T_mqruw8eJ<Sv{lI{{> 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 P6ZkVdXK}X7*d6yjX~qNN literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/lark_group_zh.png b/.github/static/img/eino/lark_group_zh.png new file mode 100644 index 0000000000000000000000000000000000000000..4c9b5ee8a498a2e7e526fee543d8f07e148ce920 GIT binary patch literal 174817 zcmeEuXFS_|+kUIYg{rnz?e=QXQnV%1rixOrNfA`lO2i(utBNl4s@i+Sh$J;aY^Akh zCn2#~d)EjR#Xr5D`?~MX{j|@^|GWEz$S1!4jT&}%ZU>w*mUpR zH92vD0ZjWD_=A!5O37A~5bf7V9}}HBCs2I?OD9fTJfVA6!|aLe>I759xKrlwCb}SX ztf_kDe9`Sp-=T=hXQ>}g>THOpUwp5ATk+C+RuTF;8v-Kl)##-J#=U&c&^_nWyLTx{ zwn$9MtA=Rpq&;*@QGVwnE0b28WwSxb-ak&Z+0cs@YAhU!+&J0^&S=XzY%pp#tA64n z{qG+9AGBQZoUqqq{J-8%b|OMTo{sL1i%(Dd`Y>?$Bpp#meJmSzx&+J z*LLnbZNccgJstU*zx(t0S*$nvZ@-jwQRVza&$V+`+WxQW^r-PuzaQ~a^?3R}z`^ug zH~x?7wCV2sA0xW>-|7CxKL2-j|8=(idzk+tX8s$y|3aq!Le;;5n`ZpI690gX|KDlGYBXION2c$E^=PsFYnT56t^VhPK4zxzncXUZ z?s6L|;C6*k=wbfy3crbz+C}LpX(CSIJ;Fr6=q%U5A$;`Qr{i5V^Hi#yeEd`L0`K5J%Tp?o_8djh=&fB2b8}AK#^~EBGjVc%lW?(_ zlLlWS-Rh%G}>?NBwf_to#Vq6>R4A!Djmbsbq&H!AK$hHCEe9+#BliHeF=!Q9f} zqDA^{d<>=Yn_*FM4<)0r;30-{H_!vPE-%5_IQ~$%!xQg>kh%;|Z4v132mH%cYmRls zd7p>RtnKVwtT>;0x4xr-?KBy54zp5YJtQ1)m)^B;voOTIIb9N-W490P>kB>jo>J{L zk;f+b)TYJ)N>mvTdMWw463hum8#SR=!AP2(JMIl+N+lWE)hfa#Xj|6QdIy*YM4Ok) zCYy^VnaN!$+93DOd3RMaeXxh?%1m6vpPm(e%WQ4KU|`_gSX?)nMHb|9sqV#kPQ72XCp5zmUAIn#}L%i3PhX@!fG&0KX}cue<@ zgAueaUu&Z3JTISHUM@+ha!nwhED@4q7tMj!2XAivI{b(wma|v`P~4#t@ZsmJ(8sDt zQsC0TBjeOZ18W>2u=LZia&yqU)za(oEE2exV=t;J!V313x5g64fT||pZ|0U>uB=O3 zDu=XMbok^A;QiL_Jb)J=0vptV@a?6zQtNzQ`S&w*-Lo(`lW0dO!$W(R2E&OHnh4dxkarSLrz`|Lk?rQT0-Ao^? zzmPzRE_zPjQH#o| z*>m^zM#2sRavGD@kRq>>px$}KlodIMo7*^dAXVe)lyal!z}W4hT9rqI3sA)nq)>-N z6=9V}r)qb)>z!Q}qj97AG~62O;E1KSM6;K^6-s>7I4En;iKqr+&kQrQT+g=SSnT<58q7~E7CGb ztKxXkE`xsJW=$o1hfH<=AgpI`G9ELi1D2v!-BBjZ$-@tG4KJUH`PTAOR8-y-cWQ4Db>L1sgt{uU zkbgrCi%};cpwgVY`3@19?6*HzRK}f_wipXAyWSI)VsR1InVepe2YDEuz2Ffre`dH z!)kJGSH6=Js|^su@qF@nUEvJ0EBZe((EmAZu|I1RE*YtTDI)qZyDB(X+B!G)NQ=|} z16$V^x>Q^c_uOL1e1?1!E3_mZt3T5M2+L?5yL@OYYk924fZo;di=P3t4&UHt3k?9u=FT%!{#x0y@;Oo*R2`dC+M%J}RoXB|V= zn(5*bxzlB;L)rAP)#spv^D>GU-(FJbUyyNwi<|d^dJG>u;0LEo1Grq^(f4DJk`k8b zfgox%A*#$zlJOmZ!Sah&U0BVKgBrN1rn2w}U3Cc|Ga><8=$fg#@-^~8r{HJV-;hnMwEDE-9(u4L z{mZkDmjw56F8fVyoihKXUFGUF6kXKJR72yi4ZT}ed7FaQYMw;&n)K&Gl5w*>XvfPk0+SEzW#35cQ$1bZJ-!Y zkp(r99RL_gCbs6PG8EoKHXXZ9`1T-4+TRp6CSU7c_$8%svZHO%E-ql~@|hjV(&!=k za8vMV;Yp8sWPPlazwBIn8jE1!9Z=K$DwgFvsJP+B4dGDO&^@vD4yt8ur4@{)`98nZ zowl-3lz65O!fO9juy1QtWyJ=Tur|K~;-DczF6}K0xrst0UMG~+K47$_(*z{a>~~6q zicm#RO@IeX%VTDYc%%&JcuQa7H4QO^newmn1n9AtCCz;Ij;uKBRLEuAievPSV`Pof zW?~WN>A*PQAQK^EJ)nWXTE*TCvFKl+ZMB#NZE-9W&0J3w&vmTisIZ$gD;2P%*Nmzn z6AIN0P!GwB^l?Q407-h~1HX%)T<`7A&WWUs2QjT$Vb_F2Dgv=IR8UeP5VU@Bw@aq!|e6u8nB8G4Sx?aO+=}Bn1Qs zpT1j6gioo|tw;02wOS2+#fTP^l=^fX!tM*NYV+kYx9>1*_@pFBjvJ=exK|3#%p`~> z^=!>`_soe9&C1iVa#3_0=8JdjVscA-0Ur^`r8D|;#AR<2W4x<>(Qx}2)Y2~9((OtX z<3J;|usdppDdF!PS%@2&cDU<}<;A*bzJN&F&j(qMpOw9p9cw!3?Y$6tu5u}oKi)2Z zMCiX_bQW}$Y&A8I>`?k5aIEvrl@Dy5uts({!@+zi=B~igF>vk#L)u88iy88-vILHkzS#9m$qNrO1Gbi z0?JvFaP{#Up;Yt_GNHgB4iu*TKUHrKuX?L73qUvrDC%aZZo;#8W7p%LlV*GJvVqR`O;_H=g?& z%SbPjG@}jkPX$T}y=|9A4&rKlXzrnGWt`W6qR)PF=JaU|iOdo;_V@|A#o_W8f&4jE zi%xbXfW`ajWH8sME@>ulCda4<=2iW|!plUjIqV=YQD-k*QowxskFbVlbEi?Os1;er z8#+AXe#A0ysReMxRwB}Ze4?t(Y-ym1B7%%Y0Vr@vga?2MOWr1N|B=c23C61*1sfR! zys}**8i$WB>r}S26Gyg3HW;i@*sGolm|w~mSOy^caBA*^S-4uge9PE zWd)C>OF8Sd1PvX<#D@2WqkTj@V>R{o8ARh7(%;^kF+S8AaP^<$!YMuBrDkyEnpHF79dPnG~W zXJGq^d{GOgNAh!~hABre;!Ni22q>n;&2JL#e=63z*k+r6dXi{$1HlK_c+&#Fo8_Pq z)q(WxEJ-pFwlJ~DoZ}f;255COD3l1XqHwq#1MW?N`-hnZal}V)0N-d91oXaJzY7<(SDu zOI1X90CYJcR(t1o#Wt7Dv`Zj~>v@w%Z@oz~e`&vuY>LlMW_otiAmkfAneBx!!{igPT)7pE^dj3LjD~fSKK|3!=0h?js2D;WYT!5SBip_R8#ibPj z$$BjzXCB`nTwL~9PE##d>h5e$Ff5hN7bh&YT4B!F?Vx=VUF%s0{%VH(1qUp|v@zMn zJm3MA)HN2fF#`(yysFQHqS!+pWwUg&74Y8Ot%XN^-Pr<1T;7knJe5dSf5YlR6f#$O zcu-#W`!Z|KasoQ)R4Ctik1IaSoVr2@uL%tcy!5wh@ozY;FQri?D#)>38Qqv9HS855 zTvXWBgKU~^4bB3 zK<KHdJteo)+vRJRhG+%$-BU^IAMHclH1h}%V-pbwW4xwVDJHRGU zKM=v8ZI3v-N=7$+Eokjp^G7(P%C!R9YNU6z1j5plr0I-?%a04Wak^9 zEl*gz{Jwg54Q>bZ*H1;cugxkmgNOZ!D34ykYc1xT@!>_y6`)T$k71{TcveXWH#*`w z=R2T&Ml{@etWpDdQM$yJa(}B^Yc8^anqkxyO5+J$lUn6M@;Mr&J$3Zp`vfjr(8d=tlk^ z{CZgu-B<+FY}g7(o1E^kOYBV!te(ho3sI-Py#W2@V}53FJizT%b@=WWAUhIE!b|s1 zdc3*q6Z=YGxfYb9wYQ-kh*gL8lKQ2p3<*2}8N z(++k)0z!ULT$0C0uRyNEtSZTQ;^uIFT1MTz2@JxdO(MNE49Oh;YMt&NB;<_oqI0sF zUUDHJvDiS&bB*zu`8F@E=zJYqdkOAF(A{%Ergw#~f-5f$>-w!efi_r&_6u0BrGmoj;A|U{BHz#q9kF9?b zyL~h*sc-DQj1LDnpvy#M=WUy;BK84P&H%Fg+>C`f%T&AA){mG~dY5jB8j$V)go1G) z-QI54v>i8qE7)P0)PPjSBtGRuSJ*&emY7g$LN5%}jD*Lc;b0k?H4;N$A<@ljEA=o< zCjtumBP#<4-byoTvM|8E6bJX9@}}{xpFxRb*-p=hH{Q>r%P}+^t}^E8i$b{_d%DWW zv(NIl_i9PuOE221m2(j84H|oD>UKmacMUnM|Lv}bP@qw!qK|#XNqD}ey^{f=%gD!CT9l~mss_Qq>O02WJ zesc^h&(Q*lYrfX&EeCt}hd9!PXOxyJjtV5n33cvK%~@E*lWg`Qu9@$A4MPUjW{HU< zC(NY8dldxdV~}~EFrw6x=?+N zcb~+NUm@-;BkG9o<$s2M(pF}{%{2 z^!jhD-bx#WEncf~jNAR<(REXJerj;x>V-F@mI-?J;x~XZ*24-8=dmIS2`t<1bT*;h z4SFuTOqDh}M#%Zt*a&FP2aHr%8B#Si(!L%D$^B5bGJMN3b`NMEqOkF}O7GAWR|_wt zi8wy(Q#T`#pxzR0p_( z^)R;9D!ZN~84OkF5zTS3IVjkmsKHGYjg?u{#XmxhPi)=*L@x?7DTQgfr8I(*j zTHP2Z(uzE^YNnpC@x#yg%&UC5`MI*g(oY~CHS)+teAOqVgM&IOPtEOY#qw4^xg+S+ap{AsD+68^{=>HSoGhxCLN)^Kj>S-r0JaP$=2D0>9ueHo3$tg)=X1>DytmcVqS=i7?NK^wsZUogVJN}Axu}K&^@rjgy_S00Z3MWRi>o)Zj(QGjdBV}H=g%K?- zJ9A5zR>}GmH(IcrbZzr9%p+W(xO;EQ?W6#utloFlWl5-=o=bkU_2b=^eU7RjA1&di zet6GXZXB(4IA_A&y>;h*!zOL$k+h+QVD)VJZZ6F2EIJ6I8oAfe6#g10`MI z7K@yxWTGa6+^1Kduu`WiKON}#Pa>BbQs~Q_%&E($#1K@)LyL*FwM3mE(|VwUCrs!Q zz-(5oq^fKIxLfH5_fr86@7vb_p9ESc^(nOci)!PlVW)9ln+fAJ5lZbb54RYT4%jz%=<}OE z*`=K+=cy3wc$Zd!ScdPbU4cn!C=oVT{jvhyo%Mbtd1hDFT_Zv{vD?Y!a%F{aKm_#G z{_@Pas3(j&sJg@x6JyZYSwL6)TCcq>iP3d58baWHZRfZK60XxG-v&tD&xe)9ACVQm zT)8)yNJo65jyaDN{pi|mBWPR@V9*qo@diBo+@H7l#!TiJN;3;$u`yi>yz`AY?$F8$LgmwQ{M{R50XF1%pZrYc+LL9>>_j7VZzgMNr( zskJ7*L*Q1T>08bMi|!i<_sWkRFW(s+iBIsS!+!&?{)q_}-_fT7a$wUy)WKb;KSBIY z=7&{rnWce_Y)d*_V83T>#ER- zvWg9H-vdrXcq#sQnfuPA{<3qzNLjmUif@wSAlIE}d9tXngFF|C63SGnqCT7p^`;6& zp<%(;XfG|t;E}Y^n}TOM5)ry5p}MMlFqWkFYp$Dv8zss`?Xj$uo?zcwO)ywgr1s2y8@3EIn6>M%^BP(9?-C}9Y zV6$$!!D1<44;|8xF*pnS#-%bY9F>3viA6vKE;?nISwum9G@1z$+ifa&mb%kY4{dGM zljHc-h&C2jl1wSN$OP}op5m>?gj!-2y#%x2a!p}*0X@}K%ZaUS(#&U>lVf!n1Df!( zE2h+6r-s7~@M(2}MshA}nhNk%fGm=Mj;l9b(@s2Dc6m1eX=16!2KH3(~ z@);uOAb=%2M`bdiEEy>W(Z(%FvS;ssV&Qj&N&BC2!5m-V7);5`-7gQf4Jwy%S0uou zm9dWq)MtGuHwE)V9Y$z!uATCAp`z-3SLsv4H*XaXJW!{z6HTZmiV@I9&Sd6Knc=uR z%Xv~t3xIly89cK~hms&lBV-jyGG<(SUGBZ+ksLX-H<5=p^55?Zo$>PhHSRBJ(kkby zDQ~bSLq3#1HkYo``No zj=DPUi41{`D$X1IFtK>w^cDb$P)6@H=rB1bA((R>>tx<2n(W_1E0tj!icO9Q2XJ#% zVssLV@rBIi$lcMz0XeEJXI&bEXrw?i&tA~g$Qhb_|IB3U%#8seTIBL6Qoa!(v&N=~ z-@U11#tFQ#n1FW!CfWeZ)@zLyVvva~_=2)|)mI+(`)>lq=GhtF&D;+wDaa8>v8CTqHp~yy z>4^rjLBaBqzM1aL3r)O1MK`biI_!Z=TAM7u*5}a**ZrhL`xpVyX5|8abpdSdWNno3 zoDV}_T+xj`Xgz<}H;Co~(`a6cNWzV4J^I`p6^B4XVi_j>o9UOj9uU?aoAY2EH$}@J zCLDD;)2le|QCGCtr1L=IB!JXQwlx(PXpBXmHV$#JgZL-9xbU#4VN1>?bOvXIac|3b z5`K&Mmz^JN{0X096k(wY103-Rwuj5Z%dt(>oCmVG_O<)=a}5pND4r-pgzL43rHnJO zI7ZYUf<`*XQ%;hefDQvDY@E&Y5>gI;3)ZL34hmiu1aG9hsFix74=NWmjen(y;6inI zy%R#sSH|+qL^TZIa{)!!?n_2%s@B`)OvCYVC5UR=azy&`Y>i|*lB?s%#}7`2e))lS zZNLfQ%cK_k?wNa(g;>YcP{g9p89M8Ih$2< z!@DoL;$DuP{Z-UVVWvsNsYaO}Z)SV8wqg1^Zy{}J%+ssyn|^uP-S_rA|J#+c8{HVL zr@8}>Q&Oh94>;uDbS<|}WM;AuNe^$a@K+8sUCq#s@ojt{iy-8938K>U2;ceOrw9dG zHKbb?Sy(ISh%S4+IT_a{x%fv-K{*Z0Htt>s?%7jo7#Fh@B5cGF zWUG)_`B3tP*|FZ&(*M@BcC(7$B5I15{O+Fkms zgb`^_T8DrB6n}P*J{?f*gh}((4#L7?d&_6WNCN=D8-3tuE1$$6Xk?*~A9^;dBuA#C zx9p|X%!ival!_cIXa&4_Z2A`jyaZW=O_Xzpef8tQCaR>VZcR1GV&Fu`gd{}V8rS$x z&2W1ckc^tUev6J+5cL%4Z@}GGDxjA2lFcgr1D=t%87{i_#Gq+Q{2DrsDqwf$UhJ5k zZxwvpNL|5$fZJ}PY`^Z}s26P)1?4jm&&4T!_ssm369Z|hV}^|@i`0g)05k0tQz_$I zB^MuV1stMgg*B6~M>@k1JmRv`Y zZ=JDDSd}R$MqdAuD<*rXlt9fEv3F4PM5a0bbHK6h^Kat98I2PWhl(~cR>FO9talTN zD+6c8q&xxkE54^d!2zFR+!Z4~UF0g!fr)(OKAqTt2?|{WiFyJW!3pOd)aO{FC}yEE zF3&X@xj#sHm0)%2M)4f0w(l^7xHz9!)7yRUF7jV4;(tq66xzOSPiH-7dYmfcKieh} zZnPq=@A9ZSA({XE%&eOf7fCduq#J+bkR{<2&3O@PP|eqhk4oD#gr4eqywdJcxy8W| z?I`Mw+ry;hAI6Ww`KN8FR$jo0!Wwk~T|fX^h`ZUjKIDZ#7=FD}0y{k0={_MbiiXV257C#n7H z!5NH=Dpl@n{y_b@tv1y;mPqfJ7JSmxcHCY5$l4JZa*0ky{tfAgS`6Rk^H^=3Dw6;! zn}D+U;B5s@3%@E&y5=D`{|x*$+Bm^^LY!bH9m~!POzrb_k{6C3Uok|Ij_!cizn&T&(?aG{CiLS@od|g z#`$q}%`t+Z@6K5H7q444+FG}Fvt)*K@xEW7&|Go#LG?Dv6SI0=dWmr0NE!ea;#fex27aRAKZn;9lu(E1c?a-K3CnG444&z`J!t8 z(f%{-I<2Pt&pQqr<&=uozCfz=Deke{6_6n%x%@PFm5ySj9p zeDPTr&f=`smbW1t!B~RLbe+WeGkyG!yR;Ihk{c_^g~S+#i+IMOLw#Ag7`mE{5+zp| z-URUk+h0V3-^l<<8aALUK2;lEqxi(Jb>H7jxzgN?K0BFxRf^{>@Ld?lZuX1Vss#_@ zHiD;5s(g7Pqr@8rqlp(0uFOwRh)B>Xg4Tgnk6CMO-kpNh?pjvsw?&>J)rHo}xs5$S ztbvf$=z0*EAOf<>U*q5yR^(RtciW-YgNB_7L5D)YKdzCWACKz7X`LNG=^h;0RZkXi z2Gkufk5eFPGT(FKY0|8+KKtmbdGn&_+(AibrIM}F^DNzIUbN6uMtPXGpF<%JPLEMH z1~ng?7)}%tVdhNtThfm~CBE%Db8hV~E-dL)m+wl{NOOSOU>Ru6qTu;%A8!|o+$Oop2!bTM>y*aU=ECT($2(#%~ zeUFp1xl5CLq<#YzanZ`;SotY5^EuOqNACjUsyG*0KHq)eC5DTuM!CN?Q%c*AwOm#` z^My|wEa4O1?gIL38Qwc$t8~+ojf(+gEN#MZ8{S`8*#ZD&3D(q9>VkF*>I>B;a#z|m z3Ob-`e#N%OiwhVHD8So&a-n58b=iU+tF4I*(?!`O@g%+9?y1JKe9aA$*EsU$l{|b)mCMdew*5h?4rymXmoUU-ys)B&=owtf_JfC0jy*f5VOvy3 z#*=5>S;PdHS||n5IG2!300vCr6E-i<|BL@0uK#QmltU|BeE$*(AD=N3>UW)JUlV6k zpeYj}V(1nxt~!N%gB`mcDjc_>L_OKohAih3Q7%X|j9!$JNQZYX$520zT{Cjj<80s0P!Dl% z{wur7MB{0QG^7-#$>zG!j*#KUwwt%YR&i`ewz3jwVarq^c#1lW$<>zgLn{s*&=Spj zp>sbQBt71mcKaX7o*iCwU0VDQt-Q3=BQ=~PS;g%_k?=q6L!sGrK4f@ zGCG-;AUD^w?9jpOmXEr7&A()3u9B0Kn$^_S)D0D=A8ezy7eGxHL528$9rNr~G< zZid9cri@*iihJoq)L{1Uj|BtwkqQ6wT0X2$#iOXO-BiQs2B|`COZG6010(;MMy%5Q zCuVTK7Q^&6LFTeiAH&1N8Dae5fRPS)qf>i6fDqjD-SKcXK6$THT-Vq{9K8F zD23&g%f77Ef4dq)Hej|3xYd0)=8Nv{3&bRXIQia=eRmQTt_nH@ z^3iBIjPTj{kRJ~Tf~op_*`wf(kFqCrNjM6Jm}lk+=*7Ou7S@3>SJ@A#y*;IWHFG-= zy_|P^u%>?+m-6pyMCqsJPwZlpgWmE_iD`~tCG;MX$W!E=ETbk(GVpVcPD}}!ppsKVwU386H6Q{3Q zp%8un&)x)d^|6B^i?hs|X4<#+L^_hc>Jh;uSDhiP)jSt=!IHwf7ER!x!el#C$=4Bq zz=eLKYwuhO25mGT-yfK5aQnvxKH1>|O5W5Di;d%M;ykx#WeKfuG#8!HLnj@)Z0ft~ zD(x|ryux-1zz{INO$+cyN8ez;5oA2Y%x!6qec z+#Sma3H)zPfrbs#SsIL*pv0saBt5O5!f+QeK3rOLo1{lE>jQ6(qFV8)<&)lD${vqS zRI~z?bkonl*Kb82VAmd!U@1|ZAyF)xb?{^aI>YY0b%qHFY+^SJ^|H&hxNcQ!zBgXj zY_qqgu+>K+nx96TKmCTz=`4D&tLO$w7vad&WBBF`Fb3kTaVwqiBpt~8Rjv599i7a) z_{ODJ8^iu8`cWE76WqsL!g(Yuu}1fKq(;6?Pgj*bF7aKXp8s&_Ur}82^X#_FjRo{K zQVe5`654B`Np(2l$onNjS1V>t&Z}s31~OvEteVlC=6r?M0g89LV5h5m1q9<2FFk zzu=0p%emHjdeme>Mlv&_gKJTpr-jG`12$vc@})e>!FJ#rt;vz#gR-dg50ZpRl&+u+ zF<%wwUevEywB&2GEe}@2O{82Pf38>-2S=v00F0!rc1a}9?Q6~HiLlk9l1}eH?d>#( zM>NlOvy&fBz`=UCt60QJ{IO@(n}=mn4VKy8`=VrS$wjV*(DphroGnEozlG*QMtz?r zns+WJePwPnN(TF3RKMZTP+T+{aF3T}RJKGvNzK|^)bSH&N;|PPJ8VF#-rS6vZ`J;H zeE9rK)X#vk#Z+f?`Mi@L6I7&lvdzARZ5+ACX#e{cm;UJc#$0xsb=t4#qvRG}E1v$O zqRStIO%sMENtUt#Bwo8=0}UGBFjYJ?|2@YhC)WL*(JPowPG3F=w=NG)Ub@ROM;ey+l(|741*;}nuV}*zb@f$;*om& z7&i^haZ7n)tsi!qI7D7;MdQ`F<&mH(mljuuvo>y?1Dj;} zG|=?mher2fnEVbXprt+~OLd9RnX_2`Z*q?kTxlI)gV(W{PDazZ?8joEyK5e>V0u$L zwLL!c4$m>AyP7OVez^bjGj-ot4VfEpRPE5P6S}bIaf~fl^LP8VH|QU?JOpe}0)Lbr zmp*GQYOiLZ8BC2oKq3ofFP9SU$AFx=mmv2!tP1l>(!er-Wh$!u)RtR5_pGMJdz_ZxJaw|U=J&;NA+TLB=o3zch^HI>#Jo#@(sjw<=)rjB&8Rf#3>j?^R< z*Jj^BTWd2w-;kwrYMfI_>8mgMRhU*DbY$7=WW5l#Nw6Z1)Qi43(myB3%zaPeta(I? z$))7sXGf}(c#pxvWaGKCrkbB8=U8JjirsL!j@52F@CgJ?+L4|*il&myGSfV0pG7Dua)0x{yy&fm~^2k2x(Pg5Fb3j@BjVD>3^@?2Ok=C2IL(x$7B^Vt!>^w z+A5*mZFI3i5!a#l*xUIm`8rsM#Fy~T>WFWpQl7w$_ay@I)|X1$fgmR*dYbu9^2o9< z)3q?D9A#2&Go5BkdBDK~Z>@LvTJqtR`4TCV=Bq~1>^&6H$c1Wb_%azVLejYeRrCbL zd_gX&O3aMzp7&C>3uiC5h4ba|_=>;o8T)LKn$sY4I_`6Rt^DrEzCU&22S(-ccpoLS zG*txpt|zK#kMc{OvB44j!!$xvb9*o6vfSzN!*AR>kmkd;laF)xJ+SU#N8BmaF`}Nh zCZVjA<(AOh!Q2Dnz)|p8;iK9D48I%%M`f#XpVQ;IUufC+(=(&N9{Ww` z#J!0k>-8pbpQ}`pe@qMBPhbt>`*PQ=DcRz>9bAN!n-rEIs$L&Hp*T_RglrcTbvoQi zdAji;^32FNCV`xfM`87y<7(?z(>;`p(u$9(zq*f_aNt0M)$LnlFmeYb=#X*ZNr#}! z?qtx^5_;r*)(>gP{;3~RZ+J2eg=_-I-B}8$Q5dPCEgn)e&-WtQ?Fk41!-T%4%R}`T z;SbGcWSfeEBN&R3ypjm?n5poySX;_`J=_GbIWnETAt}JXCu|ytmA>q>8Kfz`pQnrQ-vBXNyWstY~69Ab!Ug zr?hWtlT$y#g{pU-=KD*;ydHBBq8O6t5doc15#GCndfx7p9x)rwSv_Rl9o4kU$2%Zo zn{z*WFFY+HgQVNcJWsG+@ScXW{*|m+7^uEgx2;-AtI%CpsPjC{M)T*smVwUF2Ysxc zH06j+Wx>iTZ`1~T(J8iR$4+esjru;Ef$j3gx8Uh7OCJ-9mN#iRO0cvIi4b(~sQ^_w z$DR6lAuMp8OYYk;rRRB&(?_^Ywr4{$6O-e5wv)6EdK?pk`A0rV`(D6WNy?Pqao}Vv z-a^~&^5NE&+LF(4pz&P&{Gs3cHtt|08=xrrK5*PGU2f(5u$|1B0*16%!P+D~GqOx+ zPX1x+9J~%cPH8 z_*Ae-xBzP6t6KOWf9Qd;TK4Yug$Anc;qXlz=XH-s>HZ+H66|amWqCLB?HhY;kW91A zu5I1j{I)i>r1?FnS;^z$oZZxoLd7tvL}3$Zm*}S4Rm*|3Sm4Hsi5{BrANRPe zIcyAu;aLXTt54PkMjG}E^-wofTk$ZNuVeD_bD_ZX~n z(W#{CLn`u9In8!^dzm}w58Va*rI=85st4Cqb_+PtpJfIfbx_FMD%{5l9Kw}cxj5V7 zA5?h@@uR*c>y?yto~9hfr*8pq%v@5`=BY(ZyJKxbsWN8n$*|@7JKtke3uY4gR@jwq z0t&N#Y?_2pTQ~Y2dB9X8=U92b`L9Kx6Xq_U8?QK1$-W}2NB(DY*j{?YDM zCzW=XJ3$mx&k1PP`NJa#J4j%d>E}G5t%_l%f?LC8y>{D;8^v$?BY{JH?ZqkR&O$;`^ zr_}9`pp<3yO!|vvoQ-Tu$>9BwL+;V(qiD(}(sCsU3;6;|_DhHSTX_p>3yBSlbi0d3!xX^sx)q7n> z!9fsn#2bcWH!Zj2n+LO3;9OV&#RV5OO;)ZG6fVq9q8qH%#W|j@BkXlr?wrkM#yY0am6w|wAWV(*rqDl8`!5b ziN}X1vZd)4?yqERug1+Qm_|Uw*64qx-YW;qN6oZXGGiyVyN7+3O|!>+L8cmkJKMIS zRqrYiTKr1QMhw*syOOx5OUzZQV4%daW5r`-!K!}MVTHNgP^tRejUZg@0zjp|EYm-+ zg`zfjD5Wz0iNn8hs0#XISE+BA%6>)3*U0~$O(A^_H=8)4q_d>O|H3WVQx#~hA7fL%PP0V!0gvbD5qFJU!#zyu6VqdE!l#G4|&<@77FC;H( zSo2>Hi~k|-0vHeJQW+|ZL5#V|og%KP{V_86aj=2xk=hiV29;$8lh>Bukhv~x{rNejkC>Ey8UUO{qG!%U!ov*xhLmvFUxuQ~6r_`URw z4QrLUN!@8`WPI3z*V;NtS+h1B!nJj!wK{0yNtS~N7!8Q1ehZY zRrkMSp2v*$?YsyhSx>57UULh&rNOB2Pn_hYDV=k#)o$G=D^l56N*n#^a@g**znmb< zJsVTon}tkVa(rHz*ztJ{vW5|RlzmZts?`0leDTY=WNhcYO?>+krD^=MCQ_peQN+vCHR+iMPRnf+U#d_e52bTn-V4oSgNNSZY4 z-agvvuv*kW#gG&UV;h&Y!w$!sRSO+obK^QQ^wp1(3`61hVSlaBfhzA2!-6G~EgzH& z6ZPp)nN=Lv?`Y66+d_E5ul1$YMc+I?bXzgh42%YhqYr$>5kYjIbG>!Vz4hI`=2H*b&sFct(noytL#dv zmlPrUFYES>C?rLT_cLtbo(G)IZC6_G7Iqfwrp#tI@l9SNy|^_zaWE?(gu@a3{(>Ft9}d-M~>j^oSUGl&@|9+jn+jTv`_ z%lnR6lXujbkK4)wdITQREGDA9)UTEsi`_jt8Wd$TB4_#43W_M9_ANIqZaY6d)J#V( zgeDRi1YGtxBbM!7R~;K)Bamk^4H5clL+(TR%NG3g=X>ous?Bq?4eOT|HU{V(4P(}w zn*AdXOJWmlUS&=lT`XYSF%bLX85&q0<14=~vP8-1k60na@?(4l^H5W)nYWK)D!P}iU>Ie)!gBX z@dkV`wP_(e+Wbn|Jq66jyIK{foMQHfVBt~ynj;|+&+y#OM;@w_uCokVOaZ8Mn5m~V z{Xeq)`l0FfedESy1QdLUfJ)hPh;$<&N-8xvq zHx0|2L7$uYD^Bs*$8m$MpJq8aX^Kb_T9wy+Cc7@4n1MUco@*jkCBpTb3b?Jl6o)In zw5l1Ufizodc!-weM}h4bJ{a;+<5qgooLpSWx63g6ay!G3u2T}?_r@&1Mw^$QZE;D( ze8TDcIOKl2wcU>uV&{bwtlb!cKLpG!-hV8OUpnXBEEOrG^Y(1n7G4NL5?WR|@ZUz( zK>Ho|jZwnscqG2wilbNS7nO5K*5#~;*-KZ_VTNDe0JBD4Gw^G87_LFMdFu{g?H9ZK zvyh`Z8{6YPx%<-X>n05L;h5tnk9PA*FX!Bw&weLEO&dL!K&t`O1_txFq4D|g#}CCtm~Up3=#w&{3Vj`n z`=h+Fx8duyTnoOk4vgwbM77=G*75K~VW7K|icgNp^*+#ohA4hrH>@Q;HWrU}N$+V%R5(`I!t5uRWM6b-jYPVFx5Z2ZU3NbtH2T*DG6`C> z84sKqPmBeN-^Naahr=Lk2i&{DN=oYgvNgv*p2SI#_05qWXj&K+g!lO*1(1L6#4 ze*qk9wyUpoYWZsD5_KXZs^?#st^MsyNwB5(Fl;?UAM!i3K_9$;{8D9Z{Edxu5>0() z7+sGoP-{W2r?gzQusa+ybmW@HaOcF$QV zk7pj8vE|Ns%juu{pG$R2H6jb>io!rWxySho9b@&#B^CT$Ar`jot8iReI6ExV?j16h z;17IvPBPmiDT=d|!HBJsZO|5eu?o)>khkXd-vnh%a+k|lN?^q5V5q9;+S|b>+2Tj= z{$R|Id=9!rcBOe_j6H`q->5>IQ#z6%?ED$X7!H<=1k?U2Bo@=+K~mc&;`nB&aFpym5~|-G9IGN##Q8HPYS{mnf*`#cD~*G6wORX)yd< zoaK6$7^14f3Ufp}r^BJPXK5GLXG4$8h*g^en=n}R>8Fy-LA27y@olEXMT)#MB!Y9{+AYL8`^KL z+hP8DK)`pfcmAsRP=WFKWJOeckFG0L6YIF(P63#kzRUP%1vfcEZ!W+lRJNz>F=oK& zZV^w{s<01!n4?lM0kXv2Xs^D6f)YifM**3|BhP|139>x?f8@` zCt1tZ(H|C0PBfP%xtA4Eq09Q57E{TI^3NQ!>c{|ffY#_Aq{&2WP(vB+@_<`-(fj#ew-c0BkjsD^~zF?EIFs{OxoDgNPhR-TnY0SO|E+Y*WLY4pw%Px^zdEMyyh` zPLfjLNt9vTKhO;2^D^QoYr1;1Tqx*9Sm+r$a>76Ca-dk%5_eM77&0;czOw3kU)^_8 z0^uhs)Vcu9IrnW}nMiA#N0(=MOldCSF%}7O*$H#eQo7PBgN>8<<1&yL^xri2F^A{r zfAksR82x|!ZnK)2OTCuayu!|JM;}cI`xt$76q%#V) zV0EE~w%i|rpt0Z7vnq7kPlklBPgy2L0=DO#V#XchGStR}FOx)EI`@GCHQYzDPwPp% z%%D3-p@3wuvr@x>RXU36-&6hk7h(Ovc|8pO&Zhl3NCOOl7Vk>3&YmYo2fOT2+SW2$ zZzXt}>n5W4NJXi0)x7TNGAa$;Fb8j#ZOqqGb{SCie-`bkZV^xzpUWr&uCa61kIjfV zOujBdmm_8(TNmkM`0Gqk0#`C^AU6Xzk=!Gg@{_$d=?bNdR^_tKe zXwx3x>m}JbTr4L5-2L$kC`o}Jo{fcXKSkn4Ai^7mkK#}8#527Cet`aOEMz47iO;-_ zkv5Tlg#5Y9Edv7wK4K^07;@_rvWV5eJYnEKU z13Hquh@kW(`85uEZ}~ei6*`FXy-GUhj*dVH_Zovg-MVt^?mYohk=k=q1%&xPZ^!A$xXZb95N$Xi6yq#YWn{Aoxei@y4@$`KU59}}-L-;0z(TQe zcXlwIf$oWcOPX%E9+v;ke*Vu5cI*s?DPSibFa^GsR1_`RO@_qvv_*~9|0AkkuJD@% zQq}>kOr`EOzRByFou|=bW5fjT8P;ZFE2nn89XKiC{mmwwb5$+=bwhZAbkKaagm}Ee zj9O)Rp&|H|zV2~JSMPBdg0$x87vdhy&FWw}TO=dpSR#RKr@RSbU{XAH?RiYO{0{Q^ zT?ez7&!mwJp9FS@qqh94vc=Q4YXQHn*3Kuxp&q5sG&j7fiN94tJMpU`#4tVMpRXG8 ztSn2hPB+>}>#j2B0ao4`-Udd)BF>_pKX4!#S)rF1FxSbg$%UM$He{GTqq%lcx2@&= zvNLrO5p=13j8bLv2qK6yZ@ILewk0`SUi`&f*a!p6nR~A5C;KE5cDpw+nLJ5x)w=Z- z01Rlo%xZ_$pnXs{SS}9AI2TSVzDjc9=+jvi9am1Q(77eF+}YL;qudpPs*6BaJ7TBB*yh}sT_RL@)+}Oruu2`Suc}_iwUP2q_8<>Jh1)D zD(6RNno#C2BprW&#Slm9i5{&a@mlaW0NLhlhB$hXzX)Ort*ZTQG(}d7g_P~&>}|u8 zygCx)_u<8@O)lUX0V>Ey5MEBs#VncHuKYxMG7O9e?l`t5Y%C*nv4_>%{|-|By`s1B zL*V>DiBnBe$yCL{V9UrCCdt(6+QJ=ac=u&^xh5gyf!h3r$!cY?&U~*cJt_q+ zzBN0U1XVa)Y_McIf<9ELZaURe1Q3hK9g?f3Ag>Is8IRj|+6%Lcv^>dpG{xDGHfz{4 z#h5JUm-+reu9QwlyyIy1xe0j zQzAIaXk42P&l{Q+%h__WEu8&ksI4ZyMh^iX$S+${tHW-9wON<2_LkPa$Ju~wH!I;l zv>lGiMmJ))N)yXX|U+4$n+Ay4S9lw!$Tx|(*DY) zIoySRgXKo$jCsywss3?|RC~>ufo{m=K#8GR_Ej8-clgt!1k@R@{Vw^mctMNsR{$VL zP$N29qTMyw_~P7*t?&4!`)SM8dL{`j8Vmt{_tiY28PiPFP%{ zJFhe0!>bfRSoBgJhd=y7WnA~v2V+4N#!1~8z0TX7`k~hCEk%{N6`S|&5Grm|G{|B; z**gC6BTc|GDuh`S+22r}im8`4NqdF*o@HjSZ#*QIELukrisfctrb%@YCo`cH^P?;m zAy?cr{tgn|=&fZ0{gC|)y z!@7(GfOgbH#gG8j|ChN75%oGa`%Z6v`GmG7%kA&hQ-UnE6o@FRsjrdugm?pR-94}g z122~k;i`j`Oa5s1Wc>|MA?o6zprN|lqNa{jHEn+h24H$zaRowKsjbvrE{d}Zs+ovg z1Y{SdDX#t2Mmrk43*Q*f%^WWCZyO0=G)JD->YwyGid%^bsGMyUrEMl7n}McDCjJ!8 zK~fo9i9?p{8w=BW<|F6*0MHnb&}c*d*Sfg1a3G@CkUX9>=1#IHssf0#C+Ka@?^7J> zdMMm~?E7)qq$A?4mcq(dl1mGwMLQGJzgd+&HNBZV8yLFp%C->&eVG5amzjw*_HJ5C!RG@08@F~o{X0#T`@3mp<%5vA^g62BcR8|my8|!fa^zsu zam_qtbxms{!{yQF>TP0UJVOJ0?N|8ZV6{7H^474uLa~)k?f4b*OdCTNp=6hdPvsL8 zvxtZLnzUXiyCfo^(A}l`&o6pJ1zcipB?Ch5e*Q8<{GE8X@8T${`@P372tO zK=bRZqfizKD+Y!@AuG}H6e;tvIU}8_KV_NVCuxlfe=C+wlH2D`EG~(}@&xf^k}URr z_P`zZT~$v_{{OH5#rX8Np2A#WL81|vCx|;eEMMtzwKv1#Z4g1>j9MgT%*;!X7E(+x zV{qReCkCz{u71@#`K!K3vE1CIhkMi0qzWP)Y&GfIx+&PDuQVO2&H-T@8ohYgvGG6-oh=3FRD^U+AEk((rZe4#v$A3SH z{2Omv9-!S)g^G@i1#(4W*lqEZoQz+^aVSOxR1Q`JVYRS3Vk>;v$Uz-Iwk%qvBx505 z2^~0dzAFESwjAk&x8nX82q0MNxCe{yvrKxeU@&ldC4AswD61?mbT_ud`tg_2mrDJn z#e?>+cePo3y@p(ROWblG=ZiUO<{F3^HZ1wt4t3va3`5gQdP8x)71&W*yPA-0u{m-+ z^NyU|BYQFcqkS{>35gvS^W!W5*AZaThot_2tEuQ|36;4}zxAVy48-fOPtW%FsILyH z0qPJ7gOOlrGZ2}NNqS4pdiLHMJ2h5Yq2)ng^qYZfC5!|ls@Zj-$%Z>dU~@!K`?Zan zRmQdV94J%n(NQDjXc>j~FkEW$E>UuFipGw2AXX?OW^#wl2S( z^Ox0J7)8ui+B7DJQ_hA|L>^r;U(ncJ+zYlw!fpXUHUjS;iO~i(hrKxZHFLp=tJI6@cM(1n_HE=Nn55Op8bcNht&#V}goRvDzB$*{KND|o|KY@J^7pOn@4 zAdvjZ_d>7rgUi>WA~C-AC;{GW-h_V+-_U!*Hl{0{mG6fe_e4~h+=2-%+FiZF>n2BW_xrBf$w1dPxP(ZcpCTv35~cAl<@UX78I%e5+@$)v zhH3F-WEVW8hi*^+cv8MPO_!Sdx?uMgl|90_-J(BDd!o!$(-4IbMUb!1V*}LklY1 z_BOiS(9Mf-!LP*D1KD~>Nn9gn86f;q$tob9AIm@Sl23ZS@u_0MV*dF#@Ew1UzP%@G zrIRQ3r!$*{srQa&*a_I{ByCurdvae~2ew#f3CyU#fIa0Yi zZWwO3oT87zq7W-R@R3tTLE!?Rpw}vdjwdU;GwY$bApGR_oQKN8n}?IAech}#DOt-z z=?kH}sCQv8NLn2R(yV5ob?WH$Sf(chL23>YFk*tX_R$^5nyUT(zx2GMOAqoDobLBh zZ)5xYidpr+INAMb6)%c^jE!!ks@Vt4_ZxBX*y-rsfbL16`b8hknTmLu*F_4@R&iJc zC0o~7q~E?si@gba#>pYT+p5|FpQJ0eE(4(kSpO=^nzLG@9q@vtCW`dAEnvDSrIP&?rRz_$fpnVy z5wB1!!I7?ILJezCaBo&qzEKjZg6!qy3MtJhUiTe-O9m0&A5LVvILu^w=WPKFydFT< zCmqXAaYAVRz897W(pA1TP#!pE(xmn}3l*?e^EzTXJRChY?KtFu^aAK9C)R$Q%N(#` z%f;{0O+$lHJgvfldb7`_bQv7wiF=0wah7JW-3dJactV8w53QCrMqGewZO7kAW8F)%B!z!oA8|rh#;fn%u8KIlJE~%Rq zk6Tl+AYDCXdnyEA$x*t;FoHc<_P!xoQq>GZo#cPIhEh*3D`m_ z*fhdf9>Fm)b6Plk9%fd+Mh{dV7IJsK!h|`#c~u(0!SXc|^A1jk+$L}iP?^!Su*$A8 zLsNr{to;9!eNXHaNR3&3!uz-zv%$^v%S?B~A1-W%GL3yF3n(t~uZAK#WJul)|o^3>O27>y15KCMc-W zRi(#~$=oO3Diy4^nprDo%+^r5B!<$EffN9Z-h^IE2sQX#L_Ua_yWcILTAWP(UeP@Lz}ux1$P1@ze7?v+zA%QoNB8)G`k9A{^F1x?k}q1x@e(_$jWjkhcl@D$+UffF41_|3)qmM+g@f`H&^>#pE2%Gzvp6rC?E}c9xB&CrcK&C;pa3 z>QC|v?zJ*y;i=_e?q;2HLp(sCleVmbAXZdn(v|1ya9~ zO0m=v_a-HX@#5``_4^KGpthwATlAlbwio|XZv6bD?-EPTO>L0S=Fg>I81=h%<1aVq zeIHyg%h!{?BQR>K^3I;_#iRSw4%j3yHm{bsUj&+E_a+YIOr*}zOUe@3L?3-=b)Tx7 zgzm}UN(S{Aks~kCP;I2|9MhOqj~1fi>p-$%>oG=4ifD_;kmJU6)s(qfCOnF05xEB%7C zGXfU#x}rFF*%8m+lCMjqofv7W+iJLyh$kw?SnNo5YmWf<6=`}z1ZXik8>eIyoPfE1 zx9I_4rQ^>^ifC0+IL5mIvvtTxzt*dlxD&!bY+G}fP%G@v4qs_u2W~Hfm*XlW414lv zcapH}YeRW${l8jTYB64`w{xE%M}br4MR64tgl*(99(G|kgDqW;EmBU-HLJ-8$jBzXVCwQ8hQmk)*esVTB@CK7HxOc0>%5+H}9y1!GL zFF(#>`h4XRo^#c=#OZ3_|Mk}Y(nHmbET7>ZuH*0{a?hXTIMA(ISma6{eTpq21D@z9AZ%V`cxUm$3%7r;P}e=@swg>Q+4h|>f6Z_SPf^t|s*3HU zUv)O1XpF<#>#x<;7bXY%h~UkTphfkoqbufRbb(XPj-I)}So$oM&Z~k-#Ci*}nRcQb zOh!{?SK%!5Y7DnZqoZ*VQES@QZq2n^O9FI`>EnLS);YG_fM?}WAVM}7q?a3L&*_gh zJ;Q0S3OFVt0?quo!n~FWYe}F)29KYb&3-W32smgW8v6bL*Mv4rOf4ASkA0^jA85&j zOoCmgafFS!hG5>jswsHOS@P@!@n~TnE^+rKgD{nQN-oCsUm9sh`6`Qn~*UP~rA}wQkG|5oN<2kx|~Wzj5{I1F~_( zC#e1|npvMVLa^Xdo0{RPZ@Bwu{1(f6%RV!=$gFRElrxnZ2G^O=fp|3T%n@)Ado;H8)m``@YIc4! z0=5uf@f#XbID)_5mu|NEE^Fj?Uf&3URN>|ti+LH=8xj_AylJb{N8feM`N4VFu$Rl~ zi{Lj4O*3pfXNmb4Nd9ogC$QvO+}nJ|^qkFR4p&(KDha?syGvTVgy$yEDjM2(3~WlZ zGmYXSku`r$XR}0J{l9N2af|*X%5;ro)ExP-kV%oIP?heJ?n9f|Z!>q+;ggh)sqHLE z;VE{^!hqAd(J{Ffy@U*Qsn?3WLQGK8s~ms6EQoba?^Qu_zjH6U(fW=ne|I&sgLSZ&D1KDk2DuN~h|2tT!e)uXlj$vKQ?)TcNBOr@#2|QzYhU&s z4rOLwg0bxV)W|8PE1Iq@`%S@@Z2IHlhGD&=10v1g?9oDs=%{!DvAm^XCVmX#YVTU{REbfi!sfjk6`NOn%uwHi0rkQJ$CDRe-#! zq@Ab)9FFa2S_aUYlUUOeo^YyI0~a05k9qsq0LwSWl$72y$*RWxA7`kt_7Yz;!&YT= z^7!giEo)f^(P@`=5duAQ`&Y;kccfRB;3*t_Yql@P))a2@p-%6W%n7d_*|i3B3R(az zU!v*FdpM!phG(&yFc(Q+kpiJWo!*_#-&$ZOh0;5E`w698wlo>d=KJ_2WnWdDAIhLe zFf-2^gF!b!ZR5hzsjT0gL&24w&~I(j=|{!#nnx;LCR>C1Bf%Q*$%%hz|2Q!^Z@4+Z z6H6@N1DTR3`GZ{wYNSq{$sFm6B8Of^9mNZ040v41c(f4`WrQm9=H8W!A#xA@8BM)B zSJY?@+`LR%Jk!6}8Q!{ChQ|!REWGiT|7sC&Ldg7>D3yuHGc-&`OZ%PoZO7nmakP0C zM*Utx3c_+nPXjQSOh@e&Mbx={guuSAD*ow91Hlh5IU(gJSB7i4~aeO=Yk`5Cf*AbKvZa`-7Qh6_eDHOWVlA(*z2<1)j(KMSPb^m-mrg))*YXKV{ zw&PfZ1z3|*s7Fez4Qf^f2ddz?T$lKY^@Ob6Z&;3N`d`e5g?cCrA$??Nl8=nWnvH&@ z`URTjiKv)Z+WMNr|A;z+H{^8YVBW_Ppb2o&b9E^DgTg02m#HutA|6!cESBFtlFJUn zvY=FQd?+CWPAm%z(@LlGk#G$ka{^y(-}KG7x;h52^OL$y$YejRZF|2D;DEA-?vz!p zM!eidxu+|BG`GE3glO(HXG*MipHKj}B<_k?W{YVmj;8mpNUIgK0p%%+n!6`C8MaLz ztcs&1^W*;fhgAcE*I-G&gSdlgWU6jAIAbY;j{*(LP$TwGv6@PxBkk|4?v!Nx+K^1W zUA-xrC!Wf6tS|yP1 zlR9o$#@q|km>OObz_vEh;dRdvhUrk7yWBvC{6gl0&0;wyAXJuw#LFd{wv*!vCc(Ic zV^?C}9mQi$^%PUaiqH30#07o71u_1VXmnqBe*}x;u?#9(CA9IpChzbJKOI-AO3zB~ zWY;<0sF+RR(fn&8{;SmyRr>qb+FK+AuXv4?Jouia^&nl6T!M=Co-sWwWI6o#vb58q zG=WeykJe9=^-#wTuq}`_D_0!JE0mi-djzHhhrdCIw0iuAn9dn$sydQ4u>2gyor5vK z?jLuP7nxYFJpX)b7xCI=mQ_B>Y4TIIgb&xd{JlyJie77Q?;1awM3D z(WZj!_n@_aFW8-nl&sd`EcVsw=1re(`#?>v%+fq-jJ>0+qAB|E#X=WpX@SL>|0jTv z56L0AjS5--bPSeHv&p@9{CIzOBrAF|pshkpwfe*#dG7b-v*tCzJ}fMO`*I6m-(a%& z%3WJKU>}$JjY@~gLZsvJ#e5Jp*CABXyNs~UkSk(ptu4NVzJIbG>4Rw66{%DB)iv$Um16IPr+!(_Yh3(_*J=O(4!VsRR> zS1gH_tw_FRuCfucpx}dpc;W11*Uxa2l+qF3bwL5p^**_cpZebi8m6Bh;SCev49}4c z+Z`fnqs4ylJ#-)OiXr1CccuJlR*pTx*u#a7Zc87YR8g)eu)TvWTTYTeCRi0pxQV|W zsdd)+hhUVJf#$ffry3AM>m|lh8c$EGKEEG)gnP(9D`2`Zc$Pg^o>s>HU@| z<-OTsQNVSP$@fhyQH6(j9P~!)<10q1sh5%Fh6lm0SexLbY_MUfjRs1#dpKag!$d5J zQNz*@r==P4a-!>+5g`5B_kD2+s+VJ=tR2uK>9PF+S+sJNy`^*B5BjsIpIEUw_t2oc9jvWteMRA!&Uy9v7GBHy?b$qQ0>VRtF1Hu)aXDt?%1jnoiM815qrjazw}k?&`X}0P@#x4| zA-|AQIb@GQxX4ba(rIba5_?J7FzchwI?C-6D%Hodxk|KPz!}?8zGPS!+5)=ztLu8e zJqo}bW|o+O`)}4bsQNtGw>?5IQ&#tlUVM>=B8mfile`gj&D;@#Xngu;!9*}b8pGzl zkl;x;c2v9P6{>v=B9HY6SRu!!eakrf!3SuWe!~cT#I~v3M9eRx%)YN0b{svDb^#xl zOLHLyxFkD z>+NDMOzapMgBvKwg=YL6TifD=IMw-kOK)QzCM?|8|0*Vyy^Cv3FZX4S)MN0UzNQ5l zeu(0)_rboN3fmkLpt zA8|J2Ts{?gj;v#I-<$-%o3;Xt<3dAFO2wbBCRrM+JULmJMG}~} z!VlD5OSn+T1%p6OKKjq%k+j}lQ+=}h4vGTyV|fN-8eGt?!&WGjI$C}lO$DoZV;4`t znN~K3GHZ%T*vxP^k<#y?uzQ~l@vvh-}|-&x)RB)R2cLZWvLa!lb6)N9rFXXlklI}|JXKHdUJF|G@d89$L=m%J%tlos5r$2}M@g5-p!OkH$KEkw*Z{rDld z+DBNR4DaM*Qv8T+@~~CTi1XZqd2lA;o0^tBBH9~QX*GP4-3f%j-&R`zlPwKrLrpM~ zE%8&?9)=4-y5qq_v37vpfRV4b)Jg;1?I(B&(nG$DefHyJmS#S?0W&o2^c4&ye@P|4 z0kyS`P)KqGRKO$Ir&bbZIbNswO|Y(6=`%LU|M#1FmnFddZ<@h^UTTkNqXpr-ukL9) zlcEs)EmX!=ai&KDJVKozDj&Eo6Y8e1Qj$W=4coNJO6^ zi!i{5+Vau7i!7>=%+k+AHg zb(!c?M5F;*y7|e?)zZFeO3o={N`?8)!TdeipoY?_YC1pAMm(CX?sIjgo>zR5%Red5|M}n4?7}_=1=V@wFLw-kN@yhphCfTvWH$6sCt(w-#1ot4M?_u`$Jza4p#5*zyX)*g(C(NdnkQH z*^*}tM`PSG#sBj_{e7Oysd=~qZ!CAKe7d?_?bh%9D5w7(^ScDb`GVYYF3+Xmtjz9| zmJXf>fjP-Nw-Fw)v>ClUw@fyVH{P|ipY%xClS7Bki~=LFVt-Wc`%Jp{3w;KA9y`d$ z;^Hu+ehKdX$My+F3o&eRM-tyeWBt|cO9#xKLu zbr__r>J96zOAl!=#uRc%@uz?f@4T#Ae}ZV*2z`QAe4D9=e49)DRbDoN(nr!Zr^^Il zHWQsH{#Lj35eFn)-mIn)ria?<+jKGDrIoPdWKFMGG>-Jd0^ zWi3!y_qohA@L^PgPy!U6h_rd@U2FcEb%L}mXwL+t5g&bM?c6FYa|^waS2^07h3zq6 z3(QFB$nrmYb=DV*eCpb?W{!WRma79hHggL;aAWGvyXZCqSijO7a1#;2heTC!T8{Ec47m{v{S*s^Tf1LSS4_M3Na|K=#j<~cnuhBv0?SZQ`*M4?cgHla$aNlNUVJwk{8^>TdTig7f0}DiHFnp+G4);1h4D@eg4N(L? zGO8*T{KYOT&7A-O)!pu|7zw@TNmEX5Si32A=$f@29T&Dc+2B2e6#;d=(^ZTe7jCJa zaHh86nLY=5TIx*-9Z-3N`4EZAQLbTnM!Bq%BpT%p6>f=YTMxbo>W>WGdPV+5t#r7CNms#7HjX;_4#W85zSout$nX}RR@f}WN|?UzH&?Y8{ZEM<{w*>2 zY>gbyG??+%7A|yj?rFlb*7bZKY#cN5pS<|cZ`2NU+PZK>(kH~Qf!*P z3h<=p*&&u?gz7$#o8b*mw(Qaf9B*!i?cVQ`;vPfq;30WM*Bay8-PGivSzT zp|b)gbV3kv0+;GCYpRxiL#?^oy+Ac#O@7>?yMP9{m;X?(k$xy5Yu+a;G+zthqjLc? zB;%3c7->JmpP`RL48(o=YcDPX(I^z*6x_F~5ngh(y`qb{0Eb`YH!|9*F(zF<5t*SX zS*#BU1laO^#EX~b_x1mMO=_u1SN_eZL^IDG9mzY&yAJ~7_()FqgNB+hJ5`L0LsJ)y z9Y+?Uc>N;UNE8XUDVC8?{YSv+tTSmJ9j^Eb^0!smQ#~KBQrNYX#`QG~Z>AS5(C4-; zYmkDnxvXD8B47XIn*a3+tS?EyNQ1^57#>9YaHpM8^&Q_s<|Q{&KUr>s@7q>|1?d%! zp;xR!+bJRjwFHLiu`g;HTiH0Pu)^nXiIvV1w^l!`lvOH&(p#)rym8cOb@}D2H&ZA9 zyt4jqei}DFt}lY#P?~c#fj8e|4r{7D3ZfWIr7Ypml@a!3B&4&kC*E1c(rmHx?K_wF z_g!}=?nTQ0w9Hgw-S4?iiweQ*OuaWWj8kXW4KE$ZHJ(Nn2P}f6GMnx?A5%ko6_|%2 z+ZHdTyHdKvo@S@BN1FHCIv1ceidR)p%OaB-h^#X?XbuE8?(=bLNWKPHJ-Uv$$p0+B zJO1EF+=ZgU{Gj5OD{51FZuwv~`rt@`a3hX=;bmnvhKw&S%#_stwhHyX>E?qm?S?^U zk~OUALCU0?1CxB?vSh1dPXmy_X*k*wKDIEhwqO>zH5%}+@YXzo#lN@S5XM;Vv^ZjOY6?Eaa}w2$%ztUI73USP&SKb{-92IY8WY+>mq?D zPK`}ML5ec#iszk$q{PKDGsX40d1{jUkzmJhA-KlX?sSA)=9KH~_hd&XzSn1DzkRFoX$;=PZ@g*PWVu^)b zv{F2%yqi*MeT&~f&35j1Lu6vdkfpSeA-|}d9CTts^-Sf*AIV?64x*fz_tM2aOKK(2 zrhcmBHxPo~eK6uaG#toMZ5V>Cp?{|Sg#Lh`L^N$1xHfF;k{OOA3!F;i{wzq=#rY?l z+Ah?scm^)PEcAKK74dDr^7=!Gm0}hc=+AZNz?6azo8#fs)6z+%E5VBL`*pu%IfaOH z2jTGFl4c%NJXV8$1eqn5UgRww+95|BgZ?u`MD*1qg*NVKn_!1Y2 z?U`i}7;O)^J$HJCF6KHT%@eUFu^svM+_t&l>p5IBAcq^VhwS0e*{@Jh@TCsdsl2id zMu{&kRZwcgYY!t{**+?nZ%>ksWA}tnQu+BIROUvJA)~JQsQ}m=_aW`PKo>}rx>$-s zC|71i*T(8Y%Jc~s$)0p>@^$YnH=Oq0bs=dy3UI1WckQX+v^e-!{_cgr@^=C7x zZ+eFkJ?X6%Y+TG;sHIM7Kf7UD5_!JHt{U-&usx{kUQC-9drEMaV6| zJo&xr^D$mr6QjT5;qGLY(Dz7h3hk8AduutrW)F&VqdNhc_t;!SGb5e=^6WeByYjj@ z8CZXW^J_hSKrSvhSpX4^cxkM5R~Z6`s+0}R4lfz#n=F?>KHaTrMZ1l&!F*_k_gp$_ zg&No9Y7HMQKmOHw<72P(baB_Ew>ELSo~%IPo_OPw^-P?RxVZX1S7<(K@!euivT+kk z96R6}{JCfW`F7u+s-SM;Fzs0FuC8I zqAFu~vZ(ULgk!w!OUL_ek#2-z5Ijs;p5HDKF)xH(brW-^e0voQcU}}`J=@lx~(yA&|v*4$*X;N=z;({h54!#2{Xm7l-4!AX4>rU4CAx2aonJk*? z{R4^D^<-MfdrPbB4lEPi5m`J&cLlcYs(UW=tsRZ5P);v7^aOE2x(+hroWq7+GgV`K z8fCHGp~W7l)hqQZP$M$?F_{s6azoNT-hFD*sLuCV@Q>G;L(RJ4JpsSGhb9ezYQFdr6keU2H?LiKyCj{lZPZ|1eAy`NL zA0P90?{lbC5=c9^5GKN?z!GggL8!X>CocPTldj?Rr1pD`vCNC#&uKz5% z&}RuRlOy9>WrvC>!w(@{54P_FntY5H=cF+=sfg&m|L_Vlg93VoZz4lBgM}Jwnkm{A z$et5;@!Q2$t?}Of&y0uPe@#L%)kI^xul)Bqmlpi}1gZ@Bw=E$@3w*5+%JeTX)I->lo@5Ik|k>fnaUfvhI#Wr_X zx3<_v$7-k+T#fQ<)8lXs$_(VMJO=F<$A3HLmGpMOM@3(}=>11`^L4{r!OU5n7=e+G z_Iv6y7Uv*&J-C->a6}Lvsy~u9*VbQ73GY*^oED=NY*e}t`A*w{K7;%_g(ZVm`TbAUQ`n%xl2uEZU_gi8pQ|5RBFZ+Ca#3~a)gJY zstg0LpSAy&?78*#hCUfA;CT06`=FJE)+}9`?g@3#x-%`peYm-$_qrUDY@#jf>7x-`RCFi0C|hNCLy;|SAkC9KJ9$iPR zj6q-`n5;@ZGtKdMUG@zL?0Q0^Xo=ea7}dwh2>L+$nu{pS`Bb0TWyb{v)|xPM&r&r1 zKa9O~R1|F2Jv>bfNJ|XT-6^SrAT81{;E)mn4kaP#(2caTfOL%HFo5C!0+P~0Nr)gN zq5=lSH{QSZxu5%uXRYtA`3u&9Gv~VY-e;eE5mdqh9KT&jbn#&aOa8u_tERGc%b^LY zUzKV5u?efOi`l|$rS#9a5$D_{=Xln=*=t(iv&*ciocj!>yI)7S4{?3n@o4RT@W6pT z?!;tg)-ATb1x_;!vcIM=DwjL#Y)D3<+%-rUI#UzrH4n0$Or&3@8;83EDw)WeZ8O^k z0E^FIBu8pbEh_-8Np32*I_j7x$hDbC3ky%Ly8cUN4SHWjgo4_D?WPr2Y_O$f@_8|8 z40f;aOc3!%^R%wD7z{b?RJyd zJfS_5bC`}sfe!D=m(jJc-PKQge=p|fLs=h@J6x(qC5QiSDyAv;XKb4oKG9zNd_Qnu zPA#svSpY2#8cmVQ%iCf*_S?F>&IrDl;=+O<P&nE3}(u^s8@9E?FkuRl;e18DVW}2Ps07b;Bgyv{WTp8eSEn-Km0&Qp>y&S5tRb zX}vdbcU-GLs-Zn{ih--};)#nViP>C~DQ@$guQ&P%ser}vhJE~!9&}2Thk2{(I=9|X zWU~IXNk8({OXy&aw>&;6?U8i^-EjqEJ-mSKD71C!5Kg%nTDyKApM>v`G1tpQ z?2wnTlwfps_+YaLGTCmqiY57~hul!PPJ9dg@jJiu310VD zYPf8uHD#Bz@?_EI>(mcQx-_d=h^{#q`)!_W0zSYlK`{TbU-wiY=FlNfrHxSSOSX zDD@Vb#ez4{;4a_Rm%#71WmcoBX3-p_qD{8E%?Hu!c3~qB&Z64u5|JRIzyNRGXv?_rHL&%@{H*O}%%)TnXls02a4Xg69 zyA>8OEk8Q)i{b-vAD*vLTYM_TH2%2ig8d$isfy*P+KYD_u})t=HXpk%(ya_))W*v$ zlO=LdR;=PEew2e}kqqPpHa(vAP%~O;V;#9tWv(;cZZG zI=ytnxa{*YNLFS2 z!bXD&sd7|ryS3@leQ8AZQ^6tHA{wX9^L88Ci|-iI_&f4w@CrfWfVU~z6M^1~G7{0f zf%sJeS5D7{CqpAF;z!fmzbQ7KsnwsMhUfp;SY$`$dKXW%$!+_;8|!lw`95JW?7lYu z44rYh>N+%0oL7>@OHZlIOZgJT+9^#%24(01i%YBRWpxZ1W zwl8j!#1ltB964ZQx3yAPq6*SPWU`Dm0b9kZROxz_#(Qw^=*ow6xR8fu@;zmFzDs7m z?%z}|@39pM({%5uiIL8qpq4NKvKvVzV+FkmswM&3X0#Arh_;fpGa1`NVyPXbP`1oI zDE=J0GaHBT$f_<|lAh@tvxMMq4_Y_gi(LloJyY)OEW-@-8jY&;Hj8$Pw6dE8bUF=l z?donK2%n@=V<*I96rmpJ2pqG6CNck|cs%ZD8(T<^xskf?{M#FK!^o)@SHWUhR#0Fp zRuaW2Wc(UiVkrAlyO&-eb)M(OHH2nB!ZpNP0J3>fL8O2>c2H|=mvMS(qgk}GKG`nUH8YXfR5g;vlm%X)`W+d zw@21Lxs$F-wJ=6M%!TV3+OU5m_or^6yEo<793po(M2vWw{xi`AP;6+6K4nxOjyx10b*f;UCa z$Ah&Pz~XV`Vv|-pCU#}a(xv9{l&9WJz9Vfc1M`3^Et@2X_2p&BIV>nEEGn9s-t22g zEc8L9_#A37XEq_GkJ9`GVrAqZVrVPQoxhSvze=#KPbkDsGn&iN`MM>T*T*|q1<3+S z9ti3-c;a`gBQ-aMUJ9{JcxNyg(y`racR9Ybnnl{XSPgS>DwTC(v>c&-e)R2xleKax zpdq%Pz+S?!3!?Mja#W=7Z@Ue(gr~^A+sHg}pp^K>e{z5PK}7tg2k%OT3~u~fa$&3c z2lTA~{WXYY=VMXZYWI22w=^Fu)8uph%|`+4G8wXis)}}0==dtn_$KBg&NLT*Lm*Vo z1|6l|zJP^N&O`uhzOX{Mq7e8`8|jU+SYJV)S}b0P4%yw!F=?pVS{{@{H87ng9vIG- zD7j40+!(^4(fx)k`Fe%sLhlHB%C_VW0W5D;zYoO2&6t1{oyo#aT0GyW5Rg#o)%1~Z zY zkgzDpOA2&ImgG*f=+K-wWtBapt&j*CFUrmgPG=jP6jxfsTLERDilwKDTX@vjol8{s zT^^ic%$Zk^$eS@=Ca!6?bwf2-g@>YvI;+LFI8o>(4zrgO*=g6K%Y`dB2WLy4c*!T{ z{RM(?=TWBt-@Lr5zMM-QULgC9Z;r|_Bwm?XU5!gT|48=(|2eDw*>%k#n*OHW7QaOV zRgrD=&us@KhyFncHGBThKY|$D>b{1pb;kuAoqqkQ>4X@5EC<%h7VV=(fqiTL)>?BT z$o+EjjW8rmD&>=^Y9InPVSQ)t28|}kTNN2wD0|-6fB5CeJGX@C)38FUxUVbu7PNYO z;^_nS?vHR#&(Hzg&vKv&BwT|V}BFHTA`W~@8Ug$KN>g@a?q3JSVffv!v(`<#zUH*TC<&(3v?<0W&U__Q*PdAY%L|~5yqMl1VjtN?Nwj?FZdhssuo27Cy$*t* zVMtnt36qw^)yaxkJ01kQT!~T3S0JfNpVqsN;R;AA=H-TW#woRez2B?|vp^6yOI~(N zz#>mJ$hZ(zHKyRw#1`tFK~V=wUg?%_wT>L|zWp&Q==GutV@pXTI^L&j$hhH#QmhK` zcE}a#?C@i|=ae4e)F7ZoVw`gx+R4GbfMKnZMUDH%n)&!k(m%$b|Fxd*|BRVDEML!C z{DGLlKPH2JN6zb?*VDV2VY;7i#k;hI1OECAoec`6Y zOqulTUvgC;XUPQKsQ2{TLhrI>fEtc6OMtjWO9dYm>qcCECLNFARE1kWKNTq(nlcVfT1nm@37;#Z zI`^9Tb1i%haXCW265QnCnlqe@jZ>UG!l@NAD7(~b%G*1k8<>UIpjErA{05Z6sqh`EjTb(370e@q&eeW$*!&)(QF1u;v*j|wvS?G+u zEyFdtk2R=tmP|h38n1z}eG5--YS;Hpqf_m5P@~-WQheN(_!grX1C-|^(MbC6%5S3b z8+oBj18AHtpLu=hT`w-x?&T)Zs?p?vCIiUQGwFlwwHLJTbLX7By%Du;pms&Y9pqHv zb`^|acW-by{BbCIM3rZ(vW$XKg&w_A6SId^F4eK^zoQ z@7qXlU7xu^i_O(%Gb$*YTAEGS9@JBqRV=+^AKhfAJ0LO{1*py9-)BDLb2KT`?cvJ+ z4c>giX8M8+!<9C`7Ex>{#BkB1g6*rIH?FA$?w#7^L3cB4ng(EVb3FVq!;$HecU#C1 zpTj;$m~rQ~PC(|ksnn5Voz(%av!6fcXI)b1CPvYO$(|>nYl&m$G5^lDwQjL{6K>#t zaEpH^Nq;gkB&7c69GFYz9Di!fdYb49S-|xeQk+@VO@^j+-3ZhoZg=F6D#@Q9nPM@< z&w!PD6eBc94Nawv4{$sy99g_95?E?=n?GM4$jW9$+*q2k8cO!i1)4N7lr}B`1_!4@ z4?7zdmegXKMLB1N>Cs{ZTPja;Hom0?=*#UFGLcZAF4MznJj`@*1dknmw%c%}H=aI? z{i~eaO)cHf#`n&%${zMGM_QEU!_;?=mg9O>8D3PG2v|@XkL)#U5`(wuZa1to!N*S^ zS}F~K3Hfr^;Y6%Z5H%FtZ>%X7si!vq3zKM)73}xfW?NJYjH=PM8Pn}k=LCA_pjqv9|*Sf5A4(8$iapjz9As(6z{}` zH_ynWD=zQaQxZJDb7-C|bn_F8@s^`0c}dUa@_JW*J6Aygwt#~ZIH!&Ey+yt=eKj`O zN@VDbV5p`1rxsGFH;cx+@|MK|Zo$gDv=*wU9Z7aKC}XBVim~;I{Q=13rTwN?m+ILt zzTQ;)9LJ&Dso}22|Fw>sg}`K)pPII?1g4jX+50< zI0%LeCJX9j07_3*vO7MlfTQ8PZB&k=r0w)%&Wt!At|zz*3z^t$}Br7r?+nKM$JMQ5gR7PHNw56~yl^ zfSsO^9!Av$`HAv49IC{gjgz=$l7-05<-(+a+J>3}mIuxcA-Jt)jxSsPK?wib6?i0F z@rMeUo|cI}&@pRtp13HMqU#TGsqglkd3Brv#KAEiz3mQ!flew|@+6KO;D8{o#+bA{ zd)_Cz<>bT`@$q00j$zSl&GZbEL&_6#N^^j@Lf#e?n(#uU6V%xR!|}OsRKQ>qR68bL z=g9ifV2Y`>U|*w{HYKcP!5UEIB};M_;E1d;*A3&U;)-`Re%#s4%6YmwID)fQkY5*T zxoTy`@V!in>D8rH_GcRWdHHg-8xo^`u#Sv!tZys)sT%9SejqC&YWn?H*Ho#BljGj5Rro_xbz0VBzi$RO%?hM<*s zNU^9raZ>q;DIiaOafiE*nEMNOoSNz!BotZU)yGE>A_JFXb0fa4iad&l?`%}1z)zg+ zHKfeTNl8(ReW%rA_G4}!OFF8TV@f$Z z;*3GK!$IFcQ$@mL&g*zrzC-!9r%xlQ>qW{1C(ch=MPC; z9MfJ|x~gWYLM$rfY&N5&e7?qyD=S&=+(O1q;FJ_PAa?g-Xvh{5@GA`yVsM@c(t;|0 z$01?1b_YdAx_$7hxe#bW-s9;Ht_vS2s-!HELwWHU=vJGH3S=E~mMS5%?((E`X|gF& zmolJ(0TOqs7qA%$O|HnH`3wd$*GlZvSLVQuDmKhq*^(E8g){YpA*j9Mad#!Hc%j22 zLmJAQOwv2 z{4d7^S>=?~4QJzu>83vC5nq5dZ%qXw7&kUi{8=Dp^V;|os_b6l)}=K^qSZ<~%dN-8 z?y<;&mG?F0(4P0p)Gc-6{jfnTLv1z11E~cSf z288GzRk>Jf=H(Uj8cW0m73c|~+ROyxwR6!%q%Ld$^JDmwjDjMy2CcF23mFZ?)ZJY+ z_>7CgkFveVREWL(Uemoz?*^m+Q4#Bz>LYU-frFLY@n8buW-PoavJ~Q3%hUSXeS+0a zSDQFYXjf##DG1hWkmiObdw%smJYo(5I zMTBlvd0%3s``yR3fPmVxYp+lkH?~cStUQCkjA>f4IGIihA0IwXpPK&6c@G7W9z#_q z?C6KX_6qcY3R_X0-m7&=r^DR=Gj*5-Tg(OOUOHH*7`cx=4qVcHBBVf^rW%%C7`P6EPbJ` zN+C&L6h*gar%|zl{Y`GdLiwwEAN;hcLqQ`xwEPSgWfc_C%F3?8-*ae*)wW95v}ADT zfyNtY7w+_P1mVzE1Cz-Mn8NEpo@PU>ub!s#6t8R)cw~T-USS;VAK>P=Y_Ad9uWw-I z5~1|uM=n8CnJ^fW#ZErE&V4kZ--0PYIJ3!E>^6(0t8&s-yNT`GO61|-ZHZec<(DWu zXL;DKfHk$DakQvWPwH#8mZN%_onQDygKd^IP0VyH9GOR#3fVD^PRl7Uks5wd-HbG- za>kMp9RZI%@L^_>x8SL+_cLdZf*Q(TJ(aO(ajHR*UIUri;R<@**P}Tg^eApFdY#ll zV$ihShnLDu3wW+)5I+_k925nEYE_rpSx3rPF^t(m2;s>dIh|4iUZ3|cEU=)DE3zG` zH$J@e3(gfq&-B%h6N!sd!n*#`xybtb4?9;Ly0xcHMXeQa5Xq%w zto_iv-ND%cY?%eG@DO5{UWi*-9;Q}uql~<;*9VnwIwuoSXJCe5Txj<}{Jg8P^L{bb zRy|c;0HoWVG0)`b_Ypv{X1SikOmA&!tswp$K>7m;$*uMPR(?XQnmZqf!0lgb&<@Pn z2HC&_xrBF);abBC=tqd&wA$+jhOc-$_GD@T>FQ|;SeM#Fx;X8dalWFbPOGt%C#X)G z%Z?Y6O);|36;`r@XMe^YohLVwphHo)qNLkzJGGj`!qvFr{Xz2%&j^aVJ#5AXh&?}M zOC;_}XQnn{NnP>6(E#__ZW_8v0x2^@8wiWt~+F*VPi5#>>Qe zm$PZ%CTyY3XLPxeYT2gFefUxz$c5ssG_XOMFw0~eo9oIx_(SW5<7!6pjyA0kxzD)8 z+#E-8dGF`k-Pw3cnH=`!%R6p(%Im-(i4S7e{-JaJw-b{L7n{Y&a1knRJh|0`%R>Cn z2Ix6^Tpoqu$g|d^lSMzPi_hmQrsAD-zr0eHpWeq*f{a)B_pdoE?1ec`5a(#xs4Ih1 zbz2GiIh5W>S|mI^{GipJ%);w*@2Ht^emc&p>uG(zNOwGTZ}?}{`Hiq%@#6KL^0MQ# z2BjUsgQ)$9VsKu*DIYy0I0^b%zr3?DLk-!zG-(bX&>b3X=2tsuuT4%^n07 zk2be7vKZBvv4&;?qVa$#0Nix2$(1DawC%2VT$PYB%o28Wc+p!cQrBTa)S8Gls~7X6 zq-cKH$*`aMgxE4@tpMq((FJh0uX`i+o5>TcH7PVZ`(RqF0eoN8e8Sv=Tj?v~E50yb z=i&TQ-942uqP)Gg$tzgjcCk?gkPglAWn(O7KJ4`KMJX;E%hTu)qyHqOG%5datwx0j zN_^fv%qm_n5`LZov}timlCb$i{mC=ZSX~pg=?0#Mn+jCdN8d%1=7<&y8@PF;Z;%xNod=-C+2oI18s7mttEoP)5TD@A}H{ENtVMwrqUGD4o@`97p4n{U`h z327l8M1<)Pb6-y08BLzX$B)Y*+q>%;!S5A=J*O3{XOrh-Eh0t5wiV?PP5S#o!`#?@ zCMq-1Rk}GG*iA`3@BghBGC|WcmvYv2bV5!AJgGwN*fSHt{jR(!cE9fA?-6V|Qf}88 zaWkX(AKS@)NcP{H8};!|AI1hoyx6m_qe0(M2g|AV=gcvHe7jFUHow$ZFRq(kC^0H{ zCaBi>@CMqK_4P69IHSXdXExn3FUOsN@p?AO6Dt>5wwXDL+8JWrWPtFxbR^g|#_?Gl zE0@uyNO97jfg{SMP(2rGBNV_7U6A}b7;M8GQh$*}$U@O9duNOc{LVzahsRH?U5>0B zW`@6xGq@WSJ3sGXeO%XC50&&KBe>wMSNeiLk}&su_Md9i8A-8a)qIn|TvJ^Fxe_#G zcY?w5w~&eR+cIEW_FHcX1>-#!Oh~BNm&8-DzpHgaEX~b?)@6#)ya3j(kgi{u-`Zw9 zGwHcpGiTCJ)7U?_CEnEa3@py+GuePyH>8(u1pZ`#l`OWfFT>7cGxPM!+ppiSNmlfI zuZ%h2Xh3^w#uW8Y$2Ye1)tFhYxxrugt&L#!J>}B!ocXu}*6VuBtqzH|dlZ)Aat-^_ z1gESWhOqliTwFV7Z*2#&Euz&HstEa1V^bE*!8I5N2Ja+}^Bgt^Tg?~(Y<|ZM%|CgP zF;~3X=LKCH9_+a&%Gu>{KXd(UTfi6G=S7(GN-h*BX}lU@!%`x#81tX*$O=%h!C%~A zoPWM(q_obf0FG}df1e^iwXg?9+BwwXQ4{_ym34-k44wjC8*KB73DOd#1$^CGzPegI556~3AO>(W8WPO|*N=NQD2sxEV{IebZ zaZ5(t2mU35Wv+FRu9hx{rL^_p(*t>`HLYU5m@!W4 zU17`JpMkvbKJD20FPGrUD(pDF3P4Q%T-L@CY*t0Lb>T!P-7w1c>`{pUU&d^PQcH0o z#6bnaa*~9Ai_0l2rd!n*0<6gG~yttpmn4iCNDAOqKunn=G2hDHi%~Z{)Z(%UUa)`aiF@4O>IOt@Qi}Iz= zB3ZZ9V!g-@4b)#)SDk%OmIuJK+b+-dB5)9-fq7IJv$a;~{WxTeD}SiRne_pJYp|kB z{=D}k%$5{c$p^lU$37y?mJa$>d@C(a&-Ciw%BS5DM&(er)+aYI{e5Zzj9{F!wWkgI-4o?2bYyB z^bHq2sAqu4Nj}DJB%ZfwK4o9`{tUMX<-)z*ImGF50!f}i&vFk+W@eON6ne7C3b7d( zY;XLvGb1BH>Nm;mGVPmfeV@FU{`}Q5P3Z~y`hq4qULzBBQzGU(vGZhn^sJet z_;tIdAGDm|7P}mZB*k=115b4qf@HeBlPCfwCqt)gX*Jl7bK%A%IlwrHx0)6*tny8j zA(Vx8LN+RUfbc!9yi{6!(tICfM*XbblALp2lt*C#n^(*U^HRBU!+X!6uCz+q=M~`h zoy1^sWh8|r9Vv#q6#&A$S0(~+*JH^+Gj(z`7+dq36Z1DaZfU*x%I1Mi?lhwT+vEUA zrOw($yPlMg=JIBSebuGr8zJXaeH1UA&;2T=OP%s!Gt?`T^7ld)i<(E3b6mf~bfkl( z$_vQ|=3xwr%*+?|6&>On=Mis-@OAxU7AlK{rPdP#JwZ(45tpUYKe8A8n{pXB@W-df zGO_DGdXxg_(-$DyMntW#0Vax&Xy+eKU}0C%Bc7h>S?WzwSQD*E}CZIt!$MnUqZ zoZjVM7S>Y|8yMflp-X2$kGtzxeH%4R8mry%pvEce3;1BbGYuv-gdvZTF8#%~ZXF&j z(1mLyjVmi3_f{S@;maeFl%`awOqV~2s@X)>#n8iHr4VyCr~^Oe@*%Z&2gz~*usOt3 zMgvJh7t3x9!5=X3?EIlF1lSZ=b#WCgD6}dW_gOYmfIgmqDfzC&j;n=iZ2i)LbE+N; z!gfxAW%{(X>F1pLuv7!{>}Xnc1OYb;#o;$GR#?X@^3+Nmf9}*((BG`+mY2%LM$W3i9>5 z{<1uA?)?Lb9qp8FDoTxHnNQ6_SOZt40;3#cpzr9dVn|l`_;g=!2RE@7 zOp{k~Y9=+fe#U%kc!cv!t?<4%?5yvwNOh0l>vV*PN9p$x7;n3hdynX`Tts6`?}oyJ z`W=2HUxUZ@+4L)GQ&=)AlJ1u31J7S{y2g%6ZZe`JTB#yGFNpF(v-?>fX0+g$K&jjB z)#9F=2{)bXC|4oR-@AXoPm1O;OFTDNVl5g>(Ss2{n|xJ@o1rS|@uM~6wu^tyEUMW( z!-@6D+aO0mW$Lz0FR%h}(;FPSm=PPcPz8+tWYNxK>KvptZY zSL#z(Ie!7KXMHFqhm5|;XZxF0akoqQY`ymrFN7t?;#GP~V--;6BFc=n#T>|F^RwpR z{8z-kEBD{)NH{M_B%UY>pJk{$O0HOapj`%74p={XiTb1_6u0mpPrX09+4r&4zf=K#4J_^j=;I z=FTf zx{8X*0h+|dVX!$CsDzsFgn{|9Hw=4B&T%j@UrugzRq}g!uLFWJt?;x~y-G(SfN(c` z`_@ZETdhU=b4tG-JdN+Z{lN9rFM|UWxk%BWOgbCshjIEZc;8$?m=LFS?$H^qX|Ht1 zW+%GtcPTDhYGEZeu&nVjFDOzt&AEkuP1@jpXrKtR8M##%jop9y1957avWW+u`8PBH zE>;ya;>jWNDY-;N(TIwexCUnzo(n_xGGI~25-a-9NMyZEyd$-8;9p*V9kIxI_l>k# zsvmwNsRM@;RpgwZCzt4?PMXj1yC*Q8?vCm53=~5!T(#!TihYqNIl5ivVs>G{yc&=c z$_KmU40N?|3@duB^)=u>6;XNauV$1F|2T28OuC1)_48+pgpDrM%P70HXT>}ydQr`L zFG?3ID3L5;wEfog!OK<;`tHv|Yn1$UZjoQY;WFJpH>x1Jfv*o;uH2bqBxLpn+o~Y1 zg5!Ny2;ZrnPMCHt1P}4MOa zm+z(_f|!$b5G>w8`SSs-Sbs2^z6X~lMC)fZ1h+XPvBJZJgDO`t9~d&?eEFF`B;UJM z>m01(_SpnQ~>s1dz*Koxc z#!5Uij2$=SzIq%IIf<9F8JN!SIZ5))_5Zw-R$_6c6d*0ODx^+j&*Z;F#B))^mGSGS zJd?Lj{@(g1=)UJy_E~qJ!MwZ0m1ON-!8yxwDQ0KuAlglD*>aHM-m` zM&GOc_ex6q>+UrD`281ov2V($)~YS1(;Iaq8{dN%Ufwe(owNJ^aVqBM>8kquRu8o4m?bOq(& zPq*+qX3>npcy})8eH(Oo*v)TA|3~_yJ*A{DkskRj^Ed#bLD`KG=1_HU*d( z+)#CtDs7-~voJt&ar8kQs0egCI;NvMHRW$I`mJ=Sf9y^I$>kDoYY%$HkD--Ep1Jrp zyxPKiKcb&leM-gIDnu!rX=lqi9sn7nNhSn(Q(wHW@F2R9ZzU87vSIgHW+2>lx*3dY zKb)0uaM^8AM0*%aDpvfsP7r@A2MLPC)_;%ItTfMnE`G8^ykC$-bZ*yeY3N6=h!d%s zKn;NZ^-O6B(_+`wBpmYbKhUr?Ljb2iNyLCd`>rf(HxASe0S@0Tw`?f%Y0Rr$k7}S%{H1h za^K|6?aj#*xEP1oRFJKz3Nkp9qw~6ZlW)6c?k+(_g7a=OM2~`^>8=~(rtgv&7$>I= z6tR^`8=G(@9Sitrgfm}lkSnr9nqE4&6O64#QhM(_%-`VfD)XYUvQRJoWf%4+Jl=;A zxhXrse(LOyn?l3RwsZB{IuP+|PI2fPXN>gC{{}$*eT_w0m;hn8A(hAHlz+-9-`(k9 zrb5| zybq{!ewi{){*o^DYVxJ}`slfp9Hh1L+dj0Gd6-KJBz-GSjn2wiP?Q%8L=K@j{i4*H zRBI}jB}v|eXv6+Y=gQAO#;{9wdAV=}xLyU%)bAKHXW|u* zPJh4c)6Lt6pFi72_t|a?{NEka1gdTT!I2LFy5~G+vm;ff`+UBU1O$2i` z31O>P07}@8vBV8kmAL!>hU60d;unRV?=lknxb33LjFYCR>*^D5YUa(Dv$CHHmlY!u z%^7IYXuVstiB|kaPuj{tEIcXYktDakg=Rkud@9_fO*)#FNhT($5fXUwywSql1CiS% zCa%Bx5JE8&mos9X>|%#DhE0yC0eV`d%Y2%O)jlEcW!IsHRLIAN^Lh#m1#oOVg2oQP zMdc8C%Q;kpTN$|@DPC+qN9ttfPR#(>w)E$G=aJ`(3aM-lAeiIHl&K{K7v0ZZ6gFQ) zi3ujdik42#c}B7g1&Zhz9oTrCywH6gWU&RhB1b3e+^3|WOtpn!F$n1FI&x)IZ%hb! zlsN7~;Xhb#ve1c#Aw@A*;;tq2q$)r7lQK|2W)ltA=rrQoJt#Qz3}-#EMmNdCe%{F za7ISsd4d`p*9*rPd1a)OOF_jo!3`e{#H(0}sosqxCad@UveF;h(x9#0?9GbtJ^%-= z$WeF6^>5St^+of2@$bT3ddS&6zXwkH^~u}#b=VoB+CX*d|8=R5ZyI7P?yoXznDj3n zEu10aexA{}+{2m0HpXcYbC5V^$#2%{wI5sOiB=oqxo7z9;~uVqz3lJyJ}t|0Z)xEj zKjPVO+Lkivh%3}7Rc=!a+hlIj{XsQF=j@%VthJVZMrm6e=*CcaJMr#!%!!56l?D(=f_LsF zYNku;z_yiDP1KynGXq=CA6cKee0tF9G=gblmVLK$ggDRHs#5gaw|?xdBWX=I(uZD@ z#)g7g8#Q0MesrL4n_DBLdC^XSKsTaIoB^}ZE^*w_r|`M29vOW1?m~+P?QK8%2fd+`Rb25m-F=-BOSK5k)+`gzqN+YsNMqSpc z2>~^=FPyWxLkqf~66)^p1k}{CUnX5RP?gHr{4Rd2|6rs4|KGLq?HEvUHc4_>>xs@Q z7Bx($jt)R>P6mTAuP-nUdpE9ju%uAyK)j2y9<^;%H1_PkI^@tQAqd~76$%2eMl7&#Ol*#|>NR^AnVt&gS9T%Ega0)M^1T>FaA7ao9E0S{ zM??3i2qmV(VE*qECOI2*QyzDi2E5kS{V3v3CEgH!iH@Bgkr`t~y;$GDGJSSS6K5G` z^g8F)0#dWqb}{Q8Jo&l6D{m3sY|U?W&=&=~HX|c@YhzyHPIHL)R^LRplKM52iE6C8 zlHZG6ACgOu8{}J_sQ;z8-{r#%JL!J&!+oIB3mQ@WAY6`I5{`mogR!3I@*vJCz^iB2 zxY!4`Gn~-}`#Lw$sf;YFK0=d9;#-6M!6Yp4FAWws)Br1*`VxioDw=vKVBj{stk{wDMv^HX=Ob#x9Ll19P>8r zY??S|Etc5UER8=;kyX%zT}kwtEVgFHWxg7DKQG6lxji?n6ugmjv6XV;yo`G0pxw;} zE;F7S;Al;;N>!1c(ZCAvhiHk3<-B~+$L4%r4R*|mS<$x?Ss^frN&(Snz5bcRM63=EoIDIAj8`XAy*;2 zPwYi_4+ww0ggMVe{B+5Y==gR627|r_jIXM04nZSa2l!Y^%Z#BFF8+N7lj`RR=rinm zoR;6uU%xeaTM*^@xtQ5gFrjWZgbJxh3+hU_UbjSn`v$wvA`6cnq$s_!%7m zzb4fvV_o<`$7U};0ta-Xb@q8r_e^oyNRuO44`=)c%cV5lT20Gc7W=fZ3 zi)r1UK(7=H?fS?qsOI(Q_r>a|H0pyqU>2X>Vl=R?<+IpYlk!LrY{L+j9_-=5%R$$c z$+`JRhN4SPSvjZK-Hc-!yxbXr1mo*AuWyJGPeihEiVARJf^){SvS5AoOPOz1Vgmf_ zWt{niocBQyDJ(P#^0JZO)<8CCW|>=enu5TrDo91Hsw3y`+C9fgsEBWe^ML7*%ajrt zrlrfSjaYF3OlmAqF(HV)>jJu#Oxp=}!6(;0`Qg>EeDvi%R#)2c zuO|NPdf`X_j*=f7GQ@}jF>|6rIi3s~(tN5{SZd#N286DDjuh9D5rgY}dn{(I2_y{w z2!0t3U+dX%R`EXLqZ}@6-`if{Lp!~y%7L`AgHw-*+_-08iqmU-b)i5^u#AF2OyI;~ zC zDk#=kH}?D~<`v!F0@@rxTH$Hg+VF$Th)Eej9rE^Dq7Z#gVZ9`;yen5 z{*{?_pg2s%DcUWTIM{Q<8yo~`oprH`Vr!dNANf`D^@((7-pluUXG)No?^X;t(;HpT!mDS@ zm~oK|mbWyIq27-a@vGv-G|}vR_y-q&zm6P0I3`9ymsGDGD%6wmW^p^${swe<;=N%DeQV?VOs0mY`K+=OFR>_}zt`vUAI%cd|uQ_ZAr4%)_ca zCM7$3`OxoZ;i_zIPb}KjF%mAdsH(BbEuQd(DGA6e6mSoEeFHX(p5rMT=gXII<89p0 zDwq)uf7xI?BFut^%U8a+DHxh3#_W&2O&4EPCF>;OGIi1{0!fOZ4)DTI5%yr}NlZtv zDRMHRvt`Bo!TGNq1`O8aOy1Uj__G!JF?dxuF^$|hQKUkOek4m=WOb+Pe8F`nu4lWx z+zyx8_KxQrO$yzytIOKGmGI5+Q3ThG!T$(=TK)n+x38_Wagm>&cEud8gBr}?edBx= zMqd`0<@j=FTjy+s%K=Enfc|nYsT)CvGL} zlDb?_SNCW z<3?R%z@dFw12XFcs)L5CekwKC`Z0{6Iu+7Dcoe^WM=DdhKY_bHE|jA%D-8YeT5>oxhHi%Lc*eb-y+637;gQB05HPdBV^mK?Oy=;OE1f?? z)9pONh8cYbj-_{W9(7mJdKt5@#O+SdSkhs5^5B8-8f4O54NX*Gz{t*4;&+_TR2Y7$ zxL|AS|Il&&Z*TvXqtmd!eFTRhcF!!X54wmm;}1KZB@q|6!P(y%Zb~Faxh#E2*2#S+ zRboxku%4p-(r!%%T-sfYC7>S|ndstioQ9WEwn~K7=uj`^#x8xL8kVx-o+=w^kDWO4 z$I3df4T7d|!Kf^oB3NrAp1l(VDFFK+!bk~vKY8o1E)wdYS$B+;mE+2Om>XONZ+8^I z3Pa6AyE7d9Tnje9I^fh#4N50_t04y8D=>Z}3nP%<*W-8=omYd!AhKHd_g~Vh zbW*RbpJdyyM=P4WXB6`oY#)17!#QFU6@x=E5{A+~)uxw;; zxc6H=9dcNWr9LGpW$;-L4;ohNez!L%~paH9?d#XyZhkGVNNza ziWbq5Au?jn9P`J`eXbGo9bG!)>q-Vek47-Yuh7(CsO1BQ`)CEG(bWegYx@UKFjga{9@6Z|+N(G9Y@JxDFG2 zE1YQnTFweU|t^qR_1#ueGOnTSDLqgK?LZXqT0gTt78*gD8(+OKP zFgSQ2xnkz?1EX}4b-mZjD}rZzjQ~HT(qinH?0)B)83-lKpbRDUj^JORfGou;sWrqD zufYpakB+Wd0Ak@|?j!!eTnt(oZFM5+XZCyI4K74IP!1G~Yqcl4y_8uM zv>^n4ITX{W!ueb!_R#WeqemPxBTMNC~N+;z7hF1w6tDEu~rb6mB&yg((>jvK4?I@_^oVHWo{QC%Dri(Y^K0T42 z9*hd{-Gq^H?6+%RXz0As_N+ptBx6%EGa4>^yefJ1lQ2a(a_dUKnB3X)m4Xn3sQS~l zl+K+=kYrXn_D`R)=+gLVjPv6a5XVw*1;hhBo?iLBl5Bf%&L%R(CGJXkxZyKaw=`3ENq7`SST}vPhuL4hQxro#C{)5K;!GT9F zLv&>t_-(H1a9OD3YI3$#?ejGrLY`f{!HEA;=_d0r+hkh^cOAqG2w{|FLrl8Zx2xT~ zb>QwTziA#;d3*chV6WuvCqLm$f83f2BYH+U z@*P1%Q1*PU$Vyc_YumUlV9fWzX>0g281AT2Xg4S@c`oB{3!G5c2}AOht1l7KCCi0Z zg0J2#yW;8-szqie@2jCN%qBJnAYeLied@K2fOOFIfZs4Wb$aT(aEI_~Ipcx_ypdA; zto0GkcW{xZ9&<5&2!)WZ=gN88^{t?CS8CP&Cwxkswv>@8d62jl82{>eT`Nr}F>1p` zvoV&6HBk{mK*0TskyERN!~+jnw|5RGD^;Ris8s6QRx(1N)74ac$6kl1yTBjjJ(g3P zo;wic#(?%uGIXe~dFVOL(7QdevX$G$f*5%2OnJSaN(y4n4{YRm zt>C(}g=50B$W#AYVmeJ?yoU{rdUAyozKqSUkJv|1>V~2 zF4R~Vt}Mm;NBZCw@Nd^O2)+ z$t=px?Z|Gro&^*7uu->3kktSSIN@6P;#(IiH~@Uya^HP(b3=5yjm*T_wopy+JeWvxOi$djf5##6TC)$;u{15Ni%UBiUL}2V?3InVb8|Xb8r8EP?QZuUgue+gdAeAxhnP zxmRJMwGUXQPm;U)3CjB;ewEk}lt$GWc#680ZYvLox2Soq&SFj$9oNl^57ED#Hea>6 z%yqFZ{YpsvoK;?OjsKeYJv21#8`addM8M7byV1O6Ba&Y^dhCWl>4_~tUb2=dqV0+N$;UIeLx$<@X|vL_F5ZVAB|?;5RN z_Uelpb(UYrk8w0GnL4agD9o~|$~C+z{b>}>;Z}#2enK6a0(xH0)6&!!etT{DKdC71UuZ5H@xn=V7;z!y=zL2R6`Y???lS>2>EzXUHI zo7-Z8APPP1J%tl`KFs9Pk|41LgDtCP4*nm4n)z_b;(P{lo4mD*WP`>EQm~CB+o$wB z^+u&IpuwF0@pQsQqAp{G^cuRS>z0Sg;I^ZuVlSl&Sb-QurH5Fgn8__-;ADo{)``iTRbYFndu8dA%gG;~?{i3vd1mi0?Fty>GVh<Y-|A+=*bJa8Nm56GU!G9Ys1;9#T@Wt0%x77nE`J{e%dU zoqD8tna|$mSyE@+*Bb3Xkik=A8U*uIyM8olHY32OW9qA4;dPHR78J{Q1pRsln%C(k zyu|`;RV8OV|5b{Gq0mf^`_rAUPTrY!(lcYdKw_E{Aq*KQNgl5hc)~ZmCS}=;uUIB%wvGsr&;`z(WAhO)UjmM@9jKqpW3ouo7Rd{6L`!6oZRlSnxJwiH+d5>d* zLS<<$DECw9H^0j*a+J*Mgi(Nb-C%>YN>XP#-YvZbF!gI_`;_ftF6x4DW3d8ael%R@ zTMf}?v&v1ZtNL2^?imx}#3GoN3%0ful>1FkKgi@4zOI*mfACj=Y)3@Wt4)wQ49{_z z;skix)M~F{xo*OTzx*8t!IwAZsv7UvWeyAw6%daR=J5KKHmvN){MY({71!e;3i7#fWud1CH z{Lxo{>=uskj&W5^q)zaMZ7|g%%51$cqPjnmmbxE)1sm<@CaZ})-YT-I^M1VJvryS$ z%wDf^W|OV>6#$Z*9UTINmPLP>*wE?D=G5DKM4IzqZ=O8I4v8dnKNk)uCJS5Y5S9M6 z(kNFcY}Mx(O_$wa*g84d`ccLs`kZ?>^}i-e`2W3kc}N*?q^*2d!jrf3JM*P29k8s@NV2Nz|R-; z5QVCzg2yjeKgJ=1ZT%)Y9%`_CFKaNMHYmlfJe~YZMO8L3WRgzjA=B^A2}?IcKK!$_=wr(lr=(MkWJ z3LJ(tNpa)!eQoXxz?>t?yH)r7dCnw~WYTso-Yoriakp-rpJD3?9B_Qv-)0fp{H^zQj4Np|?Zj_1OI2yy z(7yj{WgB&aaePcX&m7@od}U=p6ZlC&Skx1rhSkvGZQUIlL+sSrn+94W zC_NwDjd%4BF;1iblqZE4f}vyMhS~={-}krT?JCak>oX`*^s{c#l`)bgg^HJD2;l5t za{b3utraYWbG@2vzS|r10axWtJ>BO`PN-#=*hJy!ZsWt|yZxy-NH=*v*p6e{Eqr!0 zCba@lXL`ekcXVkXEaz$?PDYgf(2|BmKt?F^Z+s*CXm*jWRJt|lUpG^*Z_Ho#`CS1^ zz}c%RUVpNlUgAn#%FHi8a&N~uOedatm19HSrPzJw^rjvuhvdl-fO9EIYSb#fbP8bf z%3C!lOgF;AbQ$0s6_{GdQ6SUTZx!jRx7X|r+{nAb!gZQMT*eh%Smb`Et;7p@AO9F1 z>=nU1a_m-5^M-BC@d1aN>-hK*2)i}o3GNRBKppJP_{oZ$eqL*Um<*Mpv1?%$IM53T z53(zTy$KAN&oMNRrj?hMRR0BkL#(L(%@_Fgd7!}|tf76F=mwo=_*y32Q2QGfEH3M2|NLTRx;40yl9GE?MD*Vv z^oZhYH1YuW+RghU{jjJ!p?lTf0rf!GPS?^CzIWU&jpczhj3jb~ADja7UZ;crn1ZJcNF+8+Gt;T^i1~m|?hU-I|;B+cbrAW}JWLSw!c}YHRP$1Y)GN+%hrBLK4s> z4{!dZ{1wywOG`b=&vL#ml;^h;kVWO_yuj$f9Kvxc5vX|e8Xc7Q-X_tnjx1qPP}Z&? zTfWeO;lXFxkEG?1fu}D^`1DyB0|y2NG%_a-LM%NkPD>(0c6`_{nYMmZ*pBgF#b`@+ zF6lGHheTV|@udMaW=MK;x>Fp2?HuHB87pA(vRIYHrUe4o#IMl8#v+(aDR3~w5r|ce z#*Q~~K0}acWZ=$Vb;KmZ;u8mP-awt`VhC;9 zjxkP?2vW(!*CdQn17x=33{un91e!knw66lGVAzDP2 zc+6`!^|Efpk<52>MUj2Ce>_`b$W8AinTMONHQpf`U+&q|clWm5sry~c-M7>dU)ul6 zUPRZq@_*~x@jOQ((P)mWbG_X~(eQ6{#AzvE1<$f=oM$UZmbBl0z=7+?(*G%6Qr7;X ze6Q)?#i{a0-mo2r)AK`4t6i->-hn-}1#?;-UiNzlgfauP*pnJ{24u-0X9Z5SJ&yPg zfGq>rJFO8&*ilPdx+o4bPR1zi2Tt=>H~2&J=*cEo*r2t$R=gqS2m5b}p#G*xA?EL4 z*plx$qYWV>(XN_s<-> z+Mj85t=HMjnY>c08x9;kX8RGyy6qE5>Dy6E0^XpZJ+$KYu`!Y-%ycqrW45xV>xRWK zIIr6n9YkHv)Ao5a?>zNeX1Mqa)7!W%1ZJdy~I^6t?@o9co}+V8@N5?`Yokd~m@LY=t(>@$A4~aRj!vH1Pn& zX;w`|z}iguVK>4L+9z?LY>Dxf6%odXkLvIvcU*$)TIk0@47VA!Ak^$`E)7>`z%|PE zU`e&UgxXU}1;lB^lqATvTShGTh95sU?3p<}0sJ$=6x%_GuowkHP-*{<{8ueIt$18% zinvWACDhc_6D7}u2W`uTYCuY#x~XxYq&qy#nxr?LrrKjdB~eYl&Dr6g`E{7>oxin0 zyFSrX)Elc(Wt#yNTZyT)Bb6q!N5SHG>eQw0!S7ydFhe9dIGn#r+4SC1h2sk^_2uXq z5P7_Kx%frD+*b(VAo^U>=c4lfs2Rwmu~=KY*E|*&$>+VdXpUO)?PYncF^fCMWd6^4 zAh-#Dmf@Fj-gj@vP#Jxe6cB+C%#AI1J8wROv$OgcD-yj9vI~m@_|}ah>iWACPGGTn zS0%2bh&l^NQfC5pP(?tV96usCHpfNMTlYS>rVb5b{;3)p`~99x7)FP?gah{PM_XMH zzLE&Gcp$u;h&RI?3}CQusVe{*7-CB9b*MHKJ$2Aqrs0WZ5n%F$7B-wR5g?ws>(0Z3 zAaOkKpb_N_(a+RniaMb?8Xs%<4ITCt2g0wUUrlS^?TU=l^F*@86cB!U`u67>6|>u% zyu!pNt)`jzJqEi6s`E4u?noV|_8qKV@f4E(DZ_i#<^n~2?>YbvPDjngS+!i2bEIN? zS`dIZ$O@16-mDy)rM5if+@A4USlwl0KC1?PuXP>x*Hy#RH>>-5dqxvEF@h3$zaKst zd<){_AtE$(=fx)NL+H{P;N((p_xFVGUTSB$HIq#zGdPC96p5xqVW#9mKNd5eA^?qG z?0c(uu@KH+R-NomTfD?@r3lSw-7*YJpi9#f{;{nfJ0f1Zzb}mfBFM}|N1OaTQm0@3 zxpuGvNYDfkZ3L0T0J2H4fJ~@~@~^*iPKLR_BhJZ5^M}04RlhercKBsKxi*WDUD{9| zeDCwktnK!Su;&*MEaKL-X$D~RA%Y(hsQ&6L_S@-DOGA|%yoegQvNu+TOj| zI9>lL)SSIsi~l#!e`VU=H(T8u6YSEyl6b20A>J$_xypn=JFm7dk-eU@z zZYC(V#6os&t#Ffi(>W&+RBtYW0sSn9H!48%HXSxvPmvwk30PJzP1VrCHKh(BW2&oV z2X}r7uYEljntr6!@5{L%MI}V0+DF87NW@ho$n}wkalRNOhqQk-Qv!xAJ^Do}s1vk| z(}50VvFoBXLYe7z6fONqERAI&xVIcdW|K158CRFpE6!NoIr;;dlBd6ymNq_u8tNE{ zr<0P7Pue!}-MG++KZzi{y**f`JsiloYpx&8d~8*Y2VvSr^;XE-e`62tPYEZ^@-&>- zcrnqH)*f6iQng;L?3DdG#eBiq;4^F?TmI7o?`%vJf?#>Feb7+*$JAQ zjg4TntgLGZ#|Ab?g{BT3mN5xCDgSxc*l-yS9DaQ-I`Si)Z&3d zk;=HWFO@+r_VPl?*S;rygRa$v>y*U4{}HD1gR=%~R|D0jR+q#CYLnta6`vN70DtbC zL}-Bk?SUCz>eN&0ylP6u8Wxs7xaIDLSZYRIW1Jyp$8Eoowb5=bb6mX@HY$molOD*_ z{!O73wA#8olfkLo*mEBQDnu<=Rp3FMy>Yx;pH4I5XrDOF^0oQ$el_A5(0=7Gu&ru)h?pVKUsth8)GWb^7;^^np1@s0o~=)r(w%JPi*vxi>u`6Hb|cQ<3k z#M)!f2nNsjaB{yxwd_X``OKw+;9~wg#=(>M>$ZbuerwmVn+*dI&=vPo85YgJlP6=T zLuYRDRmIQ2hoLM>$BTQ?+X1Xu%OyvS%-T=7Fpz zB55guZg=nfXj!hWM18iM3cpwzk6r~1o3qTFw*J!ShB@yI)g$M#ZlzQrSH$mjWj2<1 z`2*NI9Dk43(Oq?RtS#QNq0h>qiHH`;hf*f^AN?pik{iu4By$MRX(FGW`(pi3q;wU+ z%KQ>>P(7MR@HPm?7~p8dy*|^o1e#-KrVC?nXDv}m=(U*9$fVaR!z}A@ofJPM{88sx zc^dPRx;n-VoC^YjWpcYjJqx#OhM*eB~YY(A!NG)S3(v=V;L z8b!}~@g}@diYw28KNyx@^RePL8$S#+8Ke5Z83*!dZf= zKRa)R)m3Z1BQ6D{%=^_YZlgyUg)fp+mS|<~p7RWV))px^fXAh{;QNzuCy@r$zT%6~ zRFMro$GO@EvKKRLB7`+sJj*|5o4pJ}S`PU6J_!^bUR=6QuK-vstS+5PW%$-CTh>$V zPKC09+XBvnj2ac1=24p0XG`Kv-gBQ>r_sH}_HlC7jTo%Vbo$q=bR_x8=jj{&`geV( zESJM42gAVDX6_)z>6x5E=jL5$)EA9G$KKyY_5JosJu5?57ouSAJ*UHR@_=mfca{2k zlm~uWz6hRl9M*(v=5)%8LSWo>KE_ zS-l0c*kY=c@=Y}o|B%c#5(CdUUZA8dG`{k;=FDhDSM z*KC+XJ_7`RBh@uO^im2$RWf^uhX7^#z*C}INN?p}vN3Av%BhY><181RSo!jJhoDjs7%YwtpY zA1UGM?d1z=(J2xA(gfhhD-(STCtFx)znzb9sPm+&2mk)`Vl3f>X}kU_uhAldl%2;* zMD~o2LWG547V$LdDG#`p^BTHsF{V8nJJLPo!>!uZSU~Z&sae+~N_bc4t-k#`3sI71 zDAQ__`|App9=hCI^=awbIHw*5-?RP^O&rMWs2y5|AibNTwjX$cg*BOn-E!{oodD@` zUxSV|MR@~tJvZCP&;9R+cOf4^O=YvH3y$&v-}ji$V);ZW-h-V5y~f zcVi+VIgY`YktWWFxGhdg-a%5D7qSLjyNJjAhb|bJ(qCIS)bshMAnxwp^ofQJ zp+&&9%X{3}Zo-%AU!fw9mYaE0((kX_&J+m0RqlIJ;Xs&fItLbf{MPB_iki25UKx5p zFLN!sXfxNnA-it)lc>{Csan;0Oe6*ZnOfakrdWvJ+wUy59@b|7U!+fwbr%cGUOMdraf>?} zo$$v}C(^^V13$!}d`1YMLK|Hi=`qzx0Vd=sv$$I^q|$fbwzZBXLuu9ys~;#iTj9q9NR<^MJdU$&sz^!q|21 zjU_ebLdqFLUK%UQ?G4}Jr^&P}4I5t^owqK!D3~6taxBI1+w)P-WWF{09thvlgzt>9 z8Ni;@Z!Dt9ge=;Bc;L^Y8l3!%rg7bV1nm?qURi?u+O9=TH_~?QqkygVS)eo5O8x{p znd7Yjzr}+OE@dX?g>%ju!J&`RGds*$HEmt@w+cP~VBI&M&~JR}O{qhj4L+EGKl{FQ zq1u<+v2}-!F+DRSyyoX~b=>^%zO&y@r<)m$gEr|KGMn?r@Jf~>ay8tNpD!duq(e!c zbk4w>pYVy9<8P7^?veG^+>QdF*4NS&Yj^wHra018d+{yG@jnNVohy`&er|j?gkEQQ zTN=r!fuv7Qoj@trEjy3b|8<9DqXBEp(Yy@lMpD|WI~7K8gZ3Ln%nA}f7g2Ue|BO8~ ze#s1wG|dAY@M)r-!L8a0(AgU_Y+$rOmQLkg_Ntxo5 zqjIEOk!3|(>5I=XV(Z>fgu!qMl`DkY<8V^{TlijmAR<%R*T>-?Nx~^VXh%*aVcPV{ zPihY>$CjTksrRpUz2gRjnDzhzFLG1%GV0AGWlq;cLtI3Rm?|z0+I|j>Y4m=?f|ARiq(-l7q&Pj#Y$^hE z2l!ey02N(A_cv%S$LqnsK>Yio$%;Cr*-rju3YLJq(oZ9nX~W<6sSXgdFwdbgEO4Oz z#E{oeqzo1p#iSQSWu8|Qhs_O1N>@=NK5oLYTn z61V$l8I-u!*q6H>&&mqi6?c4dwCsPkc|ctyzA;t@o_|VMKInQqQFmxS;soE-0ekdO zf?!Qa9nR9Sa(<8hK_5cM0e|a-?{F*Gi(-TVO92ONSb7kD>DYk8*28wihKIL|^FNq8 zYUtn*h7~!Hnrzc+*C4x)@BQeUa0@E9h=c+4eoPP1OTHvC50JR)ld|dzOfj5N7yLj1 zaHtn%ep8yVPq6_e;t8^3qA5bPLy65)A%9V!s*Qx(e4qoAvegmY0CC6FdXAlvU3wxx zX-$pyLA4C02}k4kxiFkq9NbAVT|f}*!MCoJ+C&M5`9?8hh0JzoRSCG%^>Ee$FzAeX zXTi+Q13j8;i}V2*2vcg$x$;>|V2J46d$x(bFt_;(*gI*0$Hs71j$vtkBT8#hd__lP z7ika4%$tMArO=AYxq^w5hum^On*|l7W4}^OVEz?ocbnKj;$g8|8%CUQ>mASfW=;Ce zz}w?%BmD#v?rsJr+F0@cc7LTbAy)jAs+*-&BH;9Lq&BRt;(oK&m#eGRW{CdgLO08r zkoSJMuEMU=K@@ql<#aD9b5*xq^)h}bKk)KH^Ac_E?ptFi=|T!^nJqMC;+*Im|61U}M`(P7y($AOwqDZeqFA8rV3V1Ku*;9~1|o z&a9W6$*+WNc6RQd`THmMVICSNDCsfbKUCR>=Xq#WhEnVs?Orp*IUQVuW~7=MPwI!Y zz|u7u{R^Co_pzpTOaRV}6LwEJxF&#S^c3&JdOV#lp`d|f6CBRm(K_v)WQb&qd~e(D zqbig+oq*sG#2!6jnjHxmasiAcGnVQPDm~YYNUs0Iz47DoL(U#1DoQ@a(D%3mlb_X6 z7|YUPokqh!cKu7LJ5hgHszE@cBPmdeBht=+RXERE5DU0%Mb(H$V1)fv9xt!7~{-Vx%+@BxM z$@#sVCvF$+!}*uE7w_eJ1Iln9LwP!IsNzekc`;CBX*z4IKN^|5Qy7TVm|!Ebz#5z@aN|oA0@mY+IhyY7Xu9gprn(h6_e&fbXW7>6lxB z2W}&$#L_n>AGP{?gWF6-Sm5bNp>ReU>Zi8U(qxO(%$eUSnNRWGFe4%znLV4IP4m^cQ)aq4n>BQt6@Op%S#e{r_e+gRmn1DNUAy7 zX`{bVh~|44spLj(61BlwaRU{J%J{q%u4&SHVBs$4G$@?>v$DK%}U|CO=KOQwl7I69{ zt9@HX+yD8_Nv6lXEe0@}f)&w_|lgKL*~akDQZf-fXo6K3qL>kYa}Xj;S=@n)JWvtq90}9+;B(F|uVhxf(g6 z6R%HZ;j(FR&^%!F`%EWo+4W!09St-R&EciW;?{!=S@;<$@Iw2w^H}&s_tM0GXws8v zY|k#ng{|HOoc6Y)sZY+JHH)Dh_)h-6NlC9=!;yv^q)enWLS#-5vO~XR)R`iXo z>~PB!NJ~Cp$(PCHip&Sued$c5k9w;%xr9EPr2AUJDN~3>oa|t9qcM&iy7$7>=m<&o zc(;KQ?adNHM@vjctKSi>YzR%m);xZyk_^@7N_ocSyg8bGjaMmy_0(>~!L4zM`+-lz z4pB3%xdS*ueW!Fq(AW^kMf|Jp9Z_-qQiF;wmd!{nMDSzWZRkwk+#dWgK{OM&NWd|@ zr;iQqUqfC$%KFw`c(*fmF_LvtyCFZ3vgLGpjGs1)diM8q#-J{2++|t(T}RH137&Ic zXxj|ucTBJNU2HBIxKf(!teD_fg~jm@4vQfWJKh@EE;)D6jQYm@lW^Y4?>x;Esm^l$ zVZd?@69~#p@VYeRE#{sRb?P~nx%yOK7jThocj)OLcQJCmqH7>4^~Y`Kc=0TuT4hRy zyuZeOO(+xONk?t!d+ED?uQ^JF@20zhH|)THjYrx*UeGE`1F+GsT+zG8SIAkYwf@(YSuq3@k<1qb2isMG>!`x0e?@;#bfm~EUBF(# z;$$@8aHECUbiUv^SmrDHF2{z-8Qn0i8VOIPfvD4x*^4zk$$$zYRd0m5a;U5+?!YmW zT8`73MLT5X{E=MQ)Y6H( zRD2Wb38D3jQF)@K!}{Rjk*2w@r~3KAaH)~jJNF`jiLv~42u_THMwS9ewV1E zkzUfnhrXPeYdak(csss^F>N9jkbBWs(8n3x6Z)Q+b3bQttfXy~K>EI+{ManD&Hw~H zxoZoc&O9qp7V)3h5gPMiqjr7rbHR&;776_&*1+^nXyk(yy40B$Af{d1$eDS#7+m!P z*UN;yV+i9R9M3&1WwcN!zW8oFy|a!^aLN(fZDgEX77{&lUW(agrp4Qnhqh{~w{S9s z!p37d0dW=5MPCkddQxGH-qasqv62Q{kmQj8sphlHaR;8z;qx0Jx^ofgepES zlfePcM_?0&5Lx8o(%^x4$Ow2&hzZS%v8ozzjp|vSLP%@gcaoR^%GC3J;`tLyYq&et zn!H>3y?x??q=!*sO|HpLuQ?krbYizqeh)1P9Kas(Br0 z4C$hsgWflcyCYsq1lgoHltPsmvUvvsuiMF2Hl(fZV5zN-Qe~C~}uh4xg zulsEQqhU1V=9-uZGHt|CuCnIvq9-9R#PhD#%6Cg)LY&|G=*vM5t;qSkbKtQpqnOB4 z^$_}I)pX0lXdiBGZA}?<5=Ycczc-9isPCt5y@bwtO{Fi+zU0_rT|+sxkn;N}yT0tG zrcq6g;R`&MJPdLjhF+&+cDrVs{;^nGzv6BUdCO(!d7E{%Z=^$ps&*Qtt^UU_1xFRS zbwNe+ZK-V>%+xSr2fm0|4k0W3jC1oE3E7KP1CUw3KQ#r+HPE9+53237c^Y*i*EgF+ z!+(=ZhG@=ys42Pmoi^Z3IG>UWet-n=>P%w7Bf@Lwr~0C?frgLNNMjdX#nbgLZH5RM z6nD|f>ozvAt`hauBX_Wp^J2mSXpbK@`V_6={t1gzMQ;n6cA-DE`sIAC?r#FI4=nni zjl%gjWWqN2>Z?u6L3VDf^epvfeA6p9&`C$#q#&{kw8@vQFEe-~_}$#2UnKVZ>!Vi0 zH)m<4*;;VVCLOZF{q;;S#${ZYrMQxzW7~khB zOaJo5eKAD8ah?;q;M&^&F;C>r>ai+H6s2T_7C`sh$dX6{{>ugt`qJ60bsCB#a=_W%s2Qz4|h6@+iMD!b<&l@T)64vf%#z zyzO42`GC{c+{}OYsWp)w&%Fg}!`TK$tSL-%Li9rvNWe{+{e3GC2YdiF&jKF2Uo05o zCl@L|n(6>+Q}w=}2=k-DE6-B=uQC)*Fm|vz&XGi#CUzWPo^_r}u2qMmV?HiJaJN5_%?-eS}6nvlLUkEBn)g@N!zpwkIz1Gc2tMt9QsoQyo(4DNWCl*Ja@$~(@F~EONTGDqFBtrW}Md2ZVG6fYyI7=ig z67vTuR|sp!k7eG+oJt2s3aSTDvJ^P0sF7;~ws`DQ#y70AZ08N6WZL`Y1g(fB!p*{hn$D`)~+s909NKoP_vE1IN)&0rHX8bkE6i{y}I~Cx7 z>r+QLMMoc-Hz>1y&B-2(Wo~$&?W3F;PagUo zhz92FJZe3Qn>%f(xX*9h*KfbsH}z>@_HW1C$xJ`pg&Sf|Pd6Xn`b?e&I!!+>)6Z|t zJT@MY(La?QFI@JMMwy~ydVjF&`6PtjQpjOo;{5kV%mfPu+N)+VzP5ob9ttx{TF9Rf zbw)4G=xfy@CV5d`l$A2-f?GgDT1&Hb56g{%Z~c>*p~`bQW_3;bd$~Zv%2IUN8?RP* zV;vB=9WpHj5FYN(_Z`LpK3A{a$p^#;%eLlS9v0BB(}TX++Pvc$?{F(?=!;036d(qb z_fHPcj;fY5)^W*JA%sio9Zc1B2*JkgJN>V{h4kQjrsVr6=Bd_3lEEtFmHDdO)37S% z4AB;VZ$*oVXyDq_%7tn0(CHWhHGI}E;HJMGzTKUi5P0)vrd1qNVHP@=|Y#7jdy%JdN=~wIPoEyO6R~x)jYI<<%b4u=a|Kb~R zhyvcaks7G2>HEjLX?XhTr1gjwc;;4VI}jK!J!ip-gLgYq7QDmVhVng~W$}}r-fF$- zuiw@+goC`uFp_=I;YZ1!k_4Hz0M2yA`~n);tLJKr_%MZuL7GId&DD{8xi#Jwre^sIGU9>zz|W z$7IMvb9Eg7x+veB0q@x?=wvg(8}Awnt4h*u^o6IE4PG7RlJdrh3X8rAwkztUBL=?` zw&l$?S?!qmmEdwM7XV7Gw?%HVmz?jyuIp1w7mE;e<{ooxNk$LzF7s$pzlWDQ)(BOa zNf1#-^2SZs&5GBkQ(jxT56Rx_IN$Lj)~_z&NWd(4Bv?VJJT&ru5Eag)!xF ziD;|5$DH@&*v{0N$z<(R$(l2NKru~9%GAyG_mR1z;H>jnr%5A|+Y;!AT`j13Za3ki z%H3A=+R93D;Z72DpDi<2$lFs@0@N3 zF9t07D@*Nm&3x?;&=In)>xCcR;h&T&-nKkp{v!JLe}673M38_2&b(pRd=MLRf<|X@ zUR^)t9h_4|Qs>)bKQ*mhIOcKTcD&?!2;)I+^HfUx=@MozlWq~dj{hSQ6cGH!_FZ8{ zaQM659fr~@rHrO$`**jxH4m->TYZI5i?gopDm{=H{;JctMVWikf@bC0v(&+*=_5ZP z?e3gC!1{gL@e^0|>*Y9H-Z8YQaa;u%nI7N?5hVDysfY!j4#nct5JsOuMEAjq2 z!RcIwHK+(9vOz51b!0GS5=%Exhl&s-rG)Q}$NX`FoqI2DJ@dvrmOV6K@h4((_+-89 z&S095B`s^3GGMiLD$Hx#BDF!WMPEiAMtyO=aOT^%`MpNcqiHK%^0YZC@HQ<=dtu2M zd%D-y?UT&O`aWPIL9tR_Cc*px{ET?w%gf2!z=Oz1qVkT`<0?=(p^?_!;@vs%)oS2P zVXNCQuN~f+sQ+?BBhBb6`glIukOpVLk9dulX;h8GT0SM1v?Ok%rkuc8?morc81yLp z?@cIY#)(S+7HzMUy{<-$>v=_-p|Vmd8b9Zih#e*6wMyZ_-s&(%Q4c(C0DJ+@=)YtZ zo1L&$j%G^EA6U{4F!eZl!Ng|1m72U{uDl8V(NV`_z5Zq;nv#=?7WhjIb`FwO9r7?T z*^sGFGXAJX1il7bKpAbkquvkxaF?&3Gl0I?r*AWOSxTU%<_{)bc1Zxzwt|o#obKPx zMi+jl{!ro_f6uFv%WUiX^w;?7yeYSmo)39bDn)P_E)seoZuR`ky2k`bmQ{JBWy3P{ zewZiUNB1dt_w;2(0%R-BL3?E2)C%S6=Rei|1Y}ZO>QJfV_=`60Wos(Y~ta{@Cw;dsRny(e2r+ zMuT7)Ua`go?x{jhR7bbp4;}mJ;%~G-(M(4dC&8g(N(A5OMk)w+GRoZBB9%Y4QtS*Ki*EOEBt}i z>OOOe4*D%F7(b8-I6<14bqw4bRHg0y-9Km3byac1%dk=0MEh;6; zXPe+MDjD3?)SW1)ZrNw^UE%T#H+K_wl*W$*%&5%Ih+a+NQmWD%cA?0q0c7)o==CwC z5^6>?!&G>+PL}uWVR3QH_mj1TH=9xwXUDE~UUBjmsq7oQwLDRd$FsS*N=s8VloT@?dILsMl%HnmY)+Tbdn98uP zmR!ezQCgvNWXIT=L9wKpb66BB0z&CC$E~Y9VbBsJ)dzFD$ytJB6Oopl!WrC_v>CC) zTD+^=cVvuHMV5NB2~6^k)xiw*9s9&9Z_NP!?2$|p<=}BFfoVJ1Dy$JD2&BU zleD9X48QrLJBS)7fUt6%rnn)~jGElSyLirO7Re7dT$qF1wk5LDjNKiWhNHWpEQjpg zzsQBeTvy|^!l$A%^VqDPM&h*{-K2Nf?xflF%|ut4CoRx%!#d(rpS##jny??MWSK5Q zcM)`>?c|wxTQE20*c(Z7)U`l02wKoB<{jZ#>5y>?=wc5Q@N9Ld z6`;z4g4%>pljoHxS>#czK+}>XZaV*%gLfaGTPMb>hc{XYoK3TDzzWsNdjbs~jPmPw zE0b>P&&=>_`qG9Aw$_~2g00u$J}Qm};`hnBkF1pi{gg+zO}s@+uIkRO+itW31*{de z4toU-)bmZ>Z`%;+9ev0W)*D{(oZMJxCQuf~z9gu@f>@}*n$*=Mv*n|%Ysnus;X}Wh zJm6|$TXlY+Tr@t};V!&_=$y@E%~_Zpp=D-DWQoOw7R>FQUKHad_mw^3nNr>hPdF8= z>PB3(s5wxhtSK10C~02wzgmC}9I3m+3tvY(j(a1+4bP~Pr{BkL5?W#mzd7Vnee!*j zp{V?hv7jX{MB&e#x(*K7{vHzpvJ6Nsb0l z90Jxe)ia@d)X0g}cSw(!nh9dd$R)Py2(Z;B*vLz`5v?k>R)#peP zv?`jTng!0T6x*=)v5#wgMz~gs7O?yg@hz3p2lQQVF2j0cZL@YmX=4bo<&F4Ao(r?e zOiz5rZS*f4cvtD%62U~aZ; z4G+)YgBH?KCLL6~@U*$6#@e87{2SeIt}0RqTYmFsk3Nz!V!rolFkQE1?CM&aFxruJ z(UQ2rD-$?vV^*=*n7R(E=jsrgwcbqUtEz8kil(cr*-)@vw6BXFKPv78?H34Dw zU6m~BolZ}!*BYn>Z5-%B{}{1}u0Q$5h~VumfR-IjRu8H5eqSAyQD;B&AuVI?=WEk`59Zz^IuwM z5cdroe0mq?!^6m%zQ)Hjklp!MIa67XB)>s7rVvYukH49SgoWZbUk74)3S$*OF0%Gxo+D#py4 zyqro4G%E0Kj-x9Myv;k`+w_AhB(oVc+%0Ea^#C!}qi=F_Y;9hRZ4%X;?}A<}f;K;i z+6xc*u2g%YcDXab18fQrh-2CWtVvceB4}*pMU^C^{YiRb4ZCW2P1yH=!N(SXa@dIP zpq6TPR~t6R44O^wicw}fh^C+eE2<1!YOnfioeV@6RL*u1PSrk`C$GJr)wm4OG8jL( zWmauAey;d?)6lBR(Oa2UwpRBXL`ix5!D|`0cT~h}@ZRM$MuM`Y{fp$B4Rk_j;Y*2d zy765ILGC=Uz9b@U96TqnRsm8$L~q^$r0)Q2Kb*as0*-#UQT?KhIEh%>=+`KJGg$Uc z4s~`ULRFVxCZ=Xw0;&WHgd;3>@RjR7feAQQ)M^E$pZiSuM9ednbOX2M2W8QVxZsRx z#xsPc3wxl%r8E$OjXi_aESj;&oJQ&?&)ry~8*!%b(yse+8>*`-` z@ELmwt>W}v%CdgbZ4LT@N?+>?+e(+r_jB0VByja*SiEa`u`$0jwNiDkpQE*LydRn034`ksN}?^$VTu&}T}y85p)Zo%e%PFZ_K z*)aB~E@uz%j++htfNb=f94;lyXFy0WgPu5^Dy6YJnXOxlp`bf0WA=Og;JN7AZ5u=R zD$BF>%Sl>W!ddQ~e3ega^lshbC6FxYal{k36J^*&Nl=Gk<*R9{3zGCSjEW!A4RLsH zJRjS^{-LB_Mf^?42F6@%<$8D{E({ge<>xiA5keU|QC^9p&dU3t4%`X58&}*rah_#M zlmN>~f;Gj$KP;F>g`2m0G1s;AJ!Bb$IPGM9>loS#%=?zXA4&3npG#)5xZshvNo_*w zoDiXTl}O~#=BSY8aKz-siet+QyKM0jpX6Hi~stfa5Y1ZhKQ~?rD)$JcCtup@&RWpBHZ6P9q zE6`<=9?e{TesaG2h8nBkN&)IY4rMI~rodb<*a#b9rSUKs(`#dG8!lv4sLWL5fog>d zIB_DfAu!r17{`nb=rgFn&ZF}=mcA)@&wQ)$nn}9sHM4%%dpbhPDj!+D+2P7;v{ZGA zBdoE5%u!dERR@MZ(Z#Gopf@Z(^Q`&#tyvWFyVeTfjr34S?@rp6qKg5WJLl=BH8tAs zJL}c4m8oDdy6U)7pcQjtn7&2=vMh=Cc%iLW;kb>*Hwzk(ypt>*KEQcW>k2}!>r>{5 z^i{#yAMTYu2U@MZ)VD&3&Xj)TKWN+IUfcBd;Z&Ze)@7C_N7K^CU_r&>Sjt{;S5qsP z8NYoR3IoeR#mYc}0G1$gP%mj+btpx)@arPpJZ^>7{gN`W-6VnJU6PcCKS-Arz zD;=N@P6owRYF7EtVnL0xoX%y1-2_6wqf|&4YzoQ44SW2_JqmW z>NQIUP*3-h;3H8#nvdm`W*JlR#B3|ssNwdOD?`QmY8i?2musAadhZ}g#p0UTy%oPy zPNg;?Aw>eQMW~#=6Smt6F#rVqbA5&L%`e`bUB2swC7okY*m&^rYHDqOFxeDmDfVzLI*= zA_*UCV@)igBT~C&vqn1(lUU)hW?z`{%5KCl-I6-u%ExBw{`_DR^)Yd(b|$Ar0#x3R zLbv6ITsM2q53wcA#9%VgFk?j+P&w=uCH}mNM;yQO9oz-|R(bO?ERY;Uom9y>gvRT- z6KQ}Cy~<(3d)_o(_JwKmk*MrW?bo?O5v62Rtb18<`iA!at}Spa=-?9frKGAAZulO& z4H<&J}2O{yetMGoUUa*`<2ppvo3sCEA-HKsN_YZ|Ujc+3Gv#v6SpBM9uS`2$-k5 zcnc0=ro<^(>OMVd<>zR&*GF_0!ApAHf1{0l!k~OKcYl*Bsi{X8A@-zfBXzB(v3#h2 zDslG^54WZDLceQ=ea(8@wf&99|AWi@2b}u>fOCl+T=r*jn90$;*`tpkjQ)}-P@B%^ z;nPNtJWCB+IkCiCdP>_MGxs`UV{^QaVkN^tc{l=F3!=gwpoHr1do9^vn2l)konTb- z?F%QBxTDSRZV2ZE>=^vpjD+%%W`Wcp9%E}tkI5nS-1l@5=&>gek2Ubf+^3v`3=pX= zKKG&!(ow4OgE{D{v>KG#D)MI0h9tayY#A>wCrDeVJmROdBQIZwMtvQ*^Zc^yX`|c@ zXy$@8e{@!!sb9}`klv>hRd!z6%*gCOOyISrGXagFZsa*?0sZXo!>s&}1qUJMH)I@v zECkwAo(WYeom0hay!iMb7gyj{o88bzQ|=}L$MB3q2hQ0u^Utb0XepxheVYJydnss> zNuMEBbOh5ATxvAcHZ5JL#w8V08*g$=Kr?8gziRkAy%{aegWDT8VL-lV&Hkxn&;x)# z?8(5Nnrq>cDl6K16^E9_EnO%)n@t6Hbdqi>x|}{)`S29Uljr5ULtJ^m`rt6V|LKTZ zqixV14dw1_MX4N_khHN^h4sAAVc`yTwb#7mDgKb`jJ$!x{RD0WT2XqyaM$gA-5Z_^ zW0$M#1Xt%UtmZK)U4Wf8FWMcy)1?&VH$1$qMrPZlnw#|?$DB1rkY$NmAMI6hyaQ)# z`+}yXLY5E!u+OtO;pzaykyOQv7qsoTkdPb#p$#pE*D9s~5z|G>U9)A@%vU(5^UBWR znzH$g@59S*aTK^4-(|AaJRJC{706H!y%?)&2g(Mstpiq4r>IqPs_83BXZ}bg=r);D zONJkI9Q9Zr>6Wt7ELvJ_>NxMf-@9F7&wH)r2x?+FM2$Y}w=4Rns2g!E=ni`dOk9SiLV9euNuN*0Ad7HW#C69H*DgylL&NyumZ%kwe({kPtnh zd$HQpmnO3rk`h=y7R^Nw^x!XR~@yoIH_W$ww`3~Xop0$tVl`8 zEe8X!A(=f!=Yd6DQ)5rg$3bF+zKA^+HLF1Gz;QOMFoy88Yh?ysKsWi9KZZ;t_&1i& zl&S2M*$(>frZfWGrN_8Y{;goM7?(4xwYh4|Y*0g6w|y*CE!nKUd+YUz+o%>00!>FajiUN4fm5o()^ctJ_IV=@N ztNmW@w=O1QRnqb(ps{v4yM&t$Nd*)lM7Ha?zTtZ1EJSfzCzb@2fG0Ngp~%GahJ zq&F(;?l_yZhgBi@O_fu6h3npta6IHX{1)$d<3pMlE)!=x|KzJRnnN|58!I`g{Z>;$yH1H_4IC+BWJ6G{SB#Cda#WvLdxZ28;@P89x zzZ%}{KVg#UBYx#~8y&YIy^;%>+sbe65r%cHr(NKIH^w!P9#NmYhA!Pf-dBeR{Ahad zc6DCbjX{<*>i9_Xk&`r?75)yAvEZS#&UA@ca{5+;pbLdscF%o&XO z@%zP1=?KP!YExkh{sBoqy|Pg5%VT@%GcvrvC@=86G?bLZ)C@^mSL^@-I5?`EY8Yk* zL+Xng*C#MTi7i8~0%D_*1JVoO6ol769)=3@1vk;$q1R`_ehct>*43V zRd*)@oHr*eR@M}(U+RAEx;-VwbA@-bSLL%REQPzx3Mw{ttwG&$wM2isZkC~;gBS@_Lu*m! zd5U*6%Tw!;`24lsQ#HR{-MIry%gzjC`JE~oMh5L4Z+cIq?U^7dSiVYddd24&mt%C_ z(OmosDg9DW?jaAkxL&H&J;~Z+bcI)u*iXql_u~WZr&;wOzQa_R#^>mxR0u~G@l(EV zp2}LiZzA~vQDLiwJFp_}iv%34Mx~p5y?wD!6O7DL><)N_5T@FRKygPL1M{=$iwt#N z#b90)#W$FMUxdMqAE0nu_f2v30*8Wd*w~)a6JI=^>6N^r-0k@!DSog7d3fT*Rk4|BYtCYf}>aI86uSB&&INo^3VQ?a=!gNe0jb#sG!+KL+&p6(7dBOcoywXYQXa~`>K(9W zkJGMgW?y*Lomwzl`2%;Fr#7YprDMFsNQOR+$M|Lf4EF_6T)fw%*`Z|3e<) z*NlY19Kb}40bB>Sei0(shNV3GmH|QZWwYlZh&~Q*-XNVgtSP=}kVEKnT4(nfyiaU? zq2iL8=aq2Fk^A4w>;>%}XstCF7I+WAW+Y%P=iOu}cwf)iMN$1h{pQ9Ayq|7hU{FBg zFBb1A;5L1iyTP25d>B7%$j;$MzK}hwHZ0`>$w!KEf-2ASu9~;=i&oY+5)9|XENs8!5h9UPe~At0v zUf;qv&3UhPv~A?4Ff-2*=pw0X_X1jr_fQ2Ppm*PIr6T{$-~>k<0{} zSCTh&30BsoZ&ZOz1mos)<8(Bs>`S-qo+hX{6omwUKHVoCTqSEuQReCW5S9sM?k2cyeb!>882B&x2%bre z{H;vmzg9^-3^2X>{I4l*hGW%!{a-nx+8_dcI$*NIhelS@Qm@2< zm!vmZSIHG^O-r{B^a2jqKwOMr)DhvU71eEDOuk{XaDtKUyzXyXZdJLJYP|j#9xGz{ zT}uZC051SAbKWI+WZSUqtL-;IIJzd)8C)0O|z`^YTmaFo@h5aYHsj* zs)A4Z55C2(aD;pmFv{$=5n@U&&%%HBAQS>BKnFizl)4RZZ_kAXa0cq7xW?hKgzzpM z;`9X!sjTI>q^MTOoSx2csZXFiR^1NuEg^=CkJOn&^=97*zD` z0tQ^h6WaOSVL045-1D zuUz`JIPyg!@`VPH>bwKYjkv-0wZSUA=Q>QrbL0Ntdj3U&=B`c)Pv7A?=>eO z_*jL&6;B90PBxA&wA1*;(M6A@sVA++Q@exv=ct}$K?~wPSr55z`;y!nPOQZ@3uh_l zo8nDns|w$th5WRwE2k&&?{o3|jq+*Ru4U3|v8$OmDvNoQcSf(PxNQZt2qMqxAs5M5 zj#}w9oJ7%c3OF5-4|mOjLac-;?cxfPqY3SvEp?Q;#mc+Bq!wSSd>r%QS)qZ8@j*)( z-vB`&_!Lm%u)53zFR8T{!I&fR=&SwMn=pZ>QrQ%T2eMFiWg~YfQ+Fw-t*mKDr|~fj zLMNr~ka!J5XOcS_Us6MyIyL+7l9^HRMl|y&v5o@Uml+J7LN=O*JEsve0tIlLNJmr_ z0XUixLuOb3%8aPBDECd}FD4MUt$WISsN6+ip@{v+-gd@lhRFXc&fNO8!D4_VycxF7G^^XMg0&A$HXi@pR7W}OlC@MKSJ}6U%nO=A2BVam z7|-`t;moR<9Q%a_t~?hUts^#Piv8j}7=S?J<@BgDUdykCWoG%!lImj&Tf)sdN^=_S z<=>PuH7~+vT_~(Cl@bMiU^Aio6AS1evmQznYz|dxsb)Sbt!WTGAOFDQQ0O8oZN?Fs z_YR9Nxy&`+iioIik^K`O2MA_;eyp( z?O-*D({lAilOL%00k{M}Fb0t}D3}q))Gq_|E74OVidY8 zRf51dtIyNGvT)|b%wRgq2KV-UvuW01m&P^Bxy7S$l12P++AXm3P?)antMK#_)=|@@ zx=dh`sCI0RJD{&CSBamxDLI4{8}5qxr*KuI&iqJRqmH4l#*}H3 zF67|CyPEahEeIz6$_QfqV$t7%g1sUa6aY-&y@g9ejKnp~w!q~sPmI@0Fq;SIc+p4z z`kn2`87RjC1K(l3K10fdzEr9DiDwV}0BlWvh8L07{2-j&h#x2ItQWNs{Ol42`A}wa z5RNW<{n(tZJUh=RK!O_q*jDJvbNS-P)o>6!4RpqqNb-Rt>35e#(EM~;U?zHH&!Cbh zVH=fT#8lY*%(RrP{uXz+R?AjOEAe z_?y5TQomEe1#l$w?bV2};&x`gw12S>R7DppgrSXI7qu3X_F$5Yd>tW)t6DyLd}(;30Uu<}Mc!N((1_YWSOQ%nMNt9)6CSweQ)eG+)pqo9tm zIbk_ooFK7~-zf)~&4FQWVp8{0-o%1(j>w%A4HW~+;SR{5wiD>aWmEBF8MM`wm{LLA zlIa52i@2MR*C}qyuPMEk(A;&~`4fcHI_#TDV9)aHwWRTUP5GOQxd{L;cC^^$&A?K~ zLDjKXS4!)M6l1t56F4062<5w@sgBm`hMH!*!FXZqc`h37HK3}|v;Nkha_PtALz#)P zZl2e0+~`OW52(^EYJz0_TP;ANi+fM*P0UFiIkgjxjuH@*gpn*z{-TTQPJj!W(b@d$ z@X?Qei%6{vR)&Psfib7KVHp~M$MWX(V6YU=hHemeXyXxKb$QZ3tT1M)=*}N&hW-P? zAI}BUpZe7+!*_<)o}HzC8{ z1Y`Om7`d3wJI!dYItc@n6{ih2Pt+BFH}2TbO|cHuSreaQ`H7meMhXY*g7z_XOzcOj zTZ=?0zL;{|3|B}5aS%25V{-D4i2j=mvS>EBa&887WjG;1>>tD%dRW9wFr<0RQf3XL z;y-)0WnNbneF0ixb1`ACl+nDj^Nafx&&du;s@1!J*b?+N+(Qb=p3|wKU%loG`DE3k z9+S%9A;@iq$BZ-4Mae?_6&^u;MR#7|et0x?(2W$9ExVhoym2p?fh)Vs%1I2J7lK;F z=?WF=eTyNsz4|1#xKtmmBi7gM2BXq@JF+Kk-T>=2a3Mzcv2uNew)bw~VJ3JHFK)+B zLZ_UI63FCkPU>svtPje7`?-2cR3_q>csSM(`{iBs+n9^ac`qD>oxP`VG=JZ{HOWb$ ze+_l_COK^fyZz5uS5wcXtSBH9+>0=KvU+>tKV%n(AN-E?yhZB04!c(KCZ2fmbL^$I zl5>P-GqKhDBy;gRt>M;c&1+gcePv|(`Eugr%6lF@Kk);(dq8Q=BeWHm$-R zY-TLyc1g#U9dnpZxe5laEq3lZFPP9-*_oLxzQC(RvUoW2g-4=^7L$-~mg5xX(=U}d zYdsAUxCs(xMPD`(aRp zTaZdH%P}v-+K+|sLed6#vy&e<)*8y89`_|bk`61~Kv^r9NjA4(Cm+N$n9FG}UOS>$ zSf`ef*@q)DSWB=+R6u=7_kK|#G8`q-o~s2Di0pO?Mu94&^<_&hAp6rtMNC!nA<GV9T9XZ?i@r;2jcn^b1TaPE63nJnNZf#9c4|n7%ty2`|btf(N;t*BKi}dkYr5@5p`np z%6MuzR4v*@%nb5|e$X_m^dHFjNpeL$6`Fjxm1hRPt+hzd^S(Bc!Rdp2!MK3uL|zBT z5n2Y(D2k}Sk93sfL7WxkEANw6qZ*nzR%#X?AI!*C8OL+T2d0+D1Y%1)>U=uAp@J5x08Jr z{51SDb_z>nI`A zPi-8uo;IMO^t55rPKeYU1N;^?6d#^0;zc1uQrkSmzxH!$z<}uTL2*|7LbjzcPLw{3 zDX%X)5;^hMJSG8qzXdU-%~a@c7|O+lsM2>Ms!^ za4Vn$cr3Q|!uGx_#$AMu5v|Q9KrN4Wb`N9V{B4Ak`n-Y z$4S}XcsUUAfrPQUB6U<_YIDNaQpN}wT+P?Kbi|kU{H^Qtx{=au$;*<8_nofyV>9|z zm}rj;BF(vJo^wy=KXg40Fo-ct$r|%jv-y!8-&k6?m9p6zY!C%rx~p&ww%=#^nevKz zJ*2{p`ws6FGO%9)yDa4nm@^*>7`yhAok~6ilv{e$M0*XOGVMt1T zC-K(;bZ{R3t0Ih8(Ot6xiRTt=X_IAGbD{|3boj9+dQrN4P+^GksqV*RgSdWI@{;%( z=z+xFIYs@9`bbmU$gb8NKdcaXBEu6lzMnC8Ij8pqSX0m7+68HA>M#+B>YaR#gH(Y$ z6E!;(%DHHZcMTYBW#t<+%GPAeD^LNCFAty#zg_~kcz51)oJY$}GfOEPW*1hYX1&|5 z%8r7~w zb49Izx-IbM547|?3TIk$ANty367@ZjJa= z*aa0IeTEnwQh0W36|8y9#HPL5n3&h#ql=%;{~#w2$xF?&lQS+k(!69FCE4+0-VsH{ zE{IcVl;*mav_L(_qV}E`M0Pdz--p=K4OsC+Kff1k?-5=svE?_Hg|ntTZh8+g-`VlV z^ZvA5gipaBnC{F^SUA|f1!vXoyt~-e%5|jguvFhkvmwmzK})lSux6#~#`A8B{7;M| z++w-~?DPfc*0Q~@%T>TpU~_}loK}b(be|@qg+lV-vsN=9P`+wKHG^w2%?2S(?m;@m zs`)yvf5wOwVQ`MrMNpFfu5Rn?+*Z#X6vf>0M{f9-CKrSLIr% z=RFX(vQY7E)%35S@*(w+ViuIa0|BWnt4%anr7J zzxi7tZl3^BY2Cge;99+&1PJ_#CE2&Pr|W7?x3oM($|;}Es|qFh_olcs<87*rl7n-quFzN3KG(D| zVpI0u2WZ=MnzEZGLzKhUcLeh(CcW?*K!BE4)={r>^Ar%Bu@gk|WE1z&G@t6zdaGP^ zHzC6p^EynjkF~VT)SPssg00Dp3N`1FPXB!nFM$~U;raaYcPNBtm8FI~gz4B(a=&6B)-Tlr9X;638Vq{V`@X)J8)iRpsovZzsOaO?f{GOU2Vduv9uUFb zOYAtHoT>|Cz9J2jtS!lceN|{9Zg=A&FV2TIoTk%ZItOSb-(b~gN)A#7Oh3IOQ?njc z@yZBf)DgR#h}Cb*%mn8YVO$=!Ayrt<)q?t;&GA{(ozfy;h9!T#b${@j?#B-^#xihD z9mLd38SW{9$86ZIq9KA}`dV7zHi)8Q&r6%gT+obiSt;GHxvcm`-h&?CJ)Pa}+Fyrz znf532>{p2oZUF->TE(P&m*Ai_o*>yuz6}(hf`c4&mck^EQq*H*Se@AFleuOCYc3bR z&+uZ$D_dJtvjv$+I0b=^b;Rmoh!UM)E`(ZDh`!(k-aVD;=4@68I_x=JVQ<)H+GlrJ2J_Mthu7{%u}t=CofKP%pe5Z|a<@Sdm!-2G&+^4w6ePY5rqSIarIH zcgsWdzZ;CYA^^8miSIkxVV_(PkRz=$X4Ds-f)Cxv3vO-XvNQ^O3Zk!C+@4yCUbE4mmyqmLF53e)KYLl+yuEYyX@YpWy?Q?ro3Hc2HXK{^uY5J~%CD zAjR|DtJBr95So4dS;Dl9&G?eOmY5_0Wc%aZ=t1hva8bCup(%@RH(H94fX z==@x5gz5|9&jSY&Bikz!gigwQS!>VsZSPn8`q0yD`W00A9Yu9%{D;Tthp)r-n%INOlRSf&^f?Gs!l9zRB^C+zqraGkhM!LLnEL%vtwF)HZh zQ}#*&UvD(V^*YQavKR%|NbRpeE6LX>UZ?F>k40)+=7y=PY>t{cy^%6y>U_iEcC~EH zU8hxd65&_-RU5p@VO$}DKkK0p54+4`A)NN_>5HNk7VXv5ah9^ngrio#PUp#V-$j4x zVAoS+@cuA`{S>EAYMF*2iTXL zG||GS1VFJB+9h&Z>(>~}yS9=y zXrrvH^p=ob@D=1a$asAJ9-kEdVY95eHf1^D`o}cWFfE3+Mw@v6gc#}d1}SkX;kBRM z7gwy}5!Br#TK8=Tx7Z%f{!T_EYlu$lK?*eDo=5+oX#F<~Q@#vvHYZY2k1kGSH=gla z?M;{&08i!#=_6TpOrmaL8;mn2jPn&1zx#rISzBbpd*MEO&A7q7+H7T3Dqs#AuZYsU zL&?*ef+4xv=R{tHy=3GQtLx%VKOe87sCryAXZGztC54je=NYwpJM0fnP1s=RR};-j zDi8Ijo(*cn!A(~DK;_w7OYL$lD<)v1Dfg>~`IQ3V_TQZP@c~)KY}|!L{9@-Vr})e5 zQHYn~MWYHvN!qs#A7kgOiSIROQjHj;CWQ#V_EO5b2G5B;o6v(_1 zYg88J+SrCtjNHV#)IQ{)9eH@TlyJ_ag>??bbyAwJej`M;g{#H zR={E!uUJ5B5+xoYEH~7-u$`ZCbBe+-4QEk(iK_3E;%m|hlzoy9f?Ifc@cu;}$S zk^M~_xhHdo5f7^%X{QxY%@&viJ6W`YQRI>y5koa8O_y!OnIhyI4#Qzg$HV4!ltRJx zsH#zbaF7S;&5&ZvbA9AxsBcGpdcn1#bI^3Ddql0nPvSven@ZXq4pW`hDQhH9S|M0G zKC~(iSWD8H(ywBh@YtFUJjq+zY~$47b*iA9m{JZMPxN$PlA(dXsBau?8NIA)f8wN(NC0s)-i1V`^f^D6ithVgc5$Vwfz_qwiiiWmDAr6FTamnu;j3sGbats>_` zFtB#ao&v(CEc@$t8>x;sQ2EZZIC1x4C+0PE{4{aay;`~4vyDW2o!uLs2`W58zz~g> zAov_?bSVD`gYQw&Gn+rI4XS#lx|DCx8-Yz;KK{SB9!J0XxAqm8@Fn@B=${OD(=f__ z!Jd_t_jHfm)irWx(Tp6*hxoEeP)c`HESFj;6F@sM7FBVK)0q(x7F8crT?GxF;2HJ! z2|16;Pm)yOm5TA}1I zcH;`_yi?LL?fBVe{}89^ZbJ-tE%tbcLU;PK;#O%G{VTZ2W9H{Hvb9mM(A8?mT`v@` zfrq9So{)TjM-oMcLvGe)*WtdWWpEOBzcll8>VT7&7Bgw;m*Ocma9&ay0+Z%5atv@ng($;;ozpeAUn3C+ca}4iYSXFRVXg0T1SI>uT zogu$><_b;M4XquEd?2H`w?s~_rdwLQtK)$y(AR4HKqUFNzWArosFtd|<&WD!s7_z= z`A{r06p_eDAhZOloCQkY$C2ru1~*)t_lXae8B(%vbIebn*`(eosS6}q+`WSFYgo(g zP0Tw{C0uYND6oF0Ot6r&qsBQ6Dd%`EKRYHSVul%k^h9;@Ppem2^T@HLSR3ASmdc92 z;FBLNf7d6!1bYG)FCj8K`9)4$uaehvzB53%o`ZUOx)wh~V4}=hZepP~B49PKJ!S&B zw_ZyWiyETrk-VAMFA0!3=(uIw3G@3-nl;S^&A~WGR6MX!2fB+|{EoaAdbkvnhM&Yb z4IZ()g_XT^tO^W8#^u!HAn{m{Lg?P`Sn-Diz3xXWs~$y!yn}JUKhDba!~M7F<*_bBRKI?*;)4hhr3|HeKQR{ivP|F^1$h%GFQ#dOEk8|Lk;AzWU&^9X z?(k+=Q1T!#eWS+G$t_j;%mL_8IrE2!RZ?j_REpp6axZ9~i%4~4v16BKhQ!tJ* z%NP%R&_?ADc>#7K7E-Q5aF3X#dDQP{XdEgvl@lKi${{4HOV!3=jfq0}HAtd5GX{&h zgQxhfdOFHqeMiOz__+m^xeR+3u-b%`ADm4P)B4RX`CU)yb8LHMcaDUu9up!lJ(4S& zbM#Xc^(?hW$bm%D&RBlk82-pK&}1T4BIv1Iq;s$P-L|K8kUYn`%sgLL>O%tvY6*EO zkF&IQ)YL+92lt(l4XJa)c3JLwC7W2FdZ&03Yw;`*>=(zh^b9b=uTeIfB4NH*s05cp z4flN&)QOQCLONy17D7+C*@z^Bc-|`&tv%C{wbVB}P_Sphi$1rpGM%!)b8uj=i>k^x~@16&j9ZH}dNOT5~{Az$|?I4O@(Zmj? z8f(L%dIASTA_c{3SVY>vI}G;WMT5ba?ihnf4-w_uu!)tK;9iwJwV){nA0L^&2k?*T zc5kQYFI@O292ATPc*heN=5;$M9n5sb_tG z!MhpB!eJcg9xWukle49<1QR{sQRrz{#pgv&t58uaR4?~iFA#e1cWyYOpbW6X$ECaT zuWsLIo_d~5zs%2bMs74JZz8G^KNp>cdGR??!uVMEr1)Z{f*ZF-F9+N)>D>MsG|{?( z&k(F1#WYYZMxv-%C%=@x0!e9i#lDrwi;+TJOriP9;%>A&R_sx5iw%FM_!|L#SNwHu zzyuv!S$Ajsy>0uc3+h1EcHF&m>>nlYP+?m*+>8WlYhM02}H+&B4=*=LplQZ z-oGFJciR67Km32saDxyPk>6?o{^k5XO6z}B!2kCo_;=d>o%TNn{=W?OuX+6gJ8a`V z|2h``x>SF4IsbJg|4%pChY#Ulq&ToEN#(DT_M1&V4*2o!6)ti8AMGL>X3`x004Z!^ zD0kEWe0gWcz2pB!JAh)M{voRQwdCsfOCUgaBywW+KY%gY?})gK^Z&Ot>u*o&7FQop z6w!YA4{42GLunv;v-|g$`cGHEI{+l);NDo9KmGR)U*Iq!{Ne6T1N(Qlf0~Yesrx_P z{+F5mBs2eq>~03J=G3~&m;|16#V;EB^*?Dy-R3y)3R8OZ@&R&%x1FN6XX=f86W7uB zbD9^f>roUm_P=+CES+_C*!PoL%kg+YhxvsNZC%+*;;|L(tqr?o-w2;}7C#g9^vN<$ zhj@neb=!w_)QXjX%CJ3tp3cpeX3Z@G5&|wf^jN;{+a*27JQR~gLd;}<*9#a zeN;yg`peh<^P0NYj$o9S_TXRp_G_#92$JU?8{Wk{sv{oGbotwmwqFx*BkuXfhIVb> zgz~+{SN_}KKacHDdEd@|Y;X`4adY04ck&-5aMxa7$KOBy%x{B~J#wgAKBW7=Kep~7 zu>NUg|9?TQcgb~^Qx=|2M_RAntvblm_z#Z>JbARG_UYQ|UeSGrL@rbVsimJs-iiQ| zM>_vwl&{uj?PSoH>unkTZ`?VPYFn#aODQYW)2Lb&YJPKt*Ls1st1E?7>MT8g{SV=T z>vz?xkINWrF>5zz)6rW$qk-q94TAjEzMoaQy0JOGwYiM0T|swgsjYR?H&8GBN~ZF& z0PFB=|H8Y-y347nU#I8SlWQ-l{2Y1QHN4p$g!UT=-jgk9M({Y%FY;er30Cr+yDhvv zvL|Qjhw$dd<;|7apfzCQT*mZL>X!2HUzw&I4)9%)Y1sBFZnbsmyXeDgbdal}q9VqM zRh;ntH;LJQ>PIk-{~g7vSKkVuXN#%~>ehbc{$MB|Sjrh&vS!>en~yxDd83W;{Wq~$*`>-Viq>-tV)y3+t;}x@F6RWT<=pwTSzSuR zEoxd;e7iGexvPO`PTlNKThr>2v|iOw_^l@pu4?w+d`ckGEXWhsW^uTxX*E%HdzNRd zf!d}m{x>^-8~IZ|1cw&{ySxJ@z4hCIHkYmx2}S-_f)1LEyh>gFzN}ljtlK5CwJ0Mj z9TImGX#C^%Ek=6V0$Ix#TJ^+SkQo6UeEq*FtLI5wvXXapSq&5Ky~UojSE~w_v)Hx$ z{pG`l%2^NOxwb!l{zvtsimM+3C{hpTFAL~DL$f#fZ2&=R*Dc2KR7SLZ>gIBe&3_1y z3@ApWixtndr8h|Bt&h4KnHv}q>PQ`F< z&-us~4z-_;)f6^f1uYuX{n{0v0|#|R_yWK2Wv8xxNmb63vy|E%UN`41g6C!MHm9|7 z0CPKhapQCJ`ZKkcp6yk2qkwP$J+$Ha8l+9vbM@<5ZmwN#4%>H!vOVg;R{)EgJ;Qf+ z`x&VpiwU*izv6 z$4e9~6qPt>wVVozwIAd@R>`zwXyHa|`gt+h?NHUr+6{uOYMh>Eo=j zI6RCu{qpTvB^Tc>FK!*{=}emF3pF0=Od9XpWV9d9e{snKO;A5=wBG}2l+e-0iO>Cy zOhz9WISE#vSE5z1B|^HMg&ZH&o9Jjg9vsUyL)T3?Rjy-NdlvpJOm)&&d|o>7xpSKq z`SlHrX0m9{@ITXL26Y`?8Xxj8EacemM>CV1qFa<4T`-lBHD?ogk^Ib(aUnekKhnU# zpG*{;Jo?XJ%Ab%Jn2gn$1n)4<+lLnOb`wbPv?z0Q&dJ|XNEI!_40Di3r| zE_ezhO)}`cT+;Q@i4m|1W33axle)^)0)fz-Cfc_8CroP}ruLVXPt$jPdkj`9Ia+5m zR@Z+mzvSGc6Zz9Y5$}p@Q?c_hWIUXL?y8H<1{ONwSCRdQ3*9s}1a~&Y&Z`2wj~I7K z&9QqHTDHD5cUUN>jc*-w+5Fe1c#({59I`n`! z%fDU16pF8S%P>3N(&V$8$%Dx!{&a)-&Rh?#z<7?-Tjg)Br_2)X;LI!;X3;w)P5qy~ zFF`|eSLBh#R%53^9|<{h7NScw5cBV8iUqS&dKBH$iwm$damkY@rzYc$v{|EPLO;o* zE**Ql^vPc#%AGUiCSz$6Cx*jYzlJN(PDr6=jyshbIp*UJ3RZ&I`1R8;rz`|1`!9Ly zOQLte+3XjKG#>ugUkkSz#yFORl#yZ$I;0C@u|OnOe~LC4imrVmW3nsk+asn2W>EY~ z`x0wq#x@Gr=&wT%|X?+Zpps2gGR( z^gBXO(`EC&md`L=8~nj8dL`^7TQdn^mBq(zoZPP?RRUrp(4q<^4~zLcGJWoJ0oI0W{mQu!y}K4#!+_w5NBx zM+G-X7*-4YqE>6v@;Rcjy}jLY$LNgEPcv$H#)E}!<8Ru0@0RB{0f*Rx3J?CT0%*<+6Ms8dm3?4^F zbLtWnW!cetNI_?i&nRC6*Rzg)U?Cj9BFh8)-M0cp_F`}BiiX2xN^F;Q3>Dd#3rSxuv?#|fhEcw0*;JzNEa6_QIPF5ibr0|d z{4Y=SO1+1XwenNa?ZI*z7TNg23ioG#Ax=4`?Ex3FMq}+EuAvEQ*r`?MR6Ei-F`g0= zx^`@qP*?%|T9Hd=s1y|a8OMR5Liw^gv*W2@Hgg(cP-mbvbJ+i>q;NWFh2=F5f{BY>~mV>xj)_ zIdO~Tc10JVyNFIyC0f)yJ-(9YMAPgONVE)=4r=I`-gTuA=2d=x@4rolR}<*cR8i;k<*cxe%x*n{}q@0P(ndfVi-3egFUM-3ehT0+K^0JMbMa!HC{bTuWW zv_B=$E@b~BDaVmIhkqP1X{adZ7K49>5PT?uk9ux#yS?y$(9 z*G!Do^d1z3Y2PkqSmj92#5hONq#WfK>l$HHF#V-}=%BxqSHMe$YfC?#o+nNV)S@6) zbwCnMcJ9%&Qt;wSYIl0eg;wM3R;jKgBlWd0WuqTH6LmMNq!5x0&2*HYRanXusJb7{ zn+HGX5|f7McB@5yuQRJ`I4fppR|0zL2R-QW7C#SBkF@nnIS3)k_lu24JGKtwT1Zjg2kir0Y||`WOV^rxWdWRt=?Wp}&vB)x_(0S@zXu1V!auYpPxC zTjcK%nWR*qu%jAxZ~StjO-=b38Do8BZ*Xs;AnNBQ0Lep{-*Ba?vGwN8&nbl)ZdA*w zz1vE^lQEo9DDNaICX91fZz;o57T@_W^~e}D9`Cv=wlQ5ezkr<;n?PyLX1VJ6UF0jA z6ln?4VZ6Kwv01(B0o>x+OCXZh_gzh_W}xU- ziU2<}#}Z8OK@i$Au8qTx`0=hD#n`yF~D3936W1LDh*#w%;D_f;`Gcn zCo3P%tXV@?F0!n-xwW^hmNUZctwX-*fE37*Z00Z|6Xnxk2?F(-y(A zas_PBqxZMT>e@cr$?v=>W_y%AKm8)6Wre{sj97mkyX~r&8-szAArCg*n3=|c^GJ>b zTtne#egu3M24cX`f86*E=WqEamA?avk)YYav=paZ+_*S5QRI6V@P}vVXYA2Q>cdGA zRSJol#W=GrP<<%n49{!gCt&xv<)oq!v(ye|ZhzV{xy$JC8s6+EukxJyCo zsB-YiSV@~Xj?t$y>l0ybNSw*4j10Oe;c^0luW;Syg6o?(0t4-3#GIa898qMk7o;xbh|nBq4(Pz3LplE4)-MwJ zm$9WA)Y@zsm8)6n9JPoRTax*O^9o$jEVyxnwhk&LUYV}TFa#DO!Jnz-=-|%32n!TI zkVeb*b3w|z4TSLG7yR1*S#edXGb+xfv=T;F$tyd@MSEv@$nyA$XfS1C`EFs8q+@Qa zDzU05GbbuZkEQ(wmBGh}(=?v+Z-cjZz?Spv?Xp&YoTb1UV^m9QW?<+3O3aB?s}}CL zzhm5hq{RRUug1ypZ6B#p2zi941{<@T)uwxE+DS4j#%+$8YYut3IL(c8AEb2(g$)~K z-yBAgyB#EVMB@moaQW`IHwl-lJ)-sPo)MXLGb;m^!NPc|0V~$mCskNqsj!zTz{ht4p7;=5m-=Sn zD9s|I#iquT%+}1MspSj*e>_hOsYh8WRjdNkz|*;uHKnurN(2%#4ILp8Cn2({4o2E2 zaJmU{uq{BVYonA{blL-PnqF#KTiYzbyVEbFln>T^*xtk>A!l>#ELn^rs4eloHm(c- z{3?TKVjQ7Z!OYG+TBe$#0ww8AdK`ANq(nsF9Y%$qZ=_*Y}Nu-tS9%c`+qx zrQ$kHr4DzltKRr@wB6}LbY<3?2_}JuA)Yv4<8DY#F)ZVo%t=z)Fr)ZtjpIbSyd*0) zy3QAGM)FisQ#;E0ox>zVH`qLzS77Py9J#xhAjaWa`l8TX@)E@EN-44(00K}Rh(vk^ zQo3ePI=R^Yn1_{2V-Sejc!q9;wf}_&&nGq*+SjCeY^S->N30_oBq1Kd48%_QwZ(9^ z`i!-5=x$Ze-5NwKTPyXEETPESO^*qA-$d|4_MDlSo9Hc03wB=x&T*jLZM(?-?144h zvU#`>q-5=N2qelRu&2wFljRB`Mx;5nsbZWlYNJjB%;>b%0f)q>vD8Lr)(^JvTR9^# z^nH`7OJrGD?l5&Xg+S*YcU=t8BM_7&Xu*DQdqk2rA6EP8)wMEcwa6NKZ4#|^<2$|- zhc8&NUHaodpgYH4__?r)6w2RYfWG?7SX-1LUs?%=m$uLa5=F`l(cy*t`Blhw%yNgj zHhc>W|7D?Y-r$(=xsMBtcd}Pxq}k0p!VYWN2}#p1ir$kFUFO5u<}TU><6Rp69`{W~d@!+$tHl40YFzLi^hNtABif^V5+oDD$VT8q9lD@vS#HRTAB zy7o=fleNYk7jl}SKzRafU5&sejnwV^A0AgD=9E1mNn64{3tN}n}tn%Hd1_u;5iV~HmU?R#2^%(z7($2I*wJZC5fPLsc9{NeQX?~CH5Ywiwh zKI31Y`ocb*Z{K$|OU*GiF%`v1lGy+l6|rB$>cJlt0xF|9J807&_N_7m+iScR7O)ee z7p~-N=i=xepYu{n6|E+h6tdiA&ZB5X0_Uk5EoYk$Vn+%IS#o>5Tvf2-@4=Ekxlp6D zObXT{j8myzIl~P5>JM>6QYZQ1Wdh(Lq^aml%m$m9I5kHM{9>t>q6iLq3ej)Go*B6a zVj;p?#;z&<`3{KAeBT^u2eT+xpfuD}XV)ghdA!CzuR`l==E0NquJtvXeE6pmwFDtD z6u7XFpN3jVk^bkxY0BVZ+RrLR8fDv zW0s?!8Vkp|l9*$6qTScpIb6+AF1KAIi09u`;Hn95Qr`kBLlhEa$J?Wj$eXeSezhBv zH-Fc3eJCB#oQn6kuO=p@8r;qblI0Cy8T``N#>xp>W8bl?KSRW<;%rs7Rl53?XRYB435PnB`bc;O=;-&G45AeQF|w&Xd)V7aV&Lz z56Lsi+0bSS6Hg&9#&0>pS99iUeAj@p2}L)Y1}-g;q)h;EUIUQ7Q$oSe6z0PVN8VS& zbnx}DL>r}*%n)-%=ZA!|WVc}Q{jBMb#<=m}r6bl1@P1uagy|N#xw&9C9WL@C;e-Vc zJ@?dCZ<0MJ8yapU(=q ziw7w$)G{nmCUhL*N4wI`zspe)|GA39%iKxXt@S{`AmQy^53&KI2KT(k46G=_rKx4O z?)?RM#v(nQAX@v61jZ3hSH`@bCn8R_`!vpxrFP3MR%uiA$g_#ZS7Xy->Qe3EMHwfb zj8dCfS9cBvs_V-w8Vo%>Xl5o2UZPY+Y7}e14zLLmX zXFIVonC=@b-pC?@qw;%0u2YS#uh02kFNArzoL|x*$Vcmg!%wod8+6@Ub6Zy=?;ODgoS@fxhu7+**V7@aIt5xP)By-me}#e|@&WqyI` zp$3sg{|Y8$&+3Cqkn7(C3blWe-}UNio0&@2My{Agaeq}%a}x=mT_${4i41Zb@=<<-XOfET`w%_Gn&z0}I+}A{7zk9i{7# zX`#hat}}P9&9eAX=LE?=C%R5MBOHJeTys1?tGh%rg74#)8S2laYwP!>=RfjE^1=R> zJb+Z$%{-Z6I(A>B>;#}(!7e{dnA)&2T^iHj z=i{@!%IdMLF?Ozg7}+*IK;wcvDd94~+UaGNZSyoN*RVJC)~#DF@?xVfe?(Zvz2)*! z$;Oy>xX_2B7A;nw1t~H-Bv^>orkimM$+lHU_rjLL^oL7^4k}AQA4A#IDLlGEo3B;r)L3^tnju z-ffj#9MJq|L-_Fgl!i+V@ykPYWD{jsfjEH*r}s| z%9(@r*#xi#Kl3t64oyMEL(vlD5wK>#T$6Hf+RB&*O%S3JKfxl;w2DnZ|6YWqlKP#8 zudF6AjjNXXd}wRZ*8|Y-s8!K`mP97Q&4c%24+3uX{o^gOm+8wcwtXapz%Z5^tWxRy zjJFEW=`FQ5`Zn%Qt@O#?rXu}x*PjIPBxqYkr$9>LINcXP0S~}c%7o0Cf*f%tEcVe` z1<}E?)FnDbOh2rIzIx!@FLlu1khtHM1& z37$b?l_}C9V#^r>&Wky8$HKjy(U+{7r}ZtujJx%heC3lf39-+1Jp6PdfsgAb6fSaY z-|c0S8-Rx*Q%YZekB=-Xw`hiEQ^C4PwHh1t!w>#JjhDTCv3I62FIOe;Cy)-H-kL@F ziOxIWXK=K1xDnxj(`iPQoqJc8ymqAbBA@=gTdFkSs&DL$uH*0Rt5X~dG7!|!zADwR>A(Oo0Fs1R6Vy>vD**oPhzK2q zeVJmHNvZ9ec2-{PV6>X!mM-b4sD`h%>^VKg3Qj{Okpf;Fd*5&ojzIpGLKH5B!9p=- zS396;k{HC@(~r$~vlUX=SCbAm4SayeJdIQe{{y;TM@NV#HBhn!7R9=1pq~xY|03EO z6rGr`F;~Y{Jn)cOY$D+__TK_a7jCQV3QwE>LGm-jcg}@U%{e(_`HU=(>FiP99NpK*uO%kc9>Ne3rkcI2SFZ?6rPo{*MEI*WZRbG-AUWkBrBGF$)GZF65}5mxLK2^ z&(hhs9EBb1r2E*APU6_G(9JJBD6{%%X|Gd!ovHz$qG`KlZClfQeA!=U+INWn+0gq- ziovRVbd&LDam5NU`F=glLpE1gKXbYwR&s+6;GnlHSP;{VZQj9+>#Fu{?AO=JvD5mQ znV9J)-+Cl`#v+3{s7jnQNz)fTBOf}|F2A|L-n`m^wJgKp%gbPp9?IMA_KfW94$+1po0qKKnDo)d1;Y#1#v5!}D*HGnS{L3|jGv%9ba!-DDRSbKSU)}dBi#%XBE!LEI6`2U8i3%i{ z+BIJ9e&%kn%1NIs*e6fnk?aeWzk1GaE16T%MKdGTy)29Y`zuQ4Ir`wd1dN|XkC2_} z*|$w4pR<6H47zqjuPqe97J#$HvUS>*-czMQc{EZi~vY2&BL9?)(B>@dX9!8r+ugr3d@(}1btW^FQAbNxYDC&-bXy!Xg z2e>37?tkqcFre0H4q1ShmClG-NCgJPB;LkAxDU>t58*x!LGk^{|Ne0eVpT%crUUO2 zDziD59J@t$6157PVyQ2{W+GDIf2B|iB`-ShX7eSPG&KY(Lv@AjTdQ7YetYsY1Og+& zaF%1V6O=n+<0g-OA2?;4x@!w^L;JxaN&jC*5BP|V2MQGqIQjZ@&X>UWxek zkI!<{=DE+kuCJyoAXy0(oVQDK)rO*LF96=nyF~oA{d919Oop~Ei13=#L@1?01h|k? z!WsJf+1oAQ2;TSYdcK;yRV3rm;)3cM?ANdN$ejPOh()dr^v$D%Wb#rQCW6K`()ZL< zFWzly426Z)xJ_u*zH zS^u@=#3;2cRWtD{S-59O=$HG?YJme#YLQo{5h8PQbirQ2gwKgc6XZ^ES zI#2zu>|)`tSd-^f-k{AGFQ3_wqD$&juFHyikDyCY7T+M?g+mn@eo^CxdU)UlRm~Bq zVGZ5oc^H)&`1NN1*K0eTVIe|>z+Vg?@bX&#{tsf7NQ;&A`9g_;=G8U56>>lnF?ieX zPz`V#k{VA{!PVR&N>pQFhP0e~NHX{Tg&+dZFM?3XOAWPxN)VILm7lXZeGBh=sxfCT zmrMobr@%0g?A|fJXrWVT9wxr6)FT=Qgmdy^tx$g6DFsYI`Bo z1d6%Gj0EtL-{dSHEDK6Bz+UmpJbb*H431r=zE*S)IvEkVB7^=9t8$xFppvRJyy8d#)UTdqev+}gIP_j0cl79+>Q^IXh@T^|yfW(4Z12~6l<-oMqMzw>8g zLr5jJ-&+->RbHXb(#4wEDQy7^@u&x2ic_8FYd7xsIB#)yQ0Pv6tUi>%8DxmrhXNoH zsj~|Wne{VlL=+7fxt@ZWen zWc54e13~IThpwg`FhQ}s&u`#mk4b_RYJHh-*0cgISJ1Mi?4_YMfXR6_HTdcZd%?Se z?1>V<9sey5=go#RZ}2ug(ojvf*kHC*C5pDFzwM%avHZ_gQ9Xc-a;^WOhd}J@d1scP zl~~b{QV}UjR0lzsm!O0L6>LNesW2ob#%oA?WF|z99FXB18RH?l^kX#X+lc|R19st* zCYal~zZ(34Ila=VV8BSv0b;A9=mSOu1WZD-aF(M(Rwshg8bNJ5>8blrw}2|-k`zL8 z_12f}H;13HSkA&wN za7H9`Vik3n`RQAUGkq#9Mv)^HpI2M#F!N)Sou0CAS3=;}%|l2uAyWCjFWXB*$dcXA zKQlr$-#z{|6{6ar@u6W2i4#Cif_oY?L}#PLYd3!T$iNok~3f zC9jfFBukiGa9~*MtwG3USve^VguWVrs>o%pj2uTsZ?33!{YilXICMiVhki>Mnne_j zovJX5h=_R6R_SYGV6bYAURW5*DpEJsTZUJEe4PFT+i~r>aArhQp>0VAfj8#Z9FO%N zpL@p=)^OCSpFMk)a^uY@nqfYEFztwpGL)JEtaGXypY!!AhNDNU-J&tx-jTwZEdc!s z-0Gn&_rqEt5H<7rHD7F-MRMjL=0{VMTJiQA*L&l54}ibDE&293JI<8z^;ZI-#i-=i z5u)|gtd^<~uno$C_rdk}X60est(6X!8Urs&zac>bumK}cBn7p@h8f6k5;Fl#><_=o zAg-t%XU+ZCGZEJI;_8v~GoFCeU+{(c(82A}@lm_!MdSU$?qHpAnrowcquFV1G*R6o z5crF7j6AUa9vC6whyS;Z+dE_{ib*-beLGk&5uB`z6c#=746Mwcd_fDf zWG^V13B#xXKy%!-Yw?lgBm)NwD(P9kK6PwgI)dAL1t1^Vr{6|m+H4hK&d4l4F~MK< z-Y0`OZa$u)@fV9bIR_%&oU|6WygN{S&w@iKYHb5=IezS#N!$l1@UBaiF6|xcAaUWh znq-2e?MIOu6;PgrnSb6m!j7AJt2V(B2N6ac?Q4zp1>jqJ4&&Tkj`%(b0fcqG!1KDm z^FVa$!N7!Q?sJ6GiNI~lyQ0>>eAw>zc0>yLu3|HJbGv~OIW9u4d_4I^IV}(6fK|!J z`w1!u&{7NV|98BEO2ilEa%$PJq=bYk?C49;*QiCpBj0H~5 zM^f;YD>%BEW0o*s@a=D*1|hVgbc_hn#ir|VQgHF@<5!wy$mK!`-NOrL?N7Jxp=0^6 zmud+zpd)Nmg|-r)y#L6;Ah7tN)}6K~eKAAs#6LD>#H?`}=RLzfp{nT(_n5Q;pgAjv zN`<=MpBTyE`LdJag|lGQ1Vf9F8jCfho0oKred5<_*r@5 zyGH!NLZWr|=hKhvxkb-TD~MXKiU1xij{fBja3TV@@4PH)S@vG}cpAtuPetx|9?DN_9xO)rNixWo6Er}OUbbVIc!=(G#&ED2*pUyL zY-7eiOa2-NT&*UJG3>hNUE{JSDhyqkbci~?nHPGeg2M~GZm|LoG#x} zV}XpIEOylkJ7qVZO3b&DgEoYb1ONNEMI~>76SDB*ri*M= zT+<9F2#Zs0QR9z{F&mV@+2UF2q~TkVmjLZmx-qi@Wp7i#Xm$S{B|ZnO_1-kIIY0vz z5{0Byb@3wv-HyjD6KXH^HqFPdD9s%C2?Kld(Nyx5q6=B zs_QZ-F*A%_#c&`4n}FzQKf0;xb;QK7N}wG`o&c(l+xq6x;J~;uXI`WkK5g& zWQ;X}qeGnUT0!kxXZc0j-H%37tFNgT(=DXih0|YuLdSPfi{i_k?< z*PP*PCGedYHFDrf?nlS(VDZuOoYavLiORaIg2XSm8 zRD;nSLuf{eUvZvDe&`r=c;CQV zLPhXyE?)_#iK!xLBKHso&6Djkq$ZmHE|8#&G=0UsLLVP!$VHMD$-LGy1C`L01lEc= z#)x+zL^!n44%w__$gy&QJx3=cEwI}jM1}DTZ{Ble@>#dpu(Y`a0PK+OUPi5n#6OTn z6d3D;OllO`7-YhS=_-SpM9;Qt5D^I-A?ahjeZBym*54rlPm_KzLDmqIC2$|8Q}*@L*`Q^nL#A@M#j6%`UwB*0IrI^WAwt%#|oLg5X=q*=LJkBKEfVrup1 z(~)fVc!RgJ^NO}SenQVV1MEkuIO($VvDQK!RHNvow!tnGodGvtY4C=_)GJ_#4a&r6S1Ub0Bee40+jO13=PJ%`njc zDpQ-;eES>=A5U>wJz;Mq16vI~jS(lJZDT+Q2Hf4Zfv_C|_V0oMD&sPcn-3JPcx*l8c29Rc-(#Op|bwYr;t{Tt$G2~G+>00 zJ)(TuM6Q%)JxL<@j$($yYHW!)$}|JTKUQeUBS#PnPj0mk{r&F&79{NQ#z*8&Xu@YZ%zcr;y zhNp*PH)e3!E|La+`(ki&Hk)}Rn10ckV}Q}50% zAg7o{PQlcF5P3$glD*4*Qc9RyVZB5%3ZKO|!wS2qEkwX1>LL6g0xr6>cUx467&}Oc z`8V)%C7{ERJs?Lmatutg`T@5P&?SPH&xaRRZ^!CxqqJufyL*%r-`RgfO_Od|QD33QS>DZ`UoeiQZSK3YnJDH|cKMZqHj{huR4M=RZ+Sk9a+PJ^%~ z*Dm-{t~1!gkg;&oeDr+>!kyEzHzXpN1SV!S*OnJ>~ zT%R;n-zk^$^|A9BLjXGJx7dq9fSZRv6NiD8aa--J+MOR{1CM7IWn3zo8P;9p`L-XKN-K&2Igz0d%~BkkZo?wrxU z-U-K%3qgjj|3(vYw3I$%5^sP1(ZG8HM?U?lz1K>Fv*_kLmyQRpnkRw2zl zAiO-;bsL;ij%QW7R1jF{)k&e?gucNA{{YFs)~vnLQv*&N9`<?1=k?MBOfdA|#VXROcHRZ-YPhCUtJy901s--t;B#Kyhr-huWj4J(v> zetvENHFpM?`G}>v7>W&uTVe?fs@Y?}DH=#6qvlOT_&Omw&H!MMy~nj5q6k)G8=_2`4_A<-9xH{u z6bWC7%-v%t|4G`emq^krc?4$4L;?(ny#UnuM}Pn*$@|CC_07;WNU$n!GOOUit8n>$ z+2Q}Dkn2JK-Guq$ti`QyZP$2=oal2oJy4kZ`5~Z&-XW=Hd|m^Cz*2oJWEF-R(gQv- zJNBKUhV!_|Ut&2v(E8*i>+F!&2<6LqL#W|37m?n@QDI<>75(HB^;Gxk{s*6FzQrDp ze$omflXjsFV$G6}mT6aOf~Nb`Pa7PJHMYRVLTePyclS_2Lc%WAh=*_(rEGcpga~V< znHoi~aXUn0lZ-fkOro=S*?^V&sAmshCD=uEZJnjc2?gG5+heqH-Awa)wbF8QefGv5 z>AEZ^6p+aYSDko1zj0Is^PLi)0|nx?)8z(r*)2&?BV5hAkpk5WS7?v&z+$3v@AZzv zLtGxQ9;M1p!O=xHEzL!2!xp&1v|1=S%33Nzz3Y7YEaLk7B~Tz!?o%wJ!f=RI%41qz z6Q6|r?f%7Wkc0754W?n|UT^w;{fHQc=}}7LLfCR1wSgz7oVgV|L-xY6YYocF-)IV; zP5>w?%95i`c~R zV;W40Aebp2_my7c)4J^MuM&j#uvrDal%{zk1d z)U?S4z5Qz%3CeUH<>cl=({*#_pwf;m<5~}V?AaZ~JCrBHG0^*A-BJ-9gJL+JeFMRv zas@p;`97#-kq60db4Yj&0Xdz)o=wg~;MmIpXb61Pu}3g$94`UinACm}^oj zZQ?;$YjD&Y+QxB0>)Ph81TFVh){UA`bKPPTl&3mD<=EK}(g4|4piw}Z***#tj_Mc2 zJzj%>fe%TKn_Vp-g7^q)?)LKBifrU&WDT62Y-f5!VTs5s#7B8wn;kK*U`Nx}N0AnK z6Q4}f7JJ7NoP`OU>{;V61!J;vqTy=+l290p6K^c;%2~(({?o5~QC`c2K+efLr4qS` zM3GspcIbHn(Si1jnq3W;*&eW?@b9+qG8v#gmG&qNX@OiFc%=tcnHA z-No9;YG`eMXBTzwW8s%`#TSZlQTZnP#Sb7Z4w4~Nf!!SC=cqJ2efHUH-cf#F+ zx2^*+SSVdUm1Jy{fgX!d12qlp{CZ(fOS~08wt!Dpt2>1%CffT5Q!ov8 zbil8$SG!|CuqFo%*58fCJG*3U+G2M4%Wg5w`uan2$meKqSlktreAEG~+|Uzh(hpfw zO^Z38oxELE@a+CvFfmmuZGX7=HHfh}*Z&4hf=1epY6ukznVUI`jUUJ#Z?r@OcHawL zN`|_P80RH*c{hqINN5e}$AjTLk`jv}$F}{+1rVn#Z(0%$gtf1z@bN~2e3nKCcp-eX zg_$zgaCBI-3JF?_fk^{m2hRoL9Jc!?0o=MNpqq1E4K!`#;d9jIAXk=?t#g0vV)jgz&h0V ziZqA5K)-)Cor5=jYJg4vyhx#lUlIo4Ip(&zgF3usCWIbh>}4^K+MYuWnw+ z1PN(OouH7Dd01(3_xxCI^i^pKK&*}}$j8IUb-~Fs&vQ?H2?WeTkXg7e9ULdlgqk#u z=hm!Q-}O=Sd9TYi_4xg-0H|0{pli4j&Alvl0cDNX$Tv2Bi&4&?X+mA4 z`__}I6aCfO&NC+%V!dPB(AbWN>fw@63eU1H6k`&$gfF~Liy(cRZ2570Duip$-vC!~ z!?pq8IiNw;5&WWnRO@ioWqwjjjP~NMRDeL$=QQXdh>r?U2Y8<=)4TIzX=_0a_e>6X4 z06x3y&Q$pB^xuz3A&~;(R&h0N%X8eh=IHOt^4`tC8dbI{d zL8T3qy>OM52@FD=G!?kaHf4&yF(x`X`b~dk-)d4w>yzRL-=Lrb>CadE2E{l-p^FSo zcqrVjCL|`Eut@S@2K2dTPmCDBSjE-7RYz8U1p&y_H-1j$VB?Vl_c?(qReSS8Y;|Ie zVU55mAk=&#nBH{MRYmEBbU5G6S{)pksT?Gia!-pBm2b3+s`zp~WjF7N5p)97{E{@j zwZ>O+L)et7H8-?D1embqp&Lb)iN7CpMGQ~JfH7~{p>8K#ULrr2oZVPFgr{;VL)OSMpm402r z{n^UY#RV<1oXQ5{mxR7&2-Aj*ITPJXm{XYLHA1X)8-48++?q1nsIxVb4UP6OxaX(B zC1`p@8{e%WVgmbSInF3qcU=~QeE{^bd0NqOBv#WA_`h1oJ>9?TfR6so{^&cMy9u@T zmZVA02Kjmu%X3sVbDUav!{HOpcxelJ-I<{#_zqjK!C@e3sRiZhB6t3Bq|vjuhmKeI zhO(6Zqi+y2U#hLyQd$SH`n<+1If3p9I}+pzs{5!(C&pUr82ASpoWMw`JY)wfhzTwt z^=#obgqSo5_JC9MfQ$0W{1cJ-jv2vGy(Gp_TND^Hb>OU^Cou0QMiZJ1W;)dbGRpTJ zBwS2$kf0U!*0%3NDa3~{jrudtf+a%3{+qvwz}Rlxy^pB4>9WN@&8y?t&8nm?IyPj` zy^8bt(LUIu(xWkkMkOL%t71iQbhOVFtIL2eWEil zba}4LTSJ*w{7!h?qwt`ec9Vzaa1Ry=uncJ1TPkRIpbi6#Jlp`tknTYQEH`@GK96C{ zR$fgJFjejc(FIJqS=lPb!SFOa7vrQ!D)-K|TJ3`<_95ZsAEM8`D=zK*Iv#Mpq|BD> z-;1LVZl;RDEHq{UZ;^wB_E~_HwpfHfGQVFk4q9nUyP!-=j9FiHDEUb(O=O-z+IgXY z!=Tr9ZoG2t&iqiD14Qf>C-U2r>m2;HM-Ea;4jTe4N{Yj^v_>gP{!iy|8#eI4vBQmH zaD1#|=}EIKv1xuRL)eHWPLm>-L!W;P-8U4UCh@%=+&4&Dad_RGv}=^dN2hTD>?>+% zCIjq23zLD1y@YfhvK{JWj+f?2iNw<3p27civcRMtyp~7!RMc^Ygcs0yQ?l6^91{nx zf(7yZDsZ9uYAgg6zy27LhWSq9zK*Idijk3#<{lly_wDVs>s(4thM3_$_;xoi+{V4s zlla4OxrG*V@;%YlzNRElSEy7pdFbB-P_!9L}`cHsn8jrjuOgV4FBrmq83 zF!KkO4QPGOu=v}tiyjIq63@LANZrGSepRktgQnV4ZhAR-y8*~@U-=8fY&cOHGx7-@ zIO_jE6P$nokjo6AnxVja>9ySsJO#*pkZjB@n34iEh=X1wF4U! z#nOV4U<3TaGP3PUXK9r-H3;N>Y(S7*vxwN+1V?6RH^-UBnK4=;gV`|VyfmPv z`9X)8124_hYzW0rEapckPUij*=w#})5bl8U{5$A61K}`G@_Lt?sC~9v=rit194?;d z46gM}!dOf+`=)koDI0X|@%u`09&GBoSaY(c|FxA>t(~#pb24c>s8`QoOG&X6mtPo4Mg$P~I5=@}N>BIou^NynLCE0EgC zzzmet>~R?4_zf`*hpHeG05{*M1EDIjBNUU`;=b$>ql&b?eLnM zC3F7`&6%uabQ53c&Ml}mKlyRa2qW{7G57^DXFIpMWiTEHcGx5LZ##rKJJYYb;r)6M z6ZK-Gw#;s)n$c||1Ko`&g{Tqn8Q2u7k1*ghC7_B>MiT7HsM<)cM?jJDm~yqAXVb5} z@eoe=VEl8c036GFX*@Wb6v){y*5swz?Q^-B{mDi!Hj6%;)>oL((;1LdHIh4=(UJtE ziyn$^yi-m2m>`Fi;!g6yvqr4fTfR>KN_C;i;DaR$EE=KkW7cT7;xx{aJygj}&=3$A zP<6QShKtVe>zp@jvpk>+HIc`#JJW11C4Kt*3b`YQZss(I6O@yd3<84v`4wrrY-I?0 zVP5rlpibN_g~qI>3JeP{I_xNYwZZ#GJYm#^g6qGqkE!^m_KKJICl)Xy#`zR5DqzIY zz(*IPCe=5`wGP`g9^8_Ky^r$5|4>0f3M7; zXldSnXeaw&+#9Hr&r1lct^9H@L}q>{+4!?zSau&T(3#~k*h7a_1B(?=qdqGYT1$c*T8qK))`rSsCHU?rT;9w-HhDAKD!lbpn_p?X$ZrX5)QY-l)yBPhH8 z2Ol+e2kg}KqhKQiwzQYgo~_b$sR3z@63HtLjcSuz0~9l-r?zx-GSI|{PDU~u}V?S zp349GhoEn_XLfL++FE`gHWOya{XZRBlftlU!IGy-&}qsXP7}4nOEv)|s$mvvNyM~- zJ$*Tb<`g7<2r;~ZR(K&4x(xsQGZ0@Gh8j2x18rt~)pAoIiYfL+!?dl8hSUw^a`I{} z6(z_6Q}HCvz7m+@Y^$g?XG7+6UrbgKf06HmZLSlq9|lkclb@*L*V#TQaT@V&9uh-f zaBA7aAk4x)2NSF}XY%p!y>6^v?cOTj6U^fI)X(Zrdz6)gAFKwzqHTNwkaTCr^4deR zC%UqG;(?ocOP3WK@LCAfOy)<~34}07EJwUCm#<}gaa%>XVE(XGDU_&}^ zkK}R%`h^P;AhW{Za-x$z6-khCQxX2yFnU3#V$7i)J6jq5_RgQvHd+ zjE{V4S0KM0x8}lZ<(hu~iJsPGH}6~L$wn6ASb3FAT%hbs@&jQa3QTagH~ue^EV_To zU-(MJYO$?OG>`-~WcJ0;TX7h&c;vZnl^qs@*pxMm!R97MG0_r#XK;=ls^~9gCZ8 zm!4nSo^)c5thi_AsuwV;1uDs&8M+n2H9zM`|az^Ae!bq*-(Fc3Q}O`vu~hzLe`%GXPWxttC&|v zUwj(;d_r$)z)dhmS>?hP)w~Za=UBehOSq?}|CF3Rxf)1iwKbkD{My7b))P(K?|*wZ zvU}recjA?e9}*K7iSoqOa{v)L47&=Lh%zZ`+UpS|Oag@BCvyor%+?Td9AN$+4CQg{ z0Gv2hx3>S5$?&aOlM&a6?5)fHq^aVH+zPHE`}EIIxRCw`0Ud(UDJl2Apr2!1HT>aGPj$#_kgq*sye#0q;2LLG3#Ojg45TtBr7OyK3`=p4H1}f zOce0-(Z!DH#O7@kIv{Cpp{<=XqLa57?sY2!LBDm^ZHJ3JD!W0MvOUVsUf~*!XhI1d#_Yh3FTUoZ6BR531J$rHVaMt# z(7(cM5SaTOB7Q`jCN94E+P(QHCIClkVv-nx6Q~|d?tg8T2F2YZjj~)}1z{-^m&P(U zh1dbB$RxdrnusGB$U(PvwzX4r`3m}UX!x-3qx~p1rdD4ECJ=N?Vc(H_Zot6GSw@%- zrXiocb_dGtZxE)0?`0Zl_Fm-09|y2Y2j$R zWd&B$oEiopIVPLzfI1=kpyC7=d&`;`eh2B`S=FNUX0Lm-gI#~G3QdUa%uO2W>c1Qr zbOe%QlZ|85y`$C5X_rBfONd z{sRpT-5?Kj`pdEbI|;4kVj4u@vaNnT`J#BQIJjmLa+6Of@Y6jR=~%>&!VN_LG)afv zosJZ?tx)ld>?g_X%-2k)GZBYUEp*9b{xr7Kj4VwW zp5}dfDR#y>%qQiL9kJ@}%OTcyXj{t~-^RQS_u{%|^g?4xJiMtU+z*ni)7)|F(Cjp| z;hK8Sie$0qg)26k1e7HS5v!o3*L!-%nE=R?ckE0I45AHzTs%wwW zB}s1*bRWGJ^7<6HgnFa|diSUS2B!9&8=kcaiIs$lz}=T@!}~MxB_JpM`#!v6DP1qa zR=Dp*_fsfZL$J;xdfmnNFCZXw92}?<(7(P>rl z>G$3ri3K{dEZK0V#>gXAPXBtb6haKp*w8K-bBLjEyoh3O%UUrUgzMvwbTMop7si8D zti5`|j9iOQYBm2Jc4d$XQZJ$kOjK~45}JDBwB?^)?u8zuFQ%1a)8|f)t4Bx{{%Xe) zY@Yv%;QbUq^+!iHvbvF51i(Eyd~u1}%z(u6nJxoz8x*z1?jK6Y)P$0Facz0@Y7}NN z?Eg#6H}uCT6m5zNt#Cmr&1Kl*HtpDEz-r2uo4@;*x zK^!E0f7JVxbShkVw2pz=&zftV;M<*4(d;lb`sq-E8ynNVcf#5>bn%~V`T?KqAgG)W za*Z-bQ0G$NgjhxO5ZeLVsn%}Y|3&0_?aqaYH9(_c!}N^X%w+=+0np$f9ZC%xHw=rz`v}Ydxz%TO zcs_FnD6at5G{|=clu!Kj4rqt3x7b&CXn1^$aynOO)FfxHfmsX-R*B1NNs#b&TIzRw@QVjtA*T-+%O1$}`Sg-8FXWXvp zT@BG-^*d7BUSQ>^I{YJTPzZTp3=C_&cJmK}%kKk1;_2ipfoA6Ycb{;kI1SaMkzOTj z#64T*ZNqs8syxt^p>~b1rUBxJyiQpPzXdm3tsvj&NPPU;YvCi66>QG&*R0O`FC76~ zq!da+r2XIO{P%4UvF{a|=;drMp$oy+Ihd@9Jr*!ai@FwW zRcnH{PesL zbe|>#%H_YG9>2xX=h2}xVMV@FuIiI?WcTW06MPRCue;nucA7RC(am&hn{iha9MOx2 z=oBEgOmU(B)r#;@`7sm<>{Cv7H?rA22hVqDm~oiD(J4V=`Ei_FW;a~*APRI@!0RSJ zYv?idYEVQJ1J8U9ML`Z{h#%9XFxNlu{rjum4ouYZi&U|f{GoSsuvi7U^|R6x8-{5` z?LhX$#l|e|(Y*)Pir&ZPblIk0p$SYm^r87ts!W8czszLRVC~Nkh(xlm zP2>WL2o=5npDL624nv2<<=3aPy4x%YGFk6B@!pS(xfsg@_ubr}N9R9WYKG9!**j!i zpyX|4i!q2#iFRLV#e{~RToesO9YpH1B+S#HnqN7qJ7WqDkzH+2%Wq*bnU(H0XMzjT znNT7#Vy+Lbn&|a90T)}cymufK|BL-X9t{3iDhVH61XI&R)B}po=-j_!`~j!R+d=!G z;&Q-&1^KLy0?^j{<2`nNxq61eVkl>5(ueDI1TjE#wwZ9Ha+L*A9{-f6AYYW6A;SH{ z5$F&5f87#Lt)ld$p($x-wy;%vBRi-?lGJV^IWOs~>*%_s?fG)~O+nDHRtgFf(~`cyA;6F zM*~@!w$`@(&o}kKe_xeVF0U`(DDzaul-CQOUcr3%@Jax_gb^0YypKHUV+T-qBkDT` z1&&T&g)v(Sv|_l-*J9xDThLwty`e*|4g-xseph4(*?QRza3!eZUC%;WA6yMW;rsa* z@bD1SdY@e4>UxW#YS3Twi>}wj{rMw0OJ@ z_W%`GUVvNOKtNzT4#8C`r}KHJ==KWG1wE-r5ulC<@OGqEE0;S zH1vp*OwDt&5`f4Efa(ql0@gy6cVM*);}WIj&JBw~3t%`4Xn)Zg4}dE+-yPIkw1FIm ziNXzm=Q>y$RK&3kc4^%!!QxYYKMUxjPd)tqz*)4*!liAHpE30x8Xn-bgTd6MKr=6N z6AfSX1R|I-CBG{=wN;A07G-GrJz?%Eb5dkqij3;q)i_h zq@&);@#lVtX4zwi=FQ2`JS#YB{fsn8h~^XEHH`U`pP3E#{w_LDN=XGz>7KZsGXD_D zW5=^2-2$K$SrKRk*9O4G^tt^jZ6ojhB%PE0-0R2gr|=9+bRUDWW( zi>Gp_KH+U72I-@2jtu;9yLnD97H#_yZ9{lh{iU#Lr;d)N z?%@w5G_J08b-%EfAMNsJT3CvSDl;D_Gt8QCHw^xm(X&)S938;e5N}dYbDuS$qUl(y z35q;MLB+5qf}%42a4Fue>$A03d7==PIp*=p-4b%yQ#4zBggpw~2LhF(SdR29sm|7W z3w8V2o6&0rQa0g)9k21??j3!^c=SZVKL6yX`{keSvO|F14h3Yho zj4Hq$%=spb;Ke6ik$l;yUXd5SWaL;hM4P2PZWT~gQJtRscz)#~VQ&9Sf3~Amyqr^m zo;r3a_JHYaH#*nP!^Q)tuftx8U(T4@4;Le2BKyCWy(^8vcoZbDcjRN2>0Ad=EC{+2 z>iaB@9pu5?6Hsv@&qx_&)KvJ+nmi+Bm{F#Q*@HpK&UNKNq~Y(cT63pcsw#>Rosu=<+U^pBHn z+XH0jD_!0*CR|73J1{5|)-Z0=qokx!;Ff{S{e1L%`?_a?#;hdwr*3n$*s0lo3Z3e6 zSl;H{&6rq>8v}vMy1oCd%LiBYT{FwIGaO%x_m9ibbT}y8SmdFgqR5BN4Tk=7=2cAHTU|_cO{=(_+^e+3 zvZ=fV7&F!4Jv^x|h++;M^qpH2m6Ab+n`zOE-!Uo_va*Gyaoxbs=hA5V*sy$lD#ZAP zR(t8g>+0U2VST2`{OC@%>rc|GbhGo}`CpW+$lIy{+uHHw-DNZgKQj!naXX29LIo3? zfOe2^d-~y{h27f6g;t2JTGd_RQl^cti{+YAk1MN029}ti&FHyl_$tE<1%pl8xDj^g z?yazC2^Z0(QE1OmjJiEE^A#4xr86jD?)z>I&OP z8h)guhSwPkOj$VaF`j{dzZ1FJ|CWeaoUX5gXpv2jpTEUYOx-?aN{ntzG~b&XIE z%!?(&|AizT>o6bC@NfnMLkwH`baMZ_?<+p`bH~Hw&#hh6xhg`i4B~7X+cj z$K!r)7=gmOm`S0KDCwBTflja%POzFP(q2fm0gsW@8F@!}O5D7DPl=TxQ$?tW?SsYr zHbfCsZJCa=RbuBuVTWrs2{LlLXQ#3SY*zF2+GV6N`!YDxZM@{B^5%_fbNwrXstY8g zh*O`-J}`6G{Aic5kt_3SaI4A7x8)+@zYmczvi4^vn_P{(%R=Z&RPROW_{?*|K2VS~ zj~Y!#6DD-dcel2wT+ceq{LgQM=HwiC*%I4M;M*_Yn~0d~soB>;UrQ_7TcvEc=q=Rc zFizYc@_TU@F|G{L3MWr&c6LF7lR9VxvHYuhQ|VNj>GIfFPW)0ritVjw(5T;=PKClq z|L5VAn8!RsKl9+&NrgEYnP8gPS2eA>f-NU$wyO7JxXHz0rob5M^yx+<%)F{$_nMf) zR@ZdPH(@f-R}9;8Gk6QNwq-F5a@oi0qM%?2lTPU6U)MI2g4q^5N-$?lcoq+b4r~Ev#Ior(z*5 z4r6+oEK}VlDM;M~cUp~*Igkd0cDm{R^&m|2jez zsD0Aoq8xR+WW!%O?V8QsC(9=M-|Ph=;Yvn^{oJOlvQ{2+!EH0IT4!2big%1skA8ld z#u9?2JyENHy_8{ltWfVR#sgE?{yGpx7Ww_xVewP1Z?KXrLns))x6iiNhAT<9UJUaK zn0wT2?C_;&w8Kqr_t{*6U-VDe&%bIF-)IsjSSjfPi|1^`d+1?7woY_-3KVZ0-I*)X&qCtl6AtI&f7EEo^RwJ*bNMb$YMD9Iz2qLhWaog1|CdzXMG&J2H`6~=&!LXNFNF=tf3@mK0Qbmib)FTO zjzwfT4qeIHi5`~<+miHIG|4`g+JElR4$?3OFL{H@iCvC7xG~{51_F(CkI0M>=ZKx3 zdw&uAqVnwE@&D%7cH_o$Fv7eT-ezVF54&rRDM{`w_dh*P)akoxf1*&YX6hp0L?K4F#em&{shxO{jaYcVnuReR~ zeQO%~I=p*YVaiq;P*BXd-Sp%|0&I{HONL%=u<<;wNZ$*i+s?OFWhf0-3rU!;I^$j4 zP5O3tYi&xDNxRRy=5$|!1;!ju#n>1*duEVbKy>?;)X(_a_L=+eZPZA#2{4pP;Lfqz z$W0wYPVNTgW}`;QpNCz?m8$I7krh_^M zk=e&a(jl1@nYU~}J2hN9-5X)T7fz!C_ErL@bH7^TtK8J=8~Y}jJQeQtbN|B$G7wI* zB#NMM=fG5C+KpODqYXX}x^u5ee{DyLZ;F^J4&F@>l)^JwMkp9|U9{D&2P0JNQ6?xw zEH;UB2iLL)Q?`zg#2$)dte!9VJ z_pp|)$kTpRBTkjcL<6vBw65=NV1DvL5Ct(BZTbU7Tuqn;Z4JpH212|2+77@Qt0_ot zX~^yy2CFqqAP;t|Fvrh^#gW(C+i-d|eh(MPT99*OSsCD{xcLslcDry->TizZ-8@*Z zZL)gJG)E)Pk$0zM7utgLj2C=u6uDPdZ@FO+9QCND!)znz`Nk>~cCU}~l<;H7ZJPm; zHvGuENdpEfGb`VTec#GJDDS+#8Lce_KvK^(+eKP9n+gRAlJyPA&n;~8cjFid!~;)8)^HYf`DB;=K%KY!oWpK5@>Xp_A&iK+$}OZi+mY%V)org5sch#G zN0?jQ`kHh`e1eCyv2VIxwB4G-r7Z?dpLmVmOtQ8(Nrx7~um=xLz%@CX21zv|5)$8R z*d(8Hj5&>V{H2^wa|CWPO;2{@R<<8&z^9}E;H$eP%|MX9+>gYS!rKIiz)!f%ehems z+vD~yKQ}|Axl5n*rc#Uh@YEZDV9Tc_Nq9m00Vi-&VXxWM0fLmIy=9(JO-$Rac`n$3 z;5M!f)@1NTO3>pOdrPfGPRihtJ%ii>GFwjqo zJki%q>c;Ef%^SfU z0M^N(>t1RLO=I$ol&Zd-X+gkNcgbutxe#a{$U~b)2?tp54Ho@+*E&5IOMzPj?SAD= z80t8J4oq+4Tdu`o=3J+B%rxp}YFfRt091C(xDK#ay6m^`N%!0mx(-PJGYk^59lJ<2 zDaiU!aj&G%0NyaGhd57gl=>QPv(VSJeFfVts8Tk4%iyK2%%^=@?wQI+py{waDVUGe z^;8Vjt-pKdBKU;m7_45 zHxR}~GiwIbF%YD=llGCZ*MIE?H^?;J!L*?-$_8S%beC8McZg*j^lEIw?9iQur11;b@Kt_>b-m=dZ=ZIYQhZ6lR+@R5xt$SD>dk z6l@zkGQ)CxHPNrdJJQ_G8ON<7mOpU7ROW9<3*Usa=sIZ8+`S#5WZ$p}w%6eyAi_wr zU?r+9rRdX(3P-P zjM^*neJ zkQ$%cN>tS{t|mc&e!5|N4A~G=jjPjWH^rISbNSf(03q?>#=`1~gR{$SGa&edEU422 z_nK4MrVgu<(@-IU6dDlElRnwuXOJwz&}~2;M7hm(%w76;t{T(u!NZ6Seff~%oAn?J z6jUb==kRmJ5-6JC@fLZ+fqirLq4qF&pCTBFkNO8lOX2C<6Y(irpt{lEKZ~?}4%W1M zcTDzL#>Ouu^c^N{=h@M*{w)&aUq^75LSrW3l`LKP&So6r8|ZX>==S>oH<5--1MNj4 zIg(I_WyubE(#osiMLx*muvCUO8(1~aL9R`(TE-b2WR%jd+puC`cdDBSs4sZ4(Xxdv zlkb6(kSBsR7M&wcEg1nW(Z{ohq)<=B50OO-=Cs!Kqz%Ux_{Mrz!6pC2kQd z973I)tea~dl~WJgz$vQmxVBZH?tu^my!5BLDfwL0U^|$V)FE&SO_=V_ov+=E%Nzr1 zpyM{=-lWr77zIosy!-@{>F=7xOqAx#wa>IB3|*IP%&Vjo4#;&MMwW%b`xMCU?oHGU zh$>;=@OId&-Q@qIb35@72Ew)>?H)u4>VkVzAaa|VK;?ULPFkSTKoVgDR;o)BW`mQ(vCZ%CuLO7Il|aB4Up*zlIqsEv^X=o_kH87MaG0 z1{JV;9F9O~0s`b)H}&7S?0P)GZ_DSW><$+r6u43cbzKH`h~wn|)#ba+xPs;AsH*SD zepZ>^DGgd4DM*&vr@|n&2D_C?$bRoM$nC*Tr~DUyacD(~*;JT4fZ%quXW1Z-N+$|~ zJT!OXUUO0ollh>H@a3Hj2`tDgJO#dgceJXN0T~YDUl|BmCJsguMMrqR#`JNu{~Mn5$!6ufTc=^s$B^0j z&!YdklRm3Nl5rZS|9V)fE5U|PRD+9CvV17KiN_U)KD^cjY#WauS7WFjapftOO-{|p zdUK%tGi$Du9g_O2btR`JNp?fV*z|05tCX;fIw!H*V^FQWvYlNWqW@Co!?VrDgriyk z;Bdy^9kiUm)%`1ql}|ez_p27*U+4yxZ3L&mM$tIcdKOo3#?BD5_;Niu$70dc3{E-KZ6LZVk|6^)BGAeA z@oDZwq^Lu%DNzR}t<=Hq^|(Nh5`IwJqG?zj7}I}{z*L}j|944XPSOpR_nQ3P z8DlMTFlOp&p%@0jR80-D*Z+9ln|$oeR4LQ9h#7x1X9W@vVl*<%8eo;aWuc8!d&_?A ztgsZsY&!Vx4and9jWw#hp}OY-(($TRXCOqc4UiqSI}+kSA(Ot;sxaW9fKrrOo8g?C zQ-yd3{HN8csr0U-6e#_)tBFQC4oG#GjdJz=jc{hsd%hr8_f!x(9H#GNI@vv*Nf52X z(x)6q-XU&IeV0LXTCU#R=OjzL-137<(U6*4uRGb+$H)#m)k=M|WL=w=0q%qxHaIbe zAItA87aQ?f=7^~0grN<@PnFoxjCzS9nNHm@jdBiC0Ty-`;x+blix$csI(7Pnz6TyH z)Z?wqC*y4fW|+k3cL}2CCtlv>4W!`^W~}cUP$`gkMoWWCA#;~&mR#FL(vbblcAO_` zQa!K(kbuZ*h<;sFl*U3pB%P^Q&73?HLQobN3wg$EI>4K1kQs<(2`~uBC&E{dJKl2# z3Of(& z&=cmECrdMCUWKs`R`>I7Cd!3SxB1XW{F~)%-nSVRt8Ew z02UiI?VZQrKhpWw^eAmzCdcRd;Nw@SV?L1Mb2W(1Ik0=#cyX#P^-ev)w|m+?__;R} z9(?x&@oPLkK+^CcLu#_X>5$&KB9`g5fd|*QQZQMNgr{%Fur~v0_X>bHAUJ!HM$)4X z{mWonB$;blp7{woA;Ou08Ul^Chv8os^)y-z-Im}>X(~Gr;vxRpU^Zl>Q0fK}c~Tn~ z2=xqP&Pt*1;0Cn83KzI3eMVe2`YB2A;L`8z(5LN`hN!<@PFgM&B7Yt=uBHw~NKgWO zl7UnL(F6b@2_a9&=`$5zJ4AO(-uVm|>&uahE5Lb4k%eSFGCPR^keKzGTlSoC8({fA z$P=Z6*ybP|`N`Z6d%Bdas{%nz_miV`Y!cIq!32oIT=r#;eNA?LgNOWC z2)4VdX34=0+oLy+N<+>q zV=QgheWX*fMDE2oE^~YVOYPQwLr!a!?%urcW>Ztur5@jOi+pQr_73S&w8aTQ@uPIZ z#UM`LaN!N_SVoSfVN-P@xf$d-rMMc2Z0h)kSNN6JvB<lQ_w&$@>svn%5 z7SEi(0hZIc@bSv_qU~hHjUA|g^k9x6vIHgI(EQ%AbNDi1+=@6WaW9 zkgO77Z|l|3KIcwtfX%KwaGJJ=573JY(DN2_P;BrHl2sm}^2ntUm$*Gp)_li0Fto!v z!u>?o`~P#Y_v%FwWCueOxVkQ~Y^CWKK3?_4JHp{*;TNBj29zfrlK|cG;JH{v&x=bh zp$}Suyh;8?zc;AXB;hVBS}j1xlMLi2VD6L*g6F6!U6>}f=wfaX(mJ;nA4pYwEh;F` z{!P;ylGI|Ha$<+%mNR&z5PxQI$$eVF>cf_iBcPr#|CnX$_0=TrKyi*LLmGn;S0@+L zRa}x=p6W+Vv)mQsFx8lpXtX~r*Shbzh!}3ng2c}=1o6t1>z$CS> zqATwmO%B?s4}vv~6ChV}?2;_siHbGU`pO|O`frk3&TQu++Kz!sfGvC<#>#K}ODouR)+l2OWc^LS!%dVz>g>Ox4o_a?Ij~v#YKmSg zBgs8b_ptkbLjAG&;z5gg&(t(qJj*wH0hTdF$k&FYClQj+fO-I2-52=D&w)?VlQ{R5 zoC#q-&SXOi80Pa?LXQp@M)zg)LO`Tv;2S%6tzbNA9rnNsKpiE#dvth8+L8uYzfUF$ zN&%yFd+s(1pNKtdn}zT;9(EA4H+ z_2up$NIzy1HQJX`{X4mFo=@tTV}!=7Xta&wYRTqI4FuoTU&_-L495lT8}^bFLZtIP zHk;1A*5M-)%AYL*j!F5aK>}#MD_zjcj}M)Y?ty@XMthfye zZN%R&f2S-C&}a)5p33EcBBEhNr4YKox6!`#`A^J!ZD_zq7V>N)`=7s2qEojS5k5K5 z5wu38=F&or={Ed-x6)SWHG{VW_VD1-^8(1_ZvZkW=+GTFs%f#}D{BOAJQmpYpSdft zwVeOR3`V!k`CU@A454gZX42_7!1<`E$p9CbXbx><=oQ5_YU_ zD=#X%x<}vyJ%IUGuvE4;Zdua_ath>z!VdVD+1aGi2wiN2!{M}JK$yVDd2EddmQQCO zT!DNTWNzB-qs$Ej^;`Iu=q({LgsCjSkd3(Bx&zfltB-AzK>J`my0B~FNE64N-2N^3 zoTpeQbh-oQq?Y$CTG}Z&)fR{^M)owp8a~5Z^M-?xYL1mM8#Mi(C`hJllX}nTxYed= zAPOYSG>qD$Y;q1xmO4KxNPRn!xxA2P3pJFA??qD6+@4%P$@`YPgcZ$~4elS$3d$`g zj(Ap4j~rq1SbUs8EI?^CkCed^zQ7)kcQz;FB{wEfifQ{4LJ>(gIC)zWO4M<({nsT*z=!}4v_m zk)3BRnUv9M0lSzzRr+bhzTiVEN;>IDNUo{qZwq{wX4cuWaGS6yK~Mue`?Zju9OSnN zX|F<3Y_kWZevYhAC|^?9QM?E>d?RtUuB_?BUym=-h{5za1>vSSWYJo^z=_PEfcA4e zn2hRbnTMQ10LY z08Yu>93HnH?z)kFgE{vw6p}KlL*xza)E$2X%O50XdruP3(SfEINQ8b4hLF-*aRIz! z#?@2ZWHW-0zqOkN2Rff_P!6JiCiB+0H+R#tc48I9%^muOUI65BN9CFhKz}ZNHi=tD zqg}9VyHpD#pWROFLn5vQe5bA6@gZ`B#7!`Bo2hh-4{#~^K`oUfe-b}7f4YTPUG+$D zhaP#|-B=b2^&XRkzPoDmVnXB*L1QI}CER1kk2BE24CS&D_>o>b<^?WQXlD_7AEaeb z8bz{WHm&*u7{Z4cP84JfTpVC0L!i97ps8x_lC#kFw!l7Z&cPAS+LBZPiEXD%upFUnoB)-L$DmZszp( z{P5BA!v|RmZ`~vF!Ta+YIe=QogXDM0z}>Bt?F&6GAL%-9MN3YU&fNA|UHUj*t8syD z-d>bJ-~3)$#AHj(cAWc0z^@yOLt8pI(TuOd6U`}pjp{8Apj|i! zf>)QI5{0JDt^m6`cUKTkvstY4+4oNw8xpeeHbeX#9Q!m+gHa2DcJjbr@e9~9z~4Hl z+brQ`-na6WN(Rx;`NXBTI}NH|(FoR)q&=5lc=rDosA%|WOtvkrdZCyw%_oPa`v51<(y zGQa)hXd!D;I1`fSHbk8UoqN)N<|M?;5N6*_q+%kAxA0fDk~EkR_`3jChVa4t zJb4$*|1T!|&bh)XB{hnA>(@ga_Rcqq{GdTkF&n?{ga*FG#0=f!w{j*unu@+`yK$b+ zFk&I_68DjGkxCrUY(*(2I_J(uzv3b!T}V2M&ORT^Wv0O!%l$}dt!Z9~;7%x{8>q{F z@3)`tvhcIOyCt*l*_u&3(>xl&y!E)naHwCd5_N$A6%WqhidLq6i)06mS%N51Th(>` zdV7caxRq_Z4OC3FJ8)`yLTV=@#SlHl0oa8a7J5B|H@VrVvNw0^dn$-`<-Il&^KnbA zMHAhjkJp?EC(4&9`+|=I`7}s}0`I^j9g-^@SXK|j&s!iMHoc_P7S6WFT0pf%+E?jv z`R2gZ928muU^6eoB8{$pAGk(;>@<=9g#-qwCXWv@GSiM9+y+GxNAH)KF?lm;5h6Qw#M+G&*a^!V9~}g&iB~8xZ)B`X2!?`XTs~&;N8JZqxl_% z#$e}e(~$=yKTv^=3QpVn@K9oyBd(V0@3@;$!DzD5!xiK*?3p?(p9EHd$seg#llo(p z&HJc!Eq_(2GJ9Y^B%E1uRt#?dYnfeG8&|;Fsx{eHRjf>+Uc5<;fm~O$w#e;AFTgcG z<1jU;O;>we*kZ|hOhH9S0sujWHf|+33YTl`z;z{pUn~Vt5E49 z{IVGW1~YE*<0=%Wc71=NR0*r6=lNrj6y7}4jq*#Q$R_>z5`1OL+E?tUY_iI(93IPS ztw7aEKevCsPsMo21Q(%v(R%yOr!x=&e$M>Ng_ZAv^cHb0tUa{*&;Fb^lul7lb)UJA z5_g4G{m8OJz|Y?Uv$Zz!0D**!cRWF{W_@vpGFW?(-lb#Y@>WoIZvrN5!#eU# z1@JwCx1{;TUH#zJ?}A$!5Phoq_$N4vErM~*@cQo4Mr3{AMpFJXo+QE1)dCi|Eu?}d@9cw|*1%YQDuP^y>Ug>-p8@%Pep4~`YKItpw z!+hrOqeg>-v9;$?!gJT(hZhmRb6aUdN+&Cg<2~m$#n=|;wicfx7p#!wLqWj%o>=+e zSl7>YN$OsU%sxM#dkj67n>^Mh-u-58!=EjUm-q!9`p_8>fBL_ky^p$lWiHFF^^B~j z+>gmB7psF~2(D9jA3!WCqdXQl-KVmY${ipjQ4s_edcHA4OQN&Bz)hKX2{ThK9+DZb zcFb?c3_A-Oa&0#hSSfAAo-NP!TwQUv&uY3xeZ$Df49%3f7V(}}26y$)Nbsl4lIsB_ z0df4QT@4hRTv>Q=fl<{y|YT$%0CwWpx^t#p#YP@Lh_Ds`}7hRl5SJyU-7B=h` z6q4PPQ8W;3JkxN99!~SJEM>FJuXD0i|MXTkYYUs9t8w9Y@rdc7hu7czR4>Tgs9k7`l{SW8 zn3R@Ipk4UjVOJ)5Y-CM$g}_cftyDBc&y;vj@nfcG7g5JF@EHQr9WPeqjK4%^)4@Ti zJIS7I`YAvRcl;~f7dcfm9QwHPa?zJagwcIDE*J91Ezh=|%MYnpTdX*uiWrsL$rR@m z>bx%%j&-7Jc~n>6;E{B;a}82~5_lpl%N{{>0^uW$Y7e=|W^Y}K9)?O8j= zKmBb@9;HAYyL|Sd+{e$u{_}?{x55W(j;bk0~Dk4XGSB>p23zxA;HM`H;Cs+mSvJDrOmCn_hrRy`8_z&hSX0U>R!RyIaP z-+%YB#o=^6ZFprTX8rZ$5hHr9rJj%e=@(D)i{{oJGzR!Omb&bw2AfQcXfvI zInRJQJk#&dAEHzlEA4bLGPm10-50fKW$rNWTAm|Kz23DeDtPlsaK-FUYn7f&Cd}UV|%LBi^>}Jh^!)s2q%P*Ga8@<9rWPv!UWKtDj#LvVII?oqsjM@^-p0`#7{8r#^4~t~U;Qz2b)XI6A$ExM@OsX=pJ(#3 zJZ%L}0{eJzukFhC)fcindL=ABzA7D|-L=&DS|wFX_iKm}SC{U&rFX&CC4PKw^4eD> zuezSebHcr&q>|&~x0*&*>Hho2M7oNugi>qi82gd~a-gQ9e$2$LZkf0?DC<;q0e1e# zE+9CFsmmW%SFV1m7Q4r8NqQT5I z2v_L|MdpVWzdrF0QYakg>n&4Bui97CA6kA|&&p|`_v+w25z+bxhmW2MV;QbkchY8~ zPTeBlzGh~_tuh6#-YR@{fEtB(L5Dxz!LLXd6yn{7Q;m}%w1$Ur`RWDbr|u?Te;i2R zy!!sYwNLAIeU+FRN{Oc{87t7=``9%YGfxB2mfNl;K!9cKE`F<0+j+`o0vJ6K@HKnV zi$lJsyh(4e^BmEynywT3DEO#)ZAc}up&-88qt(@UG2}{?U z#Ui#%CjC+95VVN4aRrUUm!u6sOZ7NxwQ`gYDZ5c-WHx*%Nz60V&Lc2At6Tpap>BBc>E@I*#pVIS`^LbdL6V%`XPAadu`tk%Mh#5Els@eQgXfyXSgS=v)=LgT)L6(*z=FqFL1h3Ca_VS;P<3_{4k}p zhQsc?+q;KcyFk~}T!Dews1+}1$2Y^@qD}bQG{UB9`Sv(`=Z=bGHS$?kW zIjm8X9va*Jw&Y?9Z9>_?hmye;R$qd(DEPy)Dy)ul)beG{XY8um>`?xi%=4rv?o7~qf?T^a9{@pxosa5>#^S!EC!FZfy z7@gc?MfsAF;Y&-~JBvxSTZ;7`C(ew>JJ%n0-M>$3qoP!e8*3_d^#d- z%!N`xzQW+BYmR@!OTm7;5L+a~8Px))bjM zINj}3Trw=1t`2H>$bJ4PQPTZxs@RoNzuS)sECjv$Y0$$1UyU=;5+m60RrmbgSFu2& znY`63F3#iVoz;XaldZLrrqSyM(W`czFOWY4CYCo zQAIPGuAj5=UeEe9Lqu)WFw(^}a$X737a+c&{8JIIOqG7}vyaM`vs{|OGO)7zYGt3> z-j3~B{iZbdA?;y+Oz9?4bo}^!(qeH(*}m8--BzByLpGuDKl%kdS3R32p3&8{7Hr`o zhvD0gKuyIrQ-#e4tXyPz2JK$tU#K3M1%{AD&)8T^jg_F0e7Y8AU~Z;KoI^|x;nv~X z?OE$=o%gFBM9t$BKr$`Q@68 zy_B;wa_8Jv1de?{A~OTq?e_VNBw0Y3!L_}8iX0omUf8W%?~qf(W5WsID_gMf4MFac z?Z?g%2jgzhoi2jJ<)XY(lXcUGdU*0Y7zAgcS_HvrW z=ORur>iWt|JpHx8c0~pE?P|%$Gn0BJu3sIOX=O9_3>p&6C`r08J;LL8Um_>+1<-o4 zfmqw@v&2o~#QExLo-w}ZpC2#1-Vx+4Lzho36n&P@B#5VJdH z>7nt9y1nuFaf;8Ebz86=*p!8Q{An+jti?K`6()pTu4-j{0P>LpCucR7a8Ug%6Txkv zs(s?lb@k9xVO)NxU zr@khitHk=9Q~8#neK5r+XY1ndt*l)qPsF|;^SY71tLS}bLk0TWhtM`Yb#~9u!UT#B z@woQbKIiQ6ktFI=<@pW0p;9fpkW(o+Lh5!p<;Ul~c+rNl^zp_vMdhQ1mTR-=#;31y ziSA9qY7QkXGqehx9Fdrs)!JBl4V4xc2=|@z{RW&Py{bs|t5;?+L+)ew9b6HCFZ`Oj zG#2jc4*F{R=vY$T(ViIp_c{D!?2SFi=f0!NPJ()O>)wH|dY3!*=VKZceO4ghLMJxxV71tIGn3M)Pql zMBO4En-4NM1l3ip*z1c!q??=feJyGe!bRxnOO}zg+I`(CSoRis(`aUQ6kkvWBrcAc z%I=WwCliF;a4j}~3aMrYQpOXkx_mnw-|!^OopiJ&`Ld1gec>;<9`fb4ozV{Ejj8S{ z;)``|Q7+<0Q@5Gpjt<|;L;-1^hcwMEb*&hMc8KlOz4%rpN-w?MSqIy`ce!E(@;g3T z550djY~Gere$XdfzO^H5i8u6&Nr!cLPT0IKqaE#|~LAGXrTjy87*9Q8;ajyIfiUd_@wBHi_|EXi)7|PuIgx8=uqoi}YM3|cK}sku;09ODEVtaAA&ur)i&`1YW5S#*%JFP$ zMJD{|YBFkuDQ^W^_mk3Eb8_eDHLqtjx6;O6Y)SDPQm2^X-n;kEI16Fny_0(9<&v?J zLCrE5(5fX3vVLO|4_K2HM|-EG)B0a-Jo4?OJbtm>MQ1Tp)x{DUJ+8l&ngF>zE-?M6F zEaYdR%2}Scd!L`jSB34cxs_<6eahOU?>S)ddQlSrjV#G5C_=i})qP7tKRq!iecWp3 zqrOc{H(au=_Mwwi6(}p^$j`qYVKME#%$mHA)#Sbxz+akRnMKjX;sTcDWuv27V$xyC zt4r!)yW;`F)jHoRC0nnv`+@s?sj!z0rAZ8_>^ypUX!S!Dr-HrYaFFVBOTS`Z!9uu| z7@g#ZaUSLt$fV{7cfNmqadz>XN4T}5y?aD-Vj7oqPUyT@yWp26Ey9LV{?Gb?WP^$? zUGnmRS7iwDXU(}4@M#^ingIOWlQsBauX*=Ah4j3+?{+fE-R1rKVyOXD`X8oQH4OBU z@;=?%e5U{1v5dqV!J(Hr^lxgzpJUvO$*4>aK}QY=HsvRP>O6LsjRmGP-IpxL*E>iI zk0d~7auzDIwVzs5mm1atsB~-bjBNV`_*dZT7t+O+_|+d4SFem&cmjAh{l>nm^PP|f z=VhrVi8CZ2%7+X5_KOQ8C4M%~C%UBsmN~-uL-WUc=2JN%W_cTh_sku{c@zj}5bNr} zgnjw?>GdprS}*GT^5auZ;@@AW%|2cMK;U4sz;*o_seCng5n;J=d%nr8g+AyqH`33*5ZwRaXN%8$5T zMl4Y5-1ITAo=|0d=Tl$5MBH*4$u&&* z$TudMv2<(k0e@45S&&AdUQcwXyeJM%qPg*k_zA%>%k2U<{$siG@>;TyILF#X`O4Zv zF^A9>zKbzhWdaK=?VI0>t4fqnw9oHn_cBX5=jR-O75hS_?<;a4mI}jo-q%=~^q;N&#* z8L>Qx#f2E=cV}wEmwkXtHAd|lgp)nbZ=UgJTJ9G-vPn2I@BHDpHVyHZjEHBUTdP8S zu{rtZ2{-gEhJc%zv&B@6scaD^5tY~LOSi8sEQfB-%s5L_O1X>y z^F8}^i!nE?k7l-Z`{_v0`KwPp6Er>q%P)SF+3Nk?wq0ZvQ59MyMgLexp%D0ywtO{g@JE5c8 zjLpFLUGwES-4?Y|_q%N&i3K!n)4I6rlu}Jkg zx0ctl4;CAFXaz6oombm8mY&HnJj5nhi(R;-*MbR9PH%CcDw|IyXt1Z)x3BcN&C|s} z#&EGPOCcwKBl4GX#XvWZ-gWtTwFkW-`C8lh`lsK&ADIYIif6U{)il)emAJfN<)CDUR4)fpX zKYg$la-E*0l46Qu*mR6dm@?Bm_|gXKarf1Z+MORF(ks~w64zC?r2Dlt7y*akKCj;P zG2eOxoNnD{>gV{=ZUx((_Rw}f9GhpitMzg`RE5koUawPA4s|nH>h4$^SJq?9iulZQ z^;QcY!|BQPtn2xMVUEgroc>RbWe(qu7Q)R*`piEs;9E9KqR-p;+`bHoq-2)n29$Nh zR8S;0S+#3rzxoxja2n`$+aV8-wx%YGXBlEULDtCWGWuV%-)w%VgLWwd-H!N z+wgz5q(uwGEtRdZl_eo-404w(WErxPeJ5LnAw??5I`*Z-k{L6Ev2RgiUuVXWEn8!3 zVaV8?^V0qKKHul{dj5pZZ-%>>d#>xe&f_@V+xe@1V|}hfF8QOoQ1a@z)E{sYWi-m7 z)o0X~6dn!Kp9=dTE|I4>clyk0y#Xw<@%8#B`a&3{45r3ybu~ZFjagnjV*MA(k}Z&R z+vDJ2!-aZT0Rr)iwz~3aTPJLz%(0iaN;fIeb-$xt#eadT^n*5Ik-qAd!c`$lURq(F z#%4~Eg`EFJ3wKD(@~M})DJ6_D{er*2w0rwU&|0TTi37Gd$qWMaV2@9+u$uk5|4$(# zn_Q;f*4@1^wcNx=F?GS>8zC4--z`PRtfV=A2{npkD4o|AvWgcilW+9Bpt|ev2AW%0 zl1|GN=nLHka*e%Ll$wZ#SIpWk5dH?)1c8Ta^`icPP6osTVNS89oaDVMCh|UGhr_6E z5?obJhi~%~i_NOHw|bq63i^KnU}xNODoWaqH{4K-NmJ|uXuB>C-O2Il+Y=fm<5$mpAMUdrJnTp&tO6;yv3wtIwNT&MxeZHHCi_Q? zzP&E8PGiJOuSIWFNRECcyatG0y?SbeGPLD?e|^W=7@&7<*Axse=9b85rz~pfU^oRp zG=Z>6oe(koQctaAcrZlV165HT6QtDaJ4h8&DuHNwRviS;lih-Bql(Ei`E3Yav(w!J ztBe%~kj%FE$ovnih7Yf(X+P0%TEe*#&^(KgseHEayPP02_?GrETfrX>&|lz+9JwD- zZ>RXEX^b7I0r+S2yO7pw!UzyLrD7D^I=~QqJRE|f@+d$OW!U3PDM%P&R>{hA32jks z{2wjAjGuUpkrvTOGqiw}xar&zk#*EUN-7CqT8ANC-B@Zro^-JjNnwGZeWEy*ediY=B0JF$pZ0)R~(fl@$KXbSQat!29={P#3}eCjpsS6@APTS% zNH6;1;X=pz1@xm$E1W4$%WGytkmdtl+?wKGx`q~u)YSWeLbWZQ3~QYzujSQEEg%(& zLXx(9;B$Fjsl`+vUWC;^!v>jH1bJV>V=L~xCXEI`wb1!G(s+nr1%wI`9gjIIF6|Du@DrXQIGR!ScQCYBU z8n>SmWbmdEj5aggq(?Xwxo5X|Qu5w;O`NXP=6e zUsba&8DLcJ(p6tI%6bW@&oBBq&5ldnU;+ScX1yJ<4hhOy1f)+g;)Hb5$#TBV-S}Z9 zBcTZOtrO{9%@>u0@bON`rNoop*=m2?Wl54&uUG;LNIBvLe>{wZxk&fE?KN9={va->Q)yv4{b8lzEp#!khebA5Kr zb&ZlP?XR??M{ntW=P@&HiaJnCK>=^W$q)K~=H{IfxRCGHlnRid)q##C+>Zoq^A8J~ zebe5a&`J^KWBK@V_1DK}d#*6P-(LH>oBpuR_m@9jBY$GW4GR^&zBSM5j8~3$rZsyL zEezTMJzP7VPJO??8_mRyWHBBcL);cqsIU{V;qdV=NO2P%%Q)7nke8OqYsN=|O7^Qd zCwV6iJI*Da*%XJ3SF)U3n?UMbS^7DH-j}2^Rb)o#2`~X@os(2}X}V8vhFuWcq#M5eAP0S z5JzX-6!r{)q6UBf7JuIQLo66t*cqnJ;Y`B+WNy|70L(0e0a2l6;x}a1bmx?afs8mq zKDj?{Ga+qGVx{1PYuS*!b+^Li!j}GO%qevyPNny|Ecp4~Q{4Mc=1js|CapP5CLk^4 zKSHnA78PlcmOCFx?r3CnYo`JmlXF`Ti901_C2pjUMHkCcqF{XlRw*oaZ>>9*=AJOx<#M#_ zv#kO{Q4`=6hPQN#1FON{h?)dA#D$R#Y?BI;!hn9dVY|k8 z=v-cvI0lP{w-#j+hD=^%+#j}~fud<+ zUsexjoQxyqk0W=*QeZJq)Y~EYRj>igI($PxKMV5Ryk9XN^3DaGY1rEV#Lk@W2Nb@@ z?p`*s{}_25N81TnuH9T(NENeyFl zw?tlTf!YDJT;WLs0d7Psy2$q?ZbszuSLR88+$`reRrldKwzva_$RczDV<72CyC7Mo zToJiiVdw?1UJ6MBFDBA@B$KZrSnpYX{x#cxN!zNi6Y#Ge1OpEWs*{&=k8YlSg&l2O5hUTH!GGf zj3L?is47jO-?~w9tGqxhms_SkuqkIPJN!nY@5)XO+D4%W44LQ(+}A#9De!RV&aEbg zQ6RbU(1@6j9cR}EN~uRdn74FJIL6}TuS2;$abMfwOCKvxJS%*H8QihO?Ik*HnTRw$ zEfYjEEn7~+41k^WeiqJr`lA5Kd6n3q{x;92eqFkAbY0nRxO{EBUx&NBsOY3R=UcxY zUM)vE8QhdVhQxg{Ir@nZOsi-9HDA3)-n?h2_sjcFLG~`jcfwMfeD`HpzS`c#{4!uM zbmTA$BCW%UnnR;5BLI!?@St&hVl!;Tii42!bN_N3pvcL>0MN5#h&wRftJ4c-6-z2t zFgCvQ6mfd0^1{Y-YQ;J z*ZlaRE2#2E)SrY?0A!H(t|b1OGK-@OsO zCR)q-ri@Ky@6S{qEeAekvY2sy>PPbj*DLw8Ip6pxF<4WNTFGBMU?s0Ko@#m&DMDP! z;`1vXd~|9pZ^Uj>CrTKU*rT4Y7{&e?>PVr?n_)dAOO9;CR?;7B_m7jTIGVWzS54B7 zsmc+HQKMD<(L%|kI1msXK=$H1sPCZFC5qaYUI6v|@ZsHPM8f7HE%j#uZ93(==rqS=DTM1da7*I zUZ9+v+wGVLc*-t?Xj$ed61aVJk-e@{YUIs7I9lI_>h3^*oV-xm-+q@?(?%6kIT+)Z3ZiJ-0>B|DJe6P{=y zrh{U6?g6N%s#UD%|&Dc`3wZ*IbgN{>km0X^SF5`T0Dw|OI-n7b3k7<11D%j+W}#Eh3^gcR z8TaGl*xgEXXQtkrzLK9LG(Yt&2`TqfZHm8EV;@x*WY=Sn>7=~9J-!cfts>{%kg{7B z9gQhFVbn<$DgRNNw;ea%Ds%pdSxMRJN+A5D*%rDez4=}W1n|@-$wkv0(9##ZnZNco za^xgHKSWOOeuJb2Ma(fZfEXUXXlRL~z9T#|#<(3rZ2^tBpCJ#q)E2p6e7B939+>Qj z>~yi{4|qZtLzpO_1k_8x?2FH+{lgWq^Se&?1WEp%_Ud~WI9R5~WAn>0{D!ap2$;)2 zvQn!2YboX#I;QxO)oH*G$M^o0f1K%6#|8BYUdt0tK(}+=y?oR!hyKn8D{nj_qCb*B zPxP>uu}zdDV(%kkFT|`L`_NaH(W;PL^3yVj6*nJ9tmtue#J=v*I3-g00Pt&dz2&N8 z8OHJJCTjB~o=E$>4{Wv9N|r&ZYdLqhtzcXqn?UsN5hn+~UsMBg9T#a}Z9`{!K~Os& zZbM9K3`4NzG?Nh|-^DRXbD$m>Pol-JY{bz7ypi|0MoG-+#ZinxP`md3QHq8>2IEEh zq((lp%)s$HSD2b$4qbvuO#Za{_ahYH8N!W{$T;uJviYZIwMmg@al_?in2c)mk?DexF1bpY;t=>a{m}<#B=m;kM;SL<$gS1p1Y!sNqbs z%PfUp5R6e*8(d9N<99ZFu}%`Wq#`I?+l>j2Di47(kN}w~tG3FD>f0phjs)^jIQh+3 z9XS1x-tX2<8n_xQ_HBegehqpY$m`L zT>7*@Rgu>F_K=!@0zIIc>leQduB&*Mhhey$m2y8KzP_rE?ffkP7ImLL+LJYJ7l?2s z9DDPMT6F|{Y3oJZh13Vht|f+}OGQnLA{U~Xs%6yVe<@``GNKIqo(MPgb7`J%4?N-S zX13oVHUIOuYEKB7l6!n5WqoJ`Xx4{lVyW~+Ku{A7ZXzZ^J(J)_W|K1xWNEo!#+O+d zu8J6m1}A&VxZ(+hmCdbRj&pOAI{3?}2RTVPvbGBG`f~T>?J<8Kk0;xL1F?*REIn#| zH+L*K@_nRFn>CYL1oN#mQ1-_+PApm$Gy*8k|BQq~*J)__>UmgA&P_us!R;7~7S9x_ zoQ%J&d0xk`!X7y)mU8&A;d$$5&y3S>`IUMLjqohhyv^|em4qQeA2 zmPb}XIH%m@yUBI7YEOdGq8wf!#bv#p?IkrYn=29gj5*u#w>ZuqtT*R~E>>N{WZ z)Pk+cSwpE$H|w*ruelt-yuz8aCv9v!x9fSdt@G-ad&m7Rn@w?-4qt89zWBN9Vg(?Y zpBY-^GO@M}H{pdZHHEoUbpbR6FpU>(0QNl7-i4j9Q?zgYT+jJa2UBfK5Wuk*fj?{E z!F{JmLFIguSL;6|o`gu@#pfB?v|*)Zy>X?N3^C{X)iF=;bY%;+=i=ll)FRfRG@yav z_Q(fEj~->G&)?2d)Cj^u=p&qXy!N@)zIt1v@y`c=?{kgnOou=<_Ut5>Q z3l=~es5~NL9KTcvDe+Wssa*W-k~jKoN`%a)S8byti5G^18K!;uDwg)7P75j7Y^re6 zdcMn5fGY;p)oUoRIrI8~1^D2Y7j7Fw^tU@oOxLp)O;4&_*0^&f{w4D>+PaC5sIPd9 z;e{v-EdU)X8)dn(rT8f6_!rIr3+Lx$D_OUQcLa-U2L`~bGb2~k1Md~qZVaoOv%_-L zff0AxFrXR8qE7OK7jFnF07cq%+57Le4SBtE6sQAr?R;N(_f`uSJ4}sea+sKe2c*av z+dk_ja>f?oeg>w#50iP#69@Yec?C^Zt3KH-T9%JJ496@fw~jOHp66wHp>c|&%Ypy7 z0m(}m+K(ZTzsEM~q}9GcdZz}&XD0NCK3P+-1;UJ&X$ALSqv`Uj2@3mDqH}q zlhctc%^Luk5X~&L>O)_`YSV_ggNZ|oDiJMj;x0N(7&lTuRnU!@xRl7N&?mH1nrg_D z@I1~|GZU`VrZ^;WwCz03BX}*XNmrGV&`-F^xYFvYf^NO5$`xSy5CF&s6U_EkfaNQc z9v?2(-&O__qG0`C7-qw?b~;Y;S(umH40-cY5f?x|B@Nvv*?%U72O}eyH=)c7}Eh$@;u^@NKD~%L zeUT|zc&`^vbgr2XUJ6@Hu6osVFS)u$NXuO5o^A;nz2twD>f2$=SK883w-JBwd38RxH}cL@gV^hpIiQ-A2Ivegm8oy* z&mX$PAd4MnD*@frUV)5JCAp%q-72Zg87$UNU_{4G@RU| zokconguF3kZf#=twLsLEb(7-n|%U3RS& z-+HwZn8-2|tdGC)`n|KBr#Pzr<3J5XPR9!rmPN$)5EO1^luAYzF63H@UyHW*Lk>w} z2E9_}e06B07FiJj@i5VGU=vpLTf}-GUB;a|gPdf>f6^yg_1)Wey0h~nJ*)Z9CFGGuQF*g z874auN(|BVoO}GCg5=Dx8;`&^z$EulID_{KUntx{w^4GrVhG@?a+ayQ{h`OXE(HH+ z;+_?GMmew@SV2KAHczz=Aiq^0p4rg9Q!#r{*30bU5r1ALLlA_r(#Td!MBmcYYt?KI)4?R-R>`sqga zPyJ#^muQWQKAg^CyP89~*S(&)59Dmr>+OXS9S6m^4|<6)H6W|vHU?D^XKRp&rRP0_ zKZzVf6AM=Pm>}@Jk>1E~e_eLx1-rXUX~<^=BP`c$mw#Qn?0G^g@QW5Ik7PO)jvkC& z9syumd09Er~IzQa^5!=WK`b2#FBw72`@!aThla z-kO0_{8&#%2_6u zJFWXb&|4RykO{XiQDdYe{F}3}%#m1%hX1~=QNK=Ql3!7B*nSEipBf&9e)UJjKZ{;s zFV*ZuY5@XDM%%kzKH-VhnDp8SQh`EswVmcu%25Ma5l-0#iU?R<2v75^_yMf&XL;S@ z)u(1YMBbR+siO!zpPqLQb8#xaa1zQ$?2!SNra3%;5p3gHSFFzA5Hc|xeMh{E)qNdv z9*NJGDOfs9YT72CF4Y4cR(J{wfjHR zEl#gGNs<=^=~f_r2j0!GPb~}?HO@c@WB@(2H41Kc@d^23H0;tXQA#qeuFr%CZ6<;- zP!7O(-oDps5f4*%^C8$ba|bl84E~*xq&EdB+qZI!BAl1?7eiA8oS0f5kHZl~EOGK7 zP2S`xx+A-L1#hQJvl1s<|D21o^Bo+t)wj5kkBa;$F6^{@l!01sB%x}{r{O24$&$Jq z$6+g>Q2$_mJ`lJo{H8~3^~UxxLiV;4vdUZd%|wCN{+)gTc;E`N> zEnN$ALeBp-Ts12(bb$B5!zjlw4Q2>Vpn`)E2SC#=9ZoQ^RH&JiEF^QRTc6uGL23=x z?dX35<}dg8ptcO~g*C7+u7uL8NkAGT<$d~vQ$KZ9as@IFI8Z$@RqT9fVc&GPaXdqD zFarS*Gl3&Kg`R}hUf8btg_o9zA{VpE1Yf{;Ijj5t+jrV*)bgt{c^SDJcw`#`n>BVr z@4ofD*;llyD5+iQOnohc`=uhia8}-JkZQ!kce`D*GC(Cy4pe}3ynwcP* zJqy@Aif!w#-UQG}w|sqX^1KaoRO z9o?WM>*t1*<UCaYvm0=X4YR2bOj&>;6zRX|;!-}Y-9{ZrWXfJ4>raZwR-fbS1+@O@|P zw!SW6_H7nS_Y3VG;B~E)#<}3%jx$G?z>~Fy6IHhTX06hEwIyVOm~OKM9Ep=tFEc2#TCo@O+*18J>SwG^Pq zY#tgv_Am4u0eizNeW{!i$nwcx6=bHTxf(DH(ac0Mqz0{?cc6q#X7FI(Pt>?dx#BmF zNN1AsQDJ{TniICZKU^LE&=|cPJb^mfa$zczeD00gX|d>l7soF!H&`_cR$oiIrk8!K zUAQ$X>YSR+;EyG=TvmU1?>4b8cn*??quM_0R6pij|8DcL|K3oOFO~&|Zney28F2fl zHs}pHh>c7HT{WcUUv+e+ix0Cfw<)B@gt35soyRMw(PGjN*Ix$~NyK$b|3 zkwkrCh}ARyu~D70Y2n=|Sxd?bwiX;>E(mXrJ%1@*P0K1Zwp$5SGBk}|^zL~Pe_3o-aSnT;Q!D?+ zim)nfW)nF1n4A>E+^knG1dmX2O{7qT{dKqfH78OH`70A=9k&@Nq0y#Bf3m8>j_l zyB9k7Jr^q<3OyMrkYe3xUj&_tuEOefp7;ruOu<4eCbNE=HSw5NN#f0D_WjnZoz{XR z-@Te26@b+h=ol8765U@FH9azC&?5Q#vjJ)5Sc5#zm-gkIuH{l#uH`XYfE$-(mgMRQ zuc6{>OrxZ=nH1dl)bLth5-bt2^%dwZ4R-glN!&1xj?UY(r@i0coOuD?@~{-tYY%>J z{>5%2V0AYkP0fj?=$GTjLy;DZr&%kXJ^1Y14`q+uQCPj$mRd%Q6)E(UrpHxYPoF1> z+jh_0)|oAh`oTf#SKRGSapO8~o@O^D?Qt7b*5EsA@9U=XkK^^Cj@JRAhw11y-ac*c zT`oWg@hMql)8&7dM;AM52aMI1;!<6}I%CD;^QKAmH7$ZYC*9cks%HjYWmT5LksQVC z62%cYEA-RVi}%AH60K8$5pJOrKoY`h_h=MMn$nb7z^_kjGgE_W^xm?wS^HaAIsy0g z{GZbN{WpwBWv{aYTsxP)sS9M3E_yPsge-3cd#rwXG9LR>P~pkic5Iqlig#sz@mrL@ zwLX5E;U}Qx39u#t{e#(fzkl5g66(yC)cWlJFS<$k_ej&*(=s4_X>M9~`^ROqk#HxZ zZ5rFa`HER`Y~7rDni<`|YJGpd$;ncr)S9`0VNK#(8)7zByRrH4^Qs&+hgV0Y$tsU7wjyOf)fHdYPhzLnln_ndyQqg+ne3UUqSr+)xKk@ zAJPQc>>m%E#d4W{S^dFQuHy0CL1OaHIaQ9&i*NFu?aYPj&8gt7qWqC}dc&SG7?k|h zAr0m?NX~h41IMZ9#gqpN}rL}8q&SD!nE&+(eWJSnpTepP&KKv&0tCNi=t1e zRF656n#!|Z+o8R|@Ra2R_Hi~%2AR*IXV2@}>HoDx`kh{Q2JwnhjzqWPDKqai>tP2! z;_s4I1||=0wUjnU7VcNqZ&$m$Czq!E<2I)Vw7-~k(fXi1Ilh+L05}skyk4+ZT{2tz zy&Ez}88XCfGlsfYUN7WPdV1(^D5m=mSo_D$t10%E)eG~zyx-j)2HVnfF%9q5wm|HO zC)BG+OE>&U3fW5P>Qs2_7BC=%`aJXf$La-pMBOgca^IKQrzuKJ)wo#Eg%&3J_B2Pf zO?fDR#+9BACeq0WO#N0`y{zr?^3|sd9-;uP z!-iy_`(d>n{q5pc;1ayrgHnrPt6_l*iE9dJn@cFKpRWY8>q!0ax1uKty=_H`-7n_H z=}3C-1y*%g$*~X4v?kD3M}}eBpA_(Tb|UQh+h&vTPKK~F2ClVkgSp(JErRz1bNVgi zND1hXF!EN0`bf@>#JKgA1KKVPX}nfFy3j7CX*VJ7cEfHWHHx+5qBZH7klCD@ zbFWu zGMYS_`&IBB7ruKIVaNX5U>3jFQ1{v!w|!yjhMHrFU{js9npON+f&C7fCF-&Jlsdx} z%Pb`{-kay=#Fyuk+v;A^VSfVNgk+8pFclRBFYM!sX4RM$?Sul>c4OX^a51!($WD~T z3XcEy{{5IQq}mtEEf;tj8}&2QuyRNO8{$#tlBy$-?b}te=-n4L?H{t~U%oY+A5$ld z*`zOs$B9%B~Dvo(0zg`u%y9vlTaHXU+fn zO{fA-&uO!@3M$yd72#xK*Z=wNz&|KL91f+-YjMUNydQXtp@%j>c)&j!P)!rqz0|MT z^WWP-?{B3*M@OPo$O%Fp|9`#D!3VQnyX-mIa97&y|L1S?L>_SCqA?$#8~5Lb{huF- zLczP9G4s*q|3AO_e}DMjkEHwG3;Vyz=kotPB>%hn9RA;}0JH%AcO?n!J+U)ixaaH( zqFfB2G!QiC>-Y16L11~by*vLMt3rk#@N*$#?>6ENT2ZEg_8=s1akU&LJuXzaH!2@t zxrMUK2lm;bc}Vh}>FYQ7x3o3f1XaoUM+WMkTossI{-V?jk>x1PZ9qa7K*~2aPSH>b zFb(62r=ou=G31JZw2yfJaadD<;MNFatZOejEsc~H(mEVq`n7-ZlZ~8x)Nr-?xBPTQ>t}mEv8tzpKK;E7Pl& z>#7l;onosh?GD~3Aprf(?UX6#is-;x_CY&}@A+Kw>Th`-ybZp7ZHXZ+;o!B;$)KMs zS)XtfKVPfQi~wfOv=umRw{g}^4mdv*lN z8pW`*D(m$Cc!Ass*BM1+e0~5%qxzn=B|F;MuV zy@pB(*(C`mKk$|ZXnA~$XKbw1b$ChSTLV3TJAL6fKA+tv{~Eg?HC(`;G1H3YYs*M?8 z4?4M$Wc|8yqszw-7z9wPI&jirvgW+V1_3uD<~3@=k{HB{Wc3JX^JBhmc5vyydp~lg z2)AZCy83<3{g-LFV5@C{gKASyB_Wd(pFG3 z5}aZE%zyXm2Uvz)g@r@sppO68i6FqN;cft9ykHtR!G@LaDZfAy;*DLOexq{P^G-rNd2QaryM+>- zNND@ed#~We$UrDI4ER^^@H$8xmi*$9H4&6J@W}(XzC$gycE-_ByHeJ0MA=#y7cCSN%3B;9llwwTWR4;8@?68(Qi3ukCFXx$Mcg zNlWE~8fe#-O6cZ37ixdj-ue{@ZOgB&xPgKMc`$>PD{R0bfxc6Fc!LK1nCGp~mbr41 zz1b&f-A8^hjK$u(`R$IpMSwxzSgaD19Rf@f#5 zYh=Nf8W+QQSQ<}`r59|jGPGCnJod5Kde!_U9&03Jp%G4bz$1M;b@2?%LiNs)_(T`a zM(1)~UCaDh74X*Qv~9w@7sl&DOmm)$crkxZA{m!&Ci{&U6y+xQ-7_Ts&HEk|mb57O z->%8UcD7>a_fem%gV*n__iPM$1!t;0EV-_ACyHJpz|2pe1)#IKe1Cpjazmxv4nTH# z^QCw3bgjLmZ-z`RW#v??eY?$)(;4090{v$xWoPGs6=D2*fxzXi;VubgdULRgMAYP* zX74AyV~lQN%aeAat2g*9!dxR6ZRD=!1Du`(B2Uw169Rb8-p6~Ht_=Gt%o#@-7j{bW zy@7(O%4^qw?Zjee_DKou{oVGZ~NyJ7#5f&vwE zyuP(@H2t(OrY!c%lj3ta3e2x@Xy>Jd&?AI8 zxN!R9W+-tX$H}qI>i}e5jOnifF=(3R8VGsOlFyk9_>B6C6^ZpaZj;M;k-s|syXrVr zaMg!%ysQsiTX}^V+~iLXQXv`q%5l~UcPU@I%8Fx{0nV2yx8ul((Bo(bCz!ojhG+Ob zwQe3Q>Y$Z2>8c$A5Vl%*ee6j2rlX)1%Xbn_+DBh_MVXec_f}5?%z*FK;`YlYmA!-R z7`pp2kGYh)?^%#dtK)gFjC!M0Z$B@*Fb$OT)Z(6|pQl{tY@h;?Y?!!>h+R9wx-R+{ zJErn?^xFvm$cfI8gBkKdzS6}i7`Q3%4c9`=M~<2Dsc@N#242XWUj1A4C(M1j=Sl=ku zjdieay5)nO5$SPMwej{a)wMUJchv&J-q7P9Gj@ky+v;ZusHk|2!hyT@=-PsojKqo! zkt$QOTdUjn@7u$;fQ;xmQTB?xW{;hFh0C!B3kGcaZvfeJvi=e+#%&>mJY=Q=XF=YC z*|ev`c)SI{c)wWddxx}NjaBS;)7|bFVw*wum|`o?!7$){5LSRB_jcT3f*c?<60_Ngze3cRD#Q?kjFc9p%f5L zr?^!xU}IwX{6<${(2URs8odmPdCDXe?C|D%K3LUpid*#y*Goz4M==-H3^!-f0Z6Bo z`g)2!wVl3@vgA?|&;AsZrut2AT(y#+H0@q+gtvD##zg*#vx(KZ)3+56qE;sd5}%VY zGaG!J?i~NSe?14q^SHJD-`ZIec$E3twAd^?6IJb-ePf9oUH0hhsTrIw3N!S%mJ8~( zP{cD-wU_bd89P;}loeXV)LRU+chB6gYOdZsmJBfyzJT|1|ob%2?ugnh5NTl9W zSZn1sBe%%8K|$OR2BvxAk7Xa%6jL?Ve4Rw&mM#8!^ao9$eh6=YOZ?jp!Y_yXu3v2v zD(5=Y-O5}%`}@z%w|Y0;TOipeOl*vdWKFt-4V%RcjT}VuWdKa78N@90u3ca)%v!t$jm{` z6|uElyYWF7{ImISPC?JkR9lg>_XwOvs4LM5GIb4Bw+&m(%1`tG@Y8Ua4k$&(uCR-1jr=S4{()3vo1#7n<=kd4 z>88Qwx;5x&Jeqn}_<4t1*zJu$8w@b775G95>e();sPwOSc316fr5xG{F#Y``YBC># zG(XkB1Bm-AwsYs!l zYePEA!v+;nszkm?FX8RC>*uvlb^!%p!N4U)GHXG1r*uDrxpemWyCVy$DZUIVYn9H1 zp0m|g2>3{Yy&HiF=F8pGnLuG{e}%^%ug+dH4@J0(=P=iBbe_MuWen>E-Hr=o^AS<* zaG*kQ>u4$aHHx^W*?+wW9xMq0LhjPRPBwThlwPw_b+1}G^>u$;s7_^o?%R|z*?p14 zF4MX*2CP!5XzTuGRtr3P`CYDtS99q;4~=_`*C7d@IWD`FO29G@tE5Cn$PJS0iF}_5 zTCTFj!Cx_ zti5ZPwdc+)&1TvATAGi_QF=Es7|q(Fs@sb4qXUMx-s^}Gp)(PxKmcDEV;1Ka#67_B zOt)Ox#`9sQWGKQP)roEU-Ef4-0J5Q*kp>OTGmqTI+|2TQ7PiZ|;{#?UxdHt~k>zi2ES_XOvi7EsF^pj=~h9l*cpMOXPRb z|4u;{43#G23hGN(p#3j2oZj$aMOipDAWRm8Hjm!Tt&)gtlji=SHDiL{+Q5D?cl=pB ztu>eKtqq4ggHihC=ZQ5pP@nq+6a2+JwPq}c#axwL@?=ATw!Fg3^pMH#_@^8?Jdw(y^i@JkW+kP9w2x)6op7qw$>!9370ewcYyUA%h ztnVsc6)*K}WE3xX-?&tf2?DO*CeLrF-q{b^cjI~Q;{G6^zMfp2x`zR>CU+{3%KD9% zt|1ZN!7bw|EVuy%&lUk`-0A;Tv&Shg8Ew!i5zy};u*Iy_)Jc0x*oVOUB#4Q>siaF9=#z`7Kdshx7FBi$oX$tKe>YjqB> z4S11+J>Um=kRum3EHnSbZfo%QR6uFHrIcMX!Lvy5UG-^x^KR1cP1skMBSP)oM&;YJRTJwyQRUl;Z1z^XeL<(h4-WbeAp} zk1yoH+}uXSOgFIdOBGZ6yQ-Ampyj+zd)sx%j{Vts<^B5hEB8oRZ{)s5>Y}fBm%Qg- z7-3GR1&Nfkq6%5z>r1ZNoE4m{MB%aMqo7{3Y_j%#l zkDKy_{GW_9NY<+|sJ1Wn!caq%8>MmI&pbyAAf4LhiL^PlT8p)k$NIHEh=Me7yH>U! z7ohleM>7EIKrXH(09ySG*N(Qm#^M?z30P?m?4og6Z#T)1uJbHk^gs1L?d-kz*{t>G zVuLbs-;XC#QSlOXdjUH#(Jpg3&#+<9otH&w%~`%DM4QwD+b5x<8)izyKFsuYE_t>B zz=9k(fEvsxEZ911Ec94Jhs6NxVl#0+t4#gDnk~2*b-Q<`vZ7uMH?{@v_ZRRknLH1| ztz8I4n~VtRuC0ZAG>?8)ZmvV)&MU@?{OU^05%^Uj!a2Xjs|MQtt?%2VEz@&i6oi{Q z9j{Urp1<;U(d!2M&O#S^MRUC?gRas0P3kApN7+Y}&?Rdr3zwXvW6F+n#!|bNl@wn5 zxP@CzuRR!~Pn`WFaM^QlZW{i#$<{fse#T59#h}&RJI_wFGGE7A0!v@Va7Rz_cGEBKRzFm@Cdmt(;UGHJ>t7b`40 zzZ{LfS;=Ki2-3>q5Cx9|PxSI8`LNkWhWUC4LBKP^Z^58+_V*gjqQ!h!q;{Sp(qDpy z-5aro7uKw5P(hlUaNDglq;b1}6F1=dMl90s9cNz6IyeAMpFEW6JTv>Ne_Sf7dM9{p zR(&@kaXGX!MC~%P3+UiAJf`gfUl)tlEb~W5uzfxCm)!~d0w|~FqM@h$J`xy~WC71k zdy)JejiSV}mnYSB1rF=2h2%L{?L6xq?-) ztCbzIZZm^tLMb(bm`>SJg|1AEIRkl7iMPN(I$zv={6ZVCpT-~cJQTnqjn#y z)y8!{f!TDj-&Q$}b3>Z0$+m07uFk4E#blBI@3%T_nYXACT&_3tqb63bUVyc5D@aq;D=>FrW6;}^l7Zo+ghq{c%Cqze*mR&HZE+bE! z##E8p79{r|bBUN%DjwcBqx;KXM=}tT7)q&bJ zU!CuXubU7FE-^(_7ZVAEfgnMKpYUNO_wkyUrp3sPx%s!Q^%P4kC(*z-{(m~mh+=&u zl4+`VxVqwck4k)InH?XJw%&DeYTL{O@o6?!S%DVZ&AZbnDQ$XVK-n=kv8P@ny=!V8 zG7Oa077lq=7~|8oaym7TxLF5*MFH{0dmM#BhK~fKBq=tO|C!xaK|eEdhBNe__AOir zg#$?deew1C-p3aaAAoX;2?H4=Mruam--v$0=fbw8bX~xuVd;FK+Z`*L$oYqKglJgL72T=@<_)zR?Bx8 zeGkpfvc&h*+%LOWk`>n(+cH+7PLR7-=Tb~I8WNJ4b3PV}aW)rsVMHDwaR_7$fslYF zdU#8t&=6XG7OqQie@7ifIl?4=lffeG;}2dTMVGP$HP2Ly@vwqU4dOb&J^01ER%Kgo zSN@5vGf(dZZg#Ey_WtiSJAie5o3iNo_sPc6VNRN@efOhFUlp4)3qlk1UJ(}V4iYsk z7itYkH1A>EB9n{w_Nd8u+;iH;aQPbPWtBR(5HOq%mG~35m%?-)Nlssc)~Sfn$Bu8; zyWL(m&s?%C|3m))RN^J#NT1-9(4i`>ZcCpJcHNOM5e z{2fltUa$ad%g&_=y1%Pd{GgVntUhryoiVxd)!=9%{Nc>U^^^yQ3$xw)Z^TzLZYG^< zojvw|;WJC~wAioWXYK+=Nc1luS9p0MAFvE(GGxq)=}c73Q{4>dae|&A7{w96!Vx+W zJ%%BVMUV6mTNOw+w#FgKieiu)x#!(f7VuVf5#QfMDsaU{e@;6>qFow6KVK_Bz}%#- zw{_B0ceJ(bS6R9mvs!E*EC>i$#)Fb``Q{sduw3wW;5%S|UCuqc<=Ht^#6YJ5@;`~e zZz=~N^1k@CzwEFHLw3EL;0fu;--BUdPJBHJ-ySOJrH3RPy3M|LOPVtcZeH2Ng_~*C zs7#dCsAWGwD(;@YT7S#!u(jz!B5MVgcZV1peJ<@I#os!XVmhn9@Vms?c&Hp7&;j@O zn!qmRgzq{1el0K)y;(*)VK;H9dN%6i-MOs!0r^?WXghSkRFb5-`x2a6Iym2~QwxQ# zt#cAs0Z?2qX~q&bz~MwR*N~$SoYXl532;X;Fa7eh68(?5_rrp3NxBhr>Oe+9w|1(7 zy@KCSE#_eCmMB%1i3`SdwzyD)0HXnu%Et*|tif{y53}KhB8qr}oL^k`qCu`J1``&d zxQMv|FswuDq>4G+*l1~P1B zXs+?A5PtzTYXS4svM6z|5c0M&OM=W1XY<&+6cQQCyx#;FLaFETVAxL!PzQ(4wtmsm z9dp&v&71%h(O^{Cp=1BuYA(=TJR$bo|2t5YWpp?75*=>t!>(x2WgA$O9epNgCzTKY z`bbluzPIwL-_DDQa^%6hzDb8|Pq$nyJwW7eTx zAZjg*zPS3jXDv`_m%;Sg!BhI9(_XE!*H^;IWFmDuHi+o`{5@BGotEeAf1`iybU|sr zSk@GXez@YjbTVZ-`0cPoQo|ld?Z`4H%l%O6vVdxkBxU((cN6T}qsjtu%+-b^*EwaE zz@K&+ekT{SjT#5J%~dSalZlbs4a-ALKB(A`PHs- z+hvxox`>KeRBrz%sQfA}a#3&B#J#GtKhz}v3;y?afycYpm*p~{8d8f14->OU)m_pd zGT1mz@GWezU(4>ArAc1{WKNdW9g^9cc2xR4z!$NoWMK6(K&A2K4?uK8xp^OmOS@Sa zP~2}LdK8E6+}qv8>^QMUy1Ns6J4T5tPx2JGyHpR_;ATMpQB&Gd*`T}RnyoYrRH9_a zf2?Mes)2X)p@MVNd}zMmSYTbwsFk+kT^}y@7#|=vOI&f0&x3zmBs54OL)h|H+&<*! z-MU>)LMo)4&*##j0;2@#6L?TL@~5@z9vR8V33+yY)+40&o$mZS7Pt%tUF5O`Ev_ro z!Q`PCuxE`7D-J9>9Yg0^buIfFBlp1Bg!c>lA18#m3^Vb5vhd43Dd~1}{_}hr4JUQT z2^HtZyta|2ed?t_Wg<^j29Djd{nCGp?B$#3;CYh?boeVU5n@Ybt;m(Tfu(1z%BEWa z-iuVTCdi#mo*615Ypq@FuNI3oOad)_Gc(8f=)d+eEZh@M-B=SC&h$4)X6BoG zcX0zxZO2B(pB15)Mbj&SNz{+x^!Q9Ss6!q9PkYxIm1Mew8*@5R*-;!T%b3j4Vk#Rg z%xl@jZlYyCUQlaLN)a^)6|ZU9%yjW`3^gpRNik5=@RB!7t;q|arYVXJ*h(SsRv;Rt zc3!l8oVCvHIp>G``+l>gJa{p|fb``M>lN*l;%t9JecS8-aJn&P+?wXMtAkhQz8 zM%HrJ_K|P)2LYjo3}ZSC(9H964=bA}wqGDnzUweeR9UKhseIpK<4z3uF04T@5lZSv zIxrtbEnjZ652Mh#OXlRS!OCs!3OU4gV7FY6iZ9{mR;7t}ip$*S))MisfOh3g0Db)|J>l{*|}cGI5`4n2>LV02{et7_=3F-eLlGV`aAiHB#cbqSCu_GRZd zX?#JHeqUkP>dyhHp3M%Q&H|JPXvmd%Wa1U%-d(W-xS5KVWN}+Bn%Ut+DQz)gXH1CE zm1q!4=rDTV04;MZ#rs)7fMXXP{^Pp2)ehAalUTnp@~RCk!*I3ZcJi-Dsd_qYtaqRXd(kS^KfKQJZ{U*YIS(0^1V~VafT^`E@$ay9&`mn-1eR)fkxkP%$4toqy)*6a?l~{8=;BbEZ01 zjQ~D)eW9){G5))hYcY3n(UsSDML{PD41vd$$m8T(y4? zAJMUU3zb;{H`IA!bkILP(FWA#In*Z0V|o?hNb)nGPwQu}bJK5Rg>z!bjWjhzF$jD4 z`LiV!&YUcSYU8WeApM^5#SQBGeJSiRbyTK=0ODzRHEm2Vve03(sSchv&G*rUB2 zzy(Ljy|UF>(6D~c$~)V|HgKLnm!rcuU`!F+(8HGQHfRTvMV4k)klDPN7?WGp@Y+#2 z;&RLNQ()!qFVtjHIvA;{!%TGjwYGvD=tsgmLM4eyPA8FxwWt8p9nF4O9%1W|8sJ&X ze3WP@O-_HsoZl(h0}k5sMGu|reB0}ee@fG5# z#?Cc{^<(fu?d61B%lWDX@wQC7&+`KOvMPU&4w|tejR)pG5<8plp@7Pseqk&j1iV2% z`=DY>*}kr%D+w$Mp=ubm`F4V=x@(17#M@^Yg^RkmNg%kJBFW&3V9Qv&3W4|ajEpO_ zp1RjwO5A$q1R8U$=j^XU4+BS7UMsG(Gmajw>Cq@6wle~_k>X7sPx!dc3?j@cnMtWQ zagSa#P}uI{lfN(Q&Dox*+x3WWEiYXDv{2esx~0j;RgQ8ufmVnuM?7nZ4+%3HB(LWv zYM&!LRf}x9*Q!}~g}2@}&OM1$(I_`l`Zk`$ ztF57UFbUj=H|oub;|h{zdge}EN;{(Q)O%W{Nu}#)m_)UgQl*_}J3LzUHLC3X0YsIC zqnA%CV&yZjC?`+g0CztyyF4@eP#kJ~7NCAvXchzlg>e9N<=*J@IOAY$0R~CF0N-B? z3l%bI6K`4~M0`*qEX!!AK)KN+_<3bYQlz(Z`1`_NWms|-8XbPA+t7RkjkQiT@s3tk zJEpCio&9s2R{>O`&1$VCmXtL+p0lNF^{A9^sgTSk`v_Hbp5={$Ts|Fb2fw&()6b_O zQVJ1T3@B1DYHg<|AoPyKj;OY%oJi_NfO)B(BG@mb!0ongWtUv}Dl|y1PEx%`w}7|N zgR!A4G=Q{qwv|#H@EcDy^{q$<1<@z8(XwLY;)909qF#bow`_t92DYj7-Mr6`uUZ^R zS8OsrG(N6~?Qq|7!NL8iK)1?~U{cHnFUUCvW+bRg4?5-A6u$$shng@$SiLFa$WVq~ z4Z9&gFruR|^ki1u!sFd4uTO`HY&nY0{4;N92%X{VUVUOxwsC)3;sg+|GhmT`;Z{i? zSZz>!8O=wHKXaZ@ffRL#o5;C-Pq+uhY^NqfB4tD-Zpi*bbm%$lh9P+>@6RguE3kom z?sr_R3FhYz(g5%#&%=O@y4%R!POJTINqzfAi=P{r(`0v%K0!pXsxc5ZEi1Vzs#mEn z*v+n_L9(ynbac$1g3<1nTmCf|j3NP$6JpKJAs2$f8tvl=KJzjFKlQinAz})FdC%q4 zpi6>Ba}XE#x-b6g%>Yf(Qpr8Z2lTQJRCvcQsUyv*W}s-(nCLL%yMRy_-h!{l^5clZS(ZV(iyuG=7jzTC+_K+VGw+|z zhgAyIy>Ct#5LZR08pH{(F~{{;jm>7V&uPq;cyw444>G3nDfgVLGzEDVno-1-?zQup zNl~qHy`Sgk6B^#0mfc0M4W*1#sqxu!FRX2_q}1&XN$&YcSW>izRO2e5C^=2n1C{e! zUZKqX_}n~xyF1KGbXPUW?j9I|>`NvH$sZOvsX#URpVcK%4WbQJO_>m_qFr}TrR69# z{SR{YxQuZc>0-$;WmDMt6Es4hMb5PM43a)EnDG*%`IshqZ=(FvCF%_>ikZ9ZFyCcs z-Tg+Au&%6N=dR_g6g7Tix3hMJuwI$E+5F1^juR<$GjWeVu>8fY3o-oTbyQeo@72fS z30AzV*>khDgR{I3rV_`Ew^7E3V+gAI-Z~ym3W@K7A0^es-}i}WlL>U%^O)qU`679w zkxVdo6S?u4uc0}Cz2nUvYXO>8dN$K?O`p|5l5ez^QZEIp=rIrCNp|g0uVN~7Or7%A z2fA&mumutZ%f8~l@RIAmt0#%RL4vpMGjgMx6=sUA9i5ZRIfU6P)&j*Fz!CD(2b)<{ z*P|yjhFdj!qNbjAIsKuv4M)GH3v;pVwka?#Cmd+-jqdvyJEU@TD7AnYqB59 ze=ogo&#%LEE`(1;4$t+MUl6hhPaFnlTBc}u6d)8o-iD*=OYGIACRYXo_)#%GGVFJM zc5k!9FsYqu9_UBq6S>p5E3LYo@}@Q^9h>`KPR?0yyV@2g!MgQ;)5f_RB`r1wHDlwM zolItj^X6*Y#X*ygkT6d`Sqr-|qKf5BlfcBqv@&sY;4fp1VN(B-TKa0NiVWOz-!Co; zpMX@^ zCEXA=`P|d@moTryMn|&XE@FA+!Tt529OYYf?DQXUne9SsKX~2(oX6m3;bKIa$~g#2 zK%()mC@+4~xCrvEcveY*2{89zG2fwD=n|=meRx?Gn4nHo=7tjH2V?S>mR)ZkLuY^P zD}V9^t_gzif#IjU2x!d^iKdy{L8_R*4REnuh^QO901Y=TB}ZU^PRkFwW%Kp|d}1W! zU~pD+4g}VR=s8uL7>Q>;6m9wLBD1hWe6!~1F!k!*hrzW@@`!5YM%`YAU75``pu*!u zzN$(8%2@$w&QhzU^S?>$_t=8p^{M;Ktwlh+`0{-d+pk~x;?ke9baIy7j-}AC6du1N zg~jBq!ZCky?nrSMU@lt*$v$z!4)yoned&%s?Ye{qmhiw59$3NyOL$-j5B$&Zz*0v3 f|I5g+U;eQz5`FUbb%)n11D_K|9gh?qIv@9M^_6Pb literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/react.png b/.github/static/img/eino/react.png new file mode 100644 index 0000000000000000000000000000000000000000..a92ada1efb484ba6d968df71af8ce754408eb4a4 GIT binary patch literal 83594 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 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/simple_chain.png b/.github/static/img/eino/simple_chain.png new file mode 100644 index 0000000000000000000000000000000000000000..cc49a7b8b00d44eb9e07f1c802902771f38a9526 GIT binary patch literal 62136 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 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/simple_workflow.png b/.github/static/img/eino/simple_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..e74afd244dc2fecf8108439b864da6e44268886a GIT binary patch literal 231593 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

gXsGIQNR>w;4-Htsp=RJu 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 z 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 z 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 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/stream.png b/.github/static/img/eino/stream.png new file mode 100644 index 0000000000000000000000000000000000000000..17f718d69ba74ab8a70e4c7b8664dfd08a4d151f GIT binary patch literal 493369 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�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 literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/tool_call_graph.png b/.github/static/img/eino/tool_call_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..2fd8c9a5df0404c03b13331033b0607e5b8d31e4 GIT binary patch literal 192317 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%0DQN_CPTC@3@E-pDsDXaYkqaP$l*$-qJ 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≫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 QKt8&ZxV%`Yh=Je#1H?bPga7~l literal 0 HcmV?d00001 diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..3f5bbac --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,156 @@ +name: Pull Request Check + +on: [ pull_request ] + +jobs: + compliant: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check License Header + uses: apache/skywalking-eyes/header@v0.4.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check Spell + uses: crate-ci/typos@v1.42.3 + + golangci-lint: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.18 + # for self-hosted, the cache path is shared across projects, + # and it works well without the cache of GitHub actions + # Enable it if we're going to use GitHub only + cache: true + + - name: Golang CI Lint + # https://golangci-lint.run/ + uses: golangci/golangci-lint-action@v9.2.0 + with: + version: v2.8.0 + args: --timeout 5m + + commit-msg-check: + name: Commit Message Check + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@v4 + - name: Validate commit messages format and scope + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const pr = context.payload.pull_request; + if (!pr) { + core.setFailed('This workflow must run on pull_request events.'); + return; + } + + let allowedTypes = []; + let allowedScopes = []; + try { + const raw = fs.readFileSync('.github/.commit-rules.json', 'utf8'); + const cfg = JSON.parse(raw); + allowedTypes = Array.isArray(cfg.allowedTypes) ? cfg.allowedTypes : []; + allowedScopes = Array.isArray(cfg.allowedScopes) ? cfg.allowedScopes : []; + } catch (e) { + core.setFailed('Cannot read .github/.commit-rules.json: ' + e.message); + return; + } + if (!allowedTypes.length) { + core.setFailed('allowedTypes is empty in .github/.commit-rules.json'); + return; + } + + const { owner, repo } = context.repo; + const pull_number = pr.number; + const commits = await github.paginate( + github.rest.pulls.listCommits, + { owner, repo, pull_number, per_page: 100 } + ); + + let errors = []; + + for (const c of commits) { + const sha = c.sha.slice(0, 7); + const subject = (c.commit.message || '').split('\n')[0]; + const m = subject.match(/^([a-z]+)(\(([a-z0-9\-\/]+)\))?:\s(.+)$/); + if (!m) { + errors.push(`(${sha}) invalid format: "${subject}"`); + continue; + } + + const type = m[1]; + const scope = m[3]; // may be undefined + const desc = m[4]; + + if (!allowedTypes.includes(type)) { + errors.push(`(${sha}) invalid type "${type}"`); + } + + if (!desc || !desc.trim()) { + errors.push(`(${sha}) description must be non-empty`); + } + + if (scope) { + const topScope = scope.split('/')[0]; + if (allowedScopes.length && !allowedScopes.includes(topScope)) { + errors.push(`(${sha}) invalid scope "${scope}"`); + } + } + } + + if (errors.length) { + core.setFailed('Commit message check failed:\n' + errors.join('\n')); + } else { + core.info('All commit messages conform to "(optional scope): " and scope rules.'); + } + + pr-title-check: + name: PR Title Check + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@v4 + - name: Read commit rules + id: rules + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + let cfg; + try { + const raw = fs.readFileSync('.github/.commit-rules.json', 'utf8'); + cfg = JSON.parse(raw); + } catch (e) { + core.setFailed('Cannot read .github/.commit-rules.json: ' + e.message); + return; + } + const toMultiline = (list) => Array.isArray(list) ? list.join('\n') : ''; + core.setOutput('types', toMultiline(cfg.allowedTypes)); + core.setOutput('scopes', toMultiline(cfg.allowedScopes)); + - name: Validate PR title + uses: amannn/action-semantic-pull-request@v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: ${{ steps.rules.outputs.types }} + scopes: ${{ steps.rules.outputs.scopes }} + requireScope: false diff --git a/.github/workflows/tag-notification.yml b/.github/workflows/tag-notification.yml new file mode 100644 index 0000000..2b7ea3e --- /dev/null +++ b/.github/workflows/tag-notification.yml @@ -0,0 +1,120 @@ +name: Tag Notification + +on: + push: + tags: + - 'v*' + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Fetch tag info + run: | + git fetch --tags -f + + - name: Get tag info and send notification + run: | + # Get the tag name + TAG_NAME="${{ github.ref_name }}" + echo "Processing tag: $TAG_NAME" + + # Get tag message + echo "Getting tag message..." + TAG_MESSAGE=$(git tag -l --format='%(contents)' "$TAG_NAME") + echo "Tag message:" + echo "$TAG_MESSAGE" + echo "---" + + # Create base content parts + HEADER="### 🏷️ Eino New Tag Created: \`$TAG_NAME\`" + VERSION_INFO="📦 Version: \`$TAG_NAME\`" + + # Prepare the message parts for jq + if [ ! -z "$TAG_MESSAGE" ]; then + # Pass all parts to jq and let it handle the formatting + jq -n \ + --arg header "$HEADER" \ + --arg version "$VERSION_INFO" \ + --arg notes "$TAG_MESSAGE" \ + --arg repo_url "https://github.com/${{ github.repository }}/releases/tag/$TAG_NAME" \ + '{ + "msg_type": "interactive", + "card": { + "elements": [ + { + "tag": "markdown", + "content": ($header + "\n\n" + $version + "\n\n### 📝 Release Notes:\n" + $notes) + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "🔗 View Tag" + }, + "url": $repo_url, + "type": "default" + } + ] + } + ], + "header": { + "title": { + "tag": "plain_text", + "content": "🏷️ Eino New Tag Created" + } + } + } + }' > webhook_payload.json + else + # Without release notes + jq -n \ + --arg header "$HEADER" \ + --arg version "$VERSION_INFO" \ + --arg repo_url "https://github.com/${{ github.repository }}/releases/tag/$TAG_NAME" \ + '{ + "msg_type": "interactive", + "card": { + "elements": [ + { + "tag": "markdown", + "content": ($header + "\n\n" + $version) + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "🔗 View Tag" + }, + "url": $repo_url, + "type": "default" + } + ] + } + ], + "header": { + "title": { + "tag": "plain_text", + "content": "🏷️ Eino New Tag Created" + } + } + } + }' > webhook_payload.json + fi + + # Send webhook + curl -X POST \ + -H "Content-Type: application/json" \ + -d @webhook_payload.json \ + "${{ secrets.FEISHU_WEBHOOK_URL }}" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7748257 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,262 @@ +name: Eino Tests + +on: + pull_request: + push: + branches: + - main + +env: + DEFAULT_GO_VERSION: "1.18" + +jobs: + unit-test: + name: eino-unit-test + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + env: + COVERAGE_FILE: coverage.out + BREAKDOWN_FILE: main.breakdown + + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.DEFAULT_GO_VERSION }} + + - name: Exec Go Test + run: | + modules=`find . -name "go.mod" -exec dirname {} \;` + echo $modules + list="" + coverpkg="" + if [[ ! -f "go.work" ]];then go work init;fi + for module in $modules; do go work use $module; list=$module"/... "$list; coverpkg=$module"/...,"$coverpkg; done + go work sync + go test -race -v -coverprofile=${{ env.COVERAGE_FILE }} -gcflags="all=-l -N" -coverpkg=$coverpkg $list + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + name: eino-unit-test + env_vars: GOLANG,EINO + files: ${{ env.COVERAGE_FILE }} + token: ${{ secrets.CODECOV_TOKEN }} + codecov_yml_path: ./github/.codecov.yml + + benchmark-test: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.DEFAULT_GO_VERSION }} + + - name: Run Benchmark Tests + run: go test -bench=. -benchmem -run=none ./... + + compatibility-test: + strategy: + matrix: + go: [ "1.19", "1.20", "1.21", "1.22", "1.23", "1.24" ] + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + + - name: Compatibility Test + run: | + # just basic unit test, no coverage report + go test -race ./... + + api-compatibility: + name: api-compatibility-check + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Install go-apidiff + run: go install github.com/joelanford/go-apidiff@v0.8.2 + + - name: Check API compatibility + id: apidiff + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + HEAD_SHA=${{ github.event.pull_request.head.sha }} + + echo "Checking API compatibility between $BASE_SHA and $HEAD_SHA" + + go mod tidy + + if ! DIFF_OUTPUT=$(go-apidiff $BASE_SHA $HEAD_SHA 2>&1); then + echo "go-apidiff output: $DIFF_OUTPUT" + fi + + echo "diff_output<> $GITHUB_ENV + echo "$DIFF_OUTPUT" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + if echo "$DIFF_OUTPUT" | grep -q "Incompatible changes:"; then + echo "has_breaking_changes=true" >> $GITHUB_OUTPUT + else + echo "has_breaking_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Create Review Thread + if: steps.apidiff.outputs.has_breaking_changes == 'true' + continue-on-error: true + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const reviewComments = await github.rest.pulls.listReviewComments({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + const existingPackageComments = new Map(); + + for (const comment of reviewComments.data) { + if (comment.body.includes('Breaking API Changes Detected')) { + const packageMatch = comment.body.match(/Package: `([^`]+)`/); + if (packageMatch) { + const pkg = packageMatch[1]; + if (!existingPackageComments.has(pkg)) { + existingPackageComments.set(pkg, new Set()); + } + existingPackageComments.get(pkg).add(comment.path); + } + } + } + + const files = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + const diffOutput = process.env.diff_output || ''; + + const breakingChanges = new Map(); + + let currentPackage = ''; + let isInIncompatibleSection = false; + const lines = diffOutput.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (line.startsWith('github.com/')) { + currentPackage = line; + if (!breakingChanges.has(currentPackage)) { + breakingChanges.set(currentPackage, []); + } + continue; + } + + if (line === 'Incompatible changes:') { + isInIncompatibleSection = true; + continue; + } + + if (line === '') { + isInIncompatibleSection = false; + continue; + } + + if (isInIncompatibleSection && line.startsWith('- ')) { + const change = line.substring(2); + if (currentPackage) { + breakingChanges.get(currentPackage).push(change); + } + } + } + + const changedFiles = files.data; + + for (const [pkg, changes] of breakingChanges) { + if (changes.length === 0) continue; + + const pkgPath = pkg.split('/').slice(3).join('/'); + const matchingFile = changedFiles.find(file => + file.filename.includes(pkgPath) + ) || changedFiles[0]; + + const hasCommentForPackage = existingPackageComments.has(pkg) && + existingPackageComments.get(pkg).has(matchingFile.filename); + + if (matchingFile && !hasCommentForPackage) { + const changesList = changes.map(change => { + const [name, desc] = change.split(':').map(s => s.trim()); + return `- **${name}:** ${desc}`; + }).join('\n'); + + const commentBody = [ + '🚨 **Breaking API Changes Detected**', + '', + `Package: \`${pkg}\``, + '', + 'Incompatible changes:', + changesList, + '', + '

', + 'Review Guidelines', + '', + 'Please ensure that:', + '- The changes are absolutely necessary', + '- They are properly documented', + '- Migration guides are provided if needed', + '
', + '', + '⚠️ Please resolve this thread after reviewing the breaking changes.' + ].join('\n'); + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + event: 'COMMENT', + comments: [{ + path: matchingFile.filename, + position: matchingFile.patch ? matchingFile.patch.split('\n').findIndex(line => line.startsWith('+')) + 1 : 1, + body: commentBody + }] + }); + + if (!existingPackageComments.has(pkg)) { + existingPackageComments.set(pkg, new Set()); + } + existingPackageComments.get(pkg).add(matchingFile.filename); + } + } \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65ff0af --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# the result of the go build +output* +output/* + +# Files generated by IDEs +.idea/ +*.iml + +# Vim swap files +*.swp + +# Vscode files +.vscode + +/patches + +/vendor + +# Trae files +.trae + +# Specs files (internal documentation) +**/specs/ + +# Reports (generated analysis files) +reports/ +/todos + +.DS_Store +*.log* +.claude +CLAUDE.md +*.jsonl +*.txt +.agents/ + +# Specs directories +*/specs +/todos +/.claude/ + +# Internal dev setup (not for public repo) +/scripts/dev_setup_internal.sh diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..d84b83a --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,148 @@ +# output configuration options +version: "2" +# All available settings of specific linters. +# Refer to https://golangci-lint.run/usage/linters +linters: + default: standard + enable: + - revive + - godoclint + - funlen + - cyclop + disable: + - errcheck + - staticcheck + - unused + - ineffassign + exclusions: + generated: lax + paths: + - ".*_test.go" + - ".*_mock.go" + rules: + - path: "^internal/.*" + linters: + - revive + - text: "var-naming: don't use underscores in Go names" + linters: + - revive + - path: "/utils/" + text: "var-naming: avoid meaningless package names" + linters: + - revive + - text: "exported: type name will be used as agent.AgentOption by other packages" + linters: + - revive + - path: "adk/prebuilt/deep/task_tool.go" + text: "argument-limit: maximum number of arguments per function exceeded" + linters: + - revive + - path: "compose/component_to_graph_node.go" + text: "argument-limit: maximum number of arguments per function exceeded" + linters: + - revive + - path: "compose/graph_run.go" + text: "argument-limit: maximum number of arguments per function exceeded" + linters: + - revive + - path: "adk/workflow.go" + text: "argument-limit: maximum number of arguments per function exceeded" + linters: + - revive + - path: "compose/graph.go" + linters: + - cyclop + text: "calculated cyclomatic complexity for function compile" + - path: "schema/message.go" + linters: + - cyclop + text: "calculated cyclomatic complexity for function ConcatMessages" + - path: "compose/graph_run.go" + linters: + - cyclop + text: "calculated cyclomatic complexity for function run" + - path: "compose/graph.go" + linters: + - funlen + text: "Function 'compile' is too long" + - path: "compose/graph_run.go" + linters: + - funlen + text: "Function 'run' is too long" + settings: + govet: + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + disable: + - fieldalignment + revive: + # Sets the default failure confidence. + # This means that linting errors with less than 0.8 confidence will be ignored. + # Default: 0.8 + confidence: 0.8 + rules: + # Exported function and methods should have comments. + - name: exported + severity: error + exclude: + - "^internal/.*" + arguments: + - "disable-checks-on-constants" + - "disable-checks-on-variables" + - "disable-checks-on-types" + - "disable-checks-on-methods" + - name: package-comments + disabled: false + - name: var-naming + disabled: false + arguments: + # AllowList + - [ "utils", "s_", "err_", "err__", "plan_", "userInput_", "executedSteps_", "executedStep_", "iterator_", "in_", "out_" ] + # DenyList + - [ ] + - - extra-bad-package-names: + - helpers + - models + - name: argument-limit + arguments: [ 6 ] + - name: function-length + arguments: [ 120, 0 ] + godoclint: + check-exported: true + require-package-documentation: true + funlen: + lines: 200 + statements: 120 + cyclop: + max-complexity: 40 + package-average: 20 + +formatters: + enable: + - gci + - gofmt + settings: + gofmt: + # Simplify code: gofmt with `-s` option. + # Default: true + simplify: true + # Apply the rewrite rules to the source before reformatting. + # https://pkg.go.dev/cmd/gofmt + # Default: [] + rewrite-rules: + - pattern: 'interface{}' + replacement: 'any' + - pattern: 'a[b:len(a)]' + replacement: 'a[b:]' + gci: + # Section configuration to compare against. + # Section names are case-insensitive and may contain parameters in (). + # The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`. + # If `custom-order` is `true`, it follows the order of `sections` option. + # Default: ["standard", "default"] + sections: + - standard + - default + - localmodule + custom-order: true diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..9a13260 --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,27 @@ +header: + license: + spdx-id: Apache-2.0 + copyright-owner: CloudWeGo Authors + + template: | + /* + * Copyright {{ .Year }} CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + paths: + - '**/*.go' + - '**/*.s' + + comment: on-failure \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..25ceb89 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +conduct@cloudwego.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..db3d580 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# How to Contribute + +## Your First Pull Request +We use GitHub for our codebase. You can start by reading [How To Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). + +## Branch Organization +We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) as our branch organization, as known as [FDD](https://en.wikipedia.org/wiki/Feature-driven_development) + +## Bugs +### 1. How to Find Known Issues +We are using [Github Issues](https://github.com/cloudwego/{project_name}/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist. + +### 2. Reporting New Issues +Providing a reduced test code is a recommended way for reporting issues. Then can place in: +- Just in issues +- [Golang Playground](https://play.golang.org/) + +### 3. Security Bugs +Please do not report the safe disclosure of bugs to public issues. Contact us by [Support Email](mailto:conduct@cloudwego.io) + +## How to Get in Touch +- [Email](mailto:conduct@cloudwego.io) + +## Submit a Pull Request +Before you submit your Pull Request (PR) consider the following guidelines: +1. Search [GitHub](https://github.com/cloudwego/{project_name}/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts. +2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. Discussing the design upfront helps to ensure that we're ready to accept your work. +3. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the cloudwego {project_name} repo. +4. In your forked repository, make your changes in a new git branch: + ``` + git checkout -b my-fix-branch develop + ``` +5. Create your patch, including appropriate test cases. +6. Follow our [Style Guides](#code-style-guides). +7. Commit your changes using a descriptive commit message that follows [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit). + Adherence to these conventions is necessary because release notes are automatically generated from these messages. +8. Push your branch to GitHub: + ``` + git push origin my-fix-branch + ``` +9. In GitHub, send a pull request to `{project_name}:develop` + +## Contribution Prerequisites +- Our development environment keeps up with [Go Official](https://golang.org/project/). +- You need fully checking with lint tools before submit your pull request. [gofmt](https://golang.org/pkg/cmd/gofmt/) and [golangci-lint](https://github.com/golangci/golangci-lint) +- You are familiar with [GitHub](https://github.com) +- Maybe you need familiar with [Actions](https://github.com/features/actions)(our default workflow tool). + +## Code Style Guides +- [Effective Go](https://golang.org/doc/effective_go) +- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..66a27ec --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,177 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a74ba79 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# Eino + +![coverage](https://raw.githubusercontent.com/cloudwego/eino/badges/.badges/main/coverage.svg) +[![Release](https://img.shields.io/github/v/release/cloudwego/eino)](https://github.com/cloudwego/eino/releases) +[![WebSite](https://img.shields.io/website?up_message=cloudwego&url=https%3A%2F%2Fwww.cloudwego.io%2F)](https://www.cloudwego.io/) +[![License](https://img.shields.io/github/license/cloudwego/eino)](https://github.com/cloudwego/eino/blob/main/LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/cloudwego/eino)](https://goreportcard.com/report/github.com/cloudwego/eino) +[![OpenIssue](https://img.shields.io/github/issues/cloudwego/eino)](https://github.com/cloudwego/kitex/eino) +[![ClosedIssue](https://img.shields.io/github/issues-closed/cloudwego/eino)](https://github.com/cloudwego/eino/issues?q=is%3Aissue+is%3Aclosed) +![Stars](https://img.shields.io/github/stars/cloudwego/eino) +![Forks](https://img.shields.io/github/forks/cloudwego/eino) + +English | [中文](README.zh_CN.md) + +# Overview + +**Eino['aino]** is an LLM application development framework in Golang. It draws from LangChain, Google ADK, and other open-source frameworks, and is designed to follow Golang conventions. + +Eino provides: +- **[Components](https://github.com/cloudwego/eino-ext)**: reusable building blocks like `ChatModel`, `Tool`, `Retriever`, and `ChatTemplate`, with official implementations for OpenAI, Ollama, and more. +- **Agent Development Kit (ADK)**: build AI agents with tool use, multi-agent coordination, context management, interrupt/resume for human-in-the-loop, and ready-to-use agent patterns. +- **Composition**: connect components into graphs and workflows that can run standalone or be exposed as tools for agents. +- **[Examples](https://github.com/cloudwego/eino-examples)**: working code for common patterns and real-world use cases. + +![](.github/static/img/eino/eino_concept.jpeg) + +# Quick Start + +## ChatModelAgent + +Configure a ChatModel, optionally add tools, and you have a working agent: + +```Go +chatModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{ + Model: "gpt-4o", + APIKey: os.Getenv("OPENAI_API_KEY"), +}) + +agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Model: chatModel, +}) + +runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) +iter := runner.Query(ctx, "Hello, who are you?") +for { + event, ok := iter.Next() + if !ok { + break + } + fmt.Println(event.Message.Content) +} +``` + +Add tools to give the agent capabilities: + +```Go +agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Model: chatModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{weatherTool, calculatorTool}, + }, + }, +}) +``` + +The agent handles the ReAct loop internally — it decides when to call tools and when to respond. + +→ [ChatModelAgent examples](https://github.com/cloudwego/eino-examples/tree/main/adk/intro) · [docs](https://www.cloudwego.io/docs/eino/core_modules/eino_adk/agent_implementation/chat_model/) + +## DeepAgent + +For complex tasks, use DeepAgent. It breaks down problems into steps, delegates to sub-agents, and tracks progress: + +```Go +deepAgent, _ := deep.New(ctx, &deep.Config{ + ChatModel: chatModel, + SubAgents: []adk.Agent{researchAgent, codeAgent}, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{shellTool, pythonTool, webSearchTool}, + }, + }, +}) + +runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: deepAgent}) +iter := runner.Query(ctx, "Analyze the sales data in report.csv and generate a summary chart") +``` + +DeepAgent can be configured to coordinate multiple specialized agents, run shell commands, execute Python code, and search the web. + +→ [DeepAgent example](https://github.com/cloudwego/eino-examples/tree/main/adk/multiagent/deep) · [docs](https://www.cloudwego.io/docs/eino/core_modules/eino_adk/agent_implementation/deepagents/) + +## Composition + +When you need precise control over execution flow, use `compose` to build graphs and workflows: + +```Go +graph := compose.NewGraph[*Input, *Output]() +graph.AddLambdaNode("validate", validateFn) +graph.AddChatModelNode("generate", chatModel) +graph.AddLambdaNode("format", formatFn) + +graph.AddEdge(compose.START, "validate") +graph.AddEdge("validate", "generate") +graph.AddEdge("generate", "format") +graph.AddEdge("format", compose.END) + +runnable, _ := graph.Compile(ctx) +result, _ := runnable.Invoke(ctx, input) +``` + +Compositions can be exposed as tools for agents, bridging deterministic workflows with autonomous behavior: + +```Go +tool, _ := graphtool.NewInvokableGraphTool(graph, "data_pipeline", "Process and validate data") + +agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Model: chatModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool}, + }, + }, +}) +``` + +This lets you build domain-specific pipelines with exact control, then let agents decide when to use them. + +→ [GraphTool examples](https://github.com/cloudwego/eino-examples/tree/main/adk/common/tool/graphtool) · [compose docs](https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/) + +# Key Features + +## Component Ecosystem + +Eino defines component abstractions (ChatModel, Tool, Retriever, Embedding, etc.) with official implementations for OpenAI, Claude, Gemini, Ark, Ollama, Elasticsearch, and more. + +→ [eino-ext](https://github.com/cloudwego/eino-ext) + +## Stream Processing + +Eino automatically handles streaming throughout orchestration: concatenating, boxing, merging, and copying streams as data flows between nodes. Components only implement the streaming paradigms that make sense for them; the framework handles the rest. + +→ [docs](https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/stream_programming_essentials/) + +## Callback Aspects + +Inject logging, tracing, and metrics at fixed points (OnStart, OnEnd, OnError, OnStartWithStreamInput, OnEndWithStreamOutput) across components, graphs, and agents. + +→ [docs](https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/callback_manual/) + +## Interrupt/Resume + +Any agent or tool can pause execution for human input and resume from checkpoint. The framework handles state persistence and routing. + +→ [docs](https://www.cloudwego.io/docs/eino/core_modules/eino_adk/agent_hitl/) · [examples](https://github.com/cloudwego/eino-examples/tree/main/adk/human-in-the-loop) + +# Framework Structure + +![](.github/static/img/eino/eino_framework.jpeg) + +The Eino framework consists of: + +- Eino (this repo): Type definitions, streaming mechanism, component abstractions, orchestration, agent implementations, aspect mechanisms + +- [EinoExt](https://github.com/cloudwego/eino-ext): Component implementations, callback handlers, usage examples, evaluators, prompt optimizers + +- [Eino Devops](https://github.com/cloudwego/eino-ext/tree/main/devops): Visualized development and debugging + +- [EinoExamples](https://github.com/cloudwego/eino-examples): Example applications and best practices + +## Documentation + +- [Eino User Manual](https://www.cloudwego.io/zh/docs/eino/) +- [Eino: Quick Start](https://www.cloudwego.io/zh/docs/eino/quick_start/) + +## Dependencies +- Go 1.18 and above. + +## Code Style + +This repo uses `golangci-lint`. Check locally with: + +```bash +golangci-lint run ./... +``` + +Rules enforced: +- Exported functions, interfaces, packages, etc. should have GoDoc comments +- Code should be formatted with `gofmt -s` +- Import order should follow `goimports` (std -> third party -> local) + +## Security + +If you discover a potential security issue in this project, or think you may +have discovered a security issue, we ask that you notify Bytedance Security via our [security center](https://security.bytedance.com/src) or [vulnerability reporting email](mailto:sec@bytedance.com?subject=Feedback%20On%20Eino). + +Do **not** create a public GitHub issue. + +## Contact +- Membership: [COMMUNITY MEMBERSHIP](https://github.com/cloudwego/community/blob/main/COMMUNITY_MEMBERSHIP.md) +- Issues: [Issues](https://github.com/cloudwego/eino/issues) +- Lark: Scan the QR code below with [Feishu](https://www.feishu.cn/en/) to join the CloudWeGo/eino user group. + +    LarkGroup + +## License + +This project is licensed under the [Apache-2.0 License](LICENSE-APACHE). diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..c9b8ef7 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`cloudwego/eino` +- 原始仓库:https://github.com/cloudwego/eino +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README.zh_CN.md b/README.zh_CN.md new file mode 100644 index 0000000..d21e7a1 --- /dev/null +++ b/README.zh_CN.md @@ -0,0 +1,210 @@ +# Eino + +![coverage](https://raw.githubusercontent.com/cloudwego/eino/badges/.badges/main/coverage.svg) +[![Release](https://img.shields.io/github/v/release/cloudwego/eino)](https://github.com/cloudwego/eino/releases) +[![WebSite](https://img.shields.io/website?up_message=cloudwego&url=https%3A%2F%2Fwww.cloudwego.io%2F)](https://www.cloudwego.io/) +[![License](https://img.shields.io/github/license/cloudwego/eino)](https://github.com/cloudwego/eino/blob/main/LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/cloudwego/eino)](https://goreportcard.com/report/github.com/cloudwego/eino) +[![OpenIssue](https://img.shields.io/github/issues/cloudwego/eino)](https://github.com/cloudwego/kitex/eino) +[![ClosedIssue](https://img.shields.io/github/issues-closed/cloudwego/eino)](https://github.com/cloudwego/eino/issues?q=is%3Aissue+is%3Aclosed) +![Stars](https://img.shields.io/github/stars/cloudwego/eino) +![Forks](https://img.shields.io/github/forks/cloudwego/eino) + +[English](README.md) | 中文 + +# 简介 + +**Eino['aino]** 是一个 Go 语言的 LLM 应用开发框架,借鉴了 LangChain、Google ADK 等开源项目,按照 Go 的惯例设计。 + +Eino 提供: +- **[组件](https://github.com/cloudwego/eino-ext)**:`ChatModel`、`Tool`、`Retriever`、`ChatTemplate` 等可复用模块,官方实现覆盖 OpenAI、Ollama 等 +- **智能体开发套件(ADK)**:支持工具调用、多智能体协同、上下文管理、中断/恢复等人机交互,以及开箱即用的智能体模式 +- **编排**:把组件组装成图或工作流,既能独立运行,也能作为工具给智能体调用 +- **[示例](https://github.com/cloudwego/eino-examples)**:常见模式和实际场景的可运行代码 + +![](.github/static/img/eino/eino_concept.jpeg) + +# 快速上手 + +## ChatModelAgent + +配置好 ChatModel,加上工具(可选),就能跑起来: + +```Go +chatModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{ + Model: "gpt-4o", + APIKey: os.Getenv("OPENAI_API_KEY"), +}) + +agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Model: chatModel, +}) + +runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) +iter := runner.Query(ctx, "Hello, who are you?") +for { + event, ok := iter.Next() + if !ok { + break + } + fmt.Println(event.Message.Content) +} +``` + +加工具让智能体有更多能力: + +```Go +agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Model: chatModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{weatherTool, calculatorTool}, + }, + }, +}) +``` + +智能体内部自动处理 ReAct 循环,自己判断什么时候调工具、什么时候回复。 + +→ [ChatModelAgent 示例](https://github.com/cloudwego/eino-examples/tree/main/adk/intro) · [文档](https://www.cloudwego.io/zh/docs/eino/core_modules/eino_adk/agent_implementation/chat_model/) + +## DeepAgent + +复杂任务用 DeepAgent,它会把问题拆成步骤,分派给子智能体,并追踪进度: + +```Go +deepAgent, _ := deep.New(ctx, &deep.Config{ + ChatModel: chatModel, + SubAgents: []adk.Agent{researchAgent, codeAgent}, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{shellTool, pythonTool, webSearchTool}, + }, + }, +}) + +runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: deepAgent}) +iter := runner.Query(ctx, "Analyze the sales data in report.csv and generate a summary chart") +``` + +DeepAgent 可以配置成:协调多个专业智能体、跑 shell 命令、执行 Python、搜索网络。 + +→ [DeepAgent 示例](https://github.com/cloudwego/eino-examples/tree/main/adk/multiagent/deep) · [文档](https://www.cloudwego.io/zh/docs/eino/core_modules/eino_adk/agent_implementation/deepagents/) + +## 编排 + +需要精确控制执行流程时,用 `compose` 搭图或工作流: + +```Go +graph := compose.NewGraph[*Input, *Output]() +graph.AddLambdaNode("validate", validateFn) +graph.AddChatModelNode("generate", chatModel) +graph.AddLambdaNode("format", formatFn) + +graph.AddEdge(compose.START, "validate") +graph.AddEdge("validate", "generate") +graph.AddEdge("generate", "format") +graph.AddEdge("format", compose.END) + +runnable, _ := graph.Compile(ctx) +result, _ := runnable.Invoke(ctx, input) +``` + +编排出来的流程可以包装成工具给智能体用,把确定性流程和自主决策结合起来: + +```Go +tool, _ := graphtool.NewInvokableGraphTool(graph, "data_pipeline", "Process and validate data") + +agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Model: chatModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool}, + }, + }, +}) +``` + +这样你可以写出精确可控的业务流程,再让智能体决定什么时候调用。 + +→ [GraphTool 示例](https://github.com/cloudwego/eino-examples/tree/main/adk/common/tool/graphtool) · [编排文档](https://www.cloudwego.io/zh/docs/eino/core_modules/chain_and_graph_orchestration/) + +# 主要特性 + +## 组件生态 + +Eino 定义了组件抽象(ChatModel、Tool、Retriever、Embedding 等),官方实现覆盖 OpenAI、Claude、Gemini、Ark、Ollama、Elasticsearch 等。 + +→ [eino-ext](https://github.com/cloudwego/eino-ext) + +## 流式处理 + +Eino 在编排中自动处理流式:拼接、装箱、合并、复制。组件只需实现有业务意义的流式范式,框架处理剩下的。 + +→ [文档](https://www.cloudwego.io/zh/docs/eino/core_modules/chain_and_graph_orchestration/stream_programming_essentials/) + +## 回调切面 + +在固定切点(OnStart、OnEnd、OnError、OnStartWithStreamInput、OnEndWithStreamOutput)注入日志、追踪、指标,适用于组件、图、智能体。 + +→ [文档](https://www.cloudwego.io/zh/docs/eino/core_modules/chain_and_graph_orchestration/callback_manual/) + +## 中断/恢复 + +任何智能体或工具都能暂停等待人工输入,从检查点恢复。框架处理状态持久化和路由。 + +→ [文档](https://www.cloudwego.io/zh/docs/eino/core_modules/eino_adk/agent_hitl/) · [示例](https://github.com/cloudwego/eino-examples/tree/main/adk/human-in-the-loop) + +# 框架结构 + +![](.github/static/img/eino/eino_framework.jpeg) + +Eino 框架包含: + +- Eino(本仓库):类型定义、流处理机制、组件抽象、编排、智能体实现、切面机制 + +- [EinoExt](https://github.com/cloudwego/eino-ext):组件实现、回调处理器、使用示例、评估器、提示优化器 + +- [Eino Devops](https://github.com/cloudwego/eino-ext/tree/main/devops):可视化开发和调试 + +- [EinoExamples](https://github.com/cloudwego/eino-examples):示例应用和最佳实践 + +## 文档 + +- [Eino 用户手册](https://www.cloudwego.io/zh/docs/eino/) +- [Eino: 快速开始](https://www.cloudwego.io/zh/docs/eino/quick_start/) + +## 依赖 +- Go 1.18 及以上 + +## 代码规范 + +本仓库使用 `golangci-lint`,本地检查: + +```bash +golangci-lint run ./... +``` + +规则: +- 导出的函数、接口、package 等需要 GoDoc 注释 +- 代码格式符合 `gofmt -s` +- import 顺序符合 `goimports`(std -> third party -> local) + +## 安全 + +如果你在该项目中发现潜在的安全问题,或你认为可能发现了安全问题,请通过我们的[安全中心](https://security.bytedance.com/src) +或[漏洞报告邮箱](mailto:sec@bytedance.com?subject=关于Eino的反馈)通知字节跳动安全团队。 + + +请**不要**创建公开的 GitHub Issue。 + +## 联系我们 +- 成为 member:[COMMUNITY MEMBERSHIP](https://github.com/cloudwego/community/blob/main/COMMUNITY_MEMBERSHIP.md) +- Issues:[Issues](https://github.com/cloudwego/eino/issues) +- 飞书:扫码加入 CloudWeGo/eino 用户群 + +    LarkGroup + +## 开源许可证 + +本项目基于 [Apache-2.0 许可证](LICENSE-APACHE) 开源。 diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..c3b335d --- /dev/null +++ b/_typos.toml @@ -0,0 +1,14 @@ +# Typo check: https://github.com/crate-ci/typos +[default] + +[default.extend-words] +Invokable = "Invokable" +invokable = "invokable" +InvokableLambda = "InvokableLambda" +InvokableRun = "InvokableRun" +typ = "typ" +byted = "byted" +cpy = "cpy" +mak = "mak" +[files] +extend-exclude = ["go.mod", "go.sum", "check_branch_name.sh"] diff --git a/adk/agent_tool.go b/adk/agent_tool.go new file mode 100644 index 0000000..3f120c2 --- /dev/null +++ b/adk/agent_tool.go @@ -0,0 +1,417 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package adk provides core agent development kit utilities and types. +package adk + +import ( + "context" + "errors" + "fmt" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +var ( + defaultAgentToolParam = schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "request": { + Desc: "request to be processed", + Required: true, + Type: schema.String, + }, + }) +) + +type AgentToolOptions struct { + fullChatHistoryAsInput bool + agentInputSchema *schema.ParamsOneOf +} + +type AgentToolOption func(*AgentToolOptions) + +// WithFullChatHistoryAsInput enables using the full chat history as input. +func WithFullChatHistoryAsInput() AgentToolOption { + return func(options *AgentToolOptions) { + options.fullChatHistoryAsInput = true + } +} + +// WithAgentInputSchema sets a custom input schema for the agent tool. +func WithAgentInputSchema(schema *schema.ParamsOneOf) AgentToolOption { + return func(options *AgentToolOptions) { + options.agentInputSchema = schema + } +} + +func withAgentToolEnableStreaming(enabled bool) tool.Option { + return tool.WrapImplSpecificOptFn(func(opt *agentToolOptions) { + opt.enableStreaming = enabled + }) +} + +// NewAgentTool creates a tool that wraps an agent for invocation. +// +// The agent must have a non-empty Name and Description, as they are used as +// the tool's name and description respectively. This is validated when Info() +// is called during tool setup. +// +// Event Streaming: +// When EmitInternalEvents is enabled in ToolsConfig, the agent tool will emit AgentEvent +// from the inner agent to the parent agent's AsyncGenerator, allowing real-time streaming +// of the inner agent's output to the end-user via Runner. +// +// Note that these forwarded events are NOT recorded in the parent agent's runSession. +// They are only emitted to the end-user and have no effect on the parent agent's state +// or checkpoint. The only exception is Interrupted action, which is propagated via +// CompositeInterrupt to enable proper interrupt/resume across agent boundaries. +// +// Action Scoping: +// Actions emitted by the inner agent are scoped to the agent tool boundary: +// - Interrupted: Propagated via CompositeInterrupt to allow proper interrupt/resume across boundaries +// - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool; these actions only affect +// the inner agent's execution and do not propagate to the parent agent +// +// This scoping ensures that nested agents cannot unexpectedly terminate or transfer control +// of their parent agent's execution flow. +func NewAgentTool(_ context.Context, agent Agent, options ...AgentToolOption) tool.BaseTool { + opts := &AgentToolOptions{} + for _, opt := range options { + opt(opts) + } + + return &agentTool{ + agent: agent, + fullChatHistoryAsInput: opts.fullChatHistoryAsInput, + inputSchema: opts.agentInputSchema, + } +} + +// NewTypedAgentTool creates a new agent tool that wraps a TypedAgent as a tool.BaseTool. +func NewTypedAgentTool[M MessageType](_ context.Context, agent TypedAgent[M], options ...AgentToolOption) tool.BaseTool { + opts := &AgentToolOptions{} + for _, opt := range options { + opt(opts) + } + + return &typedAgentTool[M]{ + agent: agent, + fullChatHistoryAsInput: opts.fullChatHistoryAsInput, + inputSchema: opts.agentInputSchema, + } +} + +type typedAgentTool[M MessageType] struct { + agent TypedAgent[M] + + fullChatHistoryAsInput bool + inputSchema *schema.ParamsOneOf +} + +type agentTool = typedAgentTool[*schema.Message] + +type agentToolRequest struct { + Request string `json:"request"` +} + +func (at *typedAgentTool[M]) Info(ctx context.Context) (*schema.ToolInfo, error) { + name := at.agent.Name(ctx) + if name == "" { + return nil, errors.New("agent tool requires a non-empty Name") + } + desc := at.agent.Description(ctx) + if desc == "" { + return nil, errors.New("agent tool requires a non-empty Description") + } + param := at.inputSchema + if param == nil { + param = defaultAgentToolParam + } + + return &schema.ToolInfo{ + Name: name, + Desc: desc, + ParamsOneOf: param, + }, nil +} + +func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + if cancelCtx := getCancelContext(ctx); cancelCtx != nil { + cancelCtx.markAgentToolDescendant() + } + + gen, enableStreaming := getEmitGeneratorAndEnableStreaming[M](opts) + var ms *bridgeStore + var iter *AsyncIterator[*TypedAgentEvent[M]] + var err error + + wasInterrupted, hasState, state := tool.GetInterruptState[[]byte](ctx) + if !wasInterrupted { + ms = newBridgeStore() + + var input []M + if at.fullChatHistoryAsInput { + var zero M + if _, ok := any(zero).(*schema.Message); !ok { + // fullChatHistoryAsInput is only supported for *schema.Message agents and will not + // be extended to *schema.AgenticMessage. The chat history format and role semantics + // differ fundamentally between Message and AgenticMessage, and the history rewriting + // logic (role attribution, system message filtering, transfer messages) is specific + // to the Message model. + return "", fmt.Errorf("fullChatHistoryAsInput is only supported for *schema.Message agents") + } + msgInput, histErr := getReactChatHistory(ctx, at.agent.Name(ctx)) + if histErr != nil { + return "", histErr + } + input = any(msgInput).([]M) + } else { + if at.inputSchema == nil { + req := &agentToolRequest{} + err = sonic.UnmarshalString(argumentsInJSON, req) + if err != nil { + return "", err + } + argumentsInJSON = req.Request + } + input = newTypedUserMessages[M](argumentsInJSON) + } + + runner := newTypedInvokableAgentToolRunner(at.agent, ms, enableStreaming) + iter = runner.Run(ctx, input, + append(extractAndDeriveAgentToolCancelCtx(ctx, at.agent.Name(ctx), opts), WithCheckPointID(bridgeCheckpointID), withSharedParentSession())...) + } else { + if !hasState { + return "", fmt.Errorf("agent tool '%s' interrupt has happened, but cannot find interrupt state", at.agent.Name(ctx)) + } + + ms = newResumeBridgeStore(bridgeCheckpointID, state) + + agentOpts := extractAndDeriveAgentToolCancelCtx(ctx, at.agent.Name(ctx), opts) + agentOpts = append(agentOpts, withSharedParentSession()) + + runner := newTypedInvokableAgentToolRunner(at.agent, ms, enableStreaming) + iter, err = runner.Resume(ctx, bridgeCheckpointID, agentOpts...) + if err != nil { + return "", err + } + } + + var lastEvent *TypedAgentEvent[M] + for { + event, ok := iter.Next() + if !ok { + break + } + + if lastEvent != nil && + lastEvent.Output != nil && + lastEvent.Output.MessageOutput != nil && + lastEvent.Output.MessageOutput.MessageStream != nil { + lastEvent.Output.MessageOutput.MessageStream.Close() + } + + if event.Err != nil { + return "", event.Err + } + + if gen != nil { + if event.Action == nil || event.Action.Interrupted == nil { + if parentRunCtx := getRunCtx(ctx); parentRunCtx != nil && len(parentRunCtx.RunPath) > 0 { + rp := make([]RunStep, 0, len(parentRunCtx.RunPath)+len(event.RunPath)) + rp = append(rp, parentRunCtx.RunPath...) + rp = append(rp, event.RunPath...) + event.RunPath = rp + } + tmp := copyTypedAgentEvent(event) + gen.Send(event) + event = tmp + } + } + + lastEvent = event + } + + if lastEvent != nil && lastEvent.Action != nil && lastEvent.Action.Interrupted != nil { + data, existed, err_ := ms.Get(ctx, bridgeCheckpointID) + if err_ != nil { + return "", fmt.Errorf("failed to get interrupt info: %w", err_) + } + if !existed { + return "", fmt.Errorf("interrupt has happened, but cannot find interrupt info") + } + + return "", tool.CompositeInterrupt(ctx, "agent tool interrupt", data, + lastEvent.Action.internalInterrupted) + } + + if lastEvent == nil { + return "", errors.New("no event returned") + } + + var ret string + if lastEvent.Output != nil { + if output := lastEvent.Output.MessageOutput; output != nil { + msg, err := output.GetMessage() + if err != nil { + return "", err + } + ret = extractTextContent(msg) + } + } + + return ret, nil +} + +// agentToolOptions is a wrapper structure used to convert AgentRunOption slices to tool.Option. +// It stores the agent name and corresponding run options for tool-specific processing. +type agentToolOptions struct { + agentName string + opts []AgentRunOption + enableStreaming bool +} + +// typedAgentToolEventOptions carries the parent runner's event generator for a +// specific message type. This keeps forwarded internal events type-compatible +// with the parent event stream. +type typedAgentToolEventOptions[M MessageType] struct { + generator *AsyncGenerator[*TypedAgentEvent[M]] +} + +func withAgentToolOptions(agentName string, opts []AgentRunOption) tool.Option { + return tool.WrapImplSpecificOptFn(func(opt *agentToolOptions) { + opt.agentName = agentName + opt.opts = opts + }) +} + +func withAgentToolEventGenerator(gen *AsyncGenerator[*AgentEvent]) tool.Option { + return withTypedAgentToolEventGenerator(gen) +} + +func withTypedAgentToolEventGenerator[M MessageType](gen *AsyncGenerator[*TypedAgentEvent[M]]) tool.Option { + return tool.WrapImplSpecificOptFn(func(o *typedAgentToolEventOptions[M]) { + o.generator = gen + }) +} + +func getOptionsByAgentName(agentName string, opts []tool.Option) []AgentRunOption { + var ret []AgentRunOption + for _, opt := range opts { + o := tool.GetImplSpecificOptions[agentToolOptions](nil, opt) + if o != nil && o.agentName == agentName { + ret = append(ret, o.opts...) + } + } + return ret +} + +func extractAndDeriveAgentToolCancelCtx(ctx context.Context, agentName string, opts []tool.Option) []AgentRunOption { + agentOpts := getOptionsByAgentName(agentName, opts) + baseOpts := getCommonOptions(nil, agentOpts...) + parentCtx := baseOpts.cancelCtx + if parentCtx == nil { + parentCtx = getCancelContext(ctx) + } + if parentCtx != nil { + parentCtx.markAgentToolDescendant() + childCtx := parentCtx.deriveAgentToolCancelContext(ctx) + agentOpts = append(agentOpts, WrapImplSpecificOptFn(func(o *options) { + o.cancelCtx = childCtx + })) + } + return agentOpts +} + +func getEmitGeneratorAndEnableStreaming[M MessageType](opts []tool.Option) (*AsyncGenerator[*TypedAgentEvent[M]], bool) { + o := tool.GetImplSpecificOptions[agentToolOptions](nil, opts...) + eventOptions := tool.GetImplSpecificOptions[typedAgentToolEventOptions[M]](nil, opts...) + if o == nil && eventOptions == nil { + return nil, false + } + + var gen *AsyncGenerator[*TypedAgentEvent[M]] + if eventOptions != nil { + gen = eventOptions.generator + } + + var enableStreaming bool + if o != nil { + enableStreaming = o.enableStreaming + } + + return gen, enableStreaming +} + +func getReactChatHistory(ctx context.Context, destAgentName string) ([]Message, error) { + var messages []Message + err := compose.ProcessState(ctx, func(ctx context.Context, st *State) error { + if len(st.Messages) == 0 { + return nil + } + messages = make([]Message, len(st.Messages)-1) + copy(messages, st.Messages[:len(st.Messages)-1]) + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to get chat history from state: %w", err) + } + + var agentName string + if runCtx := getRunCtx(ctx); runCtx != nil && len(runCtx.RunPath) > 0 { + agentName = runCtx.RunPath[len(runCtx.RunPath)-1].agentName + } + + a, t := GenTransferMessages(ctx, destAgentName) + messages = append(messages, a, t) + history := make([]Message, 0, len(messages)) + for _, msg := range messages { + if msg.Role == schema.System { + continue + } + + if msg.Role == schema.Assistant || msg.Role == schema.Tool { + msg = rewriteMessage(msg, agentName) + } + + history = append(history, msg) + } + + return history, nil +} + +func newTypedUserMessages[M MessageType](text string) []M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any([]Message{schema.UserMessage(text)}).([]M) + case *schema.AgenticMessage: + return any([]*schema.AgenticMessage{schema.UserAgenticMessage(text)}).([]M) + default: + return nil + } +} + +func newTypedInvokableAgentToolRunner[M MessageType](agent TypedAgent[M], store compose.CheckPointStore, enableStreaming bool) *TypedRunner[M] { + return &TypedRunner[M]{ + a: agent, + enableStreaming: enableStreaming, + store: store, + } +} diff --git a/adk/agent_tool_test.go b/adk/agent_tool_test.go new file mode 100644 index 0000000..785ad99 --- /dev/null +++ b/adk/agent_tool_test.go @@ -0,0 +1,1241 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "fmt" + "strings" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type mockChatModelForAttack struct { + generateFn func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) +} + +func (m *mockChatModelForAttack) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return m.generateFn(ctx, input, opts...) +} + +func (m *mockChatModelForAttack) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + result, err := m.generateFn(ctx, input, opts...) + if err != nil { + return nil, err + } + r, w := schema.Pipe[*schema.Message](1) + go func() { defer w.Close(); w.Send(result, nil) }() + return r, nil +} + +// mockAgent implements the Agent interface for testing +type mockAgentForTool struct { + name string + description string + responses []*AgentEvent +} + +func (a *mockAgentForTool) Name(_ context.Context) string { + return a.name +} + +func (a *mockAgentForTool) Description(_ context.Context) string { + return a.description +} + +func (a *mockAgentForTool) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + defer generator.Close() + + for _, event := range a.responses { + generator.Send(event) + + // If the event has an Exit action, stop sending events + if event.Action != nil && event.Action.Exit { + break + } + } + }() + + return iterator +} + +func newMockAgentForTool(name, description string, responses []*AgentEvent) *mockAgentForTool { + return &mockAgentForTool{ + name: name, + description: description, + responses: responses, + } +} + +func TestAgentTool_Info(t *testing.T) { + // Create a mock agent + mockAgent_ := newMockAgentForTool("TestAgent", "Test agent description", nil) + + // Create an agentTool with the mock agent + agentTool_ := NewAgentTool(context.Background(), mockAgent_) + + // Test the Info method + ctx := context.Background() + info, err := agentTool_.Info(ctx) + + // Verify results + assert.NoError(t, err) + assert.NotNil(t, info) + assert.Equal(t, "TestAgent", info.Name) + assert.Equal(t, "Test agent description", info.Desc) + assert.NotNil(t, info.ParamsOneOf) +} + +func TestAgentTool_Info_EmptyName(t *testing.T) { + agentTool_ := NewAgentTool(context.Background(), newMockAgentForTool("", "desc", nil)) + _, err := agentTool_.Info(context.Background()) + assert.ErrorContains(t, err, "non-empty Name") +} + +func TestAgentTool_Info_EmptyDescription(t *testing.T) { + agentTool_ := NewAgentTool(context.Background(), newMockAgentForTool("name", "", nil)) + _, err := agentTool_.Info(context.Background()) + assert.ErrorContains(t, err, "non-empty Description") +} + +func TestAgentTool_SharedParentSessionValues(t *testing.T) { + ctx := context.Background() + + inner := &sessionValuesAgent{name: "inner"} + innerTool := NewAgentTool(ctx, inner).(tool.InvokableTool) + + input := &AgentInput{Messages: []Message{schema.UserMessage("q")}} + ctx, _ = initRunCtx(ctx, "outer", input) + AddSessionValue(ctx, "parent_key", "parent_val") + parentSession := getRunCtx(ctx).Session + + _, err := innerTool.InvokableRun(ctx, `{"request":"hello"}`) + assert.NoError(t, err) + + assert.Equal(t, "parent_val", inner.seenParentValue) + assert.NotNil(t, inner.capturedSession) + assert.NotSame(t, parentSession, inner.capturedSession) + assert.NotNil(t, parentSession.valuesMtx) + assert.Same(t, parentSession.valuesMtx, inner.capturedSession.valuesMtx) + + mtx := parentSession.valuesMtx + mtx.Lock() + inner.capturedSession.Values["direct_child_key"] = "direct_child_val" + mtx.Unlock() + + mtx.Lock() + v2, ok2 := parentSession.Values["direct_child_key"] + mtx.Unlock() + assert.True(t, ok2) + assert.Equal(t, "direct_child_val", v2) + + mtx.Lock() + parentSession.Values["direct_parent_key"] = "direct_parent_val" + mtx.Unlock() + + mtx.Lock() + v3, ok3 := inner.capturedSession.Values["direct_parent_key"] + mtx.Unlock() + assert.True(t, ok3) + assert.Equal(t, "direct_parent_val", v3) + + v, ok := GetSessionValue(ctx, "child_key") + assert.True(t, ok) + assert.Equal(t, "child_val", v) +} + +type sessionValuesAgent struct { + name string + seenParentValue any + capturedSession *runSession +} + +func (a *sessionValuesAgent) Name(context.Context) string { return a.name } +func (a *sessionValuesAgent) Description(context.Context) string { return "test" } +func (a *sessionValuesAgent) Run(ctx context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + if rc := getRunCtx(ctx); rc != nil { + a.capturedSession = rc.Session + } + a.seenParentValue, _ = GetSessionValue(ctx, "parent_key") + AddSessionValue(ctx, "child_key", "child_val") + + it, gen := NewAsyncIteratorPair[*AgentEvent]() + gen.Send(&AgentEvent{ + AgentName: a.name, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("ok", nil), + Role: schema.Assistant, + }, + }, + }) + gen.Close() + return it +} + +func TestAgentTool_InvokableRun(t *testing.T) { + // Create a context + ctx := context.Background() + + // Test cases + tests := []struct { + name string + agentResponses []*AgentEvent + request string + expectedOutput string + expectError bool + }{ + { + name: "successful model response", + agentResponses: []*AgentEvent{ + { + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Test response", nil), + Role: schema.Assistant, + }, + }, + }, + }, + request: `{"request":"Test request"}`, + expectedOutput: "Test response", + expectError: false, + }, + { + name: "successful tool call response", + agentResponses: []*AgentEvent{ + { + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.ToolMessage("Tool response", "test-id"), + Role: schema.Tool, + }, + }, + }, + }, + request: `{"request":"Test tool request"}`, + expectedOutput: "Tool response", + expectError: false, + }, + { + name: "invalid request JSON", + agentResponses: nil, + request: `invalid json`, + expectedOutput: "", + expectError: true, + }, + { + name: "no events returned", + agentResponses: []*AgentEvent{}, + request: `{"request":"Test request"}`, + expectedOutput: "", + expectError: true, + }, + { + name: "error in event", + agentResponses: []*AgentEvent{ + { + AgentName: "TestAgent", + Err: assert.AnError, + }, + }, + request: `{"request":"Test request"}`, + expectedOutput: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock agent with the test responses + mockAgent_ := newMockAgentForTool("TestAgent", "Test agent description", tt.agentResponses) + + // Create an agentTool with the mock agent + agentTool_ := NewAgentTool(ctx, mockAgent_) + + // Call InvokableRun + output, err := agentTool_.(tool.InvokableTool).InvokableRun(ctx, tt.request) + + // Verify results + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedOutput, output) + } + }) + } +} + +func TestGetReactHistory(t *testing.T) { + g := compose.NewGraph[string, []Message](compose.WithGenLocalState(func(ctx context.Context) (state *State) { + return &State{ + Messages: []Message{ + schema.UserMessage("user query"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "tool call id 1", Function: schema.FunctionCall{Name: "tool1", Arguments: "arguments1"}}}), + schema.ToolMessage("tool result 1", "tool call id 1", schema.WithToolName("tool1")), + schema.AssistantMessage("", []schema.ToolCall{{ID: "tool call id 2", Function: schema.FunctionCall{Name: "tool2", Arguments: "arguments2"}}}), + }, + } + })) + assert.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output []Message, err error) { + return getReactChatHistory(ctx, "DestAgentName") + }))) + assert.NoError(t, g.AddEdge(compose.START, "1")) + assert.NoError(t, g.AddEdge("1", compose.END)) + + ctx := context.Background() + ctx, _ = initRunCtx(ctx, "MyAgent", nil) + runner, err := g.Compile(ctx) + assert.NoError(t, err) + result, err := runner.Invoke(ctx, "") + assert.NoError(t, err) + assert.Equal(t, []Message{ + schema.UserMessage("user query"), + schema.UserMessage("For context: [MyAgent] called tool: `tool1` with arguments: arguments1."), + schema.UserMessage("For context: [MyAgent] `tool1` tool returned result: tool result 1."), + schema.UserMessage("For context: [MyAgent] called tool: `transfer_to_agent` with arguments: DestAgentName."), + schema.UserMessage("For context: [MyAgent] `transfer_to_agent` tool returned result: successfully transferred to agent [DestAgentName]."), + }, result) +} + +// mockAgentWithInputCapture implements the Agent interface for testing and captures the input it receives +type mockAgentWithInputCapture struct { + name string + description string + capturedInput []Message + responses []*AgentEvent +} + +func (a *mockAgentWithInputCapture) Name(_ context.Context) string { + return a.name +} + +func (a *mockAgentWithInputCapture) Description(_ context.Context) string { + return a.description +} + +func (a *mockAgentWithInputCapture) Run(_ context.Context, input *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + a.capturedInput = input.Messages + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + defer generator.Close() + + for _, event := range a.responses { + generator.Send(event) + + // If the event has an Exit action, stop sending events + if event.Action != nil && event.Action.Exit { + break + } + } + }() + + return iterator +} + +func newMockAgentWithInputCapture(name, description string, responses []*AgentEvent) *mockAgentWithInputCapture { + return &mockAgentWithInputCapture{ + name: name, + description: description, + responses: responses, + } +} + +func TestAgentToolWithOptions(t *testing.T) { + // Test Case 1: WithFullChatHistoryAsInput + t.Run("WithFullChatHistoryAsInput", func(t *testing.T) { + ctx := context.Background() + + // 1. Set up a mock agent that will capture the input it receives + mockAgent := newMockAgentWithInputCapture("test-agent", "a test agent", []*AgentEvent{ + { + AgentName: "test-agent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("done", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // 2. Create an agentTool with the option + agentTool := NewAgentTool(ctx, mockAgent, WithFullChatHistoryAsInput()) + + // 3. Set up a context with a chat history using a graph + history := []Message{ + schema.UserMessage("first user message"), + schema.AssistantMessage("first assistant response", nil), + } + + g := compose.NewGraph[string, string](compose.WithGenLocalState(func(ctx context.Context) (state *State) { + return &State{ + Messages: append(history, schema.AssistantMessage("tool call msg", nil)), + } + })) + + assert.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + // Run the tool within the graph context that has the state + _, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"request":"some ignored input"}`) + return "done", err + }))) + assert.NoError(t, g.AddEdge(compose.START, "1")) + assert.NoError(t, g.AddEdge("1", compose.END)) + + ctx, _ = initRunCtx(ctx, "react-agent", nil) + runner, err := g.Compile(ctx) + assert.NoError(t, err) + + // 4. Run the graph which will execute the tool with the state + _, err = runner.Invoke(ctx, "") + assert.NoError(t, err) + + // 5. Assert that the agent received the full history + // The agent should receive: history (minus last assistant message) + transfer messages + assert.Len(t, mockAgent.capturedInput, 4) // 2 from history + 2 transfer messages + assert.Equal(t, "first user message", mockAgent.capturedInput[0].Content) + assert.Equal(t, "For context: [react-agent] said: first assistant response.", mockAgent.capturedInput[1].Content) + assert.Equal(t, "For context: [react-agent] called tool: `transfer_to_agent` with arguments: test-agent.", mockAgent.capturedInput[2].Content) + assert.Equal(t, "For context: [react-agent] `transfer_to_agent` tool returned result: successfully transferred to agent [test-agent].", mockAgent.capturedInput[3].Content) + }) + + // Test Case 2: WithAgentInputSchema + t.Run("WithAgentInputSchema", func(t *testing.T) { + ctx := context.Background() + + // 1. Define a custom schema + customSchema := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "custom_arg": { + Desc: "a custom argument", + Required: true, + Type: schema.String, + }, + }) + + // 2. Set up a mock agent to capture input + mockAgent := newMockAgentWithInputCapture("schema-agent", "agent with custom schema", []*AgentEvent{ + { + AgentName: "schema-agent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("schema processed", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // 3. Create agentTool with the custom schema option + agentTool := NewAgentTool(ctx, mockAgent, WithAgentInputSchema(customSchema)) + + // 4. Verify the Info() method returns the custom schema + info, err := agentTool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, customSchema, info.ParamsOneOf) + + // 5. Run the tool with arguments matching the custom schema + _, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"custom_arg":"hello world"}`) + assert.NoError(t, err) + + // 6. Assert that the agent received the correctly parsed argument + // With custom schema, the agent should receive the raw JSON as input + assert.Len(t, mockAgent.capturedInput, 1) + assert.Equal(t, `{"custom_arg":"hello world"}`, mockAgent.capturedInput[0].Content) + }) + + // Test Case 3: WithAgentInputSchema with complex schema + t.Run("WithAgentInputSchema_ComplexSchema", func(t *testing.T) { + ctx := context.Background() + + // 1. Define a complex custom schema with multiple parameters + complexSchema := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "name": { + Desc: "user name", + Required: true, + Type: schema.String, + }, + "age": { + Desc: "user age", + Required: false, + Type: schema.Integer, + }, + "active": { + Desc: "user status", + Required: false, + Type: schema.Boolean, + }, + }) + + // 2. Set up a mock agent + mockAgent := newMockAgentWithInputCapture("complex-agent", "agent with complex schema", []*AgentEvent{ + { + AgentName: "complex-agent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("complex processed", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // 3. Create agentTool with the complex schema option + agentTool := NewAgentTool(ctx, mockAgent, WithAgentInputSchema(complexSchema)) + + // 4. Verify the Info() method returns the complex schema + info, err := agentTool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, complexSchema, info.ParamsOneOf) + + // 5. Run the tool with complex arguments + _, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"name":"John","age":30,"active":true}`) + assert.NoError(t, err) + + // 6. Assert that the agent received the complex JSON + assert.Len(t, mockAgent.capturedInput, 1) + assert.Equal(t, `{"name":"John","age":30,"active":true}`, mockAgent.capturedInput[0].Content) + }) + + // Test Case 4: Both options together + t.Run("BothOptionsTogether", func(t *testing.T) { + ctx := context.Background() + + // 1. Define a custom schema + customSchema := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": { + Desc: "search query", + Required: true, + Type: schema.String, + }, + }) + + // 2. Set up a mock agent + mockAgent := newMockAgentWithInputCapture("combined-agent", "agent with both options", []*AgentEvent{ + { + AgentName: "combined-agent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("combined processed", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // 3. Create agentTool with both options + agentTool := NewAgentTool(ctx, mockAgent, WithAgentInputSchema(customSchema), WithFullChatHistoryAsInput()) + + // 4. Set up a context with chat history using a graph + history := []Message{ + schema.UserMessage("previous conversation"), + schema.AssistantMessage("previous response", nil), + } + + g := compose.NewGraph[string, string](compose.WithGenLocalState(func(ctx context.Context) (state *State) { + return &State{ + Messages: append(history, schema.AssistantMessage("tool call", nil)), + } + })) + + assert.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + // Run the tool within the graph context that has the state + _, err = agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"query":"current query"}`) + return "done", err + }))) + assert.NoError(t, g.AddEdge(compose.START, "1")) + assert.NoError(t, g.AddEdge("1", compose.END)) + + ctx, _ = initRunCtx(ctx, "react-agent", nil) + runner, err := g.Compile(ctx) + assert.NoError(t, err) + + // 5. Run the graph which will execute the tool with the state + _, err = runner.Invoke(ctx, "") + assert.NoError(t, err) + + // 6. Verify both options work together + info, err := agentTool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, customSchema, info.ParamsOneOf) + + // The agent should receive full history + the custom query + assert.Len(t, mockAgent.capturedInput, 4) // 2 history + 2 transfer messages + assert.Equal(t, "previous conversation", mockAgent.capturedInput[0].Content) + assert.Equal(t, "For context: [react-agent] said: previous response.", mockAgent.capturedInput[1].Content) + assert.Equal(t, "For context: [react-agent] called tool: `transfer_to_agent` with arguments: combined-agent.", mockAgent.capturedInput[2].Content) + assert.Equal(t, "For context: [react-agent] `transfer_to_agent` tool returned result: successfully transferred to agent [combined-agent].", mockAgent.capturedInput[3].Content) + }) +} + +type fakeTCM struct{} + +func (f *fakeTCM) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + o := model.GetCommonOptions(&model.Options{}, opts...) + tc := schema.ToolCall{ID: "id-1", Type: "function"} + if len(o.Tools) > 0 { + tc.Function.Name = o.Tools[0].Name + } + tc.Function.Arguments = `{"request":"hello"}` + return schema.AssistantMessage("", []schema.ToolCall{tc}), nil +} +func (f *fakeTCM) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + msg, _ := f.Generate(ctx, input, opts...) + return schema.StreamReaderFromArray([]*schema.Message{msg}), nil +} +func (f *fakeTCM) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return f, nil +} + +type emitOnceModel struct{} + +func (e *emitOnceModel) Generate(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("inner2", nil), nil +} +func (e *emitOnceModel) Stream(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + m, _ := e.Generate(ctx, input) + return schema.StreamReaderFromArray([]*schema.Message{m}), nil +} +func (e *emitOnceModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return e, nil +} + +type emitEventsAgent struct{ events []*AgentEvent } + +func (e *emitEventsAgent) Name(context.Context) string { return "emit" } +func (e *emitEventsAgent) Description(context.Context) string { return "test" } +func (e *emitEventsAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] { + it, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + for _, ev := range e.events { + gen.Send(ev) + } + gen.Close() + }() + return it +} + +// spyAgent captures runSession from ctx in a single nested run +type spyAgent struct { + a Agent + mu sync.Mutex + captured *runSession +} + +func (s *spyAgent) Name(ctx context.Context) string { return s.a.Name(ctx) } +func (s *spyAgent) Description(ctx context.Context) string { return s.a.Description(ctx) } +func (s *spyAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + if rc := getRunCtx(ctx); rc != nil { + s.mu.Lock() + s.captured = rc.Session + s.mu.Unlock() + } + return s.a.Run(ctx, input, options...) +} + +func (s *spyAgent) getCaptured() *runSession { + s.mu.Lock() + defer s.mu.Unlock() + return s.captured +} + +func TestNestedAgentTool_RunPath(t *testing.T) { + ctx := context.Background() + + inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner2", + Description: "leaf", + Model: &emitOnceModel{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: true}, + }) + inner2Spy := &spyAgent{a: inner2} + inner2Tool := NewAgentTool(ctx, inner2Spy) + + inner, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner", + Description: "mid", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: true, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{inner2Tool}}}, + }) + innerSpy := &spyAgent{a: inner} + innerTool := NewAgentTool(ctx, innerSpy) + + outer, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "outer", + Description: "top", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: true, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerTool}}}, + }) + + input := &AgentInput{Messages: []Message{schema.UserMessage("q")}} + ctx, outerRunCtx := initRunCtx(ctx, "outer", input) + r := NewRunner(ctx, RunnerConfig{Agent: outer, EnableStreaming: false, CheckPointStore: newBridgeStore()}) + it := r.Run(ctx, []Message{schema.UserMessage("q")}) + + var target *AgentEvent + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming { + if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Content == "inner2" { + target = ev + break + } + } + } + if target == nil { + t.Fatalf("no inner2 event found in ephemerals") + } + + got := make([]string, len(target.RunPath)) + for i := range target.RunPath { + got[i] = target.RunPath[i].agentName + } + want := []string{"outer", "inner", "inner2"} + if len(got) != len(want) { + t.Fatalf("unexpected runPath len: got %d want %d: %+v", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("runPath mismatch at %d: got %s want %s; full: %+v", i, got[i], want[i], got) + } + } + + for _, w := range outerRunCtx.Session.getEvents() { + if w.AgentName != "outer" { + t.Fatalf("outer session contains non-outer event: %s", w.AgentName) + } + } + if innerSpy.getCaptured() == nil { + t.Fatalf("inner spy did not capture session") + } + for _, w := range innerSpy.getCaptured().getEvents() { + if w.AgentName != "inner" { + t.Fatalf("inner session contains non-inner event: %s", w.AgentName) + } + } + if inner2Spy.getCaptured() == nil { + t.Fatalf("inner2 spy did not capture session") + } + for _, w := range inner2Spy.getCaptured().getEvents() { + if w.AgentName != "inner2" { + t.Fatalf("inner2 session contains non-inner2 event: %s", w.AgentName) + } + } +} + +func TestNestedAgentTool_NoInternalEventsWhenDisabled(t *testing.T) { + ctx := context.Background() + + inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner2", + Description: "leaf", + Model: &emitOnceModel{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: false}, + }) + inner2Tool := NewAgentTool(ctx, inner2) + + inner, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner", + Description: "mid", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: false, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{inner2Tool}}}, + }) + innerTool := NewAgentTool(ctx, inner) + + outer, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "outer", + Description: "top", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: false, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerTool}}}, + }) + + r := NewRunner(ctx, RunnerConfig{Agent: outer, EnableStreaming: false, CheckPointStore: newBridgeStore()}) + it := r.Run(ctx, []Message{schema.UserMessage("q")}) + + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.AgentName == "inner2" { + t.Fatalf("inner2 internal event should not be emitted when disabled") + } + } +} + +func TestNestedAgentTool_InnerToolResultNotEmittedToOuter(t *testing.T) { + ctx := context.Background() + + innerTool := &simpleTool{name: "inner_tool", result: "inner_tool_result"} + inner, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner", + Description: "inner agent with tool", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerTool}}}, + }) + innerAgentTool := NewAgentTool(ctx, inner) + + outer, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "outer", + Description: "outer agent", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{innerAgentTool}}}, + }) + + r := NewRunner(ctx, RunnerConfig{Agent: outer, EnableStreaming: false, CheckPointStore: newBridgeStore()}) + it := r.Run(ctx, []Message{schema.UserMessage("q")}) + + var allEvents []*AgentEvent + for { + ev, ok := it.Next() + if !ok { + break + } + allEvents = append(allEvents, ev) + } + + for _, ev := range allEvents { + if ev.Output != nil && ev.Output.MessageOutput != nil && + ev.Output.MessageOutput.Message != nil && + ev.Output.MessageOutput.Message.Role == schema.Tool && + ev.AgentName == "outer" && + ev.Output.MessageOutput.Message.Content == "inner_tool_result" { + t.Fatalf("inner agent's tool result (inner_tool_result) should not be emitted as outer agent's event, but got event with AgentName=%s, Content=%s", + ev.AgentName, ev.Output.MessageOutput.Message.Content) + } + } +} + +type simpleTool struct { + name string + result string +} + +func (s *simpleTool) Info(context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: s.name, Desc: "simple tool"}, nil +} + +func (s *simpleTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + return s.result, nil +} + +func TestAgentTool_InterruptWithoutCheckpoint(t *testing.T) { + ctx := context.Background() + ctx, _ = initRunCtx(ctx, "TestAgent", &AgentInput{Messages: []Message{}}) + + interrupted := &AgentEvent{AgentName: "TestAgent"} + interrupted.Action = StatefulInterrupt(ctx, "info", "state").Action + + err := compositeInterruptFromLast(ctx, &bridgeStore{}, interrupted) + if err == nil { + t.Fatalf("expected error for interrupt without checkpoint") + } + if !strings.Contains(err.Error(), "interrupt occurred but checkpoint data is missing") { + t.Fatalf("unexpected error: %v", err) + } +} + +func compositeInterruptFromLast(ctx context.Context, ms *bridgeStore, lastEvent *AgentEvent) error { + if lastEvent == nil || lastEvent.Action == nil || lastEvent.Action.Interrupted == nil { + return nil + } + data, existed, err := ms.Get(ctx, bridgeCheckpointID) + if err != nil { + return fmt.Errorf("failed to get interrupt info: %w", err) + } + if !existed { + return fmt.Errorf("interrupt occurred but checkpoint data is missing") + } + return tool.CompositeInterrupt(ctx, "agent tool interrupt", data, lastEvent.Action.internalInterrupted) +} + +func TestAgentTool_InvokableRun_FinalOnly(t *testing.T) { + ctx := context.Background() + + inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner2", + Description: "leaf", + Model: &emitOnceModel{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: true}, + }) + invTool := NewAgentTool(ctx, inner2) + out, err := invTool.(tool.InvokableTool).InvokableRun(ctx, `{"request":"q"}`) + if err != nil { + t.Fatalf("invokable run error: %v", err) + } + if out != "inner2" { + t.Fatalf("unexpected output: %s", out) + } +} + +type streamingAgent struct{} + +func (s *streamingAgent) Name(context.Context) string { return "stream" } +func (s *streamingAgent) Description(context.Context) string { return "test" } +func (s *streamingAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] { + it, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + mv := &MessageVariant{IsStreaming: true, MessageStream: schema.StreamReaderFromArray([]Message{schema.AssistantMessage("1", nil), schema.AssistantMessage("2", nil)})} + gen.Send(&AgentEvent{AgentName: "stream", Output: &AgentOutput{MessageOutput: mv}}) + mv = &MessageVariant{IsStreaming: true, MessageStream: schema.StreamReaderFromArray([]Message{schema.AssistantMessage("a", nil), schema.AssistantMessage("b", nil)})} + gen.Send(&AgentEvent{AgentName: "stream", Output: &AgentOutput{MessageOutput: mv}}) + gen.Close() + }() + return it +} + +func TestAgentTool_InvokableRun_StreamingVariant(t *testing.T) { + ctx := context.Background() + agent := &streamingAgent{} + it := NewAgentTool(ctx, agent) + out, err := it.(tool.InvokableTool).InvokableRun(ctx, `{"request":"q"}`) + if err != nil { + t.Fatalf("invokable run error: %v", err) + } + if out != "ab" { + t.Fatalf("unexpected output: %s", out) + } +} + +func TestSequentialWorkflow_WithChatModelAgentTool_NestedRunPathAndSessions(t *testing.T) { + ctx := context.Background() + + inner2, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner2", + Description: "leaf", + Model: &emitOnceModel{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: true}, + }) + inner2Spy := &spyAgent{a: inner2} + inner2ToolSpy := NewAgentTool(ctx, inner2Spy) + + innerWithSpy, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner", + Description: "mid", + Model: &fakeTCM{}, + ToolsConfig: ToolsConfig{EmitInternalEvents: true, ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{inner2ToolSpy}}}, + }) + innerSpy := &spyAgent{a: innerWithSpy} + + outer, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "outer-seq", + Description: "workflow", + SubAgents: []Agent{innerSpy}, + }) + if err != nil { + t.Fatalf("new sequential agent err: %v", err) + } + + input := &AgentInput{Messages: []Message{schema.UserMessage("q")}} + ctx, outerRunCtx := initRunCtx(ctx, "outer-seq", input) + r := NewRunner(ctx, RunnerConfig{Agent: outer, EnableStreaming: false, CheckPointStore: newBridgeStore()}) + it := r.Run(ctx, []Message{schema.UserMessage("q")}) + + var target *AgentEvent + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming { + if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Content == "inner2" { + target = ev + break + } + } + } + if target == nil { + t.Fatalf("no inner2 event found") + } + + got := make([]string, len(target.RunPath)) + for i := range target.RunPath { + got[i] = target.RunPath[i].agentName + } + want := []string{"outer-seq", "inner", "inner2"} + if len(got) != len(want) { + t.Fatalf("unexpected runPath len: got %d want %d: %+v", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("runPath mismatch at %d: got %s want %s; full: %+v", i, got[i], want[i], got) + } + } + + for _, w := range outerRunCtx.Session.getEvents() { + if w.AgentName != "outer-seq" { + t.Fatalf("outer session contains non-outer event: %s", w.AgentName) + } + } + if innerSpy.getCaptured() == nil { + t.Fatalf("inner spy did not capture session") + } + for _, w := range innerSpy.getCaptured().getEvents() { + if w.AgentName != "inner" { + t.Fatalf("inner session contains non-inner event: %s", w.AgentName) + } + } + if inner2Spy.getCaptured() == nil { + t.Fatalf("inner2 spy did not capture session") + } + for _, w := range inner2Spy.getCaptured().getEvents() { + if w.AgentName != "inner2" { + t.Fatalf("inner2 session contains non-inner2 event: %s", w.AgentName) + } + } +} + +func TestRunPathGating_IgnoresInnerExitAndAllowsOutput(t *testing.T) { + ctx := context.Background() + + innerExit := &AgentEvent{Action: &AgentAction{Exit: true}, RunPath: []RunStep{{agentName: "inner"}}} + finalOut := EventFromMessage(schema.AssistantMessage("ok", nil), nil, schema.Assistant, "") + + sub := &emitEventsAgent{events: []*AgentEvent{innerExit, finalOut}} + fa := toFlowAgent(ctx, sub) + + it := fa.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("q")}}) + + var sawFinal bool + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming { + if ev.Output.MessageOutput.Message != nil && ev.Output.MessageOutput.Message.Content == "ok" { + sawFinal = true + } + } + } + if !sawFinal { + t.Fatalf("final output not observed; parent may have exited on inner Exit action") + } +} + +func TestRunPathGating_IgnoresInnerTransfer(t *testing.T) { + ctx := context.Background() + + innerTransfer := &AgentEvent{Action: NewTransferToAgentAction("ghost"), RunPath: []RunStep{{agentName: "inner"}}} + finalOut := EventFromMessage(schema.AssistantMessage("done", nil), nil, schema.Assistant, "") + + sub := &emitEventsAgent{events: []*AgentEvent{innerTransfer, finalOut}} + fa := toFlowAgent(ctx, sub) + + it := fa.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("q")}}) + + var outputs int + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil && !ev.Output.MessageOutput.IsStreaming { + if ev.Output.MessageOutput.Message != nil { + outputs++ + } + } + } + if outputs == 0 { + t.Fatalf("no outputs observed; parent may have transferred on inner transfer action") + } +} + +type streamAgent struct{} + +func (s *streamAgent) Name(context.Context) string { return "s" } +func (s *streamAgent) Description(context.Context) string { return "s" } +func (s *streamAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] { + it, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + frames := []*schema.Message{ + schema.AssistantMessage("hello ", nil), + schema.AssistantMessage("world", nil), + } + stream := schema.StreamReaderFromArray(frames) + gen.Send(EventFromMessage(nil, stream, schema.Assistant, "")) + gen.Close() + }() + return it +} + +func TestInvokableAgentTool_InfoAndRun(t *testing.T) { + ctx := context.Background() + + at := NewAgentTool(ctx, &streamAgent{}) + info, err := at.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "s", info.Name) + assert.Equal(t, "s", info.Desc) + js, err := info.ParamsOneOf.ToJSONSchema() + assert.NoError(t, err) + found := false + for _, r := range js.Required { + if r == "request" { + found = true + break + } + } + assert.True(t, found) + prop, ok := js.Properties.Get("request") + assert.True(t, ok) + assert.Equal(t, string(schema.String), prop.Type) + + custom := schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "x": {Desc: "arg", Required: true, Type: schema.String}, + }) + at2 := NewAgentTool(ctx, &streamAgent{}, WithAgentInputSchema(custom)) + info2, err := at2.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, custom, info2.ParamsOneOf) + out, err := at.(tool.InvokableTool).InvokableRun(ctx, `{"request":"x"}`) + assert.NoError(t, err) + assert.Equal(t, "hello world", out) +} + +type emptyAgent struct{} + +func (e *emptyAgent) Name(context.Context) string { return "empty" } +func (e *emptyAgent) Description(context.Context) string { return "empty" } +func (e *emptyAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] { + it, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { gen.Close() }() + return it +} + +type noOutputAgent struct{} + +func (n *noOutputAgent) Name(context.Context) string { return "no" } +func (n *noOutputAgent) Description(context.Context) string { return "no" } +func (n *noOutputAgent) Run(context.Context, *AgentInput, ...AgentRunOption) *AsyncIterator[*AgentEvent] { + it, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { gen.Send(&AgentEvent{}); gen.Close() }() + return it +} + +func TestInvokableAgentTool_ErrorCases(t *testing.T) { + ctx := context.Background() + + atEmpty := NewAgentTool(ctx, &emptyAgent{}) + out, err := atEmpty.(tool.InvokableTool).InvokableRun(ctx, `{"request":"x"}`) + assert.Equal(t, "", out) + assert.Error(t, err) + + atNo := NewAgentTool(ctx, &noOutputAgent{}) + out2, err := atNo.(tool.InvokableTool).InvokableRun(ctx, `{"request":"x"}`) + assert.NoError(t, err) + assert.Equal(t, "", out2) +} + +func TestCrossTypeAgentToolGracefulError(t *testing.T) { + ctx := context.Background() + + innerModel := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("inner result"), nil + }, + } + + innerAgent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticInner", + Description: "An agentic agent used as a tool", + Model: innerModel, + }) + require.NoError(t, err) + + agenticAgentTool := NewTypedAgentTool(ctx, TypedAgent[*schema.AgenticMessage](innerAgent)) + + var outerCallCount int32 + outerModel := &mockChatModelForAttack{ + generateFn: func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&outerCallCount, 1) + if count == 1 { + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "c1", Function: schema.FunctionCall{Name: "AgenticInner", Arguments: `{"request":"test"}`}}, + }, + }, nil + } + return schema.AssistantMessage("done", nil), nil + }, + } + + outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterMessageAgent", + Description: "A Message agent using an AgenticMessage sub-agent tool", + Model: outerModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{agenticAgentTool}, + }, + }, + }) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: outerAgent, EnableStreaming: true}) + iter := runner.Query(ctx, "test cross-type") + + var capturedErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + capturedErr = event.Err + t.Logf("Cross-type error message: %v", event.Err) + } + } + + if capturedErr == nil { + t.Log("DESIGN CONCERN: Cross-type agent tool (AgenticMessage sub-agent in Message agent) " + + "only errors at event forwarding time when streaming is enabled. " + + "The error check happens in the gen.Send path, which is only exercised " + + "when the outer agent actually calls the tool AND streaming is enabled. " + + "Without streaming, the tool result is returned as a string, so no type mismatch occurs.") + } else { + assert.Contains(t, capturedErr.Error(), "cross-message-type", + "Error should mention cross-message-type incompatibility") + } +} diff --git a/adk/agentic_callback_integration_test.go b/adk/agentic_callback_integration_test.go new file mode 100644 index 0000000..2473091 --- /dev/null +++ b/adk/agentic_callback_integration_test.go @@ -0,0 +1,268 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +type agenticCallbackRecorder struct { + mu sync.Mutex + onStartCalled bool + onEndCalled bool + runInfo *callbacks.RunInfo + inputReceived *TypedAgentCallbackInput[*schema.AgenticMessage] + eventsReceived []*TypedAgentEvent[*schema.AgenticMessage] + eventsDone chan struct{} + closeOnce sync.Once +} + +func (r *agenticCallbackRecorder) getOnStartCalled() bool { + r.mu.Lock() + defer r.mu.Unlock() + return r.onStartCalled +} + +func (r *agenticCallbackRecorder) getOnEndCalled() bool { + r.mu.Lock() + defer r.mu.Unlock() + return r.onEndCalled +} + +func (r *agenticCallbackRecorder) getEventsReceived() []*TypedAgentEvent[*schema.AgenticMessage] { + r.mu.Lock() + defer r.mu.Unlock() + result := make([]*TypedAgentEvent[*schema.AgenticMessage], len(r.eventsReceived)) + copy(result, r.eventsReceived) + return result +} + +func newAgenticRecordingHandler(recorder *agenticCallbackRecorder) callbacks.Handler { + recorder.eventsDone = make(chan struct{}) + return callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgenticAgent { + return ctx + } + recorder.mu.Lock() + defer recorder.mu.Unlock() + recorder.onStartCalled = true + recorder.runInfo = info + if agentInput := ConvTypedCallbackInput[*schema.AgenticMessage](input); agentInput != nil { + recorder.inputReceived = agentInput + } + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgenticAgent { + return ctx + } + recorder.mu.Lock() + recorder.onEndCalled = true + recorder.runInfo = info + recorder.mu.Unlock() + + if agentOutput := ConvTypedCallbackOutput[*schema.AgenticMessage](output); agentOutput != nil { + if agentOutput.Events != nil { + go func() { + defer recorder.closeOnce.Do(func() { close(recorder.eventsDone) }) + for { + event, ok := agentOutput.Events.Next() + if !ok { + break + } + recorder.mu.Lock() + recorder.eventsReceived = append(recorder.eventsReceived, event) + recorder.mu.Unlock() + } + }() + return ctx + } + } + recorder.closeOnce.Do(func() { close(recorder.eventsDone) }) + return ctx + }). + Build() +} + +func TestAgenticCallback(t *testing.T) { + ctx := context.Background() + + expectedContent := "This is the test response content" + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg(expectedContent), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestChatAgent", + Description: "Test chat agent", + Instruction: "You are a test agent", + Model: m, + }) + require.NoError(t, err) + + recorder := &agenticCallbackRecorder{} + handler := newAgenticRecordingHandler(recorder) + + var agentEvents []*TypedAgentEvent[*schema.AgenticMessage] + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + event, ok := iter.Next() + if !ok { + break + } + agentEvents = append(agentEvents, event) + } + + <-recorder.eventsDone + assertAgenticEventRoleFields(t, agentEvents) + + t.Run("OnStart_Invocation", func(t *testing.T) { + assert.True(t, recorder.getOnStartCalled(), "OnStart should be called") + require.NotNil(t, recorder.inputReceived, "Input should be received") + require.NotNil(t, recorder.inputReceived.Input, "AgentInput should be set") + assert.Len(t, recorder.inputReceived.Input.Messages, 1) + }) + + t.Run("OnEnd_Invocation", func(t *testing.T) { + assert.True(t, recorder.getOnEndCalled(), "OnEnd should be called") + assert.Len(t, recorder.getEventsReceived(), 1) + }) + + t.Run("RunInfo_Fields", func(t *testing.T) { + require.NotNil(t, recorder.runInfo) + assert.Equal(t, "TestChatAgent", recorder.runInfo.Name) + assert.Equal(t, ComponentOfAgenticAgent, recorder.runInfo.Component) + }) + + t.Run("Events_MatchAgentOutput", func(t *testing.T) { + require.NotEmpty(t, agentEvents, "Agent should emit events") + received := recorder.getEventsReceived() + require.NotEmpty(t, received, "Callback should receive events") + + require.Len(t, received, 1, "Callback should receive exactly 1 event") + require.NotNil(t, received[0].Output) + require.NotNil(t, received[0].Output.MessageOutput) + require.NotNil(t, received[0].Output.MessageOutput.Message) + assert.Equal(t, expectedContent, agenticTextContent(received[0].Output.MessageOutput.Message)) + }) +} + +func TestAgenticCallbackMultipleHandlers(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("test response"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test agent", + Model: m, + }) + require.NoError(t, err) + + recorder1 := &agenticCallbackRecorder{} + recorder2 := &agenticCallbackRecorder{} + handler1 := newAgenticRecordingHandler(recorder1) + handler2 := newAgenticRecordingHandler(recorder2) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler1, handler2)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + <-recorder1.eventsDone + <-recorder2.eventsDone + + assert.True(t, recorder1.getOnStartCalled(), "Handler1 OnStart should be called") + assert.True(t, recorder2.getOnStartCalled(), "Handler2 OnStart should be called") + assert.True(t, recorder1.getOnEndCalled(), "Handler1 OnEnd should be called") + assert.True(t, recorder2.getOnEndCalled(), "Handler2 OnEnd should be called") + + assert.NotEmpty(t, recorder1.getEventsReceived(), "Handler1 should receive events") + assert.NotEmpty(t, recorder2.getEventsReceived(), "Handler2 should receive events") +} + +func TestCoverage_WrapAgenticIterWithOnEnd(t *testing.T) { + ctx := context.Background() + + var onEndCalled bool + handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component == ComponentOfAgenticAgent { + onEndCalled = true + } + return ctx + }). + Build() + + ctx = initAgenticCallbacks(ctx, "test-agent", "ChatModel", + WithCallbacks(handler)) + + cbInput := &TypedAgentCallbackInput[*schema.AgenticMessage]{ + Input: &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("Hi")}, + }, + } + ctx = callbacks.OnStart(ctx, cbInput) + + origIter, origGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer origGen.Close() + origGen.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("done"), + }, + }, + }) + }() + + wrappedIter := wrapAgenticIterWithOnEnd(ctx, origIter) + + for { + _, ok := wrappedIter.Next() + if !ok { + break + } + } + + assert.True(t, onEndCalled, "OnEnd callback should have been called") +} diff --git a/adk/agentic_integration_test.go b/adk/agentic_integration_test.go new file mode 100644 index 0000000..2c6df1e --- /dev/null +++ b/adk/agentic_integration_test.go @@ -0,0 +1,665 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "encoding/json" + "sync/atomic" + "testing" + "time" + + "github.com/eino-contrib/jsonschema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +func agenticMsg(text string) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: text}), + }, + } +} + +func agenticTextContent(msg *schema.AgenticMessage) string { + for _, b := range msg.ContentBlocks { + if b.AssistantGenText != nil { + return b.AssistantGenText.Text + } + } + return "" +} + +func TestAgenticIntegration_ChatModelSingleShot(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("Handled internally with tool result: 42"), nil + }, + } + + dummyTool := newSlowTool("calculator", 0, "42") + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "ToolCallAgent", + Description: "Agent with tools for agentic model", + Instruction: "You are a calculator.", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{dummyTool}, + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + + iter := runner.Query(ctx, "What is 6*7?") + + var events []*TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + require.Len(t, events, 1) + assertAgenticEventRoleFields(t, events) + lastEvent := events[len(events)-1] + require.Nil(t, lastEvent.Err) + require.NotNil(t, lastEvent.Output) + require.NotNil(t, lastEvent.Output.MessageOutput) + assert.Equal(t, "Handled internally with tool result: 42", + agenticTextContent(lastEvent.Output.MessageOutput.Message)) +} + +func TestAgenticIntegration_ChatModelToolsPassedViaOptions(t *testing.T) { + ctx := context.Background() + + var receivedTools []*schema.ToolInfo + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + o := model.GetCommonOptions(&model.Options{}, opts...) + receivedTools = o.Tools + return agenticMsg("done"), nil + }, + } + + dummyTool := newSlowTool("my_tool", 0, "result") + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "ToolOptAgent", + Description: "Agent verifying tools are passed via options", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{dummyTool}, + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + iter := runner.Query(ctx, "test tools") + for { + _, ok := iter.Next() + if !ok { + break + } + } + + require.NotNil(t, receivedTools, "tools should be passed via model.Options") + require.Len(t, receivedTools, 1) + assert.Equal(t, "my_tool", receivedTools[0].Name) +} + +func TestAgenticIntegration_StreamingWithRunner(t *testing.T) { + ctx := context.Background() + + chunk1 := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hello "}), + }, + } + chunk2 := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "world"}), + }, + } + + m := &mockAgenticModel{ + streamFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + defer w.Close() + w.Send(chunk1, nil) + w.Send(chunk2, nil) + }() + return r, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "StreamRunner", + Description: "Streaming runner agent", + Model: m, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + }) + + iter := runner.Query(ctx, "stream me") + + event, ok := iter.Next() + require.True(t, ok) + assert.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + + if event.Output.MessageOutput.IsStreaming { + require.NotNil(t, event.Output.MessageOutput.MessageStream) + var chunks []*schema.AgenticMessage + for { + chunk, err := event.Output.MessageOutput.MessageStream.Recv() + if err != nil { + break + } + chunks = append(chunks, chunk) + } + assert.Equal(t, 2, len(chunks)) + } else { + assert.NotNil(t, event.Output.MessageOutput.Message) + } + + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestAgenticIntegration_CancelDuringExecution(t *testing.T) { + ctx := context.Background() + + modelStarted := make(chan struct{}, 1) + modelBlocked := make(chan struct{}) + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + select { + case modelStarted <- struct{}{}: + default: + } + select { + case <-modelBlocked: + return agenticMsg("should not reach"), nil + case <-ctx.Done(): + return nil, ctx.Err() + } + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "CancelAgent", + Description: "cancel test", + Model: m, + }) + require.NoError(t, err) + + cancelCtx, cancel := context.WithCancel(ctx) + defer cancel() + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + iter := runner.Run(cancelCtx, []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hi"), + }) + + <-modelStarted + cancel() + + var capturedErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + capturedErr = event.Err + } + } + require.Error(t, capturedErr, "should propagate cancel error") + assert.ErrorIs(t, capturedErr, context.Canceled) +} + +func TestAgenticIntegration_CancelWithTimeout(t *testing.T) { + ctx := context.Background() + + sa := &myAgenticAgent{ + name: "slow-agent", + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + select { + case <-time.After(10 * time.Second): + generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("slow response"), + }, + }, + }) + case <-ctx.Done(): + generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + Err: ctx.Err(), + }) + } + }() + return iter + }, + } + + timeoutCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) + defer cancel() + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: sa, + }) + iter := runner.Run(timeoutCtx, []*schema.AgenticMessage{ + schema.UserAgenticMessage("slow request"), + }) + + var capturedErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + capturedErr = event.Err + } + } + + require.Error(t, capturedErr, "should get timeout/cancel error") + assert.ErrorIs(t, capturedErr, context.DeadlineExceeded) +} +func TestAgenticIntegration_AgentTool(t *testing.T) { + ctx := context.Background() + + innerModel := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("inner tool result"), nil + }, + } + + innerAgent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "InnerAgent", + Description: "An agent used as a tool", + Model: innerModel, + }) + require.NoError(t, err) + + agentTool := NewTypedAgentTool(ctx, TypedAgent[*schema.AgenticMessage](innerAgent)) + require.NotNil(t, agentTool) + + info, err := agentTool.Info(ctx) + require.NoError(t, err) + assert.Equal(t, "InnerAgent", info.Name) + assert.Equal(t, "An agent used as a tool", info.Desc) + + outerModel := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("outer response after inner tool"), nil + }, + } + + outerAgent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "OuterAgent", + Description: "Outer agent with agent tool", + Model: outerModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{agentTool}, + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: outerAgent, + }) + iter := runner.Query(ctx, "delegate to inner") + + var events []*TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + require.NotEmpty(t, events) + assertAgenticEventRoleFields(t, events) + lastEvent := events[len(events)-1] + assert.Nil(t, lastEvent.Err) + assert.NotNil(t, lastEvent.Output) +} +func TestAgenticIntegration_InterruptEventFormation(t *testing.T) { + ctx := context.Background() + + t.Run("simple interrupt", func(t *testing.T) { + agent := &myAgenticAgent{ + name: "int-agent", + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + intEvent := TypedInterrupt[*schema.AgenticMessage](ctx, "approval needed") + intEvent.Action.Interrupted.Data = "approval data" + generator.Send(intEvent) + }() + return iter + }, + } + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + iter := runner.Query(ctx, "interrupt test") + + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + } + + require.NotNil(t, interruptEvent) + assert.Equal(t, "approval data", interruptEvent.Action.Interrupted.Data) + require.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts) + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts[0].ID) + assert.Equal(t, "approval needed", interruptEvent.Action.Interrupted.InterruptContexts[0].Info) + assert.True(t, interruptEvent.Action.Interrupted.InterruptContexts[0].IsRootCause) + }) + + t.Run("stateful interrupt", func(t *testing.T) { + agent := &myAgenticAgent{ + name: "st-agent", + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + intEvent := TypedStatefulInterrupt[*schema.AgenticMessage](ctx, "state interrupt", "my-state") + intEvent.Action.Interrupted.Data = "stateful data" + generator.Send(intEvent) + }() + return iter + }, + } + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + iter := runner.Query(ctx, "stateful test") + + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + } + + require.NotNil(t, interruptEvent) + assert.Equal(t, "stateful data", interruptEvent.Action.Interrupted.Data) + require.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts) + assert.Equal(t, "state interrupt", interruptEvent.Action.Interrupted.InterruptContexts[0].Info) + }) +} +func TestAgenticIntegration_CheckpointInterruptResume(t *testing.T) { + ctx := context.Background() + + var resumeCalled int32 + agent := &myAgenticAgent{ + name: "ckpt-agent", + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + AgentName: "ckpt-agent", + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("before interrupt"), + }, + }, + }) + intEvent := TypedInterrupt[*schema.AgenticMessage](ctx, "need approval") + intEvent.Action.Interrupted.Data = "approval data" + generator.Send(intEvent) + }() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + atomic.StoreInt32(&resumeCalled, 1) + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + AgentName: "ckpt-agent", + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("after resume"), + }, + }, + }) + }() + return iter + }, + } + + store := newMyStore() + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + CheckPointStore: store, + }) + + iter := runner.Query(ctx, "checkpoint test", WithCheckPointID("ckpt-1")) + + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + var preInterruptOutputs []string + for { + event, ok := iter.Next() + if !ok { + break + } + require.Nil(t, event.Err) + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.Message != nil { + preInterruptOutputs = append(preInterruptOutputs, agenticTextContent(event.Output.MessageOutput.Message)) + } + } + + require.NotNil(t, interruptEvent, "should receive interrupt event") + assert.Contains(t, preInterruptOutputs, "before interrupt") + require.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts) + + interruptID := interruptEvent.Action.Interrupted.InterruptContexts[0].ID + require.NotEmpty(t, interruptID) + + resumeIter, err := runner.ResumeWithParams(ctx, "ckpt-1", &ResumeParams{ + Targets: map[string]any{ + interruptID: nil, + }, + }) + require.NoError(t, err) + + var postResumeOutputs []string + for { + event, ok := resumeIter.Next() + if !ok { + break + } + if event.Err != nil { + t.Fatalf("unexpected error during resume: %v", event.Err) + } + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.Message != nil { + postResumeOutputs = append(postResumeOutputs, agenticTextContent(event.Output.MessageOutput.Message)) + } + } + + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeCalled), "resume function should have been called") + assert.Contains(t, postResumeOutputs, "after resume") +} + +func TestAgenticIntegration_CheckpointWithMCPListToolsResult(t *testing.T) { + ctx := context.Background() + + inputSchemaJSON := `{ + "type": "object", + "properties": { + "query": {"type": "string", "description": "search query"}, + "limit": {"type": "integer", "description": "max results"} + }, + "required": ["query"] + }` + var inputSchema jsonschema.Schema + require.NoError(t, json.Unmarshal([]byte(inputSchemaJSON), &inputSchema)) + + mcpMsg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeMCPListToolsResult, + MCPListToolsResult: &schema.MCPListToolsResult{ + ServerLabel: "test-server", + Tools: []*schema.MCPListToolsItem{ + { + Name: "search", + Description: "search the web", + InputSchema: &inputSchema, + }, + }, + }, + }, + schema.NewContentBlock(&schema.AssistantGenText{Text: "here are tools"}), + }, + } + + var resumeCalled int32 + agent := &myAgenticAgent{ + name: "mcp-agent", + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer gen.Close() + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + AgentName: "mcp-agent", + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{Message: mcpMsg}, + }, + }) + gen.Send(TypedInterrupt[*schema.AgenticMessage](ctx, "approve tools")) + }() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + atomic.StoreInt32(&resumeCalled, 1) + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer gen.Close() + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + AgentName: "mcp-agent", + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{Message: agenticMsg("tools approved")}, + }, + }) + }() + return iter + }, + } + + store := newMyStore() + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + CheckPointStore: store, + }) + + iter := runner.Query(ctx, "list tools", WithCheckPointID("mcp-1")) + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + for { + ev, ok := iter.Next() + if !ok { + break + } + require.Nil(t, ev.Err) + if ev.Action != nil && ev.Action.Interrupted != nil { + interruptEvent = ev + } + } + require.NotNil(t, interruptEvent) + interruptID := interruptEvent.Action.Interrupted.InterruptContexts[0].ID + + resumeIter, err := runner.ResumeWithParams(ctx, "mcp-1", &ResumeParams{ + Targets: map[string]any{interruptID: nil}, + }) + require.NoError(t, err) + + var outputs []string + for { + ev, ok := resumeIter.Next() + if !ok { + break + } + require.Nil(t, ev.Err) + if ev.Output != nil && ev.Output.MessageOutput != nil && ev.Output.MessageOutput.Message != nil { + outputs = append(outputs, agenticTextContent(ev.Output.MessageOutput.Message)) + } + } + + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeCalled)) + assert.Contains(t, outputs, "tools approved") +} diff --git a/adk/agentic_react_test.go b/adk/agentic_react_test.go new file mode 100644 index 0000000..df505c6 --- /dev/null +++ b/adk/agentic_react_test.go @@ -0,0 +1,1288 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type agenticAgentEvent = TypedAgentEvent[*schema.AgenticMessage] + +func agenticToolCallMsg(toolName, callID, args string) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolCall, + FunctionToolCall: &schema.FunctionToolCall{Name: toolName, CallID: callID, Arguments: args}, + }, + }, + } +} + +type sequentialAgenticModel struct { + responses []*schema.AgenticMessage + callCount int32 +} + +func (m *sequentialAgenticModel) Generate(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + idx := atomic.AddInt32(&m.callCount, 1) - 1 + if int(idx) >= len(m.responses) { + return nil, fmt.Errorf("sequentialAgenticModel: no more responses (call #%d)", idx) + } + return m.responses[idx], nil +} + +func (m *sequentialAgenticModel) Stream(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + result, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + r, w := schema.Pipe[*schema.AgenticMessage](1) + go func() { defer w.Close(); w.Send(result, nil) }() + return r, nil +} + +type agenticEchoTool struct { + name string +} + +func (t *agenticEchoTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: "echoes input"}, nil +} + +func (t *agenticEchoTool) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + return "echo:" + argumentsInJSON, nil +} + +type agenticInterruptTool struct { + name string +} + +func (t *agenticInterruptTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: "interrupts on first call, returns on resume"}, nil +} + +func (t *agenticInterruptTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + wasInterrupted, _, _ := tool.GetInterruptState[any](ctx) + if !wasInterrupted { + return "", tool.Interrupt(ctx, "need_approval") + } + isResume, hasData, data := tool.GetResumeContext[string](ctx) + if isResume && hasData { + return "approved:" + data, nil + } + return "resumed_no_data", nil +} + +type agenticArgCaptureTool struct { + name string + onInvoke func(args string) string +} + +func (t *agenticArgCaptureTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: "captures args"}, nil +} + +func (t *agenticArgCaptureTool) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + return t.onInvoke(argumentsInJSON), nil +} + +type agenticSignalTool struct { + name string + started chan struct{} + result string + done chan struct{} + once sync.Once +} + +func (t *agenticSignalTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: "blocks until finish() is called"}, nil +} + +func (t *agenticSignalTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + t.once.Do(func() { t.done = make(chan struct{}) }) + select { + case t.started <- struct{}{}: + default: + } + <-t.done + return t.result, nil +} + +func (t *agenticSignalTool) finish() { + t.once.Do(func() { t.done = make(chan struct{}) }) + close(t.done) +} + +type agenticReactTestStore struct { + m map[string][]byte +} + +func (s *agenticReactTestStore) Set(_ context.Context, key string, value []byte) error { + s.m[key] = value + return nil +} + +func (s *agenticReactTestStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.m[key] + return v, ok, nil +} + +func newAgenticAgent(t *testing.T, ctx context.Context, mdl model.BaseModel[*schema.AgenticMessage], tools []tool.BaseTool) TypedAgent[*schema.AgenticMessage] { + t.Helper() + config := &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: t.Name(), + Description: "test agentic agent", + Model: mdl, + } + if len(tools) > 0 { + config.ToolsConfig = ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: tools, + }, + } + } + agent, err := NewTypedChatModelAgent(ctx, config) + require.NoError(t, err) + return agent +} + +func newAgenticRunner(t *testing.T, ctx context.Context, mdl model.BaseModel[*schema.AgenticMessage], tools []tool.BaseTool) *TypedRunner[*schema.AgenticMessage] { + t.Helper() + agent := newAgenticAgent(t, ctx, mdl, tools) + return NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) +} + +func newAgenticRunnerWithStore(t *testing.T, ctx context.Context, mdl model.BaseModel[*schema.AgenticMessage], tools []tool.BaseTool, store CheckPointStore) *TypedRunner[*schema.AgenticMessage] { + t.Helper() + agent := newAgenticAgent(t, ctx, mdl, tools) + return NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + CheckPointStore: store, + }) +} + +func drainAgenticEvents(iter *AsyncIterator[*agenticAgentEvent]) []*agenticAgentEvent { + var events []*agenticAgentEvent + for { + ev, ok := iter.Next() + if !ok { + break + } + events = append(events, ev) + } + return events +} + +func lastAgenticEvent(events []*agenticAgentEvent) *agenticAgentEvent { + if len(events) == 0 { + return nil + } + return events[len(events)-1] +} + +func firstAgenticEventError(events []*agenticAgentEvent) error { + for _, ev := range events { + if ev.Err != nil { + return ev.Err + } + } + return nil +} + +func findInterruptEvent(events []*agenticAgentEvent) *agenticAgentEvent { + for _, ev := range events { + if ev.Action != nil && ev.Action.Interrupted != nil { + return ev + } + } + return nil +} + +func TestAgenticReact_BasicInvoke(t *testing.T) { + ctx := context.Background() + + mdl := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCallMsg("echo", "call-1", `"hello"`), + agenticMsg("done: echo result received"), + }, + } + + runner := newAgenticRunner(t, ctx, mdl, []tool.BaseTool{&agenticEchoTool{name: "echo"}}) + events := drainAgenticEvents(runner.Query(ctx, "test input")) + last := lastAgenticEvent(events) + + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + assert.Equal(t, "done: echo result received", agenticTextContent(last.Output.MessageOutput.Message)) + assert.Equal(t, int32(2), atomic.LoadInt32(&mdl.callCount)) +} + +func TestAgenticReact_MultiTurnToolCalling(t *testing.T) { + ctx := context.Background() + + mdl := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCallMsg("echo", "call-1", `"step1"`), + agenticToolCallMsg("echo", "call-2", `"step2"`), + agenticToolCallMsg("echo", "call-3", `"step3"`), + agenticMsg("all done"), + }, + } + + runner := newAgenticRunner(t, ctx, mdl, []tool.BaseTool{&agenticEchoTool{name: "echo"}}) + events := drainAgenticEvents(runner.Query(ctx, "do three steps")) + last := lastAgenticEvent(events) + + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + assert.Equal(t, "all done", agenticTextContent(last.Output.MessageOutput.Message)) + assert.Equal(t, int32(4), atomic.LoadInt32(&mdl.callCount)) +} + +func TestAgenticReact_Stream(t *testing.T) { + ctx := context.Background() + + mdl := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCallMsg("echo", "call-1", `"hello"`), + agenticMsg("stream done"), + }, + } + + agent := newAgenticAgent(t, ctx, mdl, []tool.BaseTool{&agenticEchoTool{name: "echo"}}) + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + }) + + events := drainAgenticEvents(runner.Query(ctx, "stream test")) + + var finalText string + for _, ev := range events { + if ev.Output != nil && ev.Output.MessageOutput != nil { + msg, err := ev.Output.MessageOutput.GetMessage() + if err == nil && msg != nil { + txt := agenticTextContent(msg) + if txt != "" { + finalText = txt + } + } + } + } + + assert.Equal(t, "stream done", finalText) +} + +func TestAgenticReact_MaxIterations(t *testing.T) { + ctx := context.Background() + + t.Run("within_limit", func(t *testing.T) { + mdl := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCallMsg("echo", "c1", `"1"`), + agenticToolCallMsg("echo", "c2", `"2"`), + agenticMsg("done within limit"), + }, + } + + runner := newAgenticRunner(t, ctx, mdl, []tool.BaseTool{&agenticEchoTool{name: "echo"}}) + events := drainAgenticEvents(runner.Query(ctx, "go")) + last := lastAgenticEvent(events) + + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + assert.Equal(t, "done within limit", agenticTextContent(last.Output.MessageOutput.Message)) + }) + + t.Run("exceeded", func(t *testing.T) { + responses := make([]*schema.AgenticMessage, 25) + for i := range responses { + responses[i] = agenticToolCallMsg("echo", fmt.Sprintf("c%d", i), `"x"`) + } + + mdl := &sequentialAgenticModel{responses: responses} + config := &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "exceed-agent", + Description: "test max iterations exceeded", + Model: mdl, + MaxIterations: 3, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&agenticEchoTool{name: "echo"}}, + }, + }, + } + agent, err := NewTypedChatModelAgent(ctx, config) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + events := drainAgenticEvents(runner.Query(ctx, "go")) + last := lastAgenticEvent(events) + + require.NotNil(t, last) + require.NotNil(t, last.Err) + assert.ErrorIs(t, last.Err, ErrExceedMaxIterations) + }) +} + +func TestAgenticReact_ReturnDirectly(t *testing.T) { + ctx := context.Background() + + mdl := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + // Model calls the return-directly tool. + agenticToolCallMsg("direct", "call-1", `"final answer"`), + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: t.Name(), + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&agenticEchoTool{name: "direct"}}, + }, + ReturnDirectly: map[string]bool{"direct": true}, + }, + }) + require.NoError(t, err) + + t.Run("Invoke", func(t *testing.T) { + atomic.StoreInt32(&mdl.callCount, 0) + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, EnableStreaming: false, + }) + events := drainAgenticEvents(runner.Query(ctx, "test")) + + // Model should be called only once (for the tool call), not a second + // time, because the tool is return-directly. + assert.Equal(t, int32(1), atomic.LoadInt32(&mdl.callCount)) + + // Find the final output event — should be the return-directly tool result. + last := lastAgenticEvent(events) + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + + msg := last.Output.MessageOutput.Message + require.NotNil(t, msg) + require.GreaterOrEqual(t, len(msg.ContentBlocks), 1) + ftr := msg.ContentBlocks[0].FunctionToolResult + require.NotNil(t, ftr, "expected FunctionToolResult in final output, got type=%v", msg.ContentBlocks[0].Type) + assert.Equal(t, "call-1", ftr.CallID) + }) + + t.Run("Stream", func(t *testing.T) { + atomic.StoreInt32(&mdl.callCount, 0) + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, EnableStreaming: true, + }) + events := drainAgenticEvents(runner.Query(ctx, "test")) + + assert.Equal(t, int32(1), atomic.LoadInt32(&mdl.callCount)) + + last := lastAgenticEvent(events) + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + + mo := last.Output.MessageOutput + if mo.IsStreaming { + var finalMsg *schema.AgenticMessage + for { + chunk, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + break + } + finalMsg = chunk + } + require.NotNil(t, finalMsg) + require.GreaterOrEqual(t, len(finalMsg.ContentBlocks), 1) + ftr := finalMsg.ContentBlocks[0].FunctionToolResult + require.NotNil(t, ftr) + assert.Equal(t, "call-1", ftr.CallID) + } else { + msg := mo.Message + require.NotNil(t, msg) + require.GreaterOrEqual(t, len(msg.ContentBlocks), 1) + ftr := msg.ContentBlocks[0].FunctionToolResult + require.NotNil(t, ftr) + assert.Equal(t, "call-1", ftr.CallID) + } + }) +} + +func TestAgenticReact_CancelAfterChatModel(t *testing.T) { + ctx := context.Background() + + toolStarted := make(chan struct{}, 1) + var modelCallCount int32 + mdl := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + count := atomic.AddInt32(&modelCallCount, 1) + switch count { + case 1: + return agenticToolCallMsg("slow", "c1", `"hi"`), nil + case 2: + return agenticToolCallMsg("slow", "c2", `"hi2"`), nil + default: + return agenticMsg("should not reach"), nil + } + }, + } + + slowTool := &agenticSignalTool{ + name: "slow", + started: toolStarted, + result: "slow result", + } + + agent := newAgenticAgent(t, ctx, mdl, []tool.BaseTool{slowTool}) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("trigger cancel")}, + }, cancelOpt) + + <-toolStarted + + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + _ = handle.Wait() + }() + + time.Sleep(10 * time.Millisecond) + slowTool.finish() + + var capturedErr error + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + capturedErr = ev.Err + } + } + require.Error(t, capturedErr, "expected CancelError event") + var cancelErr *CancelError + require.ErrorAs(t, capturedErr, &cancelErr) +} + +func TestAgenticReact_CancelAfterToolCalls(t *testing.T) { + ctx := context.Background() + + toolStarted := make(chan struct{}, 1) + var modelCallCount int32 + mdl := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + count := atomic.AddInt32(&modelCallCount, 1) + if count == 1 { + return agenticToolCallMsg("slow", "c1", `"hi"`), nil + } + return agenticMsg("should not reach on second call"), nil + }, + } + + slowTool := &agenticSignalTool{ + name: "slow", + started: toolStarted, + result: "slow result", + } + + agent := newAgenticAgent(t, ctx, mdl, []tool.BaseTool{slowTool}) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("trigger cancel")}, + }, cancelOpt) + + <-toolStarted + + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + _ = handle.Wait() + }() + + time.Sleep(10 * time.Millisecond) + slowTool.finish() + + var capturedErr error + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + capturedErr = ev.Err + } + } + require.Error(t, capturedErr, "expected CancelError event") + var cancelErr *CancelError + require.ErrorAs(t, capturedErr, &cancelErr) + assert.Equal(t, int32(1), atomic.LoadInt32(&modelCallCount)) +} + +func TestAgenticReact_DoubleInterruptResume(t *testing.T) { + ctx := context.Background() + + var modelCallCount int32 + mdl := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + count := atomic.AddInt32(&modelCallCount, 1) + switch count { + case 1: + return agenticToolCallMsg("approval_tool", "c1", `"first"`), nil + case 2: + return agenticToolCallMsg("approval_tool", "c2", `"second"`), nil + case 3: + return agenticMsg("all approved"), nil + default: + return nil, fmt.Errorf("unexpected call #%d", count) + } + }, + } + + store := &agenticReactTestStore{m: map[string][]byte{}} + runner := newAgenticRunnerWithStore(t, ctx, mdl, []tool.BaseTool{&agenticInterruptTool{name: "approval_tool"}}, store) + + events1 := drainAgenticEvents(runner.Query(ctx, "approve twice", WithCheckPointID("dbl-cp"))) + require.NoError(t, firstAgenticEventError(events1)) + int1Event := findInterruptEvent(events1) + require.NotNil(t, int1Event, "expected first interrupt") + int1ID := int1Event.Action.Interrupted.InterruptContexts[0].ID + + iter2, err := runner.ResumeWithParams(ctx, "dbl-cp", &ResumeParams{ + Targets: map[string]any{int1ID: "approved_1"}, + }) + require.NoError(t, err) + + events2 := drainAgenticEvents(iter2) + require.NoError(t, firstAgenticEventError(events2)) + int2Event := findInterruptEvent(events2) + require.NotNil(t, int2Event, "expected second interrupt") + int2ID := int2Event.Action.Interrupted.InterruptContexts[0].ID + + iter3, err := runner.ResumeWithParams(ctx, "dbl-cp", &ResumeParams{ + Targets: map[string]any{int2ID: "approved_2"}, + }) + require.NoError(t, err) + + events3 := drainAgenticEvents(iter3) + require.NoError(t, firstAgenticEventError(events3)) + last := lastAgenticEvent(events3) + + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + assert.Contains(t, agenticTextContent(last.Output.MessageOutput.Message), "all approved") +} + +func TestAgenticReact_ResumeThenCancelAfterChatModelCheckpoint(t *testing.T) { + ctx := context.Background() + + var modelCallCount int32 + mdl := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + count := atomic.AddInt32(&modelCallCount, 1) + switch count { + case 1: + return agenticToolCallMsg("approval_tool", "c1", `"first"`), nil + case 2: + return agenticToolCallMsg("approval_tool", "c2", `"second"`), nil + default: + return nil, fmt.Errorf("unexpected call #%d", count) + } + }, + } + + store := &agenticReactTestStore{m: map[string][]byte{}} + runner := newAgenticRunnerWithStore(t, ctx, mdl, []tool.BaseTool{&agenticInterruptTool{name: "approval_tool"}}, store) + + events1 := drainAgenticEvents(runner.Query(ctx, "approve then cancel", WithCheckPointID("resume-cancel-cp"))) + require.NoError(t, firstAgenticEventError(events1)) + int1Event := findInterruptEvent(events1) + require.NotNil(t, int1Event, "expected tool interrupt") + int1ID := int1Event.Action.Interrupted.InterruptContexts[0].ID + + cancelOpt, cancelFn := WithCancel() + handle, contributed := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + require.True(t, contributed) + + iter2, err := runner.ResumeWithParams(ctx, "resume-cancel-cp", &ResumeParams{ + Targets: map[string]any{int1ID: "approved"}, + }, cancelOpt) + require.NoError(t, err) + + events2 := drainAgenticEvents(iter2) + require.NoError(t, handle.Wait()) + for _, event := range events2 { + if event.Err == nil { + continue + } + var cancelErr *CancelError + require.ErrorAs(t, event.Err, &cancelErr) + } +} + +func TestAgenticReact_ChatModelAgent_NoTools(t *testing.T) { + ctx := context.Background() + + mdl := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("no tools response"), nil + }, + } + + runner := newAgenticRunner(t, ctx, mdl, nil) + events := drainAgenticEvents(runner.Query(ctx, "hello")) + last := lastAgenticEvent(events) + + require.NotNil(t, last) + require.Nil(t, last.Err) + require.NotNil(t, last.Output) + require.NotNil(t, last.Output.MessageOutput) + assert.Equal(t, "no tools response", agenticTextContent(last.Output.MessageOutput.Message)) +} + +func TestAgenticReact_ChatModelAgent_ToolsReceiveArgs(t *testing.T) { + ctx := context.Background() + + var receivedArgs string + captureTool := &agenticArgCaptureTool{ + name: "capture", + onInvoke: func(args string) string { + receivedArgs = args + return "captured" + }, + } + + mdl := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCallMsg("capture", "c1", `{"foo":"bar"}`), + agenticMsg("done"), + }, + } + + runner := newAgenticRunner(t, ctx, mdl, []tool.BaseTool{captureTool}) + drainAgenticEvents(runner.Query(ctx, "call capture")) + + assert.Equal(t, `{"foo":"bar"}`, receivedArgs) +} + +func TestCoverage_AgenticReact_Streaming(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + streamFn: func(_ context.Context, input []*schema.AgenticMessage, _ ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + r, w := schema.Pipe[*schema.AgenticMessage](1) + go func() { + defer w.Close() + w.Send(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "streamed response"}), + }, + }, nil) + }() + return r, nil + }, + } + + echoTool := &agenticEchoTool{name: "echo"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "stream-react", + Description: "streaming agentic react", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{echoTool}, + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + }) + + iter := runner.Query(ctx, "stream me") + + var events []*TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming { + stream := event.Output.MessageOutput.MessageStream + for { + _, sErr := stream.Recv() + if sErr != nil { + break + } + } + } + events = append(events, event) + } + + require.NotEmpty(t, events) + assertAgenticEventRoleFields(t, events) +} + +func TestCoverage_ConcatMessageStream_Agentic(t *testing.T) { + t.Run("Success", func(t *testing.T) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + defer w.Close() + w.Send(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hello "}), + }, + }, nil) + w.Send(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "world"}), + }, + }, nil) + }() + + result, err := concatMessageStream(r) + assert.NoError(t, err) + assert.NotNil(t, result) + }) + + t.Run("ErrorDuringRecv", func(t *testing.T) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + w.Send(nil, fmt.Errorf("recv error")) + w.Close() + }() + + _, err := concatMessageStream(r) + assert.Error(t, err) + }) +} + +func TestCoverage_AgenticReact_InterruptResume(t *testing.T) { + ctx := context.Background() + + interruptTool := &agenticInterruptTool{name: "approval"} + + var callIdx int32 + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + idx := atomic.AddInt32(&callIdx, 1) + if idx == 1 { + return agenticToolCallMsg("approval", "call1", `{}`), nil + } + return agenticMsg("approved and done"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "interrupt-agent", + Description: "tests interrupt and resume", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{interruptTool}, + }, + }, + }) + require.NoError(t, err) + + store := newDTTestStore() + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []*schema.AgenticMessage{ + schema.UserAgenticMessage("need approval"), + }, WithCheckPointID("cp-int")) + + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + } + + require.NotNil(t, interruptEvent, "should have interrupt event") + + var rootCauseID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + rootCauseID = intCtx.ID + break + } + } + require.NotEmpty(t, rootCauseID) + + resumeIter, err := runner.ResumeWithParams(ctx, "cp-int", &ResumeParams{ + Targets: map[string]any{rootCauseID: "approved"}, + }) + require.NoError(t, err) + + var events []*TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := resumeIter.Next() + if !ok { + break + } + events = append(events, event) + } + require.NotEmpty(t, events) +} + +func TestCoverage_AgenticMessageHasToolCalls(t *testing.T) { + t.Run("NilMessage", func(t *testing.T) { + assert.False(t, agenticMessageHasToolCalls(nil)) + }) + + t.Run("NoToolCalls", func(t *testing.T) { + msg := agenticMsg("just text") + assert.False(t, agenticMessageHasToolCalls(msg)) + }) + + t.Run("HasToolCalls", func(t *testing.T) { + msg := agenticToolCallMsg("tool1", "id1", `{}`) + assert.True(t, agenticMessageHasToolCalls(msg)) + }) + + t.Run("NilBlock", func(t *testing.T) { + msg := &schema.AgenticMessage{ + ContentBlocks: []*schema.ContentBlock{nil}, + } + assert.False(t, agenticMessageHasToolCalls(msg)) + }) + + t.Run("ToolCallBlockNilFunctionToolCall", func(t *testing.T) { + msg := &schema.AgenticMessage{ + ContentBlocks: []*schema.ContentBlock{ + {Type: schema.ContentBlockTypeFunctionToolCall, FunctionToolCall: nil}, + }, + } + assert.False(t, agenticMessageHasToolCalls(msg)) + }) +} + +func TestCoverage_ChatModelAgent_StreamError(t *testing.T) { + ctx := context.Background() + + testErr := errors.New("stream failed") + m := &mockAgenticModel{ + streamFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + return nil, testErr + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "stream-error-agent", + Description: "tests stream error", + Model: m, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + }) + + iter := runner.Query(ctx, "trigger stream error") + + var capturedErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + capturedErr = event.Err + } + } + require.Error(t, capturedErr, "should propagate stream error") +} + +func TestCoverage_AgenticReact_GobStateRoundTrip(t *testing.T) { + ctx := context.Background() + + var callIdx int32 + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + idx := atomic.AddInt32(&callIdx, 1) + if idx == 1 { + return agenticToolCallMsg("interrupt_tool", "call1", `{}`), nil + } + return agenticMsg("completed"), nil + }, + } + + interruptTool := &agenticInterruptTool{name: "interrupt_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "gob-test", + Description: "tests gob state round trip", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{interruptTool}, + }, + }, + }) + require.NoError(t, err) + + store := newDTTestStore() + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []*schema.AgenticMessage{ + schema.UserAgenticMessage("test gob"), + }, WithCheckPointID("gob-cp")) + + var interrupted bool + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interrupted = true + interruptEvent = event + } + } + + if !interrupted || interruptEvent == nil { + t.Skip("no interrupt occurred, skipping gob round-trip test") + } + + _, exists, err := store.Get(ctx, "gob-cp") + assert.NoError(t, err) + assert.True(t, exists, "checkpoint should be saved") + + var rootCauseID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + rootCauseID = intCtx.ID + break + } + } + require.NotEmpty(t, rootCauseID) + + resumeIter, err := runner.ResumeWithParams(ctx, "gob-cp", &ResumeParams{ + Targets: map[string]any{rootCauseID: "approved"}, + }) + require.NoError(t, err) + + var resumed bool + for { + event, ok := resumeIter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + resumed = true + } + } + assert.True(t, resumed, "should successfully resume from gob checkpoint") +} + +func TestCoverage_GetMessageFromTypedWrappedEvent_Agentic(t *testing.T) { + t.Run("NilOutput", func(t *testing.T) { + wrapper := &typedAgentEventWrapper[*schema.AgenticMessage]{ + event: &TypedAgentEvent[*schema.AgenticMessage]{}, + } + msg, err := getMessageFromTypedWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Nil(t, msg) + }) + + t.Run("NonStreaming", func(t *testing.T) { + expected := agenticMsg("hello") + wrapper := &typedAgentEventWrapper[*schema.AgenticMessage]{ + event: &TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: expected, + }, + }, + }, + } + msg, err := getMessageFromTypedWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Equal(t, expected, msg) + }) + + t.Run("StreamingAlreadyConcatenated", func(t *testing.T) { + expected := agenticMsg("already concatenated") + wrapper := &typedAgentEventWrapper[*schema.AgenticMessage]{ + concatenatedMessage: expected, + event: &TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + }, + }, + }, + } + msg, err := getMessageFromTypedWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Equal(t, expected, msg) + }) + + t.Run("StreamingWithPriorError", func(t *testing.T) { + testErr := errors.New("prior stream error") + wrapper := &typedAgentEventWrapper[*schema.AgenticMessage]{ + event: &TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + }, + }, + }, + } + wrapper.StreamErr = testErr + msg, err := getMessageFromTypedWrappedEvent(wrapper) + assert.Equal(t, testErr, err) + assert.Nil(t, msg) + }) +} + +func TestCoverage_GetMessageFromWrappedEvent_ErrorPaths(t *testing.T) { + t.Run("NilOutput", func(t *testing.T) { + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{}, + } + msg, err := getMessageFromWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Nil(t, msg) + }) + + t.Run("NonStreaming", func(t *testing.T) { + expected := schema.AssistantMessage("hello", nil) + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: expected, + }, + }, + }, + } + msg, err := getMessageFromWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Equal(t, expected, msg) + }) + + t.Run("AlreadyConcatenated", func(t *testing.T) { + expected := schema.AssistantMessage("concatenated", nil) + wrapper := &agentEventWrapper{ + concatenatedMessage: expected, + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + }, + }, + }, + } + msg, err := getMessageFromWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Equal(t, expected, msg) + }) + + t.Run("PriorStreamError", func(t *testing.T) { + testErr := errors.New("prior error") + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + }, + }, + }, + } + wrapper.StreamErr = testErr + msg, err := getMessageFromWrappedEvent(wrapper) + assert.Equal(t, testErr, err) + assert.Nil(t, msg) + }) +} + +func TestCoverage_ConsumeStream_ErrorDuringRecv(t *testing.T) { + testErr := errors.New("stream recv error") + r, w := schema.Pipe[*schema.Message](2) + go func() { + w.Send(schema.AssistantMessage("partial", nil), nil) + w.Send(nil, testErr) + w.Close() + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + }, + }, + }, + } + + wrapper.consumeStream() + + assert.NotNil(t, wrapper.StreamErr) + assert.Nil(t, wrapper.concatenatedMessage) +} + +func TestCoverage_ConsumeStream_EmptyStream(t *testing.T) { + r, w := schema.Pipe[*schema.Message](1) + go func() { w.Close() }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + }, + }, + }, + } + + wrapper.consumeStream() + + require.NotNil(t, wrapper.StreamErr) + assert.Contains(t, wrapper.StreamErr.Error(), "no messages") +} + +func TestCoverage_ConsumeStream_MultipleMessages(t *testing.T) { + r, w := schema.Pipe[*schema.Message](3) + go func() { + defer w.Close() + w.Send(schema.AssistantMessage("chunk1", nil), nil) + w.Send(schema.AssistantMessage("chunk2", nil), nil) + w.Send(schema.AssistantMessage("chunk3", nil), nil) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + }, + }, + }, + } + + wrapper.consumeStream() + + assert.Nil(t, wrapper.StreamErr) + assert.NotNil(t, wrapper.concatenatedMessage) +} + +func TestCoverage_ConsumeStream_SingleMessage(t *testing.T) { + r, w := schema.Pipe[*schema.Message](1) + go func() { + defer w.Close() + w.Send(schema.AssistantMessage("single", nil), nil) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + }, + }, + }, + } + + wrapper.consumeStream() + + assert.Nil(t, wrapper.StreamErr) + require.NotNil(t, wrapper.concatenatedMessage) + assert.Equal(t, "single", wrapper.concatenatedMessage.Content) +} + +func TestCoverage_ConsumeStream_Idempotent(t *testing.T) { + r, w := schema.Pipe[*schema.Message](1) + go func() { + defer w.Close() + w.Send(schema.AssistantMessage("once", nil), nil) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + }, + }, + }, + } + + wrapper.consumeStream() + msg1 := wrapper.concatenatedMessage + + wrapper.consumeStream() + msg2 := wrapper.concatenatedMessage + + assert.Equal(t, msg1, msg2, "second call should be no-op") +} diff --git a/adk/agentic_test.go b/adk/agentic_test.go new file mode 100644 index 0000000..5a4ae88 --- /dev/null +++ b/adk/agentic_test.go @@ -0,0 +1,1745 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "io" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type mockAgenticModel struct { + generateFn func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) + streamFn func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) +} + +func (m *mockAgenticModel) Generate(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return m.generateFn(ctx, input, opts...) +} + +func (m *mockAgenticModel) Stream(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + if m.streamFn != nil { + return m.streamFn(ctx, input, opts...) + } + result, err := m.generateFn(ctx, input, opts...) + if err != nil { + return nil, err + } + r, w := schema.Pipe[*schema.AgenticMessage](1) + go func() { defer w.Close(); w.Send(result, nil) }() + return r, nil +} + +type testAgenticMiddleware struct { + *TypedBaseChatModelAgentMiddleware[*schema.AgenticMessage] + beforeFn func(context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) + afterFn func(context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) +} + +func (m *testAgenticMiddleware) BeforeModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage], mc *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) { + if m.beforeFn != nil { + return m.beforeFn(ctx, state, mc) + } + return ctx, state, nil +} + +func (m *testAgenticMiddleware) AfterModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage], mc *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) { + if m.afterFn != nil { + return m.afterFn(ctx, state, mc) + } + return ctx, state, nil +} + +func TestAgenticChatModelAgentRun_NoTools(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hello from agentic model"}), + }, + } + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticResponse, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticTestAgent", + Description: "Agentic test agent", + Instruction: "You are helpful.", + Model: m, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hi"), + }, + } + iter := agent.Run(ctx, input) + require.NotNil(t, iter) + + event, ok := iter.Next() + assert.True(t, ok) + require.NotNil(t, event) + assert.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + require.NotNil(t, msg) + assert.Equal(t, schema.AgenticRoleTypeAssistant, msg.Role) + assert.Len(t, msg.ContentBlocks, 1) + assert.Equal(t, "Hello from agentic model", msg.ContentBlocks[0].AssistantGenText.Text) + + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestAgenticChatModelAgentRun_WithTools(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Used tool and got result"}), + }, + } + + var receivedToolInfos []*schema.ToolInfo + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + o := model.GetCommonOptions(&model.Options{}, opts...) + receivedToolInfos = o.Tools + return agenticResponse, nil + }, + } + + dummyTool := newSlowTool("dummy_tool", 0, "ok") + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticToolAgent", + Description: "Agentic agent with tools", + Instruction: "You are helpful.", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{dummyTool}, + }, + }, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Call a tool"), + }, + } + iter := agent.Run(ctx, input) + + event, ok := iter.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + + _, ok = iter.Next() + assert.False(t, ok) + + require.Len(t, receivedToolInfos, 1) + assert.Equal(t, "dummy_tool", receivedToolInfos[0].Name) +} + +func TestAgenticChatModelAgentRun_Streaming(t *testing.T) { + ctx := context.Background() + + chunk1 := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hello "}), + }, + } + chunk2 := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "world"}), + }, + } + + m := &mockAgenticModel{ + streamFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + defer w.Close() + w.Send(chunk1, nil) + w.Send(chunk2, nil) + }() + return r, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticStreamAgent", + Description: "Agentic streaming agent", + Instruction: "You are helpful.", + Model: m, + }) + assert.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hi"), + }, + EnableStreaming: true, + } + iter := agent.Run(ctx, input) + + event, ok := iter.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + require.NotNil(t, event.Output.MessageOutput.MessageStream) + event.Output.MessageOutput.MessageStream.Close() + + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestDefaultAgenticGenModelInput(t *testing.T) { + ctx := context.Background() + + t.Run("WithInstruction", func(t *testing.T) { + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello"), + }, + } + msgs, err := newDefaultGenModelInput[*schema.AgenticMessage]()(ctx, "Be helpful", input) + assert.NoError(t, err) + assert.Len(t, msgs, 2) + assert.Equal(t, schema.AgenticRoleTypeSystem, msgs[0].Role) + assert.Equal(t, schema.AgenticRoleTypeUser, msgs[1].Role) + }) + + t.Run("WithoutInstruction", func(t *testing.T) { + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello"), + }, + } + msgs, err := newDefaultGenModelInput[*schema.AgenticMessage]()(ctx, "", input) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + assert.Equal(t, schema.AgenticRoleTypeUser, msgs[0].Role) + }) +} + +func TestAgenticRunnerQuery(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "query response"}), + }, + } + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticResponse, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "QueryAgent", + Description: "Query test agent", + Instruction: "Be helpful.", + Model: m, + }) + assert.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + + iter := runner.Query(ctx, "What's up?") + + event, ok := iter.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + + _, ok = iter.Next() + assert.False(t, ok) +} + +func agenticAssistantMessage(text string) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: text}), + }, + } +} + +type mockAgenticRunnerAgent struct { + name string + description string + responses []*TypedAgentEvent[*schema.AgenticMessage] + callCount int + lastInput *TypedAgentInput[*schema.AgenticMessage] + enableStreaming bool +} + +func (a *mockAgenticRunnerAgent) Name(_ context.Context) string { return a.name } +func (a *mockAgenticRunnerAgent) Description(_ context.Context) string { return a.description } +func (a *mockAgenticRunnerAgent) Run(_ context.Context, input *TypedAgentInput[*schema.AgenticMessage], _ ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + a.callCount++ + a.lastInput = input + a.enableStreaming = input.EnableStreaming + + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + for _, event := range a.responses { + generator.Send(event) + if event.Action != nil && event.Action.Exit { + break + } + } + }() + return iterator +} + +type mockAgenticAgent struct { + name string + description string + responses []*TypedAgentEvent[*schema.AgenticMessage] +} + +func (a *mockAgenticAgent) Name(_ context.Context) string { return a.name } +func (a *mockAgenticAgent) Description(_ context.Context) string { return a.description } +func (a *mockAgenticAgent) Run(_ context.Context, _ *TypedAgentInput[*schema.AgenticMessage], _ ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer generator.Close() + for _, event := range a.responses { + generator.Send(event) + if event.Action != nil && event.Action.Exit { + break + } + } + }() + return iterator +} + +type myAgenticAgent struct { + name string + runFn func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] + resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] +} + +func (m *myAgenticAgent) Name(_ context.Context) string { + if len(m.name) > 0 { + return m.name + } + return "myAgenticAgent" +} +func (m *myAgenticAgent) Description(_ context.Context) string { return "my agentic agent description" } +func (m *myAgenticAgent) Run(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + return m.runFn(ctx, input, options...) +} +func (m *myAgenticAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + return m.resumeFn(ctx, info, opts...) +} + +func TestAgenticChatModelAgentRun_WithMiddleware(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hello from agentic agent"}), + }, + } + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticResponse, nil + }, + } + + afterModelExecuted := false + + mw := &testAgenticMiddleware{ + beforeFn: func(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage], mc *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) { + state.Messages = append(state.Messages, schema.UserAgenticMessage("extra")) + return ctx, state, nil + }, + afterFn: func(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage], mc *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) { + assert.Len(t, state.Messages, 4) + afterModelExecuted = true + return ctx, state, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticMiddlewareAgent", + Description: "Agentic agent with middleware", + Instruction: "You are helpful.", + Model: m, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{mw}, + }) + assert.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hi"), + }, + } + iter := agent.Run(ctx, input) + event, ok := iter.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + require.NotNil(t, event.Output.MessageOutput.Message) + assert.Equal(t, schema.AgenticRoleTypeAssistant, event.Output.MessageOutput.Message.Role) + _, ok = iter.Next() + assert.False(t, ok) + assert.True(t, afterModelExecuted) +} + +func TestAgenticAfterModel_NoTools_ModifyDoesNotAffectEvent(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticAssistantMessage("original content"), nil + }, + } + + var capturedMessages []*schema.AgenticMessage + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticAfterModelAgent", + Description: "Test AfterModelRewriteState", + Instruction: "You are helpful.", + Model: m, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{ + &testAgenticMiddleware{ + afterFn: func(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage], mc *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) { + capturedMessages = make([]*schema.AgenticMessage, len(state.Messages)) + copy(capturedMessages, state.Messages) + state.Messages = append(state.Messages, agenticAssistantMessage("appended content")) + return ctx, state, nil + }, + }, + }, + }) + assert.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello"), + }, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + require.NotNil(t, msg) + assert.Equal(t, "original content", msg.ContentBlocks[0].AssistantGenText.Text) + + _, ok = iterator.Next() + assert.False(t, ok) + + assert.Len(t, capturedMessages, 3) +} + +func TestAgenticGetComposeOptions_WithChatModelOptions(t *testing.T) { + ctx := context.Background() + + var capturedTemperature float32 + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + options := model.GetCommonOptions(&model.Options{}, opts...) + if options.Temperature != nil { + capturedTemperature = *options.Temperature + } + return agenticAssistantMessage("response"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticOptionsAgent", + Description: "Test agent", + Model: m, + }) + assert.NoError(t, err) + + temp := float32(0.7) + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("test")}}, + WithChatModelOptions([]model.Option{model.WithTemperature(temp)})) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, temp, capturedTemperature) +} + +func TestAgenticChatModelAgent_PrepareExecContextError(t *testing.T) { + ctx := context.Background() + + expectedErr := errors.New("tool info error") + errTool := &errorTool{infoErr: expectedErr} + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticAssistantMessage("response"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticErrToolAgent", + Description: "Test agent", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{errTool}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("test")}}) + + event, ok := iter.Next() + assert.True(t, ok) + assert.NotNil(t, event.Err) + assert.Contains(t, event.Err.Error(), "tool info error") + + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestAgenticChatModelAgentOutputKey(t *testing.T) { + t.Run("OutputKeyStoresInSession", func(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticAssistantMessage("Hello from agentic assistant."), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticOutputKeyAgent", + Description: "Test agent for output key", + Instruction: "You are helpful.", + Model: m, + OutputKey: "agent_output", + }) + assert.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello"), + }, + } + ctx, runCtx := initTypedRunCtx(ctx, "AgenticOutputKeyAgent", input) + require.NotNil(t, runCtx) + require.NotNil(t, runCtx.Session) + + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + + msg := event.Output.MessageOutput.Message + assert.Equal(t, "Hello from agentic assistant.", msg.ContentBlocks[0].AssistantGenText.Text) + + _, ok = iterator.Next() + assert.False(t, ok) + + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "agent_output") + assert.Equal(t, "Hello from agentic assistant.", sessionValues["agent_output"]) + }) + + t.Run("OutputKeyWithStreamingStoresInSession", func(t *testing.T) { + ctx := context.Background() + + chunk1 := agenticAssistantMessage("Hello") + chunk2 := agenticAssistantMessage(", world.") + + m := &mockAgenticModel{ + streamFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + defer w.Close() + w.Send(chunk1, nil) + w.Send(chunk2, nil) + }() + return r, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticStreamOutputKeyAgent", + Description: "Test agent for streaming output key", + Instruction: "You are helpful.", + Model: m, + OutputKey: "agent_output", + }) + assert.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello"), + }, + EnableStreaming: true, + } + ctx, runCtx := initTypedRunCtx(ctx, "AgenticStreamOutputKeyAgent", input) + require.NotNil(t, runCtx) + require.NotNil(t, runCtx.Session) + + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + _, ok = iterator.Next() + assert.False(t, ok) + }) + + t.Run("SetOutputToSessionAgenticMessage", func(t *testing.T) { + ctx := context.Background() + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("test")}, + } + ctx, runCtx := initTypedRunCtx(ctx, "TestAgent", input) + require.NotNil(t, runCtx) + require.NotNil(t, runCtx.Session) + + msg := agenticAssistantMessage("Test response") + err := setOutputToSession(ctx, msg, nil, "test_output") + assert.NoError(t, err) + + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "test_output") + assert.Equal(t, "Test response", sessionValues["test_output"]) + }) +} + +func TestAgenticRunner_Run_WithStreaming(t *testing.T) { + ctx := context.Background() + + mockAgent_ := &mockAgenticRunnerAgent{ + name: "AgenticStreamRunnerAgent", + description: "Test agent for agentic runner streaming", + responses: []*TypedAgentEvent[*schema.AgenticMessage]{ + { + AgentName: "AgenticStreamRunnerAgent", + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + MessageStream: schema.StreamReaderFromArray([]*schema.AgenticMessage{ + agenticAssistantMessage("Streaming response"), + }), + }, + }, + }, + }, + } + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{EnableStreaming: true, Agent: mockAgent_}) + + msgs := []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello, agent!"), + } + + iterator := runner.Run(ctx, msgs) + + assert.Equal(t, 1, mockAgent_.callCount) + assert.Equal(t, msgs, mockAgent_.lastInput.Messages) + assert.True(t, mockAgent_.enableStreaming) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.Equal(t, "AgenticStreamRunnerAgent", event.AgentName) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestAgenticRunner_Query_WithStreaming(t *testing.T) { + ctx := context.Background() + + mockAgent_ := &mockAgenticRunnerAgent{ + name: "AgenticStreamQueryAgent", + description: "Test agent for agentic runner query streaming", + responses: []*TypedAgentEvent[*schema.AgenticMessage]{ + { + AgentName: "AgenticStreamQueryAgent", + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + MessageStream: schema.StreamReaderFromArray([]*schema.AgenticMessage{ + agenticAssistantMessage("Streaming query response"), + }), + }, + }, + }, + }, + } + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{EnableStreaming: true, Agent: mockAgent_}) + + iterator := runner.Query(ctx, "Test query") + + assert.Equal(t, 1, mockAgent_.callCount) + assert.Len(t, mockAgent_.lastInput.Messages, 1) + assert.True(t, mockAgent_.enableStreaming) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.Equal(t, "AgenticStreamQueryAgent", event.AgentName) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestAgenticSimpleInterrupt(t *testing.T) { + data := "hello world" + agent := &myAgenticAgent{ + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + MessageStream: schema.StreamReaderFromArray([]*schema.AgenticMessage{ + schema.UserAgenticMessage("hello "), + schema.UserAgenticMessage("world"), + }), + }, + }, + }) + intEvent := TypedInterrupt[*schema.AgenticMessage](ctx, data) + intEvent.Action.Interrupted.Data = data + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + assert.True(t, info.WasInterrupted) + assert.Nil(t, info.InterruptState) + assert.True(t, info.EnableStreaming) + assert.Equal(t, data, info.Data) + + assert.True(t, info.IsResumeTarget) + iter, generator := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + generator.Close() + return iter + }, + } + store := newMyStore() + ctx := context.Background() + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + CheckPointStore: store, + }) + iter := runner.Query(ctx, "hello world", WithCheckPointID("1")) + + var interruptEvent *TypedAgentEvent[*schema.AgenticMessage] + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + } + + require.NotNil(t, interruptEvent) + assert.Equal(t, data, interruptEvent.Action.Interrupted.Data) + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts[0].ID) + assert.True(t, interruptEvent.Action.Interrupted.InterruptContexts[0].IsRootCause) + assert.Equal(t, data, interruptEvent.Action.Interrupted.InterruptContexts[0].Info) + assert.Equal(t, Address{{Type: AddressSegmentAgent, ID: "myAgenticAgent"}}, + interruptEvent.Action.Interrupted.InterruptContexts[0].Address) +} + +func TestCascadingFrom_NewChatModelAgentFrom(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "from response"}), + }, + } + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticResponse, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "FromAgent", + Description: "Test cascading constructor", + Instruction: "Be helpful.", + Model: m, + }) + assert.NoError(t, err) + assert.Equal(t, "FromAgent", agent.Name(ctx)) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + + iter := runner.Run(ctx, []*schema.AgenticMessage{ + schema.UserAgenticMessage("Hello"), + }) + + event, ok := iter.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestCascadingTyped_TypedStatefulInterrupt(t *testing.T) { + ctx := context.Background() + ctx = AppendAddressSegment(ctx, AddressSegmentAgent, "test-agent") + + type myState struct { + Count int + } + + event := TypedStatefulInterrupt[*schema.AgenticMessage](ctx, "please confirm", &myState{Count: 42}) + require.NotNil(t, event) + require.NotNil(t, event.Action) + require.NotNil(t, event.Action.Interrupted) +} + +func TestCascadingTyped_EventFromAgenticMessage(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "hello"}), + }, + } + + event := EventFromAgenticMessage(msg, nil, schema.AgenticRoleTypeAssistant) + require.NotNil(t, event) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + assert.Equal(t, msg, event.Output.MessageOutput.Message) + assert.False(t, event.Output.MessageOutput.IsStreaming) + assert.Equal(t, schema.RoleType(""), event.Output.MessageOutput.Role) + assert.Equal(t, schema.AgenticRoleTypeAssistant, event.Output.MessageOutput.AgenticRole) + assert.Empty(t, event.Output.MessageOutput.ToolName) +} + +// assertAgenticEventRoleFields asserts that all AgenticMessage events in the +// list have zero-valued Role and ToolName fields (which are *schema.Message-only), +// and that AgenticRole is populated with a non-zero value. +func assertAgenticEventRoleFields(t *testing.T, events []*TypedAgentEvent[*schema.AgenticMessage]) { + t.Helper() + for i, event := range events { + if event.Output == nil || event.Output.MessageOutput == nil { + continue + } + mo := event.Output.MessageOutput + assert.Equal(t, schema.RoleType(""), mo.Role, "event[%d]: AgenticMessage must have zero Role", i) + assert.Empty(t, mo.ToolName, "event[%d]: AgenticMessage must have empty ToolName", i) + assert.NotEmpty(t, mo.AgenticRole, "event[%d]: AgenticMessage must have non-zero AgenticRole", i) + } +} + +func TestCoverage_FlowAgent_ResumeNotResumable(t *testing.T) { + ctx := context.Background() + + agent := &mockAgenticAgent{ + name: "non-resumable", + description: "cannot resume", + responses: []*TypedAgentEvent[*schema.AgenticMessage]{ + {Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("done"), + }, + }}, + }, + } + + fa := toTypedFlowAgent[*schema.AgenticMessage](agent) + + info := &ResumeInfo{WasInterrupted: true} + iter := fa.Resume(ctx, info) + + var capturedErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + capturedErr = event.Err + } + } + require.Error(t, capturedErr, "should get error for non-resumable agent") +} + +func TestCoverage_GenAgenticErrorIter(t *testing.T) { + testErr := errors.New("test agentic error") + iter := genAgenticErrorIter(testErr) + + event, ok := iter.Next() + require.True(t, ok) + assert.Equal(t, testErr, event.Err) + + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestCoverage_ChatModelAgent_OnSetSubAgents_FrozenError(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("done"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "freeze-test", + Description: "frozen test agent", + Model: m, + }) + require.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("Hi")}, + } + iter := agent.Run(ctx, input) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + err = agent.OnSetSubAgents(ctx, []TypedAgent[*schema.AgenticMessage]{ + &mockAgenticAgent{name: "late-child"}, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frozen") +} + +func TestCoverage_ChatModelAgent_OnSetAsSubAgent_FrozenError(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("done"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "freeze-child", + Description: "frozen child agent", + Model: m, + }) + require.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("Hi")}, + } + iter := agent.Run(ctx, input) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + err = agent.OnSetAsSubAgent(ctx, &mockAgenticAgent{name: "parent"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frozen") +} + +func TestCoverage_ChatModelAgent_OnSetAsSubAgent_DuplicateError(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("done"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "dup-child", + Description: "duplicate child agent", + Model: m, + }) + require.NoError(t, err) + + err = agent.OnSetAsSubAgent(ctx, &mockAgenticAgent{name: "parent1"}) + assert.NoError(t, err) + + err = agent.OnSetAsSubAgent(ctx, &mockAgenticAgent{name: "parent2"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "already been set as a sub-agent") +} + +func TestCoverage_ChatModelAgent_OnDisallowTransferToParent_FrozenError(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("done"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "disallow-test", + Description: "disallow transfer test", + Model: m, + }) + require.NoError(t, err) + + input := &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("Hi")}, + } + iter := agent.Run(ctx, input) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + err = agent.OnDisallowTransferToParent(ctx) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frozen") +} + +func TestCoverage_TypedGetMessage_AgenticNonStreaming(t *testing.T) { + msg := agenticMsg("hello") + event := &TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: msg, + }, + }, + } + + result, retEvent, err := TypedGetMessage(event) + assert.NoError(t, err) + assert.Equal(t, msg, result) + assert.Equal(t, event, retEvent) +} + +func TestCoverage_TypedGetMessage_AgenticStreaming(t *testing.T) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + defer w.Close() + w.Send(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hello "}), + }, + }, nil) + w.Send(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "world"}), + }, + }, nil) + }() + + event := &TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + MessageStream: r, + }, + }, + } + + result, retEvent, err := TypedGetMessage(event) + assert.NoError(t, err) + assert.NotNil(t, result) + require.NotNil(t, retEvent) + assert.NotNil(t, retEvent.Output.MessageOutput.MessageStream) +} + +func TestCoverage_TypedGetMessage_NilOutput(t *testing.T) { + event := &TypedAgentEvent[*schema.AgenticMessage]{} + + result, retEvent, err := TypedGetMessage(event) + assert.NoError(t, err) + assert.Nil(t, result) + assert.Equal(t, event, retEvent) +} + +func TestCoverage_GetMessage_NonStreaming(t *testing.T) { + msg := schema.AssistantMessage("hello", nil) + event := &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: msg, + }, + }, + } + + result, retEvent, err := GetMessage(event) + assert.NoError(t, err) + assert.Equal(t, msg, result) + assert.Equal(t, event, retEvent) +} + +func TestCoverage_GetMessage_Streaming(t *testing.T) { + r, w := schema.Pipe[*schema.Message](2) + go func() { + defer w.Close() + w.Send(schema.AssistantMessage("Hello ", nil), nil) + w.Send(schema.AssistantMessage("world", nil), nil) + }() + + event := &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + }, + }, + } + + result, retEvent, err := GetMessage(event) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.NotNil(t, retEvent) +} + +func TestCoverage_NewTypedAgentTool_Agentic(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("tool response"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "tool-agent", + Description: "agent wrapped as tool", + Model: m, + }) + require.NoError(t, err) + + agentTool := NewTypedAgentTool[*schema.AgenticMessage](ctx, agent) + + info, err := agentTool.Info(ctx) + require.NoError(t, err) + assert.Equal(t, "tool-agent", info.Name) + + result, err := agentTool.(tool.InvokableTool).InvokableRun(ctx, `{"request":"test"}`) + require.NoError(t, err) + assert.Contains(t, result, "tool response") +} +func TestCoverage_CopyAgenticEvent(t *testing.T) { + original := &TypedAgentEvent[*schema.AgenticMessage]{ + AgentName: "agent1", + RunPath: []RunStep{{agentName: "root"}, {agentName: "agent1"}}, + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("hello"), + AgenticRole: schema.AgenticRoleTypeAssistant, + }, + }, + Action: &AgentAction{ + TransferToAgent: &TransferToAgentAction{DestAgentName: "agent2"}, + }, + } + + copied := copyTypedAgentEvent(original) + assert.Equal(t, original.AgentName, copied.AgentName) + assert.Equal(t, len(original.RunPath), len(copied.RunPath)) + assert.Equal(t, original.Action, copied.Action) + assert.Equal(t, schema.AgenticRoleTypeAssistant, copied.Output.MessageOutput.AgenticRole) + + copied.RunPath[0].agentName = "mutated" + assert.NotEqual(t, original.RunPath[0].agentName, copied.RunPath[0].agentName) +} + +func TestCoverage_ChatModelAgent_ModelGenerateError(t *testing.T) { + ctx := context.Background() + + testErr := errors.New("model generate failed") + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return nil, testErr + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "error-model-agent", + Description: "tests model generate error", + Model: m, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + }) + + iter := runner.Query(ctx, "trigger error") + + var capturedErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + capturedErr = event.Err + } + } + require.Error(t, capturedErr, "should propagate model error") +} + +func TestCoverage_NewTypedUserMessages(t *testing.T) { + t.Run("Message", func(t *testing.T) { + msgs := newTypedUserMessages[*schema.Message]("hello") + require.Len(t, msgs, 1) + assert.Equal(t, schema.User, msgs[0].Role) + assert.Equal(t, "hello", msgs[0].Content) + }) + + t.Run("AgenticMessage", func(t *testing.T) { + msgs := newTypedUserMessages[*schema.AgenticMessage]("hello") + require.Len(t, msgs, 1) + assert.Equal(t, schema.AgenticRoleTypeUser, msgs[0].Role) + }) +} + +func TestCoverage_TypedEndpointModel_NilEndpoints(t *testing.T) { + ctx := context.Background() + + m := &typedEndpointModel[*schema.AgenticMessage]{} + + _, err := m.Generate(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "generate endpoint not set") + + _, err = m.Stream(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "stream endpoint not set") +} + +func TestCoverage_TypedEndpointModel_WithEndpoints(t *testing.T) { + ctx := context.Background() + + expected := agenticMsg("generated") + m := &typedEndpointModel[*schema.AgenticMessage]{ + generate: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return expected, nil + }, + stream: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + r, w := schema.Pipe[*schema.AgenticMessage](1) + go func() { + defer w.Close() + w.Send(expected, nil) + }() + return r, nil + }, + } + + result, err := m.Generate(ctx, nil) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + stream, err := m.Stream(ctx, nil) + assert.NoError(t, err) + require.NotNil(t, stream) + msg, err := stream.Recv() + assert.NoError(t, err) + assert.Equal(t, expected, msg) + _, err = stream.Recv() + assert.Equal(t, io.EOF, err) +} + +func TestCoverage_SetAutomaticClose(t *testing.T) { + r, w := schema.Pipe[*schema.AgenticMessage](1) + go func() { + defer w.Close() + w.Send(agenticMsg("data"), nil) + }() + + event := &TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + IsStreaming: true, + MessageStream: r, + }, + }, + } + + typedSetAutomaticClose(event) +} + +func TestConcatMessageStream_AgenticClosesStream(t *testing.T) { + r, w := schema.Pipe[*schema.AgenticMessage](2) + go func() { + defer w.Close() + w.Send(agenticMsg("a"), nil) + w.Send(agenticMsg("b"), nil) + }() + + result, err := concatMessageStream(r) + require.NoError(t, err) + require.NotNil(t, result) + + _, recvErr := r.Recv() + assert.Error(t, recvErr, + "stream should be closed after concatMessageStream returns") +} + +// --- Agentic retry/failover stream test helpers --- + +func agenticStreamWithMidError(chunks []*schema.AgenticMessage, err error) *schema.StreamReader[*schema.AgenticMessage] { + sr, sw := schema.Pipe[*schema.AgenticMessage](len(chunks) + 1) + go func() { + defer sw.Close() + for _, c := range chunks { + sw.Send(c, nil) + } + sw.Send(nil, err) + }() + return sr +} + +func agenticStreamOK(chunks []*schema.AgenticMessage) *schema.StreamReader[*schema.AgenticMessage] { + sr, sw := schema.Pipe[*schema.AgenticMessage](len(chunks)) + go func() { + defer sw.Close() + for _, c := range chunks { + sw.Send(c, nil) + } + }() + return sr +} + +func drainTypedAgenticEvents(t *testing.T, iter *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]]) *schema.AgenticMessage { + t.Helper() + var lastMsg *schema.AgenticMessage + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + var willRetry *WillRetryError + if errors.As(ev.Err, &willRetry) { + continue + } + t.Fatalf("unexpected error event: %v", ev.Err) + } + if ev.Output != nil && ev.Output.MessageOutput != nil { + if ev.Output.MessageOutput.IsStreaming && ev.Output.MessageOutput.MessageStream != nil { + sr := ev.Output.MessageOutput.MessageStream + for { + chunk, err := sr.Recv() + if err != nil { + break + } + lastMsg = chunk + } + } else if ev.Output.MessageOutput.Message != nil { + lastMsg = ev.Output.MessageOutput.Message + } + } + } + return lastMsg +} + +func TestAgenticRetryWithShouldRetry_Generate(t *testing.T) { + ctx := context.Background() + + var callCount int32 + var shouldRetryCalls int32 + genErr := errors.New("transient generate error") + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + n := atomic.AddInt32(&callCount, 1) + if n == 1 { + return nil, genErr + } + return agenticMsg("retry ok"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "retry-gen-agent", + Description: "test retry generate", + Model: m, + ModelRetryConfig: &TypedModelRetryConfig[*schema.AgenticMessage]{ + MaxRetries: 1, + ShouldRetry: func(_ context.Context, retryCtx *TypedRetryContext[*schema.AgenticMessage]) *TypedRetryDecision[*schema.AgenticMessage] { + n := atomic.AddInt32(&shouldRetryCalls, 1) + if n == 1 { + assert.Nil(t, retryCtx.OutputMessage, "OutputMessage should be nil when Generate returns error") + assert.ErrorIs(t, retryCtx.Err, genErr, "Err should be the generate error") + assert.Equal(t, 1, retryCtx.RetryAttempt) + return &TypedRetryDecision[*schema.AgenticMessage]{Retry: true} + } + return &TypedRetryDecision[*schema.AgenticMessage]{Retry: false} + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return time.Millisecond }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + iter := runner.Run(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("hello")}) + + msg := drainTypedAgenticEvents(t, iter) + require.NotNil(t, msg, "should have received a final message") + assert.Equal(t, "retry ok", agenticTextContent(msg)) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount), "model should be called twice") + assert.Equal(t, int32(2), atomic.LoadInt32(&shouldRetryCalls), "ShouldRetry should be called for both attempts") +} + +func TestAgenticRetryWithShouldRetry_Stream(t *testing.T) { + ctx := context.Background() + + var streamCallCount int32 + var shouldRetryCalls int32 + streamErr := errors.New("mid-stream error") + + m := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return nil, errors.New("generate should not be called") + }, + streamFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + n := atomic.AddInt32(&streamCallCount, 1) + if n == 1 { + return agenticStreamWithMidError( + []*schema.AgenticMessage{agenticMsg("partial")}, + streamErr, + ), nil + } + return agenticStreamOK([]*schema.AgenticMessage{agenticMsg("stream ok")}), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "retry-stream-agent", + Description: "test retry stream", + Model: m, + ModelRetryConfig: &TypedModelRetryConfig[*schema.AgenticMessage]{ + MaxRetries: 1, + ShouldRetry: func(_ context.Context, retryCtx *TypedRetryContext[*schema.AgenticMessage]) *TypedRetryDecision[*schema.AgenticMessage] { + n := atomic.AddInt32(&shouldRetryCalls, 1) + if n == 1 { + assert.NotNil(t, retryCtx.OutputMessage, "OutputMessage should be non-nil from partial stream") + assert.Error(t, retryCtx.Err, "Err should be the stream error") + return &TypedRetryDecision[*schema.AgenticMessage]{Retry: true} + } + return nil + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return time.Millisecond }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + }) + iter := runner.Run(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("hello")}) + + var lastMsg *schema.AgenticMessage + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + var willRetry *WillRetryError + if errors.As(ev.Err, &willRetry) { + continue + } + t.Fatalf("unexpected error: %v", ev.Err) + } + if ev.Output != nil && ev.Output.MessageOutput != nil { + if ev.Output.MessageOutput.IsStreaming && ev.Output.MessageOutput.MessageStream != nil { + sr := ev.Output.MessageOutput.MessageStream + for { + chunk, err := sr.Recv() + if err != nil { + break + } + lastMsg = chunk + } + } else if ev.Output.MessageOutput.Message != nil { + lastMsg = ev.Output.MessageOutput.Message + } + } + } + require.NotNil(t, lastMsg, "should have received final stream message") + assert.Contains(t, agenticTextContent(lastMsg), "stream ok") + assert.Equal(t, int32(2), atomic.LoadInt32(&shouldRetryCalls), "ShouldRetry should be called for both attempts") +} + +func TestAgenticFailoverGenerate(t *testing.T) { + ctx := context.Background() + + m1Err := errors.New("m1 generate failed") + var m1Calls, m2Calls int32 + + m1 := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, m1Err + }, + } + m2 := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + atomic.AddInt32(&m2Calls, 1) + return agenticMsg("failover ok"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "failover-gen-agent", + Description: "test failover generate", + Model: m1, + ModelFailoverConfig: &ModelFailoverConfig[*schema.AgenticMessage]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.AgenticMessage, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.AgenticMessage]) (model.BaseModel[*schema.AgenticMessage], []*schema.AgenticMessage, error) { + assert.Equal(t, uint(1), failoverCtx.FailoverAttempt) + assert.Nil(t, failoverCtx.LastOutputMessage, "LastOutputMessage should be nil when Generate returns error") + assert.ErrorIs(t, failoverCtx.LastErr, m1Err) + return m2, nil, nil + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + iter := runner.Run(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("hello")}) + + msg := drainTypedAgenticEvents(t, iter) + require.NotNil(t, msg) + assert.Equal(t, "failover ok", agenticTextContent(msg)) + assert.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + assert.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) +} + +func TestAgenticFailoverStream_MidStreamError(t *testing.T) { + ctx := context.Background() + + streamErr := errors.New("m1 mid-stream error") + var m1Calls, m2Calls int32 + var capturedLastOutput *schema.AgenticMessage + + m1 := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return nil, errors.New("unused") + }, + streamFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + atomic.AddInt32(&m1Calls, 1) + return agenticStreamWithMidError( + []*schema.AgenticMessage{agenticMsg("partial chunk")}, + streamErr, + ), nil + }, + } + m2 := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + return nil, errors.New("unused") + }, + streamFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + atomic.AddInt32(&m2Calls, 1) + return agenticStreamOK([]*schema.AgenticMessage{agenticMsg("failover stream ok")}), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "failover-stream-agent", + Description: "test failover stream", + Model: m1, + ModelFailoverConfig: &ModelFailoverConfig[*schema.AgenticMessage]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.AgenticMessage, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.AgenticMessage]) (model.BaseModel[*schema.AgenticMessage], []*schema.AgenticMessage, error) { + capturedLastOutput = failoverCtx.LastOutputMessage + return m2, nil, nil + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + EnableStreaming: true, + }) + iter := runner.Run(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("hello")}) + + var lastMsg *schema.AgenticMessage + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + var willRetry *WillRetryError + if errors.As(ev.Err, &willRetry) { + continue + } + t.Fatalf("unexpected error: %v", ev.Err) + } + if ev.Output != nil && ev.Output.MessageOutput != nil { + if ev.Output.MessageOutput.IsStreaming && ev.Output.MessageOutput.MessageStream != nil { + sr := ev.Output.MessageOutput.MessageStream + for { + chunk, err := sr.Recv() + if err != nil { + break + } + lastMsg = chunk + } + } else if ev.Output.MessageOutput.Message != nil { + lastMsg = ev.Output.MessageOutput.Message + } + } + } + + require.NotNil(t, lastMsg, "should have received final stream from m2") + assert.Contains(t, agenticTextContent(lastMsg), "failover stream ok") + assert.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + assert.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + assert.NotNil(t, capturedLastOutput, "failoverCtx.LastOutputMessage should contain partial stream from m1") +} + +// TestAgenticFailoverGenerate_WithTools verifies that ModelFailoverConfig is +// honored on the ReAct (with-tools) path for *schema.AgenticMessage. This +// guards against the regression where buildAgenticReActRunFunc dropped the +// failover config, leaving ModelFailoverConfig as a no-op for typed agents +// that have any tools configured. +func TestAgenticFailoverGenerate_WithTools(t *testing.T) { + ctx := context.Background() + + m1Err := errors.New("m1 generate failed") + var m1Calls, m2Calls, getFailoverCalls int32 + + m1 := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, m1Err + }, + } + m2 := &mockAgenticModel{ + generateFn: func(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + atomic.AddInt32(&m2Calls, 1) + return agenticMsg("failover ok with tools"), nil + }, + } + + dummyTool := newSlowTool("dummy_tool", 0, "ok") + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "failover-react-agent", + Description: "test failover on ReAct path", + Model: m1, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{dummyTool}, + }, + }, + ModelFailoverConfig: &ModelFailoverConfig[*schema.AgenticMessage]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.AgenticMessage, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.AgenticMessage]) (model.BaseModel[*schema.AgenticMessage], []*schema.AgenticMessage, error) { + atomic.AddInt32(&getFailoverCalls, 1) + assert.Equal(t, uint(1), failoverCtx.FailoverAttempt) + assert.ErrorIs(t, failoverCtx.LastErr, m1Err) + return m2, nil, nil + }, + }, + }) + require.NoError(t, err) + + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent}) + iter := runner.Run(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("hello")}) + + msg := drainTypedAgenticEvents(t, iter) + require.NotNil(t, msg) + assert.Equal(t, "failover ok with tools", agenticTextContent(msg)) + assert.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + assert.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + assert.Equal(t, int32(1), atomic.LoadInt32(&getFailoverCalls), + "GetFailoverModel must be invoked on the ReAct path; if zero, the failover config was dropped by buildAgenticReActRunFunc") +} diff --git a/adk/call_option.go b/adk/call_option.go new file mode 100644 index 0000000..7a1cc1b --- /dev/null +++ b/adk/call_option.go @@ -0,0 +1,211 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import "github.com/cloudwego/eino/callbacks" + +type options struct { + sharedParentSession bool + sessionValues map[string]any + checkPointID *string + skipTransferMessages bool + handlers []callbacks.Handler + cancelCtx *cancelContext +} + +// AgentRunOption is the call option for adk Agent. +type AgentRunOption struct { + implSpecificOptFn any + + // specify which Agent can see this AgentRunOption, if empty, all Agents can see this AgentRunOption + agentNames []string +} + +func (o AgentRunOption) DesignateAgent(name ...string) AgentRunOption { + o.agentNames = append(o.agentNames, name...) + return o +} + +func getCommonOptions(base *options, opts ...AgentRunOption) *options { + if base == nil { + base = &options{} + } + + return GetImplSpecificOptions(base, opts...) +} + +// WithSessionValues sets session-scoped values for the agent run. +func WithSessionValues(v map[string]any) AgentRunOption { + return WrapImplSpecificOptFn(func(o *options) { + o.sessionValues = v + }) +} + +// WithSkipTransferMessages disables forwarding transfer messages during execution. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func WithSkipTransferMessages() AgentRunOption { + return WrapImplSpecificOptFn(func(t *options) { + t.skipTransferMessages = true + }) +} + +func withSharedParentSession() AgentRunOption { + return WrapImplSpecificOptFn(func(o *options) { + o.sharedParentSession = true + }) +} + +// WithCallbacks adds callback handlers to receive agent lifecycle events. +// Handlers receive OnStart with AgentCallbackInput and OnEnd with AgentCallbackOutput. +// Multiple handlers can be added; each receives an independent copy of the event stream. +func WithCallbacks(handlers ...callbacks.Handler) AgentRunOption { + return WrapImplSpecificOptFn(func(o *options) { + o.handlers = append(o.handlers, handlers...) + }) +} + +// WrapImplSpecificOptFn is the option to wrap the implementation specific option function. +func WrapImplSpecificOptFn[T any](optFn func(*T)) AgentRunOption { + return AgentRunOption{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions extract the implementation specific options from AgentRunOption list, optionally providing a base options with default values. +// e.g. +// +// myOption := &MyOption{ +// Field1: "default_value", +// } +// +// myOption := model.GetImplSpecificOptions(myOption, opts...) +func GetImplSpecificOptions[T any](base *T, opts ...AgentRunOption) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} + +// filterCallbackHandlersForNestedAgents removes callback handlers that have already been applied +// to the current agent before passing opts to nested inner agents. +// +// This is necessary for workflow agents (LoopAgent, SequentialAgent, ParallelAgent) because: +// 1. Callback handlers designated for the current agent are applied via initAgentCallbacks(), +// which stores them in the context. +// 2. Nested inner agents inherit this context, so they automatically receive these callbacks. +// 3. If we also pass these handlers in opts to inner agents, they would be applied twice, +// causing duplicate callback invocations. +// +// Note: This only applies to workflow agents where inner agents inherit context from the parent. +// For flowAgent's sub-agents (which are peer agents that transfer to each other), the full opts +// are passed since they don't inherit the parent's callback context. +func filterCallbackHandlersForNestedAgents(currentAgentName string, opts []AgentRunOption) []AgentRunOption { + if len(opts) == 0 { + return nil + } + var filteredOpts []AgentRunOption + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn == nil { + filteredOpts = append(filteredOpts, opt) + continue + } + if _, isCallbackOpt := opt.implSpecificOptFn.(func(*options)); isCallbackOpt { + testOpt := &options{} + opt.implSpecificOptFn.(func(*options))(testOpt) + if len(testOpt.handlers) > 0 { + if len(opt.agentNames) == 0 { + continue + } + matched := false + for _, name := range opt.agentNames { + if name == currentAgentName { + matched = true + break + } + } + if matched { + continue + } + } + } + filteredOpts = append(filteredOpts, opt) + } + return filteredOpts +} + +// filterCancelOption removes any AgentRunOption that sets a cancelCtx on *options. +// This prevents inner (nested) agents from receiving the cancel option when the +// outer flowAgent owns the cancel lifecycle. Inner agents access the cancelContext +// via the Go context (getCancelContext) instead. +func filterCancelOption(opts []AgentRunOption) []AgentRunOption { + if len(opts) == 0 { + return nil + } + var filteredOpts []AgentRunOption + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn == nil { + filteredOpts = append(filteredOpts, opt) + continue + } + if _, isCommonOpt := opt.implSpecificOptFn.(func(*options)); isCommonOpt { + testOpt := &options{} + opt.implSpecificOptFn.(func(*options))(testOpt) + if testOpt.cancelCtx != nil { + continue + } + } + filteredOpts = append(filteredOpts, opt) + } + return filteredOpts +} + +func filterOptions(agentName string, opts []AgentRunOption) []AgentRunOption { + if len(opts) == 0 { + return nil + } + var filteredOpts []AgentRunOption + for i := range opts { + opt := opts[i] + if len(opt.agentNames) == 0 { + filteredOpts = append(filteredOpts, opt) + continue + } + for j := range opt.agentNames { + if opt.agentNames[j] == agentName { + filteredOpts = append(filteredOpts, opt) + break + } + } + } + return filteredOpts +} diff --git a/adk/callback.go b/adk/callback.go new file mode 100644 index 0000000..3818500 --- /dev/null +++ b/adk/callback.go @@ -0,0 +1,202 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + icb "github.com/cloudwego/eino/internal/callbacks" +) + +// AgentCallbackInput represents the input passed to agent callbacks during OnStart. +// Use ConvAgentCallbackInput to safely convert from callbacks.CallbackInput. +type AgentCallbackInput struct { + // Input contains the agent input for a new run. Nil when resuming. + Input *AgentInput + // ResumeInfo contains resume information when resuming from an interrupt. Nil for new runs. + ResumeInfo *ResumeInfo +} + +// AgentCallbackOutput represents the output passed to agent callbacks during OnEnd. +// Use ConvAgentCallbackOutput to safely convert from callbacks.CallbackOutput. +// +// Important: The Events iterator should be consumed asynchronously to avoid blocking +// the agent execution. Each callback handler receives an independent copy of the iterator. +type AgentCallbackOutput struct { + // Events provides a stream of agent events. Each handler receives its own copy. + Events *AsyncIterator[*AgentEvent] +} + +func copyTypedEventIterator[M MessageType](iter *AsyncIterator[*TypedAgentEvent[M]], n int) []*AsyncIterator[*TypedAgentEvent[M]] { + if n <= 0 { + return nil + } + if n == 1 { + return []*AsyncIterator[*TypedAgentEvent[M]]{iter} + } + + iterators := make([]*AsyncIterator[*TypedAgentEvent[M]], n) + generators := make([]*AsyncGenerator[*TypedAgentEvent[M]], n) + for i := 0; i < n; i++ { + iterators[i], generators[i] = NewAsyncIteratorPair[*TypedAgentEvent[M]]() + } + + go func() { + defer func() { + for _, g := range generators { + g.Close() + } + }() + + for { + event, ok := iter.Next() + if !ok { + break + } + for i := 0; i < n-1; i++ { + generators[i].Send(copyTypedAgentEvent(event)) + } + generators[n-1].Send(event) + } + }() + + return iterators +} + +func copyAgentCallbackOutput(out *AgentCallbackOutput, n int) []*AgentCallbackOutput { + if out == nil || out.Events == nil { + result := make([]*AgentCallbackOutput, n) + for i := 0; i < n; i++ { + result[i] = out + } + return result + } + iters := copyTypedEventIterator(out.Events, n) + result := make([]*AgentCallbackOutput, n) + for i, iter := range iters { + result[i] = &AgentCallbackOutput{Events: iter} + } + return result +} + +// ConvAgentCallbackInput converts a generic CallbackInput to AgentCallbackInput. +// Returns nil if the input is not an AgentCallbackInput. +func ConvAgentCallbackInput(input callbacks.CallbackInput) *AgentCallbackInput { + if v, ok := input.(*AgentCallbackInput); ok { + return v + } + return nil +} + +// ConvAgentCallbackOutput converts a generic CallbackOutput to AgentCallbackOutput. +// Returns nil if the output is not an AgentCallbackOutput. +func ConvAgentCallbackOutput(output callbacks.CallbackOutput) *AgentCallbackOutput { + if v, ok := output.(*AgentCallbackOutput); ok { + return v + } + return nil +} + +func initAgentCallbacks(ctx context.Context, agentName, agentType string, opts ...AgentRunOption) context.Context { + ri := &callbacks.RunInfo{ + Name: agentName, + Type: agentType, + Component: ComponentOfAgent, + } + + o := getCommonOptions(nil, opts...) + if len(o.handlers) == 0 { + return icb.ReuseHandlers(ctx, ri) + } + return icb.AppendHandlers(ctx, ri, o.handlers...) +} + +func getAgentType(agent Agent) string { + if typer, ok := agent.(components.Typer); ok { + return typer.GetType() + } + return "" +} + +// TypedAgentCallbackInput represents the input passed to typed agent callbacks during OnStart. +// Use ConvTypedCallbackInput to safely convert from callbacks.CallbackInput. +type TypedAgentCallbackInput[M MessageType] struct { + // Input contains the agent input for a new run. Nil when resuming. + Input *TypedAgentInput[M] + // ResumeInfo contains resume information when resuming from an interrupt. Nil for new runs. + ResumeInfo *ResumeInfo +} + +// TypedAgentCallbackOutput represents the output passed to typed agent callbacks during OnEnd. +// Use ConvTypedCallbackOutput to safely convert from callbacks.CallbackOutput. +// +// Important: The Events iterator should be consumed asynchronously to avoid blocking +// the agent execution. Each callback handler receives an independent copy of the iterator. +type TypedAgentCallbackOutput[M MessageType] struct { + // Events provides a stream of agent events. Each handler receives its own copy. + Events *AsyncIterator[*TypedAgentEvent[M]] +} + +// ConvTypedCallbackInput converts a callbacks.CallbackInput to *TypedAgentCallbackInput[M]. +// Returns nil if the input is not of the expected type. +func ConvTypedCallbackInput[M MessageType](input callbacks.CallbackInput) *TypedAgentCallbackInput[M] { + if v, ok := input.(*TypedAgentCallbackInput[M]); ok { + return v + } + return nil +} + +// ConvTypedCallbackOutput converts a callbacks.CallbackOutput to *TypedAgentCallbackOutput[M]. +// Returns nil if the output is not of the expected type. +func ConvTypedCallbackOutput[M MessageType](output callbacks.CallbackOutput) *TypedAgentCallbackOutput[M] { + if v, ok := output.(*TypedAgentCallbackOutput[M]); ok { + return v + } + return nil +} + +func copyTypedCallbackOutput[M MessageType](out *TypedAgentCallbackOutput[M], n int) []*TypedAgentCallbackOutput[M] { + if out == nil || out.Events == nil { + result := make([]*TypedAgentCallbackOutput[M], n) + for i := 0; i < n; i++ { + result[i] = out + } + return result + } + iters := copyTypedEventIterator(out.Events, n) + result := make([]*TypedAgentCallbackOutput[M], n) + for i, iter := range iters { + result[i] = &TypedAgentCallbackOutput[M]{Events: iter} + } + return result +} + +func initAgenticCallbacks(ctx context.Context, agentName, agentType string, opts ...AgentRunOption) context.Context { + ri := &callbacks.RunInfo{ + Name: agentName, + Type: agentType, + Component: ComponentOfAgenticAgent, + } + + o := getCommonOptions(nil, opts...) + if len(o.handlers) == 0 { + return icb.ReuseHandlers(ctx, ri) + } + return icb.AppendHandlers(ctx, ri, o.handlers...) +} diff --git a/adk/callback_integration_test.go b/adk/callback_integration_test.go new file mode 100644 index 0000000..fd708a0 --- /dev/null +++ b/adk/callback_integration_test.go @@ -0,0 +1,1062 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/callbacks" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type callbackRecorder struct { + mu sync.Mutex + onStartCalled bool + onEndCalled bool + runInfo *callbacks.RunInfo + inputReceived *AgentCallbackInput + eventsReceived []*AgentEvent + eventsDone chan struct{} + closeOnce sync.Once +} + +func (r *callbackRecorder) getOnStartCalled() bool { + r.mu.Lock() + defer r.mu.Unlock() + return r.onStartCalled +} + +func (r *callbackRecorder) getOnEndCalled() bool { + r.mu.Lock() + defer r.mu.Unlock() + return r.onEndCalled +} + +func (r *callbackRecorder) getEventsReceived() []*AgentEvent { + r.mu.Lock() + defer r.mu.Unlock() + result := make([]*AgentEvent, len(r.eventsReceived)) + copy(result, r.eventsReceived) + return result +} + +func newRecordingHandler(recorder *callbackRecorder) callbacks.Handler { + recorder.eventsDone = make(chan struct{}) + return callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + recorder.mu.Lock() + defer recorder.mu.Unlock() + recorder.onStartCalled = true + recorder.runInfo = info + if agentInput := ConvAgentCallbackInput(input); agentInput != nil { + recorder.inputReceived = agentInput + } + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + recorder.mu.Lock() + recorder.onEndCalled = true + recorder.runInfo = info + recorder.mu.Unlock() + + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil { + if agentOutput.Events != nil { + go func() { + defer recorder.closeOnce.Do(func() { close(recorder.eventsDone) }) + for { + event, ok := agentOutput.Events.Next() + if !ok { + break + } + recorder.mu.Lock() + recorder.eventsReceived = append(recorder.eventsReceived, event) + recorder.mu.Unlock() + } + }() + return ctx + } + } + recorder.closeOnce.Do(func() { close(recorder.eventsDone) }) + return ctx + }). + Build() +} + +func TestCallbackOnStartInvocation(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("test response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for callback", + Instruction: "You are a test agent", + Model: cm, + }) + assert.NoError(t, err) + + recorder := &callbackRecorder{} + handler := newRecordingHandler(recorder) + + runner := NewRunner(ctx, RunnerConfig{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + <-recorder.eventsDone + + assert.True(t, recorder.onStartCalled, "OnStart should be called") + assert.NotNil(t, recorder.inputReceived, "Input should be received") + assert.NotNil(t, recorder.inputReceived.Input, "AgentInput should be set") + assert.Len(t, recorder.inputReceived.Input.Messages, 1) +} + +func TestCallbackOnEndInvocation(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("test response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for callback", + Instruction: "You are a test agent", + Model: cm, + }) + assert.NoError(t, err) + + recorder := &callbackRecorder{} + handler := newRecordingHandler(recorder) + + runner := NewRunner(ctx, RunnerConfig{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + <-recorder.eventsDone + + assert.True(t, recorder.onEndCalled, "OnEnd should be called") + assert.NotEmpty(t, recorder.eventsReceived, "Events should be received") +} + +func TestCallbackRunInfoForChatModelAgent(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("test response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestChatAgent", + Description: "Test chat agent", + Instruction: "You are a test agent", + Model: cm, + }) + assert.NoError(t, err) + + recorder := &callbackRecorder{} + handler := newRecordingHandler(recorder) + + runner := NewRunner(ctx, RunnerConfig{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + <-recorder.eventsDone + + assert.NotNil(t, recorder.runInfo) + assert.Equal(t, "TestChatAgent", recorder.runInfo.Name) + assert.Equal(t, "ChatModel", recorder.runInfo.Type) + assert.Equal(t, ComponentOfAgent, recorder.runInfo.Component) +} + +func TestMultipleCallbackHandlers(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("test response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test agent", + Model: cm, + }) + assert.NoError(t, err) + + recorder1 := &callbackRecorder{} + recorder2 := &callbackRecorder{} + handler1 := newRecordingHandler(recorder1) + handler2 := newRecordingHandler(recorder2) + + runner := NewRunner(ctx, RunnerConfig{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler1, handler2)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + <-recorder1.eventsDone + <-recorder2.eventsDone + + assert.True(t, recorder1.onStartCalled, "Handler1 OnStart should be called") + assert.True(t, recorder2.onStartCalled, "Handler2 OnStart should be called") + assert.True(t, recorder1.onEndCalled, "Handler1 OnEnd should be called") + assert.True(t, recorder2.onEndCalled, "Handler2 OnEnd should be called") + + assert.NotEmpty(t, recorder1.eventsReceived, "Handler1 should receive events") + assert.NotEmpty(t, recorder2.eventsReceived, "Handler2 should receive events") +} + +func TestCallbackWithWorkflowAgent(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response 1", nil), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response 2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "SequentialAgent", + Description: "Sequential workflow", + SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + var callbackInfos []*callbacks.RunInfo + handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component == ComponentOfAgent { + callbackInfos = append(callbackInfos, info) + } + return ctx + }). + Build() + + runner := NewRunner(ctx, RunnerConfig{Agent: seqAgent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.NotEmpty(t, callbackInfos, "OnStart should be called for agents") + foundAgent1 := false + foundAgent2 := false + for _, info := range callbackInfos { + if info.Name == "Agent1" && info.Type == "ChatModel" { + foundAgent1 = true + } + if info.Name == "Agent2" && info.Type == "ChatModel" { + foundAgent2 = true + } + } + assert.True(t, foundAgent1, "Agent1 callback should be invoked") + assert.True(t, foundAgent2, "Agent2 callback should be invoked") +} + +func TestCallbackEventsMatchAgentOutput(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectedContent := "This is the test response content" + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage(expectedContent, nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test agent", + Model: cm, + }) + assert.NoError(t, err) + + recorder := &callbackRecorder{} + handler := newRecordingHandler(recorder) + + var agentEvents []*AgentEvent + runner := NewRunner(ctx, RunnerConfig{Agent: agent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + event, ok := iter.Next() + if !ok { + break + } + agentEvents = append(agentEvents, event) + } + + <-recorder.eventsDone + + assert.NotEmpty(t, agentEvents, "Agent should emit events") + assert.NotEmpty(t, recorder.eventsReceived, "Callback should receive events") + + foundExpectedContent := false + for _, event := range recorder.eventsReceived { + if event.Output != nil && event.Output.MessageOutput != nil { + msg := event.Output.MessageOutput.Message + if msg != nil && msg.Content == expectedContent { + foundExpectedContent = true + break + } + } + } + assert.True(t, foundExpectedContent, "Callback events should contain the expected content") +} + +func TestCallbackOnEndForWorkflowAgent(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response 1", nil), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response 2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "SequentialAgent", + Description: "Sequential workflow", + SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + recorder := &callbackRecorder{} + handler := newRecordingHandler(recorder) + + runner := NewRunner(ctx, RunnerConfig{Agent: seqAgent}) + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + <-recorder.eventsDone + + assert.True(t, recorder.getOnStartCalled(), "OnStart should be called for workflow agent") + assert.True(t, recorder.getOnEndCalled(), "OnEnd should be called for workflow agent") + assert.NotEmpty(t, recorder.getEventsReceived(), "Events should be received for workflow agent") +} + +type ctxKeyForTest string + +const testOnStartMarkerKey ctxKeyForTest = "onStartMarker" + +func TestSubAgentContextIsolation(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("transferring to Agent2", + []schema.ToolCall{ + { + ID: "transfer_1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "Agent2"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response from Agent2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent that transfers to Agent2", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, agent1, []Agent{agent2}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: agentWithSubAgents}) + + var mu sync.Mutex + onStartContextMarkers := make(map[string][]string) + + handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + marker, _ := ctx.Value(testOnStartMarkerKey).(string) + onStartContextMarkers[info.Name] = append(onStartContextMarkers[info.Name], marker) + mu.Unlock() + + return context.WithValue(ctx, testOnStartMarkerKey, info.Name+"_marker") + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.NotEmpty(t, onStartContextMarkers["Agent1"], "Agent1's OnStart should be called") + assert.NotEmpty(t, onStartContextMarkers["Agent2"], "Agent2's OnStart should be called") + + if len(onStartContextMarkers["Agent1"]) > 0 { + assert.Equal(t, "", onStartContextMarkers["Agent1"][0], + "Agent1's OnStart should receive context without marker (initial context)") + } + if len(onStartContextMarkers["Agent2"]) > 0 { + assert.Equal(t, "", onStartContextMarkers["Agent2"][0], + "Agent2's first OnStart should NOT inherit Agent1's marker - context should be isolated") + } +} + +func TestCallbackDesignatedToSpecificAgent(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("transferring to Agent2", + []schema.ToolCall{ + { + ID: "transfer_1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "Agent2"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response from Agent2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent that transfers to Agent2", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, agent1, []Agent{agent2}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: agentWithSubAgents}) + + var mu sync.Mutex + onStartCalls := make(map[string]int) + + agent2OnlyHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + onStartCalls[info.Name]++ + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + iter := runner.Query(ctx, "hello", WithCallbacks(agent2OnlyHandler).DesignateAgent("Agent2")) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.Equal(t, 0, onStartCalls["Agent1"], "Agent1's OnStart should NOT be called when handler is designated to Agent2") + assert.Equal(t, 1, onStartCalls["Agent2"], "Agent2's OnStart should be called exactly once") +} + +func TestCallbackDesignatedToMultipleAgents(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("transferring to Agent2", + []schema.ToolCall{ + { + ID: "transfer_1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "Agent2"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response from Agent2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, agent1, []Agent{agent2}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: agentWithSubAgents}) + + var mu sync.Mutex + onStartCalls := make(map[string]int) + + agent1And2Handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + onStartCalls[info.Name]++ + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + iter := runner.Query(ctx, "hello", WithCallbacks(agent1And2Handler).DesignateAgent("Agent1", "Agent2")) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.Equal(t, 1, onStartCalls["Agent1"], "Agent1's OnStart should be called exactly once") + assert.Equal(t, 1, onStartCalls["Agent2"], "Agent2's OnStart should be called exactly once") +} + +func TestCallbackDesignatedExcludesNonMatchingAgents(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("transferring to Agent2", + []schema.ToolCall{ + { + ID: "transfer_1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "Agent2"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response from Agent2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, agent1, []Agent{agent2}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: agentWithSubAgents}) + + var mu sync.Mutex + onStartCalls := make(map[string]int) + + agent1OnlyHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + onStartCalls[info.Name]++ + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + iter := runner.Query(ctx, "hello", WithCallbacks(agent1OnlyHandler).DesignateAgent("Agent1")) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.Equal(t, 1, onStartCalls["Agent1"], "Agent1's OnStart should be called exactly once") + assert.Equal(t, 0, onStartCalls["Agent2"], "Agent2's OnStart should NOT be called when handler is designated only to Agent1") +} + +func TestMixedDesignatedAndGlobalCallbacks(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("transferring to Agent2", + []schema.ToolCall{ + { + ID: "transfer_1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "Agent2"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response from Agent2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent that transfers to Agent2", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, agent1, []Agent{agent2}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: agentWithSubAgents}) + + var mu sync.Mutex + globalHandlerCalls := make(map[string]int) + agent2OnlyHandlerCalls := make(map[string]int) + + globalHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + globalHandlerCalls[info.Name]++ + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + agent2OnlyHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + agent2OnlyHandlerCalls[info.Name]++ + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + iter := runner.Query(ctx, "hello", + WithCallbacks(globalHandler), + WithCallbacks(agent2OnlyHandler).DesignateAgent("Agent2"), + ) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.Equal(t, 1, globalHandlerCalls["Agent1"], "Global handler should fire for Agent1") + assert.Equal(t, 1, globalHandlerCalls["Agent2"], "Global handler should fire for Agent2") + + assert.Equal(t, 0, agent2OnlyHandlerCalls["Agent1"], "Agent2-only handler should NOT fire for Agent1") + assert.Equal(t, 1, agent2OnlyHandlerCalls["Agent2"], "Agent2-only handler should fire for Agent2") +} + +func TestOnStartCalledOncePerAgentWithDesignation(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("transferring to Agent2", + []schema.ToolCall{ + { + ID: "transfer_1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "Agent2"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response from Agent2", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent1", + Description: "First agent that transfers to Agent2", + Instruction: "You are agent 1", + Model: cm1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "Agent2", + Description: "Second agent", + Instruction: "You are agent 2", + Model: cm2, + }) + assert.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, agent1, []Agent{agent2}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: agentWithSubAgents}) + + var mu sync.Mutex + onStartCalls := make(map[string]int) + + handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + mu.Lock() + onStartCalls[info.Name]++ + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != ComponentOfAgent { + return ctx + } + if agentOutput := ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + iter := runner.Query(ctx, "hello", WithCallbacks(handler)) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.Equal(t, 1, onStartCalls["Agent1"], "Agent1's OnStart should be called exactly once") + assert.Equal(t, 1, onStartCalls["Agent2"], "Agent2's OnStart should be called exactly once") +} diff --git a/adk/callback_test.go b/adk/callback_test.go new file mode 100644 index 0000000..efd66f5 --- /dev/null +++ b/adk/callback_test.go @@ -0,0 +1,388 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +func TestCopyTypedEventIterator(t *testing.T) { + t.Run("n=0 returns nil", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + gen.Send(&AgentEvent{AgentName: "test"}) + gen.Close() + }() + + result := copyTypedEventIterator(iter, 0) + assert.Nil(t, result) + }) + + t.Run("n=1 returns original iterator", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + gen.Send(&AgentEvent{AgentName: "test"}) + gen.Close() + }() + + result := copyTypedEventIterator(iter, 1) + assert.Len(t, result, 1) + assert.Equal(t, iter, result[0]) + }) + + t.Run("n>1 creates n independent copies", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + events := []*AgentEvent{ + {AgentName: "agent1", Output: &AgentOutput{MessageOutput: &MessageVariant{Message: schema.AssistantMessage("msg1", nil)}}}, + {AgentName: "agent2", Output: &AgentOutput{MessageOutput: &MessageVariant{Message: schema.AssistantMessage("msg2", nil)}}}, + } + + go func() { + for _, e := range events { + gen.Send(e) + } + gen.Close() + }() + + n := 3 + copies := copyTypedEventIterator(iter, n) + assert.Len(t, copies, n) + + var wg sync.WaitGroup + receivedEvents := make([][]*AgentEvent, n) + + for i := 0; i < n; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + for { + event, ok := copies[idx].Next() + if !ok { + break + } + receivedEvents[idx] = append(receivedEvents[idx], event) + } + }(i) + } + + wg.Wait() + + for i := 0; i < n; i++ { + assert.Len(t, receivedEvents[i], len(events), "iterator %d should receive all events", i) + for j, e := range receivedEvents[i] { + assert.Equal(t, events[j].AgentName, e.AgentName) + } + } + }) +} + +func TestCopyAgentCallbackOutput(t *testing.T) { + t.Run("nil output", func(t *testing.T) { + result := copyAgentCallbackOutput(nil, 3) + assert.Len(t, result, 3) + for _, r := range result { + assert.Nil(t, r) + } + }) + + t.Run("output with nil Events", func(t *testing.T) { + out := &AgentCallbackOutput{Events: nil} + result := copyAgentCallbackOutput(out, 3) + assert.Len(t, result, 3) + for _, r := range result { + assert.Equal(t, out, r) + } + }) + + t.Run("valid output with events", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + gen.Send(&AgentEvent{AgentName: "test"}) + gen.Close() + }() + + out := &AgentCallbackOutput{Events: iter} + result := copyAgentCallbackOutput(out, 2) + assert.Len(t, result, 2) + + for i, r := range result { + require.NotNil(t, r, "result[%d] should not be nil", i) + assert.NotNil(t, r.Events, "result[%d].Events should not be nil", i) + } + }) +} + +func TestConvAgentCallbackInput(t *testing.T) { + t.Run("valid AgentCallbackInput", func(t *testing.T) { + input := &AgentCallbackInput{ + Input: &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + } + result := ConvAgentCallbackInput(input) + assert.Equal(t, input, result) + }) + + t.Run("invalid type returns nil", func(t *testing.T) { + result := ConvAgentCallbackInput("invalid") + assert.Nil(t, result) + }) + + t.Run("nil returns nil", func(t *testing.T) { + result := ConvAgentCallbackInput(nil) + assert.Nil(t, result) + }) +} + +func TestConvAgentCallbackOutput(t *testing.T) { + t.Run("valid AgentCallbackOutput", func(t *testing.T) { + iter, _ := NewAsyncIteratorPair[*AgentEvent]() + output := &AgentCallbackOutput{Events: iter} + result := ConvAgentCallbackOutput(output) + assert.Equal(t, output, result) + }) + + t.Run("invalid type returns nil", func(t *testing.T) { + result := ConvAgentCallbackOutput("invalid") + assert.Nil(t, result) + }) + + t.Run("nil returns nil", func(t *testing.T) { + result := ConvAgentCallbackOutput(nil) + assert.Nil(t, result) + }) +} + +type mockTyperAgent struct { + name string + agentType string +} + +func (a *mockTyperAgent) Name(_ context.Context) string { return a.name } +func (a *mockTyperAgent) Description(_ context.Context) string { return "mock agent" } +func (a *mockTyperAgent) GetType() string { return a.agentType } +func (a *mockTyperAgent) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + gen.Close() + return iter +} + +type mockNonTyperAgent struct { + name string +} + +func (a *mockNonTyperAgent) Name(_ context.Context) string { return a.name } +func (a *mockNonTyperAgent) Description(_ context.Context) string { return "mock agent" } +func (a *mockNonTyperAgent) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + gen.Close() + return iter +} + +func TestGetAgentType(t *testing.T) { + t.Run("agent implementing Typer", func(t *testing.T) { + agent := &mockTyperAgent{name: "test", agentType: "CustomType"} + result := getAgentType(agent) + assert.Equal(t, "CustomType", result) + }) + + t.Run("agent not implementing Typer", func(t *testing.T) { + agent := &mockNonTyperAgent{name: "test"} + result := getAgentType(agent) + assert.Equal(t, "", result) + }) +} + +func TestWithCallbacksOption(t *testing.T) { + handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + return ctx + }). + Build() + + opt := WithCallbacks(handler) + opts := getCommonOptions(nil, opt) + + assert.Len(t, opts.handlers, 1) +} + +func TestWithMultipleCallbacksOption(t *testing.T) { + handler1 := callbacks.NewHandlerBuilder().Build() + handler2 := callbacks.NewHandlerBuilder().Build() + opt := WithCallbacks(handler1, handler2) + + opts := getCommonOptions(nil, opt) + + assert.Len(t, opts.handlers, 2) +} + +func TestCopyTypedEventIteratorAgentic(t *testing.T) { + t.Run("n=0 returns nil", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{AgentName: "test"}) + gen.Close() + }() + + result := copyTypedEventIterator(iter, 0) + assert.Nil(t, result) + }) + + t.Run("n=1 returns original iterator", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{AgentName: "test"}) + gen.Close() + }() + + result := copyTypedEventIterator(iter, 1) + assert.Len(t, result, 1) + assert.Equal(t, iter, result[0]) + }) + + t.Run("n>1 creates n independent copies", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + events := []*TypedAgentEvent[*schema.AgenticMessage]{ + {AgentName: "agent1", Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{Message: agenticMsg("msg1")}, + }}, + {AgentName: "agent2", Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{Message: agenticMsg("msg2")}, + }}, + } + + go func() { + for _, e := range events { + gen.Send(e) + } + gen.Close() + }() + + n := 3 + copies := copyTypedEventIterator(iter, n) + assert.Len(t, copies, n) + + var wg sync.WaitGroup + receivedEvents := make([][]*TypedAgentEvent[*schema.AgenticMessage], n) + + for i := 0; i < n; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + for { + event, ok := copies[idx].Next() + if !ok { + break + } + receivedEvents[idx] = append(receivedEvents[idx], event) + } + }(i) + } + + wg.Wait() + + for i := 0; i < n; i++ { + assert.Len(t, receivedEvents[i], len(events), "iterator %d should receive all events", i) + for j, e := range receivedEvents[i] { + assert.Equal(t, events[j].AgentName, e.AgentName) + } + } + }) +} + +func TestCopyTypedCallbackOutput(t *testing.T) { + t.Run("nil output", func(t *testing.T) { + result := copyTypedCallbackOutput[*schema.AgenticMessage](nil, 3) + assert.Len(t, result, 3) + for _, r := range result { + assert.Nil(t, r) + } + }) + + t.Run("output with nil Events", func(t *testing.T) { + out := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: nil} + result := copyTypedCallbackOutput(out, 3) + assert.Len(t, result, 3) + for _, r := range result { + assert.Equal(t, out, r) + } + }) + + t.Run("valid output with events", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{AgentName: "test"}) + gen.Close() + }() + + out := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: iter} + result := copyTypedCallbackOutput(out, 2) + assert.Len(t, result, 2) + + for i, r := range result { + require.NotNil(t, r, "result[%d] should not be nil", i) + assert.NotNil(t, r.Events, "result[%d].Events should not be nil", i) + } + }) +} + +func TestConvTypedCallbackInput(t *testing.T) { + t.Run("valid TypedAgentCallbackInput", func(t *testing.T) { + input := &TypedAgentCallbackInput[*schema.AgenticMessage]{ + Input: &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("test")}, + }, + } + result := ConvTypedCallbackInput[*schema.AgenticMessage](input) + assert.Equal(t, input, result) + }) + + t.Run("invalid type returns nil", func(t *testing.T) { + result := ConvTypedCallbackInput[*schema.AgenticMessage]("invalid") + assert.Nil(t, result) + }) + + t.Run("nil returns nil", func(t *testing.T) { + result := ConvTypedCallbackInput[*schema.AgenticMessage](nil) + assert.Nil(t, result) + }) +} + +func TestConvTypedCallbackOutput(t *testing.T) { + t.Run("valid TypedAgentCallbackOutput", func(t *testing.T) { + iter, _ := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + output := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: iter} + result := ConvTypedCallbackOutput[*schema.AgenticMessage](output) + assert.Equal(t, output, result) + }) + + t.Run("invalid type returns nil", func(t *testing.T) { + result := ConvTypedCallbackOutput[*schema.AgenticMessage]("invalid") + assert.Nil(t, result) + }) + + t.Run("nil returns nil", func(t *testing.T) { + result := ConvTypedCallbackOutput[*schema.AgenticMessage](nil) + assert.Nil(t, result) + }) +} diff --git a/adk/cancel.go b/adk/cancel.go new file mode 100644 index 0000000..6639144 --- /dev/null +++ b/adk/cancel.go @@ -0,0 +1,975 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + "sync/atomic" + "time" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +func init() { + schema.RegisterName[*CancelError]("_eino_adk_cancel_error") + schema.RegisterName[*AgentCancelInfo]("_eino_adk_agent_cancel_info") + schema.RegisterName[*StreamCanceledError]("_eino_adk_stream_cancelled_error") +} + +// CancelMode specifies when an agent should be canceled. +// Modes can be combined with bitwise OR to cancel at multiple safe-points. +// For example, CancelAfterChatModel | CancelAfterToolCalls cancels the agent +// after whichever safe-point is reached first. +type CancelMode int + +const ( + // CancelImmediate cancels the agent as soon as the signal is received, + // without waiting for a ChatModel or ToolCalls safe-point. + // By default, only the root agent is interrupted; descendant agents inside + // AgentTools are torn down via context cancellation as a side effect. + // Use WithRecursive to propagate explicit immediate-cancel signals to + // descendants for clean teardown with grace period. + CancelImmediate CancelMode = 0 + // CancelAfterChatModel cancels after the root agent's next chat model call + // completes. By default, only the root agent checks this safe-point; + // nested sub-agents inside AgentTools are unaware of the cancel. + // Use WithRecursive to propagate the cancel to all descendants — whichever + // ChatModel finishes first triggers the cancel. + CancelAfterChatModel CancelMode = 1 << iota + // CancelAfterToolCalls cancels after the root agent's next set of tool calls + // completes. By default, only the root agent checks this safe-point. + // Use WithRecursive to propagate to all descendants. + CancelAfterToolCalls +) + +// CancelHandle represents a cancel operation that can be waited on. +type CancelHandle struct { + wait func() error +} + +// Wait blocks until the cancel request reaches a terminal outcome. +// +// It reports the result of the cancel operation itself, not the agent's final +// business error: +// - nil: cancellation succeeded, including the case where a business interrupt +// was absorbed into CancelError while cancellation was active +// - ErrCancelTimeout: the requested safe-point cancellation timed out and was +// escalated to immediate cancellation +// - ErrExecutionEnded: the execution ended before cancellation took effect, +// meaning the stream drained to completion without any interrupt +func (h *CancelHandle) Wait() error { + return h.wait() +} + +// AgentCancelFunc is called to request cancellation of a running agent. +// It returns after the cancel request is committed; use the returned handle's +// Wait to block for completion and outcome. +// +// The returned bool reports whether this call contributed to the CancelError +// for the current execution. "Contributed" means this call's cancel options +// were included before cancellation was finalized. It is false when cancellation +// was already finalized (handled or execution completed). +type AgentCancelFunc func(...AgentCancelOption) (*CancelHandle, bool) + +type agentCancelConfig struct { + Mode CancelMode + Recursive bool + Timeout *time.Duration +} + +// AgentCancelOption configures cancel behavior. +type AgentCancelOption func(*agentCancelConfig) + +// WithAgentCancelMode sets the cancel mode for the agent cancel operation. +func WithAgentCancelMode(mode CancelMode) AgentCancelOption { + return func(config *agentCancelConfig) { + config.Mode = mode + } +} + +// WithAgentCancelTimeout sets a timeout for the cancel operation. +// This only applies to safe-point modes (CancelAfterChatModel, CancelAfterToolCalls): +// if the safe-point hasn't fired within this duration, the cancel escalates to +// CancelImmediate. The escalated cancel still saves a checkpoint, so the execution +// can be resumed via Runner.Resume or Runner.ResumeWithParams. +// For CancelImmediate this timeout is ignored — the cancel fires immediately. +func WithAgentCancelTimeout(timeout time.Duration) AgentCancelOption { + return func(config *agentCancelConfig) { + config.Timeout = &timeout + } +} + +// WithRecursive opts into recursive cancel propagation. By default, cancel +// modes only affect the root agent; descendant agents inside AgentTools are +// not notified. WithRecursive makes the cancel propagate to all descendants: +// - CancelAfterChatModel / CancelAfterToolCalls: descendants check their own safe-points. +// - CancelImmediate: descendants receive explicit immediate-cancel signals for +// clean teardown; the root uses a grace period to collect child interrupts. +// +// With recursive cancellation, each descendant agent also triggers cancellation +// and cascades its interrupt information upward. The root agent ultimately +// produces a complete checkpoint that includes descendant checkpoints, enabling +// resumption from the exact point where each descendant was interrupted. +// +// Once any cancel call includes WithRecursive, the flag stays set for the +// entire cancel lifecycle (monotonic escalation). +func WithRecursive() AgentCancelOption { + return func(config *agentCancelConfig) { + config.Recursive = true + } +} + +// AgentCancelInfo contains information about a cancel operation. +type AgentCancelInfo struct { + Mode CancelMode + Escalated bool + Timeout bool +} + +// CancelError is sent via AgentEvent.Err when an agent is canceled. +// Use errors.As to match and extract *CancelError from event errors. +// +// Interrupt absorption: when a cancel is active (shouldCancel() == true), ANY +// interrupt — whether from a cancel safe-point node or from business logic +// (e.g. tool.Interrupt in a tool) — is converted to a CancelError. The +// cancel "absorbs" the business interrupt. This is intentional: +// +// - In concurrent execution (parallel workflows, concurrent tool calls), +// cancel-induced and business interrupts can arrive as a single composite +// signal that cannot be split apart. +// - Even in sequential execution, treating business interrupts as CancelError +// during active cancel gives consistent semantics. +// - The business interrupt is NOT lost — the checkpoint preserves the full +// interrupt hierarchy. On resume (Runner.Resume or Runner.ResumeWithParams), +// the agent re-executes the interrupting code path and the business +// interrupt re-fires naturally. +type CancelError struct { + Info *AgentCancelInfo + + // InterruptContexts provides the interrupt contexts needed for targeted + // resumption via Runner.ResumeWithParams. Each context represents a step + // in the agent hierarchy that was interrupted. This is a slice because + // composite agents (e.g. parallel workflows) may interrupt at multiple + // points simultaneously, matching the shape of AgentAction.Interrupted.InterruptContexts. + // Use each InterruptCtx.ID as a key in ResumeParams.Targets. + InterruptContexts []*InterruptCtx + + interruptSignal *InterruptSignal // unexported — only Runner needs it for checkpoint +} + +func (e *CancelError) Error() string { + return fmt.Sprintf("agent canceled: mode=%v, escalated=%v", e.Info.Mode, e.Info.Escalated) +} + +// Sentinel errors for cancel outcomes. +var ( + // ErrCancelTimeout is returned by CancelHandle.Wait when the cancel operation timed out. + ErrCancelTimeout = errors.New("cancel timed out") + + // ErrExecutionEnded is returned by CancelHandle.Wait when the agent ended + // before the cancel took effect. "Ended" means the event stream was fully + // drained without any interrupt — normal completion or a fatal error. + // + // Note: business interrupts that occur while cancel is active are absorbed + // into CancelError (see CancelError doc), so they result in nil (cancel + // succeeded), NOT ErrExecutionEnded. Only execution that completes with + // no interrupt at all produces this error. + ErrExecutionEnded = errors.New("execution already ended") + + // ErrStreamCanceled is the error sent through the stream when CancelImmediate aborts it. + // It is a *StreamCanceledError so it can be gob-serialized during checkpoint save + // (when stored as agentEventWrapper.StreamErr). + ErrStreamCanceled error = &StreamCanceledError{} +) + +// StreamCanceledError is the concrete error type for ErrStreamCanceled. +// It is exported so that gob can serialize it during checkpoint save when the error +// is stored in agentEventWrapper.StreamErr. +type StreamCanceledError struct{} + +func (e *StreamCanceledError) Error() string { + return "stream canceled" +} + +// WithCancel creates an AgentRunOption that enables cancellation for an agent run. +// It returns the option to pass to Run/Resume and a cancel function. +// Cancel options (mode, timeout) are passed to the returned AgentCancelFunc at call time. +func WithCancel() (AgentRunOption, AgentCancelFunc) { + cc := newCancelContext() + opt := WrapImplSpecificOptFn(func(o *options) { + o.cancelCtx = cc + }) + cancelFn := cc.buildCancelFunc() + return opt, cancelFn +} + +// cancelContext state constants (for int32 CAS). +// +// State transition rules: +// +// stateRunning -> stateCancelling (cancel requested by AgentCancelFunc) +// stateRunning -> stateDone (execution finished without interrupt) +// stateCancelling -> stateCancelHandled (ANY interrupt absorbed as CancelError) +// stateCancelling -> stateDone (execution finished without interrupt while cancel pending) +// +// Terminal states: stateDone, stateCancelHandled. +// +// Note: We intentionally do NOT distinguish between "completed" and "errored" +// terminal states. End-users get the actual outcome from AgentEvent. +// This simplification keeps the state machine minimal — only the cancel/non-cancel +// distinction matters for the AgentCancelFunc return value. +// +// Business interrupt handling: when cancel is active (stateCancelling) and any +// interrupt arrives — cancel-induced OR business — wrapIterWithCancelCtx absorbs +// it as a CancelError and transitions to stateCancelHandled. The business interrupt +// data is preserved in the checkpoint for re-emission on resume. +const ( + // stateRunning is the initial state: agent is executing normally. + stateRunning int32 = 0 + // stateCancelling means AgentCancelFunc has been called and cancelChan is + // closed, but the cancel has not yet been handled by the runFunc. + stateCancelling int32 = 1 + // stateDone means execution has finished through any non-cancel path: + // normal completion, business interrupt, or error. The specific outcome + // is conveyed through AgentEvent, not through the cancel state machine. + stateDone int32 = 2 + // stateCancelHandled means the cancel was processed by the runFunc and a + // CancelError was emitted through the event stream. This is the success + // terminal state for cancellation. + stateCancelHandled int32 = 5 +) + +// interruptSent constants (for int32 CAS). +// +// Transition rules: +// +// interruptNotSent -> interruptImmediate (CancelImmediate or escalation) +const ( + // interruptNotSent means no compose graph interrupt has been sent. + interruptNotSent int32 = 0 + // interruptImmediate means an immediate graph interrupt was sent with + // timeout=0, forcing the graph to stop as soon as possible. + interruptImmediate int32 = 1 +) + +// defaultCancelImmediateGracePeriod is the bounded time a recursive +// AgentTool cancel waits before forcing the current level's graph interrupt. +// This gives deeper AgentTool/internal-agent interrupts a chance to bubble up +// as CompositeInterrupts. If this proves insufficient for deeply nested +// structures or too slow for latency-sensitive use cases, consider making it +// configurable via an AgentCancelOption. +const defaultCancelImmediateGracePeriod = 1 * time.Second + +type cancelContextKey struct{} + +// withCancelContext stores a cancelContext in the Go context. +func withCancelContext(ctx context.Context, cc *cancelContext) context.Context { + if cc == nil { + return ctx + } + return context.WithValue(ctx, cancelContextKey{}, cc) +} + +// getCancelContext retrieves the cancelContext from the Go context, or nil. +func getCancelContext(ctx context.Context) *cancelContext { + if v := ctx.Value(cancelContextKey{}); v != nil { + return v.(*cancelContext) + } + return nil +} + +type cancelContext struct { + mode int32 // atomic, CancelMode + + cancelChan chan struct{} // closed when cancel is requested (all modes, not just safe-point) + immediateChan chan struct{} // closed when an immediate graph interrupt fires + doneChan chan struct{} // closed when execution completes (by any mark* method) + doneOnce sync.Once // ensures doneChan is closed exactly once + + state int32 // stateRunning, stateCancelling, stateDone, stateCancelHandled + interruptSent int32 // interruptNotSent, interruptImmediate + escalated int32 // 1 if escalated from safe-point to immediate + timeoutEscalated int32 // 1 if escalation was triggered by timeout + startedMode int32 // atomic, mode when state transitioned to cancelling + deadlineUnixNano int64 // atomic, 0 means no deadline + + recursive int32 // atomic; 1 if cancel should propagate into AgentTool internal agents + recursiveChan chan struct{} // closed when recursive transitions from 0 to 1 + + root bool // true for the original cancelContext created by WithCancel(); false for AgentTool internal agents + parent *cancelContext // non-nil for AgentTool internal agents; used to propagate AgentTool boundary markers upward + + agentToolDescendant int32 // atomic; 1 once an AgentTool runs under this cancel context + + cancelMu sync.Mutex + timeoutOnce sync.Once + timeoutNotify chan struct{} + + mu sync.Mutex + graphInterruptFuncs []func(...compose.GraphInterruptOption) +} + +func newCancelContext() *cancelContext { + return &cancelContext{ + cancelChan: make(chan struct{}), + immediateChan: make(chan struct{}), + doneChan: make(chan struct{}), + timeoutNotify: make(chan struct{}, 1), + recursiveChan: make(chan struct{}), + root: true, + } +} + +func (cc *cancelContext) isRoot() bool { + return cc != nil && cc.root +} + +func (cc *cancelContext) isRecursive() bool { + return cc != nil && atomic.LoadInt32(&cc.recursive) == 1 +} + +// setRecursive(false) is a no-op; recursive is monotonically escalating: +// once set to true, it cannot be reverted. +func (cc *cancelContext) setRecursive(v bool) { + if v && atomic.CompareAndSwapInt32(&cc.recursive, 0, 1) { + close(cc.recursiveChan) + } +} + +// deriveAgentToolCancelContext creates the cancelContext used by an AgentTool's +// internal agent. It receives recursive cancel propagation from the parent +// AgentTool call. The caller MUST ensure the child's markDone() is eventually +// called (e.g., via wrapIterWithCancelCtx's defer) or that ctx is canceled; +// otherwise the two propagation goroutines will leak. +func (cc *cancelContext) deriveAgentToolCancelContext(ctx context.Context) *cancelContext { + if cc == nil { + return nil + } + child := newCancelContext() + child.root = false + child.parent = cc + + // Each goroutine below propagates one signal class (cancel / immediate) to + // the child. The pattern is a two-phase select: + // Phase 1: wait for the parent signal (or child/ctx completion). + // Phase 2: if the signal fired but recursive mode is not active yet, + // enter a second select waiting for either recursive escalation + // (recursiveChan) or child/ctx completion. This ensures + // non-recursive cancels leave children unaware, while a late + // escalation to recursive still propagates. + go func() { + select { + case <-cc.cancelChan: + if cc.isRecursive() { + child.setRecursive(true) + child.triggerCancel(cc.getMode()) + return + } + select { + case <-cc.recursiveChan: + child.setRecursive(true) + child.triggerCancel(cc.getMode()) + case <-child.doneChan: + case <-ctx.Done(): + } + case <-child.doneChan: + case <-ctx.Done(): + } + }() + + go func() { + select { + case <-cc.immediateChan: + if cc.isRecursive() { + child.setRecursive(true) + child.triggerImmediateCancel() + return + } + select { + case <-cc.recursiveChan: + child.setRecursive(true) + child.triggerImmediateCancel() + case <-child.doneChan: + case <-ctx.Done(): + } + case <-child.doneChan: + case <-ctx.Done(): + } + }() + + return child +} + +func (cc *cancelContext) triggerCancel(mode CancelMode) { + cc.setMode(mode) + if atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateCancelling) { + close(cc.cancelChan) + } +} + +func (cc *cancelContext) triggerImmediateCancel() { + atomic.StoreInt32(&cc.escalated, 1) + cc.setMode(CancelImmediate) + if atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateCancelling) { + close(cc.cancelChan) + } + cc.sendImmediateInterrupt() +} + +func (cc *cancelContext) getMode() CancelMode { + if cc == nil { + return CancelImmediate + } + return CancelMode(atomic.LoadInt32(&cc.mode)) +} + +func (cc *cancelContext) setMode(mode CancelMode) { + atomic.StoreInt32(&cc.mode, int32(mode)) +} + +func (cc *cancelContext) getDeadlineUnixNano() int64 { + return atomic.LoadInt64(&cc.deadlineUnixNano) +} + +func (cc *cancelContext) setDeadlineUnixNano(v int64) { + atomic.StoreInt64(&cc.deadlineUnixNano, v) +} + +func (cc *cancelContext) wakeTimeoutController() { + select { + case cc.timeoutNotify <- struct{}{}: + default: + } +} + +// shouldCancel returns true if a cancel has been requested (cancelChan is closed). +func (cc *cancelContext) shouldCancel() bool { + if cc == nil { + return false + } + select { + case <-cc.cancelChan: + return true + default: + return false + } +} + +// isImmediateCancelled returns true if an immediate graph interrupt has been +// fired (CancelImmediate or timeout escalation). This is stronger than +// shouldCancel: it means the compose graph is being torn down right now and +// orphaned goroutines should not attempt to send events. +func (cc *cancelContext) isImmediateCancelled() bool { + if cc == nil { + return false + } + select { + case <-cc.immediateChan: + return true + default: + return false + } +} + +// sendImmediateInterrupt sends the compose graph interrupt signal via graphInterruptFuncs. +// Also closes immediateChan (used by cancelMonitoredModel to abort an in-progress stream). +// Returns false if an interrupt was already sent or if no graphInterruptFuncs have been +// registered yet (the deferred fire in setGraphInterruptFunc will handle that case). +func (cc *cancelContext) sendImmediateInterrupt() bool { + cc.mu.Lock() + + if !atomic.CompareAndSwapInt32(&cc.interruptSent, interruptNotSent, interruptImmediate) { + cc.mu.Unlock() + return false + } + + close(cc.immediateChan) + + fns := make([]func(...compose.GraphInterruptOption), len(cc.graphInterruptFuncs)) + copy(fns, cc.graphInterruptFuncs) + + if cc.isRecursive() && cc.hasAgentToolDescendant() { + select { + case <-cc.doneChan: + cc.mu.Unlock() + return true + case <-time.After(defaultCancelImmediateGracePeriod): + } + } + + if len(fns) == 0 { + cc.mu.Unlock() + return false + } + + for _, fn := range fns { + fn(compose.WithGraphInterruptTimeout(0)) + } + cc.mu.Unlock() + return true +} + +// setGraphInterruptFunc appends a graph interrupt function to the list. +// If an immediate cancel was already requested, fires it retroactively. +// Multiple functions can be registered (e.g. one per parallel sub-agent). +// +// Both this method and sendImmediateInterrupt hold cc.mu across the entire +// check-and-fire sequence, ensuring each interrupt function is called exactly +// once (compose.WithGraphInterrupt returns a non-idempotent closure that panics +// on double-call). +func (cc *cancelContext) setGraphInterruptFunc(interrupt func(...compose.GraphInterruptOption)) { + cc.mu.Lock() + cc.graphInterruptFuncs = append(cc.graphInterruptFuncs, interrupt) + + shouldFire := atomic.LoadInt32(&cc.interruptSent) == interruptImmediate + if shouldFire { + interrupt(compose.WithGraphInterruptTimeout(0)) + } + cc.mu.Unlock() +} + +// markDone marks the execution as finished through any non-cancel path +// (normal completion, business interrupt, or error). +// This is safe to call even if a cancel is in progress — it allows the +// cancel func to detect that execution finished before cancel took effect. +func (cc *cancelContext) markDone() { + if cc == nil { + return + } + if atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateDone) || + atomic.CompareAndSwapInt32(&cc.state, stateCancelling, stateDone) { + cc.doneOnce.Do(func() { close(cc.doneChan) }) + } +} + +func (cc *cancelContext) hasAgentToolDescendant() bool { + return cc != nil && atomic.LoadInt32(&cc.agentToolDescendant) == 1 +} + +func (cc *cancelContext) markAgentToolDescendant() { + for cur := cc; cur != nil; cur = cur.parent { + atomic.StoreInt32(&cur.agentToolDescendant, 1) + } +} + +// markCancelHandled signals that the cancel path in the runFunc has created +// and sent a CancelError. Transitions state to stateCancelHandled so that: +// 1. The deferred markDone() becomes a no-op (CAS from cancelling fails). +// 2. buildCancelFunc sees stateCancelHandled and returns nil (cancel succeeded). +// Returns true if the transition succeeded, false if cancel was already handled +// (e.g., by a sub-agent). This prevents duplicate CancelError emission. +func (cc *cancelContext) markCancelHandled() bool { + if cc == nil { + return false + } + if atomic.CompareAndSwapInt32(&cc.state, stateCancelling, stateCancelHandled) { + cc.doneOnce.Do(func() { close(cc.doneChan) }) + return true + } + return false +} + +// createCancelError creates a CancelError based on the current cancel state. +func (cc *cancelContext) createCancelError() *CancelError { + info := &AgentCancelInfo{} + info.Mode = cc.getMode() + if atomic.LoadInt32(&cc.escalated) == 1 { + info.Escalated = true + info.Timeout = atomic.LoadInt32(&cc.timeoutEscalated) == 1 + } + return &CancelError{ + Info: info, + } +} + +func (cc *cancelContext) createAndMarkCancelHandled() (*CancelError, bool) { + cc.cancelMu.Lock() + defer cc.cancelMu.Unlock() + cancelErr := cc.createCancelError() + ok := cc.markCancelHandled() + return cancelErr, ok +} + +// buildCancelFunc builds the AgentCancelFunc for external use. +func (cc *cancelContext) buildCancelFunc() AgentCancelFunc { + joinMode := func(a, b CancelMode) CancelMode { + if a == CancelImmediate || b == CancelImmediate { + return CancelImmediate + } + return a | b + } + + parseReq := func(callOpts ...AgentCancelOption) *agentCancelConfig { + cfg := &agentCancelConfig{Mode: CancelImmediate} + for _, opt := range callOpts { + opt(cfg) + } + return cfg + } + + startTimeoutController := func() { + cc.timeoutOnce.Do(func() { + go func() { + for { + select { + case <-cc.doneChan: + return + default: + } + + mode := cc.getMode() + if mode == CancelImmediate { + return + } + + deadline := cc.getDeadlineUnixNano() + if deadline == 0 { + select { + case <-cc.timeoutNotify: + continue + case <-cc.doneChan: + return + } + } + + now := time.Now().UnixNano() + wait := time.Duration(deadline - now) + if wait <= 0 { + atomic.StoreInt32(&cc.escalated, 1) + atomic.StoreInt32(&cc.timeoutEscalated, 1) + cc.sendImmediateInterrupt() + return + } + + timer := time.NewTimer(wait) + select { + case <-timer.C: + timer.Stop() + atomic.StoreInt32(&cc.escalated, 1) + atomic.StoreInt32(&cc.timeoutEscalated, 1) + cc.sendImmediateInterrupt() + return + case <-cc.timeoutNotify: + timer.Stop() + continue + case <-cc.doneChan: + timer.Stop() + return + } + } + }() + }) + } + + newHandle := func(wait func() error) *CancelHandle { + return &CancelHandle{wait: wait} + } + + waitForCompletion := func() error { + <-cc.doneChan + + st := atomic.LoadInt32(&cc.state) + switch st { + case stateDone: + return ErrExecutionEnded + default: + if atomic.LoadInt32(&cc.timeoutEscalated) == 1 { + return ErrCancelTimeout + } + return nil + } + } + + return func(callOpts ...AgentCancelOption) (*CancelHandle, bool) { + req := parseReq(callOpts...) + + st := atomic.LoadInt32(&cc.state) + switch st { + case stateCancelHandled: + return newHandle(func() error { return nil }), false + case stateDone: + return newHandle(func() error { return ErrExecutionEnded }), false + } + + var needImmediate, needTimeoutCtl bool + + cc.cancelMu.Lock() + + st = atomic.LoadInt32(&cc.state) + switch st { + case stateCancelHandled: + cc.cancelMu.Unlock() + return newHandle(func() error { return nil }), false + case stateDone: + cc.cancelMu.Unlock() + return newHandle(func() error { return ErrExecutionEnded }), false + } + + curMode := cc.getMode() + if st == stateRunning { + if !atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateCancelling) { + st = atomic.LoadInt32(&cc.state) + cc.cancelMu.Unlock() + if st == stateDone { + return newHandle(func() error { return ErrExecutionEnded }), false + } + return newHandle(waitForCompletion), true + } + + curMode = req.Mode + cc.setMode(curMode) + atomic.StoreInt32(&cc.startedMode, int32(curMode)) + cc.setRecursive(req.Recursive) + close(cc.cancelChan) + } else { + // Recursive is monotonic: once set, cannot be unset. The first + // cancel call uses the bool directly; subsequent calls only + // escalate (false → true) — setRecursive(false) is a no-op. + curMode = joinMode(curMode, req.Mode) + cc.setMode(curMode) + if req.Recursive { + cc.setRecursive(true) + } + } + + if curMode == CancelImmediate { + cc.setDeadlineUnixNano(0) + needImmediate = true + } else if req.Timeout != nil && *req.Timeout > 0 { + proposed := time.Now().Add(*req.Timeout).UnixNano() + existing := cc.getDeadlineUnixNano() + if existing == 0 || proposed < existing { + cc.setDeadlineUnixNano(proposed) + cc.wakeTimeoutController() + } + needTimeoutCtl = cc.getDeadlineUnixNano() != 0 + } + + cc.cancelMu.Unlock() + + if needImmediate { + if atomic.LoadInt32(&cc.startedMode) != int32(CancelImmediate) { + atomic.StoreInt32(&cc.escalated, 1) + } + cc.sendImmediateInterrupt() + } + if needTimeoutCtl { + startTimeoutController() + } + + return newHandle(waitForCompletion), true + } +} + +// wrapIterWithCancelCtx wraps an iterator with cancel lifecycle management. +// It calls markDone when the inner iterator is fully drained, ensuring the +// cancelContext's doneChan is closed and propagation goroutines can exit. +// +// For root cancelContexts (created by WithCancel, not deriveAgentToolCancelContext), it also +// converts interrupt ACTION events to CancelError when cancel is active. +// This is the single point of interrupt-to-CancelError conversion in the +// system — Runner.handleIter only enriches the resulting CancelError with +// checkpoint metadata. +// +// Interrupt absorption: ALL interrupts are converted when cancel is active, +// including business interrupts (compose.Interrupt from user code). Cancel and +// business interrupts cannot be reliably distinguished in concurrent execution +// (parallel workflows, concurrent tool calls) where they merge into a single +// composite signal. The business interrupt data is preserved in the checkpoint +// and re-fires naturally on resume. +// +// This conversion MUST happen in this wrapper (not deferred to Runner.handleIter) +// because markDone runs as a defer in this goroutine — if the interrupt event +// were passed through unconverted, markDone would transition stateCancelling→stateDone +// before the Runner goroutine could call createAndMarkCancelHandled, causing it +// to fail the CAS. +func wrapIterWithCancelCtx[M MessageType](iter *AsyncIterator[*TypedAgentEvent[M]], cancelCtx *cancelContext) *AsyncIterator[*TypedAgentEvent[M]] { + if cancelCtx == nil { + return iter + } + it, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go func() { + defer cancelCtx.markDone() + defer gen.Close() + for { + event, ok := iter.Next() + if !ok { + break + } + + if cancelCtx.isRoot() && event.Action != nil && event.Action.internalInterrupted != nil { + if cancelCtx.shouldCancel() { + cancelErr, ok := cancelCtx.createAndMarkCancelHandled() + if ok { + cancelErr.interruptSignal = event.Action.internalInterrupted + gen.Send(&TypedAgentEvent[M]{Err: cancelErr}) + } + return + } + } + + gen.Send(event) + } + }() + return it +} + +// typedCancelMonitoredModel wraps a model with cancel monitoring. +// Generate: pure delegate to the inner model (CancelAfterChatModel is handled +// by a dedicated node after the ChatModel in the compose graph). +// Stream: pipes chunks through a goroutine that selects on immediateChan for +// CancelImmediate abort. +type typedCancelMonitoredModel[M MessageType] struct { + inner model.BaseModel[M] + cancelContext *cancelContext +} + +type recvResult[T any] struct { + data T + err error +} + +func (m *typedCancelMonitoredModel[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + return m.inner.Generate(ctx, input, opts...) +} + +func (m *typedCancelMonitoredModel[M]) Stream(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + stream, err := m.inner.Stream(ctx, input, opts...) + if err != nil { + return nil, err + } + wrapped := wrapStreamWithCancelMonitoring(stream, m.cancelContext) + return wrapped, nil +} + +// wrapStreamWithCancelMonitoring wraps a stream with cancel monitoring. +// When immediateChan fires (CancelImmediate or timeout escalation), the output +// stream is terminated with ErrStreamCanceled. +func wrapStreamWithCancelMonitoring[T any](stream *schema.StreamReader[T], cc *cancelContext) *schema.StreamReader[T] { + if cc == nil { + return stream + } + + // Already canceled — terminate immediately + select { + case <-cc.immediateChan: + stream.Close() + r, w := schema.Pipe[T](1) + var zero T + w.Send(zero, ErrStreamCanceled) + w.Close() + return r + default: + } + + reader, writer := schema.Pipe[T](1) + + go func() { + done := make(chan struct{}) + defer close(done) + defer writer.Close() + defer stream.Close() + + ch := make(chan recvResult[T]) + go func() { + defer close(ch) + for { + chunk, recvErr := stream.Recv() + select { + case ch <- recvResult[T]{chunk, recvErr}: + case <-done: + return + } + if recvErr != nil { + return + } + } + }() + + for { + select { + case <-cc.immediateChan: + var zero T + writer.Send(zero, ErrStreamCanceled) + return + + case r, ok := <-ch: + if !ok { + return + } + if r.err != nil { + if r.err == io.EOF { + return + } + var zero T + writer.Send(zero, r.err) + return + } + if closed := writer.Send(r.data, nil); closed { + return + } + } + } + }() + + return reader +} + +// cancelMonitoredToolHandler wraps streamable tool calls with cancel monitoring. +// When CancelImmediate fires, the tool output stream is terminated with ErrStreamCanceled. +// This handler reads the cancelContext from the Go context via getCancelContext. +type cancelMonitoredToolHandler struct{} + +func (h *cancelMonitoredToolHandler) WrapStreamableToolCall(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + output, err := next(ctx, input) + if err != nil { + return nil, err + } + + cc := getCancelContext(ctx) + if cc == nil { + return output, nil + } + + wrapped := wrapStreamWithCancelMonitoring(output.Result, cc) + return &compose.StreamToolOutput{Result: wrapped}, nil + } +} + +func (h *cancelMonitoredToolHandler) WrapEnhancedStreamableToolCall(next compose.EnhancedStreamableToolEndpoint) compose.EnhancedStreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + output, err := next(ctx, input) + if err != nil { + return nil, err + } + + cc := getCancelContext(ctx) + if cc == nil { + return output, nil + } + + wrapped := wrapStreamWithCancelMonitoring(output.Result, cc) + return &compose.EnhancedStreamableToolOutput{Result: wrapped}, nil + } +} diff --git a/adk/cancel_edge_test.go b/adk/cancel_edge_test.go new file mode 100644 index 0000000..0c2a80d --- /dev/null +++ b/adk/cancel_edge_test.go @@ -0,0 +1,2085 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// --- helpers shared across edge-case tests --- + +// blockingChatModel blocks until unblockCh is closed, then returns a fixed response. +type blockingChatModel struct { + unblockCh chan struct{} + response *schema.Message + started chan struct{} + callCount int32 +} + +func newBlockingChatModel(response *schema.Message) *blockingChatModel { + return &blockingChatModel{ + unblockCh: make(chan struct{}), + response: response, + started: make(chan struct{}, 1), + } +} + +func (m *blockingChatModel) Generate(ctx context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m.callCount, 1) + select { + case m.started <- struct{}{}: + default: + } + <-m.unblockCh + return m.response, nil +} + +func (m *blockingChatModel) Stream(ctx context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m.callCount, 1) + select { + case m.started <- struct{}{}: + default: + } + <-m.unblockCh + return schema.StreamReaderFromArray([]*schema.Message{m.response}), nil +} + +func (m *blockingChatModel) BindTools(_ []*schema.ToolInfo) error { return nil } + +// errorChatModel returns an error from Generate/Stream. +type errorChatModel struct { + err error + started chan struct{} +} + +func (m *errorChatModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + if m.started != nil { + select { + case m.started <- struct{}{}: + default: + } + } + return nil, m.err +} + +func (m *errorChatModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, m.err +} + +func (m *errorChatModel) BindTools(_ []*schema.ToolInfo) error { return nil } + +// plainResponseModel returns immediately with a fixed text response (no tool calls). +type plainResponseModel struct { + text string +} + +func (m *plainResponseModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage(m.text, nil), nil +} + +func (m *plainResponseModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage(m.text, nil)}), nil +} + +func (m *plainResponseModel) BindTools(_ []*schema.ToolInfo) error { return nil } + +// blockingTool blocks until unblockCh is closed. +type blockingTool struct { + name string + unblockCh chan struct{} + started chan struct{} + callCount int32 +} + +func newBlockingTool(name string) *blockingTool { + return &blockingTool{ + name: name, + unblockCh: make(chan struct{}), + started: make(chan struct{}, 4), + } +} + +func (t *blockingTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "blocking tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string"}, + }), + }, nil +} + +func (t *blockingTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + atomic.AddInt32(&t.callCount, 1) + select { + case t.started <- struct{}{}: + default: + } + <-t.unblockCh + return "result", nil +} + +func toolCallMsg(calls ...schema.ToolCall) *schema.Message { + return &schema.Message{Role: schema.Assistant, ToolCalls: calls} +} + +func toolCall(id, name, args string) schema.ToolCall { + return schema.ToolCall{ID: id, Type: "function", Function: schema.FunctionCall{Name: name, Arguments: args}} +} + +func drainEvents(iter *AsyncIterator[*AgentEvent]) ([]*AgentEvent, bool) { + var events []*AgentEvent + hasCancelError := false + for { + e, ok := iter.Next() + if !ok { + break + } + events = append(events, e) + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + hasCancelError = true + } + } + return events, hasCancelError +} + +// --- tests --- + +// TestWithCancel_BeforeExecutionStarts verifies that a cancel issued before +// the graph begins executing still produces a CancelError without invoking +// the model or tools. +func TestWithCancel_BeforeExecutionStarts(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(toolCallMsg(toolCall("c1", "bt", `{"input":"x"}`))) + bt := newBlockingTool("bt") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{bt}}, + }, + }) + assert.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + + // Extract the cancelContext so we can wait for cancelChan to close, + // ensuring the cancel is fully registered before Run starts. + cc := getCommonOptions(nil, cancelOpt).cancelCtx + + // Call cancel BEFORE calling agent.Run. + // The cancelFunc must succeed (not hang) even though execution hasn't started. + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn() + cancelDone <- handle.Wait() + }() + + // Wait for cancelChan to close so the pre-execution check in runFunc + // deterministically sees shouldCancel()=true (eliminates goroutine scheduling race). + <-cc.cancelChan + + // Now start the run — it should see shouldCancel()=true and emit CancelError immediately. + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, cancelOpt) + + _, hasCancelError := drainEvents(iter) + assert.True(t, hasCancelError, "expected CancelError when cancel precedes execution") + + // cancelFn must have already returned (or return quickly now that doneChan is closed). + select { + case cancelErr := <-cancelDone: + // Either nil (cancel handled) or ErrExecutionEnded is acceptable + // depending on exact timing; what matters is it didn't hang. + _ = cancelErr + case <-time.After(3 * time.Second): + t.Fatal("cancelFn blocked indefinitely after pre-start cancel") + } + + // Model and tool must not have been invoked. + assert.Equal(t, int32(0), atomic.LoadInt32(&bt.callCount), "tool must not be called") +} + +// TestWithCancel_AfterCompletion verifies cancelFn returns ErrExecutionEnded +// when called after a normal run finishes. +func TestWithCancel_AfterCompletion(t *testing.T) { + ctx := context.Background() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: &plainResponseModel{text: "done"}, + }) + require.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, cancelOpt) + + // Drain all events so the run completes. + for { + _, ok := iter.Next() + if !ok { + break + } + } + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.ErrorIs(t, cancelErr, ErrExecutionEnded) +} + +// TestWithCancel_DerivedAgentToolCancelContextMarkedDoneAfterRun verifies that +// an explicitly derived AgentTool child cancel context is owned by the child run, +// even when the Go context also carries the parent cancel context. +func TestWithCancel_DerivedAgentToolCancelContextMarkedDoneAfterRun(t *testing.T) { + ctx := context.Background() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ChildAgent", + Description: "test child agent", + Model: &plainResponseModel{text: "done"}, + }) + require.NoError(t, err) + + parent := newCancelContext() + parentCtx := withCancelContext(ctx, parent) + child := parent.deriveAgentToolCancelContext(parentCtx) + + childOpt := WrapImplSpecificOptFn(func(o *options) { + o.cancelCtx = child + }) + iter := agent.Run(parentCtx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, childOpt) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + select { + case <-child.doneChan: + case <-time.After(time.Second): + t.Fatal("derived AgentTool cancel context was not marked done after child run completion") + } +} + +// TestWithCancel_AfterBusinessInterrupt verifies cancelFn returns ErrExecutionEnded +// when called after the agent has been interrupted by business logic. +func TestWithCancel_AfterBusinessInterrupt(t *testing.T) { + ctx := context.Background() + + // Use a model that triggers a compose.Interrupt so the agent stops with an interrupt. + interruptModel := &interruptingChatModel{} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: interruptModel, + }) + require.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hi")}, cancelOpt, WithCheckPointID("biz-interrupt-1")) + + // Drain — expect an interrupt action event, no cancel error. + var gotInterrupt bool + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Action != nil && e.Action.Interrupted != nil { + gotInterrupt = true + } + } + assert.True(t, gotInterrupt, "expected business interrupt event") + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.ErrorIs(t, cancelErr, ErrExecutionEnded) +} + +// TestWithCancel_AfterError verifies cancelFn returns ErrExecutionEnded +// when called after the agent errors out. +func TestWithCancel_AfterError(t *testing.T) { + ctx := context.Background() + + modelErr := errors.New("model exploded") + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: &errorChatModel{err: modelErr}, + }) + require.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, cancelOpt) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.ErrorIs(t, cancelErr, ErrExecutionEnded) +} + +// TestWithCancel_TimeoutEscalation tests that WithAgentCancelTimeout causes the +// cancel to escalate to immediate when the safe-point hasn't fired yet, and +// that the resulting CancelError has Escalated=true. +// +// Strategy: use CancelAfterChatModel mode. The model blocks (never completes), +// so the safe-point can't fire naturally. After the timeout, escalateToImmediate +// closes immediateChan which aborts the model stream via cancelMonitoredModel +// and causes a CancelError — no compose graph-interrupt races involved. +func TestWithCancel_TimeoutEscalation(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(schema.AssistantMessage("hello", nil)) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + }) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, // use streaming so cancelMonitoredModel.Stream is exercised + }) + + timeout := 300 * time.Millisecond + // CancelAfterChatModel + timeout: safe-point can't fire (model never finishes), + // so after 300ms the timeout goroutine escalates to immediate. + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("go")}, cancelOpt) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start") + } + + // Fire cancelFn; it will wait for escalation to complete. + start := time.Now() + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel), WithAgentCancelTimeout(timeout)) + cancelErr := handle.Wait() + elapsed := time.Since(start) + + assert.ErrorIs(t, cancelErr, ErrCancelTimeout, "cancel should return ErrCancelTimeout after timeout escalation") + assert.True(t, elapsed >= timeout, "should wait at least the timeout duration, elapsed=%v", elapsed) + assert.True(t, elapsed < 3*time.Second, "should complete shortly after timeout, elapsed=%v", elapsed) + + var cancelError *CancelError + for { + e, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + cancelError = ce + } + } + if assert.NotNil(t, cancelError, "expected CancelError after timeout escalation") { + assert.True(t, cancelError.Info.Escalated, "CancelError should report Escalated=true") + assert.True(t, cancelError.Info.Timeout, "CancelError should report Timeout=true") + } +} + +// TestWithCancel_AfterChatModel_WithTools verifies CancelAfterChatModel fires +// when the model returns tool calls (the safe-point is on the tool-calls path). +func TestWithCancel_AfterChatModel_WithTools(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(toolCallMsg(toolCall("c1", "bt", `{"input":"x"}`))) + bt := newBlockingTool("bt") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{bt}}, + }, + }) + require.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, cancelOpt) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start") + } + + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + cancelDone <- handle.Wait() + }() + + time.Sleep(20 * time.Millisecond) + + close(blk.unblockCh) + + cancelErr := <-cancelDone + assert.NoError(t, cancelErr) + + _, hasCancelError := drainEvents(iter) + assert.True(t, hasCancelError, "CancelError expected after model returns tool calls") +} + +// TestWithCancel_CancelImmediate_StreamAborted verifies that CancelImmediate +// during model execution surfaces CancelError and completes quickly. +// Uses blockingChatModel which blocks in Stream(), keeping the agent's run +// function alive so the cancel context stays in stateRunning. +func TestWithCancel_CancelImmediate_StreamAborted(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(schema.AssistantMessage("hello", nil)) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + }) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hi")}, cancelOpt) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start") + } + time.Sleep(50 * time.Millisecond) + + start := time.Now() + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + elapsed := time.Since(start) + assert.True(t, elapsed < 2*time.Second, "cancel should complete quickly, elapsed=%v", elapsed) + + var foundCancelError bool + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Action != nil && e.Action.Interrupted != nil { + foundCancelError = true + } + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + foundCancelError = true + } + } + assert.True(t, foundCancelError, "expected CancelError in event stream") +} + +// TestWithCancel_MultipleToolsConcurrent verifies that CancelAfterToolCalls +// waits for ALL concurrent tool calls to complete before cancelling. +func TestWithCancel_MultipleToolsConcurrent(t *testing.T) { + ctx := context.Background() + + bt1 := newBlockingTool("tool1") + bt2 := newBlockingTool("tool2") + + // Model calls both tools in one response. + modelResp := toolCallMsg( + toolCall("c1", "tool1", `{"input":"a"}`), + toolCall("c2", "tool2", `{"input":"b"}`), + ) + modelWithTools := &simpleChatModel{response: modelResp} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: modelWithTools, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{bt1, bt2}}, + }, + }) + assert.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}, cancelOpt) + + // Wait for both tools to start. + for i := 0; i < 2; i++ { + select { + case <-bt1.started: + case <-bt2.started: + case <-time.After(5 * time.Second): + t.Fatal("tools did not start") + } + } + + // Request cancel after tool calls while both are still blocking. + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + cancelDone <- handle.Wait() + }() + + // Unblock both tools — cancel should fire only after both complete. + time.Sleep(50 * time.Millisecond) + close(bt1.unblockCh) + time.Sleep(50 * time.Millisecond) + close(bt2.unblockCh) + + cancelErr := <-cancelDone + assert.NoError(t, cancelErr) + + assert.Equal(t, int32(1), atomic.LoadInt32(&bt1.callCount), "tool1 should complete") + assert.Equal(t, int32(1), atomic.LoadInt32(&bt2.callCount), "tool2 should complete") + + _, hasCancelError := drainEvents(iter) + assert.True(t, hasCancelError, "expected CancelError after concurrent tools completed") +} + +// TestWithCancel_GraphInterruptRaceBeforeSet verifies that a CancelImmediate +// issued before setGraphInterruptFunc is called still results in cancellation. +// This exercises the retroactive-fire path in setGraphInterruptFunc. +func TestWithCancel_GraphInterruptRaceBeforeSet(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(schema.AssistantMessage("hi", nil)) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + }) + require.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + + // Cancel immediately before run starts. + go func() { + handle, _ := cancelFn() + _ = handle.Wait() + }() + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, cancelOpt) + + done := make(chan struct{}) + go func() { + defer close(done) + drainEvents(iter) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("iteration did not complete after pre-start CancelImmediate") + } +} + +// TestWithCancel_NoCheckpointStore verifies cancel completes and does not panic +// when no checkpoint store is configured. +func TestWithCancel_NoCheckpointStore(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(schema.AssistantMessage("hi", nil)) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + }) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + // No CheckPointStore set. + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hi")}, cancelOpt) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start") + } + time.Sleep(30 * time.Millisecond) + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + var ce *CancelError + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Err != nil && errors.As(e.Err, &ce) { + break + } + } + assert.NotNil(t, ce, "expected CancelError even without checkpoint store") +} + +// TestWithCancel_ModelError verifies that a model error marks the cancelCtx as +// done so that a subsequent cancelFn call returns ErrExecutionEnded. +func TestWithCancel_ModelError(t *testing.T) { + ctx := context.Background() + + modelErr := errors.New("model failed") + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: &errorChatModel{err: modelErr}, + }) + require.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hi")}}, cancelOpt) + + var gotModelErr bool + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Err != nil && !errors.As(e.Err, new(*CancelError)) { + gotModelErr = true + } + } + assert.True(t, gotModelErr, "expected non-cancel error event from model failure") + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.ErrorIs(t, cancelErr, ErrExecutionEnded, "cancelFn should return ErrExecutionEnded after model error") +} + +// TestWithCancel_Resume_SafePoint covers CancelAfterChatModel and +// CancelAfterToolCalls on a Resume path. +func TestWithCancel_Resume_SafePoint(t *testing.T) { + ctx := context.Background() + + // --- phase 1: run to get a checkpoint via CancelImmediate --- + blk := newBlockingChatModel(toolCallMsg(toolCall("c1", "bt", `{"input":"x"}`))) + bt := newSlowTool("bt", 50*time.Millisecond, "result") + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{bt}}, + }, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner1 := NewRunner(ctx, RunnerConfig{ + Agent: agent1, + CheckPointStore: store, + }) + + cancelOpt1, cancelFn1 := WithCancel() + iter1 := runner1.Run(ctx, []Message{schema.UserMessage("hi")}, cancelOpt1, WithCheckPointID("resume-sp-1")) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start in phase 1") + } + _, _ = cancelFn1() + drainEvents(iter1) + + // --- phase 2: resume, cancel after chat model --- + resumeModel := newBlockingChatModel(toolCallMsg(toolCall("c1", "bt", `{"input":"x"}`))) + + bt2 := newSlowTool("bt", 50*time.Millisecond, "result") + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: resumeModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{bt2}}, + }, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: agent2, + CheckPointStore: store, + }) + + cancelOpt2, cancelFn2 := WithCancel() + resumeIter, err := runner2.Resume(ctx, "resume-sp-1", cancelOpt2) + require.NoError(t, err) + + select { + case <-resumeModel.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start in phase 2") + } + + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn2(WithAgentCancelMode(CancelAfterChatModel)) + cancelDone <- handle.Wait() + }() + + time.Sleep(50 * time.Millisecond) + + close(resumeModel.unblockCh) + + cancelErr := <-cancelDone + assert.NoError(t, cancelErr) + + _, hasCancelError := drainEvents(resumeIter) + assert.True(t, hasCancelError, "CancelError expected after resumed model returns tool calls") +} + +// callbackTool is a tool that calls onCall when invoked. +type callbackTool struct { + name string + onCall func() +} + +func (t *callbackTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "callback tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string"}, + }), + }, nil +} + +func (t *callbackTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + if t.onCall != nil { + t.onCall() + } + return "ok", nil +} + +// interruptingChatModel returns a compose.Interrupt error to simulate a +// business interrupt during execution. +type interruptingChatModel struct{} + +func (m *interruptingChatModel) Generate(ctx context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, compose.Interrupt(ctx, "test interrupt") +} + +func (m *interruptingChatModel) Stream(ctx context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, compose.Interrupt(ctx, "test interrupt") +} + +func (m *interruptingChatModel) BindTools(_ []*schema.ToolInfo) error { return nil } + +// TestWithCancel_TargetedResume_CancelImmediate cancels an agent via CancelImmediate, +// extracts InterruptContexts from the resulting CancelError, and uses them +// for targeted resumption via Runner.ResumeWithParams. +func TestWithCancel_TargetedResume_CancelImmediate(t *testing.T) { + ctx := context.Background() + + blk := newBlockingChatModel(toolCallMsg(toolCall("c1", "st", `{"input":"x"}`))) + st := newSlowTool("st", 50*time.Millisecond, "result") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("go")}, cancelOpt, WithCheckPointID("targeted-imm-1")) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start") + } + + handle, _ := cancelFn() // CancelImmediate (default) + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + var cancelError *CancelError + for { + e, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + cancelError = ce + } + } + + require.NotNil(t, cancelError, "expected CancelError") + require.NotEmpty(t, cancelError.InterruptContexts, "CancelError should have InterruptContexts for targeted resume") + + // --- resume with targeted params --- + targets := make(map[string]any) + for _, ic := range cancelError.InterruptContexts { + targets[ic.ID] = nil + } + + resumeModel := &plainResponseModel{text: "resumed"} + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: resumeModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: agent2, + CheckPointStore: store, + }) + + resumeIter, err := runner2.ResumeWithParams(ctx, "targeted-imm-1", &ResumeParams{Targets: targets}) + require.NoError(t, err) + + var gotOutput bool + for { + e, ok := resumeIter.Next() + if !ok { + break + } + if e.Err != nil { + t.Fatalf("unexpected error during targeted resume: %v", e.Err) + } + if e.Output != nil && e.Output.MessageOutput != nil { + gotOutput = true + } + } + assert.True(t, gotOutput, "targeted resume should produce output") +} + +// TestWithCancel_TargetedResume_SafePoint cancels an agent via CancelAfterChatModel +// (safe-point) and verifies that InterruptContexts are populated on the CancelError +// and that targeted resume via ResumeWithParams succeeds. +// Since safe-point cancels now use compose.Interrupt, compose saves checkpoint data, +// making the cancel fully resumable. +func TestWithCancel_TargetedResume_SafePoint(t *testing.T) { + ctx := context.Background() + + // The model returns a tool call so the react graph routes to toolPreHandle, + // which detects CancelAfterChatModel and fires compose.Interrupt. + blk := newBlockingChatModel(toolCallMsg(toolCall("c1", "st", `{"input":"x"}`))) + st := newSlowTool("st", 0, "result") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("go")}, cancelOpt, WithCheckPointID("targeted-sp-1")) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start") + } + + // Start cancelFn in background so the CAS happens before the model unblocks. + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + cancelDone <- handle.Wait() + }() + time.Sleep(50 * time.Millisecond) + close(blk.unblockCh) + + cancelErr := <-cancelDone + assert.NoError(t, cancelErr) + + var cancelError *CancelError + for { + e, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + cancelError = ce + } + } + + require.NotNil(t, cancelError, "expected CancelError") + require.NotEmpty(t, cancelError.InterruptContexts, "CancelError should have InterruptContexts for targeted resume") + + // --- resume with targeted params --- + targets := make(map[string]any) + for _, ic := range cancelError.InterruptContexts { + targets[ic.ID] = nil + } + + resumeModel := &plainResponseModel{text: "resumed"} + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: resumeModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: agent2, + CheckPointStore: store, + }) + + resumeIter, err := runner2.ResumeWithParams(ctx, "targeted-sp-1", &ResumeParams{Targets: targets}) + require.NoError(t, err) + + var gotOutput bool + for { + e, ok := resumeIter.Next() + if !ok { + break + } + if e.Err != nil { + t.Fatalf("unexpected error during targeted resume: %v", e.Err) + } + if e.Output != nil && e.Output.MessageOutput != nil { + gotOutput = true + } + } + assert.True(t, gotOutput, "targeted resume should produce output") +} + +// TestWithCancel_Resume_CancelAfterChatModel_MessagePreserved tests both the +// ReAct (with-tools) and noTools paths to ensure that when a +// CancelAfterChatModel safe-point fires and the run is later resumed, the +// original Message returned by the chat model is preserved through the +// StatefulInterrupt checkpoint. +// +// For the ReAct path: the model returns a tool-call message. On resume the +// cancelCheck node must return that same message so the branch routes to the +// ToolNode and the tool actually executes. +// +// For the noTools path: the model returns a plain text message. On resume the +// cancel-check lambda must return that same message as the chain output. +func TestWithCancel_Resume_CancelAfterChatModel_MessagePreserved(t *testing.T) { + t.Run("react_path_tool_call_preserved", func(t *testing.T) { + ctx := context.Background() + + // Phase-2 model returns no tool calls so the graph ends. + // We track whether the tool actually executes on resume. + toolExecuted := make(chan struct{}, 1) + st := &callbackTool{ + name: "my_tool", + onCall: func() { + select { + case toolExecuted <- struct{}{}: + default: + } + }, + } + + // Phase-1 model returns a tool call. + blk := newBlockingChatModel(toolCallMsg(toolCall("c1", "my_tool", `{"input":"x"}`))) + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: blk, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + store := newCancelTestStore() + runner1 := NewRunner(ctx, RunnerConfig{ + Agent: agent1, + CheckPointStore: store, + }) + + cancelOpt1, cancelFn1 := WithCancel() + iter1 := runner1.Run(ctx, []Message{schema.UserMessage("hi")}, + cancelOpt1, WithCheckPointID("react-msg-preserved-1")) + + select { + case <-blk.started: + case <-time.After(5 * time.Second): + t.Fatal("model did not start in phase 1") + } + + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn1(WithAgentCancelMode(CancelAfterChatModel)) + cancelDone <- handle.Wait() + }() + time.Sleep(50 * time.Millisecond) + close(blk.unblockCh) + + cancelErr := <-cancelDone + assert.NoError(t, cancelErr) + + _, hasCancelError := drainEvents(iter1) + assert.True(t, hasCancelError, "expected CancelError from phase 1") + + // Phase 2: resume. The model for phase-2 returns plain text (no tool + // calls) so the react graph ends after one iteration. But first the + // tool from the checkpoint must execute. + resumeModel := &plainResponseModel{text: "done"} + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: resumeModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: agent2, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, "react-msg-preserved-1") + require.NoError(t, err) + + for { + e, ok := resumeIter.Next() + if !ok { + break + } + if e.Err != nil { + t.Fatalf("unexpected error during resume: %v", e.Err) + } + } + + // The key assertion: the tool must have been called during resume, + // which can only happen if the tool-call message was preserved. + select { + case <-toolExecuted: + // success + default: + t.Fatal("tool was not executed on resume — the tool-call message was lost") + } + }) + +} + +// TestHandleRunFuncError_AlreadyHandled_NoDuplicate verifies that when +// markCancelHandled() was already claimed by a sub-agent's handleRunFuncError, +// the sequential workflow's checkCancel does not emit a second CancelError. +// +// Setup: sequential[cma1, cma2] with CancelAfterToolCalls. cma1 has tools, +// cancel fires while tool is running. After tool completes, the safe-point +// fires in cma1's handleRunFuncError (claiming markCancelHandled). The +// sequential workflow's checkCancel at the transition point should find +// markCancelHandled returns false and skip — producing exactly 1 CancelError. +func TestHandleRunFuncError_AlreadyHandled_NoDuplicate(t *testing.T) { + ctx := context.Background() + + bt := newBlockingTool("bt") + + // cma1: model returns a tool call immediately, tool blocks until unblocked + cma1Model := newBlockingChatModel(toolCallMsg(toolCall("c1", "bt", `{"input":"x"}`))) + close(cma1Model.unblockCh) // model returns immediately + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", + Model: cma1Model, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{bt}}, + }, + }) + require.NoError(t, err) + + agent2Model := &plainResponseModel{text: "agent2-response"} + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", + Model: agent2Model, + }) + require.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "sequential", SubAgents: []Agent{agent1, agent2}, + }) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + // Wait for tool to start + select { + case <-bt.started: + case <-time.After(5 * time.Second): + t.Fatal("Tool did not start") + } + + // Cancel while tool is still running (in goroutine because cancelFn blocks + // until execution finishes), then unblock tool so safe-point fires + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + _ = handle.Wait() + }() + + // Give cancel time to register, then unblock tool + time.Sleep(50 * time.Millisecond) + close(bt.unblockCh) + + cancelCount := 0 + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + cancelCount++ + } + } + + assert.Equal(t, 1, cancelCount, "Should have exactly one CancelError, no duplicate from handleRunFuncError + checkCancel") +} + +func TestWithCancel_CancelAfterChatModel_NestedAgentTool(t *testing.T) { + ctx := context.Background() + + subAgentModel := newBlockingChatModel(toolCallMsg(toolCall("c1", "sub_tool", `{"input":"x"}`))) + subAgentModelStarted := subAgentModel.started + subTool := newBlockingTool("sub_tool") + + subAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "sub_agent", + Description: "test sub agent", + Instruction: "you are a sub agent", + Model: subAgentModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{subTool}}, + }, + }) + require.NoError(t, err) + + supervisorModel := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{{ + ID: "call_1", Type: "function", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "sub_agent"}`, + }, + }}, + }, + } + + supervisorAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "supervisor", + Description: "supervisor agent (equivalent to DeepAgent)", + Instruction: "you are a supervisor", + Model: supervisorModel, + }) + require.NoError(t, err) + + agentWithSubAgents, err := SetSubAgents(ctx, supervisorAgent, []Agent{subAgent}) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agentWithSubAgents, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + select { + case <-subAgentModelStarted: + case <-time.After(10 * time.Second): + t.Fatal("Sub-agent model did not start") + } + + time.Sleep(50 * time.Millisecond) + + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel), WithRecursive()) + cancelDone <- handle.Wait() + }() + + time.Sleep(100 * time.Millisecond) + close(subAgentModel.unblockCh) + + cancelErr := <-cancelDone + assert.NoError(t, cancelErr) + + hasCancelError := false + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "CancelError expected from nested agent tool with tools") +} + +// slowStreamingTool implements StreamableTool (but NOT InvokableTool), streaming +// chunks slowly so CancelImmediate can fire mid-stream. +type slowStreamingTool struct { + name string + chunkInterval time.Duration + chunks []string + started chan struct{} + gate chan struct{} // if non-nil, blocks after first chunk until closed +} + +func (t *slowStreamingTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "slow streaming tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string"}, + }), + }, nil +} + +func (t *slowStreamingTool) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) { + r, w := schema.Pipe[string](1) + go func() { + defer w.Close() + select { + case t.started <- struct{}{}: + default: + } + for i, chunk := range t.chunks { + time.Sleep(t.chunkInterval) + if closed := w.Send(chunk, nil); closed { + return + } + // After the second chunk, block on gate so the caller can + // issue a cancel while the tool is deterministically still streaming. + // We wait until chunk index 1 (second chunk) so that the framework + // has time to receive the first chunk and forward the streaming + // event to the iterator, ensuring ErrStreamCanceled is observable. + if i == 1 && t.gate != nil { + <-t.gate + } + } + }() + return r, nil +} + +// toolCallStreamModel returns a tool-call message on the first Stream call, +// then a plain text response on subsequent calls. +type toolCallStreamModel struct { + callCount int32 +} + +func (m *toolCallStreamModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + if atomic.AddInt32(&m.callCount, 1) == 1 { + return toolCallMsg(toolCall("c1", "slow_tool", `{"input":"x"}`)), nil + } + return schema.AssistantMessage("done", nil), nil +} + +func (m *toolCallStreamModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + msg, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.Message{msg}), nil +} + +func (m *toolCallStreamModel) BindTools(_ []*schema.ToolInfo) error { return nil } + +// TestWithCancel_CancelImmediate_StreamableToolAborted verifies that CancelImmediate +// during StreamableTool streaming surfaces ErrStreamCanceled on the tool's +// MessageStream.Recv(), just like it does for ChatModel streaming. +func TestWithCancel_CancelImmediate_StreamableToolAborted(t *testing.T) { + ctx := context.Background() + + tcm := &toolCallStreamModel{} + gate := make(chan struct{}) + st := &slowStreamingTool{ + name: "slow_tool", + chunkInterval: 100 * time.Millisecond, + chunks: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + started: make(chan struct{}, 1), + gate: gate, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: tcm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{st}}, + }, + }) + require.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hi")}, cancelOpt) + + // Wait for the tool to start streaming and send its first chunk. + // The tool then blocks on the gate, guaranteeing the execution is + // still in progress when we issue the cancel. + select { + case <-st.started: + case <-time.After(5 * time.Second): + t.Fatal("tool did not start streaming") + } + + // Drain events in a separate goroutine so we can issue the cancel + // from the main goroutine after confirming the tool stream event + // has been received. + type result struct { + foundStreamCanceled bool + foundCancelError bool + } + resultCh := make(chan result, 1) + toolStreamReady := make(chan struct{}) + go func() { + var r result + for { + e, ok := iter.Next() + if !ok { + break + } + + // ErrStreamCanceled appears on the tool's MessageStream.Recv() + if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.IsStreaming && + e.Output.MessageOutput.Role == schema.Tool { + // Signal that the tool stream event has been received. + close(toolStreamReady) + stream := e.Output.MessageOutput.MessageStream + for { + _, recvErr := stream.Recv() + if recvErr != nil { + if errors.Is(recvErr, ErrStreamCanceled) { + r.foundStreamCanceled = true + } + break + } + } + } + + if e.Action != nil && e.Action.Interrupted != nil { + r.foundCancelError = true + } + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + r.foundCancelError = true + } + } + resultCh <- r + }() + + // Wait for the iterator goroutine to receive the tool streaming event. + // At this point the tool goroutine is blocked on the gate, and the + // iterator goroutine is blocked on stream.Recv(), so the execution is + // guaranteed to still be in progress. + select { + case <-toolStreamReady: + case <-time.After(5 * time.Second): + t.Fatal("tool stream event was not received by the iterator") + } + + // Issue cancel while the tool goroutine is blocked on gate. + // wrapStreamWithCancelMonitoring detects immediateChan and sends + // ErrStreamCanceled to the consumer side. We do NOT close gate here — + // keeping the tool goroutine blocked ensures the graph interrupt (timeout=0) + // wins the race against normal completion. Close gate in defer for cleanup. + defer close(gate) + handle, _ := cancelFn() + cancelErr := handle.Wait() + + r := <-resultCh + + if errors.Is(cancelErr, ErrExecutionEnded) { + // On slower runtimes (e.g. Go 1.19 CI), the execution can complete + // before the cancel signal is delivered — this is a valid race outcome. + t.Log("cancel raced with completion (ErrExecutionEnded) — skipping cancel assertions") + return + } + assert.NoError(t, cancelErr) + assert.True(t, r.foundStreamCanceled, "expected ErrStreamCanceled on tool's MessageStream.Recv()") + assert.True(t, r.foundCancelError, "expected CancelError in event stream") +} + +// TestWithCancel_CancelImmediate_NestedAgentTool_ResumeFromToolsNode verifies that +// when a nested ChatModelAgent (wrapped as an AgentTool inside an outer ChatModelAgent) +// is canceled via CancelImmediate and then resumed with Runner.Resume (no params), +// the outer agent resumes from the ToolsNode rather than restarting from the beginning. +// +// Regression test: previously, the outer ChatModelAgent would restart from its Init/ChatModel +// node instead of resuming from the ToolsNode, causing the outer model to be called again +// with the original user message before the AgentTool and inner ChatModelAgent were resumed. +func TestWithCancel_CancelImmediate_NestedAgentTool_ResumeFromToolsNode(t *testing.T) { + for _, tc := range []struct { + name string + enableStreaming bool + innerHasTools bool + recursive bool + }{ + {"Invoke_InnerNoTools_NonRecursive", false, false, false}, + {"Stream_InnerNoTools_NonRecursive", true, false, false}, + {"Invoke_InnerWithTools_NonRecursive", false, true, false}, + {"Stream_InnerWithTools_NonRecursive", true, true, false}, + {"Invoke_InnerNoTools_Recursive", false, false, true}, + {"Stream_InnerNoTools_Recursive", true, false, true}, + {"Invoke_InnerWithTools_Recursive", false, true, true}, + {"Stream_InnerWithTools_Recursive", true, true, true}, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + + // --- inner agent: its model blocks so we can cancel mid-execution --- + var innerTools []tool.BaseTool + var innerModelResp *schema.Message + if tc.innerHasTools { + innerModelResp = toolCallMsg(toolCall("ic1", "inner_tool", `{"input":"x"}`)) + innerTools = []tool.BaseTool{newBlockingTool("inner_tool")} + } else { + innerModelResp = &schema.Message{Role: schema.Assistant, Content: "inner agent done"} + } + innerModel := newBlockingChatModel(innerModelResp) + t.Cleanup(func() { + close(innerModel.unblockCh) + }) + + innerCfg := &ChatModelAgentConfig{ + Name: "InnerAgent", + Description: "inner agent that blocks", + Instruction: "you are an inner agent", + Model: innerModel, + } + if len(innerTools) > 0 { + innerCfg.ToolsConfig = ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: innerTools}, + } + } + innerAgent, err := NewChatModelAgent(ctx, innerCfg) + require.NoError(t, err) + + // --- outer agent: counting model --- + // Call 1: returns a tool call that invokes InnerAgent. + // Call 2 (only needed on resume): returns a plain final answer. + outerModelCallCount := int32(0) + outerModel := &countingChatModel{ + callCount: &outerModelCallCount, + responses: []*schema.Message{ + toolCallMsg(toolCall("c1", "InnerAgent", `{"request":"do something"}`)), + schema.AssistantMessage("outer completed", nil), + }, + } + + outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "outer agent with nested agent tool", + Instruction: "you are an outer agent", + Model: outerModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, innerAgent)}, + }, + }, + }) + require.NoError(t, err) + + store := newCancelTestStore() + checkpointID := "cancel-nested-resume-" + tc.name + + runner1 := NewRunner(ctx, RunnerConfig{ + Agent: outerAgent, + EnableStreaming: tc.enableStreaming, + CheckPointStore: store, + }) + + // --- phase 1: run and cancel while inner agent model is blocked --- + cancelOpt, cancelFn := WithCancel() + iter := runner1.Run(ctx, []Message{schema.UserMessage("go")}, cancelOpt, WithCheckPointID(checkpointID)) + + // Wait for inner model to start (meaning outer model already returned tool call). + select { + case <-innerModel.started: + case <-time.After(5 * time.Second): + t.Fatal("inner model did not start") + } + + // At this point outerModel should have been called exactly once. + assert.Equal(t, int32(1), atomic.LoadInt32(&outerModelCallCount), + "outer model should have been called once before cancel") + + // Cancel immediately. Recursive cases additionally propagate the cancel + // request into the AgentTool's internal ChatModelAgent. + var handle *CancelHandle + if tc.recursive { + handle, _ = cancelFn(WithRecursive()) + } else { + handle, _ = cancelFn() + } + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + _, hasCancelError := drainEvents(iter) + assert.True(t, hasCancelError, "expected CancelError from canceled nested agent tool") + + // --- phase 2: resume with Runner.Resume (no ResumeWithParams, no interrupt ID) --- + // Build fresh agents for resume. Recursive cancel should resume the + // inner ChatModelAgent inside AgentTool before the top-level + // ChatModelAgent produces the final answer. + resumeFirstModelCall := make(chan string, 5) + resumeOuterModelCallCount := int32(0) + resumeOuterModel := &countingChatModel{ + callCount: &resumeOuterModelCallCount, + callCh: resumeFirstModelCall, + callLabel: "outer", + responses: []*schema.Message{ + schema.AssistantMessage("outer completed after resume", nil), + }, + } + + resumeInnerModelCallCount := int32(0) + resumeInnerResponses := []*schema.Message{schema.AssistantMessage("inner agent done after resume", nil)} + if len(innerTools) > 0 { + resumeInnerResponses = []*schema.Message{ + toolCallMsg(toolCall("ic1", "inner_tool", `{"input":"x"}`)), + schema.AssistantMessage("inner agent done after resume", nil), + } + } + resumeInnerModel := &countingChatModel{ + callCount: &resumeInnerModelCallCount, + callCh: resumeFirstModelCall, + callLabel: "inner", + responses: resumeInnerResponses, + } + resumeInnerCfg := &ChatModelAgentConfig{ + Name: "InnerAgent", + Description: "inner agent that returns immediately on resume", + Instruction: "you are an inner agent", + Model: resumeInnerModel, + } + if len(innerTools) > 0 { + resumeInnerCfg.ToolsConfig = ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{newSlowTool("inner_tool", 0, "inner tool result")}, + }, + } + } + resumeInnerAgent, err := NewChatModelAgent(ctx, resumeInnerCfg) + require.NoError(t, err) + + resumeOuterAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "outer agent with nested agent tool", + Instruction: "you are an outer agent", + Model: resumeOuterModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, resumeInnerAgent)}, + }, + }, + }) + require.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumeOuterAgent, + EnableStreaming: tc.enableStreaming, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, checkpointID) + require.NoError(t, err) + + select { + case firstModel := <-resumeFirstModelCall: + if tc.recursive { + assert.Equal(t, "inner", firstModel, + "recursive cancel should resume the AgentTool/internal ChatModelAgent first") + } else { + assert.Contains(t, []string{"outer", "inner"}, firstModel, + "non-recursive cancel does not define whether a root or already-persisted inner checkpoint resumes first") + } + case <-time.After(5 * time.Second): + t.Fatal("no model call observed during resume") + } + + var resumeEvents []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + if event.Err != nil { + t.Fatalf("unexpected error during resume: %v", event.Err) + } + resumeEvents = append(resumeEvents, event) + } + + // The outer model should have been called exactly once during resume + // (to produce the final answer after receiving tool results). + // If it was called with the original user message (restarting from scratch), + // the counting model would either exceed its response list or the call count + // would be wrong. + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeOuterModelCallCount), + "outer model should be called exactly once during resume (for final answer after tool results), "+ + "not restarted from the beginning") + + // Verify we got the completion output. + var gotOutput bool + for _, event := range resumeEvents { + content, err := messageOutputContent(event) + require.NoError(t, err) + if content == "outer completed after resume" { + gotOutput = true + } + } + assert.True(t, gotOutput, "should get final output from resumed outer agent") + }) + } +} + +func TestWithCancel_CancelImmediate_RecursiveAgentTool_ResumeDeepestAgentTool(t *testing.T) { + ctx := context.Background() + + leafModel := newBlockingChatModel(schema.AssistantMessage("leaf done", nil)) + t.Cleanup(func() { + close(leafModel.unblockCh) + }) + leafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "LeafAgent", + Description: "leaf agent that blocks", + Instruction: "you are a leaf agent", + Model: leafModel, + }) + require.NoError(t, err) + + middleModelCallCount := int32(0) + middleModel := &countingChatModel{ + callCount: &middleModelCallCount, + responses: []*schema.Message{ + toolCallMsg(toolCall("middle-leaf", "LeafAgent", `{"request":"leaf work"}`)), + schema.AssistantMessage("middle done", nil), + }, + } + middleAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "MiddleAgent", + Description: "middle agent with an agent tool", + Instruction: "you are a middle agent", + Model: middleModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, leafAgent)}, + }, + }, + }) + require.NoError(t, err) + + outerModelCallCount := int32(0) + outerModel := &countingChatModel{ + callCount: &outerModelCallCount, + responses: []*schema.Message{ + toolCallMsg(toolCall("outer-middle", "MiddleAgent", `{"request":"middle work"}`)), + schema.AssistantMessage("outer done", nil), + }, + } + outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "outer agent with recursive agent tool nesting", + Instruction: "you are an outer agent", + Model: outerModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, middleAgent)}, + }, + }, + }) + require.NoError(t, err) + + store := newCancelTestStore() + checkpointID := "cancel-recursive-agent-tool-resume" + runner1 := NewRunner(ctx, RunnerConfig{Agent: outerAgent, CheckPointStore: store}) + + cancelOpt, cancelFn := WithCancel() + iter := runner1.Run(ctx, []Message{schema.UserMessage("go")}, cancelOpt, WithCheckPointID(checkpointID)) + + select { + case <-leafModel.started: + case <-time.After(5 * time.Second): + t.Fatal("leaf model did not start") + } + + handle, _ := cancelFn(WithRecursive()) + require.NoError(t, handle.Wait()) + _, hasCancelError := drainEvents(iter) + assert.True(t, hasCancelError, "expected CancelError from recursive nested agent tool") + + firstModelCall := make(chan string, 8) + resumeLeafModelCallCount := int32(0) + resumeLeafModel := &countingChatModel{ + callCount: &resumeLeafModelCallCount, + callCh: firstModelCall, + callLabel: "leaf", + responses: []*schema.Message{schema.AssistantMessage("leaf done after resume", nil)}, + } + resumeLeafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "LeafAgent", + Description: "leaf agent that returns on resume", + Instruction: "you are a leaf agent", + Model: resumeLeafModel, + }) + require.NoError(t, err) + + resumeMiddleModelCallCount := int32(0) + resumeMiddleModel := &countingChatModel{ + callCount: &resumeMiddleModelCallCount, + callCh: firstModelCall, + callLabel: "middle", + responses: []*schema.Message{schema.AssistantMessage("middle done after resume", nil)}, + } + resumeMiddleAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "MiddleAgent", + Description: "middle agent with an agent tool", + Instruction: "you are a middle agent", + Model: resumeMiddleModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, resumeLeafAgent)}, + }, + }, + }) + require.NoError(t, err) + + resumeOuterModelCallCount := int32(0) + resumeOuterModel := &countingChatModel{ + callCount: &resumeOuterModelCallCount, + callCh: firstModelCall, + callLabel: "outer", + responses: []*schema.Message{schema.AssistantMessage("outer done after resume", nil)}, + } + resumeOuterAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "outer agent with recursive agent tool nesting", + Instruction: "you are an outer agent", + Model: resumeOuterModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, resumeMiddleAgent)}, + }, + }, + }) + require.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{Agent: resumeOuterAgent, CheckPointStore: store}) + resumeIter, err := runner2.Resume(ctx, checkpointID) + require.NoError(t, err) + + select { + case first := <-firstModelCall: + assert.Equal(t, "leaf", first, "recursive AgentTool nesting should resume the deepest internal agent first") + case <-time.After(5 * time.Second): + t.Fatal("no model call observed during resume") + } + + resumeEvents, hasResumeCancelError := drainEvents(resumeIter) + require.False(t, hasResumeCancelError, "resume should complete without another CancelError") + assert.NotEmpty(t, resumeEvents) + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeLeafModelCallCount)) + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeMiddleModelCallCount)) + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeOuterModelCallCount)) +} + +func TestWithCancel_CancelImmediate_ConcurrentAgentTools_ResumeWithoutRestart(t *testing.T) { + ctx := context.Background() + + innerAModel := newBlockingChatModel(schema.AssistantMessage("inner A done", nil)) + innerBModel := newBlockingChatModel(schema.AssistantMessage("inner B done", nil)) + t.Cleanup(func() { + close(innerAModel.unblockCh) + close(innerBModel.unblockCh) + }) + + innerAAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "InnerAgentA", + Description: "inner agent A", + Instruction: "you are inner agent A", + Model: innerAModel, + }) + require.NoError(t, err) + innerBAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "InnerAgentB", + Description: "inner agent B", + Instruction: "you are inner agent B", + Model: innerBModel, + }) + require.NoError(t, err) + + outerModelCallCount := int32(0) + outerModel := &countingChatModel{ + callCount: &outerModelCallCount, + responses: []*schema.Message{ + toolCallMsg( + toolCall("outer-a", "InnerAgentA", `{"request":"work A"}`), + toolCall("outer-b", "InnerAgentB", `{"request":"work B"}`), + ), + schema.AssistantMessage("outer done", nil), + }, + } + outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "outer agent with concurrent agent tools", + Instruction: "you are an outer agent", + Model: outerModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{ + NewAgentTool(ctx, innerAAgent), + NewAgentTool(ctx, innerBAgent), + }, + }, + }, + }) + require.NoError(t, err) + + store := newCancelTestStore() + checkpointID := "cancel-concurrent-agent-tools-resume" + runner1 := NewRunner(ctx, RunnerConfig{Agent: outerAgent, CheckPointStore: store}) + + cancelOpt, cancelFn := WithCancel() + iter := runner1.Run(ctx, []Message{schema.UserMessage("go")}, cancelOpt, WithCheckPointID(checkpointID)) + + for _, started := range []chan struct{}{innerAModel.started, innerBModel.started} { + select { + case <-started: + case <-time.After(5 * time.Second): + t.Fatal("both concurrent inner models should start before cancel") + } + } + + handle, _ := cancelFn(WithRecursive()) + require.NoError(t, handle.Wait()) + _, hasCancelError := drainEvents(iter) + assert.True(t, hasCancelError, "expected CancelError from concurrent agent tools") + + firstModelCall := make(chan string, 8) + resumeInnerAModelCallCount := int32(0) + resumeInnerAModel := &countingChatModel{ + callCount: &resumeInnerAModelCallCount, + callCh: firstModelCall, + callLabel: "innerA", + responses: []*schema.Message{schema.AssistantMessage("inner A done after resume", nil)}, + } + resumeInnerBModelCallCount := int32(0) + resumeInnerBModel := &countingChatModel{ + callCount: &resumeInnerBModelCallCount, + callCh: firstModelCall, + callLabel: "innerB", + responses: []*schema.Message{schema.AssistantMessage("inner B done after resume", nil)}, + } + + resumeInnerAAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "InnerAgentA", + Description: "inner agent A", + Instruction: "you are inner agent A", + Model: resumeInnerAModel, + }) + require.NoError(t, err) + resumeInnerBAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "InnerAgentB", + Description: "inner agent B", + Instruction: "you are inner agent B", + Model: resumeInnerBModel, + }) + require.NoError(t, err) + + resumeOuterModelCallCount := int32(0) + resumeOuterModel := &countingChatModel{ + callCount: &resumeOuterModelCallCount, + callCh: firstModelCall, + callLabel: "outer", + responses: []*schema.Message{schema.AssistantMessage("outer done after resume", nil)}, + } + resumeOuterAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "outer agent with concurrent agent tools", + Instruction: "you are an outer agent", + Model: resumeOuterModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{ + NewAgentTool(ctx, resumeInnerAAgent), + NewAgentTool(ctx, resumeInnerBAgent), + }, + }, + }, + }) + require.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{Agent: resumeOuterAgent, CheckPointStore: store}) + resumeIter, err := runner2.Resume(ctx, checkpointID) + require.NoError(t, err) + + select { + case first := <-firstModelCall: + assert.Contains(t, []string{"innerA", "innerB"}, first, + "concurrent AgentTools should resume an internal agent before the outer model") + case <-time.After(5 * time.Second): + t.Fatal("no model call observed during resume") + } + + resumeEvents, hasResumeCancelError := drainEvents(resumeIter) + require.False(t, hasResumeCancelError, "resume should complete without another CancelError") + assert.NotEmpty(t, resumeEvents) + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeInnerAModelCallCount)) + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeInnerBModelCallCount)) + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeOuterModelCallCount)) +} + +// countingChatModel is a chat model that counts calls and records inputs. +// It returns responses from a fixed slice, indexed by call count. +type countingChatModel struct { + callCount *int32 + inputsCh chan []*schema.Message // optional: receives a copy of each input + callCh chan string // optional: receives callLabel when Generate is called + callLabel string + responses []*schema.Message +} + +func (m *countingChatModel) Generate(_ context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) { + idx := int(atomic.AddInt32(m.callCount, 1)) - 1 + if m.callCh != nil { + select { + case m.callCh <- m.callLabel: + default: + } + } + if m.inputsCh != nil { + cp := make([]*schema.Message, len(input)) + copy(cp, input) + select { + case m.inputsCh <- cp: + default: + } + } + if idx >= len(m.responses) { + return nil, fmt.Errorf("countingChatModel: call %d exceeds %d responses (outer model was called too many times - possible restart from beginning)", idx+1, len(m.responses)) + } + return m.responses[idx], nil +} + +func (m *countingChatModel) Stream(_ context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + msg, err := m.Generate(context.Background(), input, opts...) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.Message{msg}), nil +} + +func (m *countingChatModel) BindTools(_ []*schema.ToolInfo) error { return nil } + +func messageOutputContent(event *AgentEvent) (string, error) { + if event.Output == nil || event.Output.MessageOutput == nil { + return "", nil + } + mo := event.Output.MessageOutput + if mo.IsStreaming { + msg, err := schema.ConcatMessageStream(mo.MessageStream) + if err != nil { + return "", err + } + if msg == nil { + return "", nil + } + return msg.Content, nil + } + if mo.Message == nil { + return "", nil + } + return mo.Message.Content, nil +} diff --git a/adk/cancel_multicall_test.go b/adk/cancel_multicall_test.go new file mode 100644 index 0000000..790d14f --- /dev/null +++ b/adk/cancel_multicall_test.go @@ -0,0 +1,125 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/compose" +) + +func TestAgentCancelFunc_MultiCall_EscalateToImmediate(t *testing.T) { + cc := newCancelContext() + var interruptCalls int32 + cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) { + atomic.AddInt32(&interruptCalls, 1) + }) + cancelFn := cc.buildCancelFunc() + + handle1, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + handle2, _ := cancelFn(WithAgentCancelMode(CancelImmediate)) + assert.Equal(t, int32(1), atomic.LoadInt32(&interruptCalls)) + + cancelErr := cc.createCancelError() + assert.Equal(t, CancelImmediate, cancelErr.Info.Mode) + assert.True(t, cancelErr.Info.Escalated) + assert.False(t, cancelErr.Info.Timeout) + + assert.True(t, cc.markCancelHandled()) + assert.NoError(t, handle1.Wait()) + assert.NoError(t, handle2.Wait()) +} + +func TestAgentCancelFunc_MultiCall_JoinSafePointModes(t *testing.T) { + cc := newCancelContext() + cancelFn := cc.buildCancelFunc() + + handle1, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + handle2, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + + want := CancelAfterChatModel | CancelAfterToolCalls + assert.Equal(t, want, cc.getMode()) + + assert.True(t, cc.markCancelHandled()) + assert.NoError(t, handle1.Wait()) + assert.NoError(t, handle2.Wait()) +} + +func TestAgentCancelFunc_MultiCall_TimeoutDeadlineJoinUsesAbsoluteTime(t *testing.T) { + cc := newCancelContext() + cancelFn := cc.buildCancelFunc() + + handle1, _ := cancelFn( + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(200*time.Millisecond), + ) + + firstDeadline := cc.getDeadlineUnixNano() + assert.NotZero(t, firstDeadline) + + time.Sleep(50 * time.Millisecond) + + handle2, _ := cancelFn( + WithAgentCancelMode(CancelAfterToolCalls), + WithAgentCancelTimeout(60*time.Millisecond), + ) + + secondDeadline := cc.getDeadlineUnixNano() + assert.NotZero(t, secondDeadline) + assert.Less(t, secondDeadline, firstDeadline) + + assert.True(t, cc.markCancelHandled()) + assert.NoError(t, handle1.Wait()) + assert.NoError(t, handle2.Wait()) +} + +func TestAgentCancelFunc_MultiCall_TimeoutEscalationReturnsErrCancelTimeout(t *testing.T) { + cc := newCancelContext() + var interruptCalls int32 + interruptCh := make(chan struct{}, 1) + cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) { + atomic.AddInt32(&interruptCalls, 1) + select { + case interruptCh <- struct{}{}: + default: + } + }) + cancelFn := cc.buildCancelFunc() + handle, _ := cancelFn( + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(30*time.Millisecond), + ) + + select { + case <-interruptCh: + case <-time.After(1 * time.Second): + t.Fatal("timeout escalation did not interrupt") + } + assert.Equal(t, int32(1), atomic.LoadInt32(&interruptCalls)) + + cancelErr := cc.createCancelError() + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + assert.True(t, cancelErr.Info.Escalated) + assert.True(t, cancelErr.Info.Timeout) + + assert.True(t, cc.markCancelHandled()) + assert.Equal(t, ErrCancelTimeout, handle.Wait()) +} diff --git a/adk/cancel_recursive_test.go b/adk/cancel_recursive_test.go new file mode 100644 index 0000000..cd7e427 --- /dev/null +++ b/adk/cancel_recursive_test.go @@ -0,0 +1,387 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "runtime" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func assertNotClosedWithin(t *testing.T, ch <-chan struct{}, d time.Duration) { + t.Helper() + select { + case <-ch: + t.Fatal("channel was closed but should not have been") + case <-time.After(d): + } +} + +func setupParentChild(t *testing.T) (parent, child *cancelContext, cleanup func()) { + parent = newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + child = parent.deriveAgentToolCancelContext(ctx) + cleanup = func() { + child.markDone() + cancel() + } + t.Cleanup(cleanup) + return parent, child, cleanup +} + +func TestDeriveAgentToolCancelContext(t *testing.T) { + t.Run("Shallow", func(t *testing.T) { + t.Run("DoesNotPropagateSafePoint", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.triggerCancel(CancelAfterChatModel) + + assertNotClosedWithin(t, child.cancelChan, 50*time.Millisecond) + }) + + t.Run("ImmediateDoesNotPropagate", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.triggerImmediateCancel() + + assertNotClosedWithin(t, child.immediateChan, 50*time.Millisecond) + }) + + t.Run("GrandchildNoPropagation", func(t *testing.T) { + a := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + + b := a.deriveAgentToolCancelContext(ctx) + c := b.deriveAgentToolCancelContext(ctx) + t.Cleanup(func() { + c.markDone() + b.markDone() + cancel() + }) + + a.triggerCancel(CancelAfterChatModel) + + assertNotClosedWithin(t, b.cancelChan, 50*time.Millisecond) + assertNotClosedWithin(t, c.cancelChan, 50*time.Millisecond) + }) + + t.Run("NeverRecursive_GoroutineCleanup", func(t *testing.T) { + runtime.GC() + time.Sleep(50 * time.Millisecond) + before := runtime.NumGoroutine() + + parent := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + + child := parent.deriveAgentToolCancelContext(ctx) + + parent.triggerCancel(CancelAfterChatModel) + time.Sleep(100 * time.Millisecond) + + child.markDone() + cancel() + + time.Sleep(200 * time.Millisecond) + runtime.GC() + time.Sleep(50 * time.Millisecond) + after := runtime.NumGoroutine() + + assert.InDelta(t, before, after, 5, "goroutine leak detected: before=%d after=%d", before, after) + }) + }) + + t.Run("Recursive", func(t *testing.T) { + t.Run("PropagatesSafePoint", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.setRecursive(true) + parent.triggerCancel(CancelAfterChatModel) + + select { + case <-child.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("child did not receive cancel within 1s") + } + assert.True(t, child.shouldCancel()) + }) + + t.Run("ImmediatePropagates", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.setRecursive(true) + parent.triggerImmediateCancel() + + select { + case <-child.immediateChan: + case <-time.After(1 * time.Second): + t.Fatal("child did not receive immediate cancel within 1s") + } + assert.True(t, child.isImmediateCancelled()) + }) + + t.Run("GrandchildPropagation", func(t *testing.T) { + a := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + + b := a.deriveAgentToolCancelContext(ctx) + c := b.deriveAgentToolCancelContext(ctx) + t.Cleanup(func() { + c.markDone() + b.markDone() + cancel() + }) + + a.setRecursive(true) + a.triggerCancel(CancelAfterChatModel) + + select { + case <-b.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("B did not receive cancel within 1s") + } + + select { + case <-c.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("C did not receive cancel within 1s") + } + + assert.True(t, b.shouldCancel()) + assert.True(t, c.shouldCancel()) + }) + + t.Run("SetBeforeCancel", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.setRecursive(true) + + parent.triggerCancel(CancelAfterChatModel) + + select { + case <-child.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("child did not receive cancel within 1s") + } + assert.True(t, child.shouldCancel()) + }) + + t.Run("AfterRecursiveAndCancelAlreadySet", func(t *testing.T) { + parent := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + + parent.setRecursive(true) + parent.triggerCancel(CancelAfterChatModel) + + child := parent.deriveAgentToolCancelContext(ctx) + t.Cleanup(func() { + child.markDone() + cancel() + }) + + select { + case <-child.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("child did not immediately receive cancel") + } + assert.True(t, child.shouldCancel()) + }) + }) + + t.Run("Escalation", func(t *testing.T) { + t.Run("EscalateFromNonRecursive", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.triggerCancel(CancelAfterChatModel) + + assertNotClosedWithin(t, child.cancelChan, 50*time.Millisecond) + + parent.setRecursive(true) + + select { + case <-child.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("child did not receive cancel after escalation within 1s") + } + assert.True(t, child.shouldCancel()) + }) + + t.Run("EscalateImmediate", func(t *testing.T) { + parent, child, _ := setupParentChild(t) + + parent.triggerImmediateCancel() + + assertNotClosedWithin(t, child.immediateChan, 50*time.Millisecond) + + parent.setRecursive(true) + + select { + case <-child.immediateChan: + case <-time.After(1 * time.Second): + t.Fatal("child did not receive immediate cancel after escalation within 1s") + } + assert.True(t, child.isImmediateCancelled()) + }) + }) +} + +func TestDeriveAgentToolCancelContext_Race(t *testing.T) { + t.Run("SetRecursiveConcurrentWithCancelChan", func(t *testing.T) { + for i := 0; i < 100; i++ { + parent := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + + child := parent.deriveAgentToolCancelContext(ctx) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + parent.setRecursive(true) + }() + + go func() { + defer wg.Done() + parent.triggerCancel(CancelAfterChatModel) + }() + + wg.Wait() + + select { + case <-child.cancelChan: + case <-time.After(1 * time.Second): + t.Fatalf("iteration %d: child did not receive cancel within 1s", i) + } + + assert.True(t, child.shouldCancel()) + child.markDone() + cancel() + } + }) + + t.Run("ChildCompletesBeforeEscalation", func(t *testing.T) { + parent := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + child := parent.deriveAgentToolCancelContext(ctx) + + parent.triggerCancel(CancelAfterChatModel) + time.Sleep(50 * time.Millisecond) + + child.markDone() + time.Sleep(50 * time.Millisecond) + + parent.setRecursive(true) + + assertNotClosedWithin(t, child.cancelChan, 50*time.Millisecond) + }) + + t.Run("MultipleChildren_PartialCompletion", func(t *testing.T) { + parent := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + child1 := parent.deriveAgentToolCancelContext(ctx) + child2 := parent.deriveAgentToolCancelContext(ctx) + + parent.triggerCancel(CancelAfterChatModel) + time.Sleep(50 * time.Millisecond) + + child1.markDone() + time.Sleep(50 * time.Millisecond) + + parent.setRecursive(true) + + select { + case <-child2.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("running child did not receive cancel within 1s") + } + + assert.True(t, child2.shouldCancel()) + assert.False(t, child1.shouldCancel()) + child2.markDone() + }) + + t.Run("ContextCancelConcurrentWithRecursive", func(t *testing.T) { + done := make(chan struct{}) + go func() { + defer close(done) + + parent := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + + child := parent.deriveAgentToolCancelContext(ctx) + + parent.triggerCancel(CancelAfterChatModel) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + cancel() + }() + + go func() { + defer wg.Done() + parent.setRecursive(true) + }() + + wg.Wait() + child.markDone() + }() + + select { + case <-done: + case <-time.After(1 * time.Second): + t.Fatal("deadlock detected") + } + }) + + t.Run("ConcurrentSetRecursive", func(t *testing.T) { + parent := newCancelContext() + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + parent.setRecursive(true) + }() + } + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(1 * time.Second): + t.Fatal("deadlock or panic in concurrent setRecursive") + } + + assert.True(t, parent.isRecursive()) + }) +} diff --git a/adk/cancel_test.go b/adk/cancel_test.go new file mode 100644 index 0000000..ea36afe --- /dev/null +++ b/adk/cancel_test.go @@ -0,0 +1,3794 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "io" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type cancelTestChatModel struct { + delayNs int64 + response *schema.Message + startedChan chan struct{} + doneChan chan struct{} +} + +func (m *cancelTestChatModel) getDelay() time.Duration { + return time.Duration(atomic.LoadInt64(&m.delayNs)) +} + +func (m *cancelTestChatModel) setDelay(d time.Duration) { + atomic.StoreInt64(&m.delayNs, int64(d)) +} + +func (m *cancelTestChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + select { + case m.startedChan <- struct{}{}: + default: + } + select { + case <-time.After(m.getDelay()): + case <-ctx.Done(): + return nil, ctx.Err() + } + select { + case m.doneChan <- struct{}{}: + default: + } + return m.response, nil +} + +func (m *cancelTestChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + m.startedChan <- struct{}{} + time.Sleep(m.getDelay()) + m.doneChan <- struct{}{} + return schema.StreamReaderFromArray([]*schema.Message{m.response}), nil +} + +func (m *cancelTestChatModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +type slowTool struct { + name string + delay time.Duration + result string + callCount int32 + startedChan chan struct{} +} + +func newSlowTool(name string, delay time.Duration, result string) *slowTool { + return &slowTool{ + name: name, + delay: delay, + result: result, + startedChan: make(chan struct{}, 10), + } +} + +func (t *slowTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "A slow tool for testing", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string", Desc: "Input parameter"}, + }), + }, nil +} + +func (t *slowTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + atomic.AddInt32(&t.callCount, 1) + select { + case t.startedChan <- struct{}{}: + default: + } + select { + case <-time.After(t.delay): + case <-ctx.Done(): + return "", ctx.Err() + } + return t.result, nil +} + +type cancelTestStore struct { + m map[string][]byte + mu sync.Mutex +} + +func newCancelTestStore() *cancelTestStore { + return &cancelTestStore{m: make(map[string][]byte)} +} + +func (s *cancelTestStore) Set(_ context.Context, key string, value []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + s.m[key] = value + return nil +} + +func (s *cancelTestStore) Get(_ context.Context, key string) ([]byte, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + v, ok := s.m[key] + return v, ok, nil +} + +func assertHasCancelError(t *testing.T, events []*AgentEvent) { + t.Helper() + for _, e := range events { + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + return + } + } + t.Fatal("expected CancelError in events") +} + +func drainAndAssertCancelError(t *testing.T, iter *AsyncIterator[*AgentEvent]) { + t.Helper() + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + return + } + } + t.Fatal("expected CancelError in event stream") +} + +func drainEventsAndAssertCancelError(t *testing.T, iter *AsyncIterator[*AgentEvent]) []*AgentEvent { + t.Helper() + var events []*AgentEvent + hasCancelError := false + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + hasCancelError = true + } + events = append(events, event) + } + assert.True(t, hasCancelError, "expected CancelError in event stream") + return events +} + +func TestCancelContext(t *testing.T) { + t.Run("BasicCancelContext", func(t *testing.T) { + cc := newCancelContext() + assert.False(t, cc.shouldCancel(), "Should not be cancelled initially") + + cc.setMode(CancelImmediate) + close(cc.cancelChan) + + assert.True(t, cc.shouldCancel(), "Should be cancelled after close(cancelChan)") + assert.Equal(t, CancelImmediate, cc.getMode()) + }) +} + +func TestWithCancel_WithTools(t *testing.T) { + ctx := context.Background() + + t.Run("CancelImmediate_DuringModelCall", func(t *testing.T) { + modelStarted := make(chan struct{}, 1) + st := newSlowTool("slow_tool", 100*time.Millisecond, "tool result") + + slowModel := &cancelTestChatModel{ + delayNs: int64(2 * time.Second), + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt) + assert.NotNil(t, iter) + assert.NotNil(t, cancelFn) + + eventsCh := make(chan []*AgentEvent, 1) + go func() { + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + eventsCh <- events + }() + + select { + case <-modelStarted: + case <-time.After(5 * time.Second): + t.Fatal("Model did not start within 5 seconds") + } + + time.Sleep(100 * time.Millisecond) + + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err) + + var events []*AgentEvent + select { + case events = <-eventsCh: + case <-time.After(5 * time.Second): + t.Fatal("Timed out waiting for events") + } + + assert.NotEmpty(t, events) + + assertHasCancelError(t, events) + }) + + t.Run("CancelAfterChatModel_DuringToolCall", func(t *testing.T) { + toolStarted := make(chan struct{}, 1) + st := &slowToolWithSignal{ + name: "slow_tool", + delay: 2 * time.Second, + result: "tool result", + startedChan: toolStarted, + } + + modelWithToolCall := &simpleChatModel{ + delay: 1 * time.Second, + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: modelWithToolCall, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("Use the tool")}, + }, cancelOpt) + assert.NotNil(t, iter) + assert.NotNil(t, cancelFn) + + <-toolStarted + + time.Sleep(100 * time.Millisecond) + + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + err = handle.Wait() + assert.NoError(t, err) + + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + continue + } + events = append(events, event) + } + + assert.NotEmpty(t, events) + assert.True(t, atomic.LoadInt32(&st.callCount) >= 1, "Tool should have been called") + }) + + t.Run("CancelAfterToolCalls_CompletesToolExecution", func(t *testing.T) { + toolStarted := make(chan struct{}, 1) + st := &slowToolWithSignal{ + name: "slow_tool", + delay: 500 * time.Millisecond, + result: "tool result", + startedChan: toolStarted, + } + + modelWithToolCall := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: modelWithToolCall, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("Use the tool")}, + }, cancelOpt) + assert.NotNil(t, iter) + assert.NotNil(t, cancelFn) + + <-toolStarted + + time.Sleep(100 * time.Millisecond) + + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + err = handle.Wait() + assert.NoError(t, err) + + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + continue + } + events = append(events, event) + } + + assert.NotEmpty(t, events) + assert.True(t, atomic.LoadInt32(&st.callCount) >= 1, "Tool should have been called") + }) + + t.Run("NestedCancelPropagation", func(t *testing.T) { + cc := newCancelContext() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + child := cc.deriveAgentToolCancelContext(ctx) + assert.NotNil(t, child) + + cc.setRecursive(true) + cc.setMode(CancelImmediate) + + if atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateCancelling) { + close(cc.cancelChan) + } + + select { + case <-child.cancelChan: + case <-time.After(1 * time.Second): + t.Fatal("Child did not receive cancel signal") + } + + assert.True(t, child.shouldCancel()) + assert.Equal(t, CancelImmediate, child.getMode()) + }) + + t.Run("DeepAgentIntegrationCancel", func(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + + leafModel := &cancelTestChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "Leaf result", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + leafModel.setDelay(500 * time.Millisecond) + leafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "LeafAgent", + Description: "desc", + Model: leafModel, + }) + assert.NoError(t, err) + + rootModel := &cancelTestChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "LeafAgent", + Arguments: `{}`, + }, + }, + }, + }, + startedChan: make(chan struct{}, 1), + doneChan: make(chan struct{}, 1), + } + rootAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RootAgent", + Description: "desc", + Model: rootModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, leafAgent)}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: rootAgent, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Run leaf")}, cancelOpt) + + <-modelStarted + + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel), WithRecursive()) + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + var ce *CancelError + if errors.As(event.Err, &ce) { + hasCancelError = true + assert.NotNil(t, ce.interruptSignal, "CancelError should carry interrupt signal") + } + } + } + assert.True(t, hasCancelError, "Should have received CancelError") + }) +} + +type slowToolWithSignal struct { + name string + delay time.Duration + result string + callCount int32 + startedChan chan struct{} +} + +func (t *slowToolWithSignal) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "A slow tool for testing", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string", Desc: "Input parameter"}, + }), + }, nil +} + +func (t *slowToolWithSignal) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + atomic.AddInt32(&t.callCount, 1) + t.startedChan <- struct{}{} + time.Sleep(t.delay) + return t.result, nil +} + +type simpleChatModel struct { + delay time.Duration + response *schema.Message +} + +func (m *simpleChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + if m.delay > 0 { + select { + case <-time.After(m.delay): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + return m.response, nil +} + +func (m *simpleChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + if m.delay > 0 { + select { + case <-time.After(m.delay): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + return schema.StreamReaderFromArray([]*schema.Message{m.response}), nil +} + +func (m *simpleChatModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +func TestWithCancel_WithCheckpoint(t *testing.T) { + ctx := context.Background() + + t.Run("CancelWithCheckpoint", func(t *testing.T) { + modelStarted := make(chan struct{}, 1) + st := newSlowTool("slow_tool", 100*time.Millisecond, "tool result") + + slowModel := &cancelTestChatModel{ + delayNs: int64(1 * time.Second), + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt, WithCheckPointID("cancel-1")) + + <-modelStarted + + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + hasCancelError = true + continue + } + } + + assert.True(t, hasCancelError, "Should have CancelError event after cancel") + }) +} + +func TestAgentCancelFuncMultipleCalls(t *testing.T) { + ctx := context.Background() + + t.Run("SecondCancelReturnsErrAgentFinished", func(t *testing.T) { + modelStarted := make(chan struct{}, 1) + st := newSlowTool("slow_tool", 100*time.Millisecond, "tool result") + + slowModel := &cancelTestChatModel{ + delayNs: int64(1 * time.Second), + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt) + + <-modelStarted + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + }) +} + +func TestWithCancel_Streaming(t *testing.T) { + ctx := context.Background() + + t.Run("CancelImmediate_DuringModelStream", func(t *testing.T) { + modelStarted := make(chan struct{}, 1) + st := newSlowTool("slow_tool", 100*time.Millisecond, "tool result") + + slowModel := &cancelTestChatModel{ + delayNs: int64(2 * time.Second), + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt) + assert.NotNil(t, iter) + assert.NotNil(t, cancelFn) + + eventsCh := make(chan []*AgentEvent, 1) + go func() { + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + eventsCh <- events + }() + + select { + case <-modelStarted: + case <-time.After(5 * time.Second): + t.Fatal("Model did not start within 5 seconds") + } + + time.Sleep(100 * time.Millisecond) + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + var events []*AgentEvent + select { + case events = <-eventsCh: + case <-time.After(5 * time.Second): + t.Fatal("Timed out waiting for events") + } + + assert.NotEmpty(t, events) + + assertHasCancelError(t, events) + }) + + t.Run("CancelAfterToolCalls_Streaming", func(t *testing.T) { + toolStarted := make(chan struct{}, 1) + st := &slowToolWithSignal{ + name: "slow_tool", + delay: 500 * time.Millisecond, + result: "tool result", + startedChan: toolStarted, + } + + modelWithToolCall := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: modelWithToolCall, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt) + assert.NotNil(t, iter) + assert.NotNil(t, cancelFn) + + <-toolStarted + + time.Sleep(100 * time.Millisecond) + + handle, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + continue + } + events = append(events, event) + } + + assert.NotEmpty(t, events) + assert.True(t, atomic.LoadInt32(&st.callCount) >= 1, "Tool should have been called") + }) +} + +// TestWithCancel_Resume tests the workflow of Cancel followed by Resume. +// +// To avoid data races, we create new agent and runner instances for the Resume phase +// instead of reusing and modifying the original model instance. +func TestWithCancel_Resume(t *testing.T) { + ctx := context.Background() + + t.Run("Cancel_ThenResume", func(t *testing.T) { + modelStarted := make(chan struct{}, 1) + modelCallCount := int32(0) + st := newSlowTool("slow_tool", 100*time.Millisecond, "tool result") + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + checkpointID := "resume-cancel-test-1" + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt, WithCheckPointID(checkpointID)) + + <-modelStarted + atomic.AddInt32(&modelCallCount, 1) + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + hasCancelErr := false + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + var ce *CancelError + if errors.As(event.Err, &ce) { + hasCancelErr = true + continue + } + t.Fatalf("unexpected error: %v", event.Err) + } + } + assert.True(t, hasCancelErr, "Should have CancelError event after cancel") + + newModelStarted := make(chan struct{}, 1) + slowModel2 := &cancelTestChatModel{ + delayNs: int64(100 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Final response after resume", + }, + startedChan: newModelStarted, + doneChan: make(chan struct{}, 1), + } + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel2, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: agent2, + EnableStreaming: false, + CheckPointStore: store, + }) + + resumeCancelOpt, _ := WithCancel() + resumeIter, err := runner2.Resume(ctx, checkpointID, resumeCancelOpt) + assert.NoError(t, err) + assert.NotNil(t, resumeIter) + + var resumeEvents []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + assert.Nil(t, event.Err, "Should not have error event during resume") + resumeEvents = append(resumeEvents, event) + } + + assert.NotEmpty(t, resumeEvents, "Resume should produce events") + }) + + t.Run("Resume_ThenCancel", func(t *testing.T) { + firstModelStarted := make(chan struct{}, 1) + modelCallCount := int32(0) + st := newSlowTool("slow_tool", 100*time.Millisecond, "tool result") + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + startedChan: firstModelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + checkpointID := "resume-then-cancel-test-1" + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt, WithCheckPointID(checkpointID)) + + <-firstModelStarted + atomic.AddInt32(&modelCallCount, 1) + + handle, _ := cancelFn() + cancelErr := handle.Wait() + assert.NoError(t, cancelErr) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + slowModel2 := newBlockingChatModel(toolCallMsg(toolCall("call_1", "slow_tool", `{"input": "test"}`))) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tool", + Instruction: "You are a test assistant", + Model: slowModel2, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: agent2, + EnableStreaming: false, + CheckPointStore: store, + }) + + resumeCancelOpt, resumeCancelFn := WithCancel() + resumeIter, err := runner2.Resume(ctx, checkpointID, resumeCancelOpt) + assert.NoError(t, err) + + resumeEventsCh := make(chan []*AgentEvent, 1) + go func() { + var events []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + events = append(events, event) + } + resumeEventsCh <- events + }() + + <-slowModel2.started + atomic.AddInt32(&modelCallCount, 1) + + cancelHandle, _ := resumeCancelFn() + close(slowModel2.unblockCh) + err = cancelHandle.Wait() + assert.True(t, err == nil || errors.Is(err, ErrExecutionEnded), "unexpected cancel wait error: %v", err) + + start := time.Now() + resumeEvents := <-resumeEventsCh + elapsed := time.Since(start) + + assert.True(t, elapsed < 1*time.Second, "Resume should return quickly after cancel, elapsed: %v", elapsed) + assert.NotEmpty(t, resumeEvents) + + hasCancelError := false + for _, e := range resumeEvents { + var ce *CancelError + if e.Err != nil && errors.As(e.Err, &ce) { + hasCancelError = true + } + } + executionCompletedBeforeCancel := errors.Is(err, ErrExecutionEnded) + assert.True(t, hasCancelError || executionCompletedBeforeCancel, "Resume should have CancelError event after cancel, or execution completed before cancel") + }) +} + +func TestCancelMonitoredToolHandler_StreamableToolCall(t *testing.T) { + t.Run("NoCancelContext_PassesThrough", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + + // Create a stream with some data + r, w := schema.Pipe[string](1) + go func() { + w.Send("chunk1", nil) + w.Send("chunk2", nil) + w.Close() + }() + + next := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return &compose.StreamToolOutput{Result: r}, nil + } + + wrapped := handler.WrapStreamableToolCall(next) + // No cancelContext in the Go context + output, err := wrapped(context.Background(), &compose.ToolInput{Name: "test"}) + assert.NoError(t, err) + + // Should get the original stream unchanged + chunk1, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, "chunk1", chunk1) + + chunk2, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, "chunk2", chunk2) + + _, err = output.Result.Recv() + assert.ErrorIs(t, err, io.EOF) + }) + + t.Run("WithCancelContext_NoCancel_StreamsNormally", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + cc := newCancelContext() + + r, w := schema.Pipe[string](1) + go func() { + w.Send("data1", nil) + w.Send("data2", nil) + w.Close() + }() + + next := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return &compose.StreamToolOutput{Result: r}, nil + } + + wrapped := handler.WrapStreamableToolCall(next) + ctx := withCancelContext(context.Background(), cc) + output, err := wrapped(ctx, &compose.ToolInput{Name: "test"}) + assert.NoError(t, err) + + chunk1, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, "data1", chunk1) + + chunk2, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, "data2", chunk2) + + _, err = output.Result.Recv() + assert.ErrorIs(t, err, io.EOF) + }) + + t.Run("WithCancelContext_ImmediateCancel_TerminatesStream", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + cc := newCancelContext() + + // Create a slow stream that we'll cancel mid-way + r, w := schema.Pipe[string](1) + go func() { + defer w.Close() + w.Send("chunk1", nil) + time.Sleep(200 * time.Millisecond) + w.Send("chunk2", nil) + }() + + next := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return &compose.StreamToolOutput{Result: r}, nil + } + + wrapped := handler.WrapStreamableToolCall(next) + ctx := withCancelContext(context.Background(), cc) + output, err := wrapped(ctx, &compose.ToolInput{Name: "test"}) + assert.NoError(t, err) + + // Read first chunk + chunk1, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, "chunk1", chunk1) + + // Fire immediate cancel + close(cc.immediateChan) + + // Next recv should get ErrStreamCanceled + _, err = output.Result.Recv() + assert.ErrorIs(t, err, ErrStreamCanceled) + }) + + t.Run("WithCancelContext_AlreadyCancelled_TerminatesImmediately", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + cc := newCancelContext() + close(cc.immediateChan) // Already canceled + + r, w := schema.Pipe[string](1) + go func() { + w.Send("should-not-see", nil) + w.Close() + }() + + next := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return &compose.StreamToolOutput{Result: r}, nil + } + + wrapped := handler.WrapStreamableToolCall(next) + ctx := withCancelContext(context.Background(), cc) + output, err := wrapped(ctx, &compose.ToolInput{Name: "test"}) + assert.NoError(t, err) + + _, err = output.Result.Recv() + assert.ErrorIs(t, err, ErrStreamCanceled) + }) + + t.Run("NextReturnsError_PropagatesError", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + cc := newCancelContext() + + nextErr := errors.New("tool execution failed") + next := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return nil, nextErr + } + + wrapped := handler.WrapStreamableToolCall(next) + ctx := withCancelContext(context.Background(), cc) + _, err := wrapped(ctx, &compose.ToolInput{Name: "test"}) + assert.ErrorIs(t, err, nextErr) + }) +} + +func TestCancelMonitoredToolHandler_EnhancedStreamableToolCall(t *testing.T) { + t.Run("NoCancelContext_PassesThrough", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + + tr1 := &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "chunk1"}}} + r, w := schema.Pipe[*schema.ToolResult](1) + go func() { + w.Send(tr1, nil) + w.Close() + }() + + next := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{Result: r}, nil + } + + wrapped := handler.WrapEnhancedStreamableToolCall(next) + output, err := wrapped(context.Background(), &compose.ToolInput{Name: "test"}) + assert.NoError(t, err) + + result, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, tr1, result) + + _, err = output.Result.Recv() + assert.ErrorIs(t, err, io.EOF) + }) + + t.Run("WithCancelContext_ImmediateCancel_TerminatesStream", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + cc := newCancelContext() + + tr1 := &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "chunk1"}}} + tr2 := &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "chunk2"}}} + r, w := schema.Pipe[*schema.ToolResult](1) + go func() { + defer w.Close() + w.Send(tr1, nil) + time.Sleep(200 * time.Millisecond) + w.Send(tr2, nil) + }() + + next := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{Result: r}, nil + } + + wrapped := handler.WrapEnhancedStreamableToolCall(next) + ctx := withCancelContext(context.Background(), cc) + output, err := wrapped(ctx, &compose.ToolInput{Name: "test"}) + assert.NoError(t, err) + + result, err := output.Result.Recv() + assert.NoError(t, err) + assert.Equal(t, tr1, result) + + close(cc.immediateChan) + + _, err = output.Result.Recv() + assert.ErrorIs(t, err, ErrStreamCanceled) + }) + + t.Run("NextReturnsError_PropagatesError", func(t *testing.T) { + handler := &cancelMonitoredToolHandler{} + cc := newCancelContext() + + nextErr := errors.New("enhanced tool failed") + next := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return nil, nextErr + } + + wrapped := handler.WrapEnhancedStreamableToolCall(next) + ctx := withCancelContext(context.Background(), cc) + _, err := wrapped(ctx, &compose.ToolInput{Name: "test"}) + assert.ErrorIs(t, err, nextErr) + }) +} + +func TestCancelContextKey(t *testing.T) { + t.Run("WithAndGet_RoundTrips", func(t *testing.T) { + cc := newCancelContext() + ctx := withCancelContext(context.Background(), cc) + got := getCancelContext(ctx) + assert.Equal(t, cc, got) + }) + + t.Run("Get_NoValue_ReturnsNil", func(t *testing.T) { + got := getCancelContext(context.Background()) + assert.Nil(t, got) + }) + + t.Run("With_NilCancelContext_ReturnsOriginalCtx", func(t *testing.T) { + ctx := context.Background() + result := withCancelContext(ctx, nil) + assert.Equal(t, ctx, result) + }) +} + +// -- Tests for cancel support across all agent types -- + +// cancelTestAgent is a ChatModelAgent-based agent where the model blocks until +// signalled, allowing tests to control exactly when to issue a cancel. +func newCancelTestAgent(t *testing.T, name string, modelDelay time.Duration, modelStarted chan struct{}) *ChatModelAgent { + t.Helper() + slowModel := &cancelTestChatModel{ + delayNs: int64(modelDelay), + response: &schema.Message{ + Role: schema.Assistant, + Content: "response from " + name, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(context.Background(), &ChatModelAgentConfig{ + Name: name, + Description: "Test agent " + name, + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + return agent +} + +func newCancelTestAgentWithTools(t *testing.T, name string, modelDelay time.Duration, modelStarted chan struct{}) *ChatModelAgent { + t.Helper() + toolName := name + "_tool" + slowModel := &cancelTestChatModel{ + delayNs: int64(modelDelay), + response: &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{{ + ID: "call_1", Type: "function", + Function: schema.FunctionCall{ + Name: toolName, + Arguments: `{"input": "test"}`, + }, + }}, + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + st := newSlowTool(toolName, 10*time.Millisecond, "tool result") + + agent, err := NewChatModelAgent(context.Background(), &ChatModelAgentConfig{ + Name: name, + Description: "Test agent " + name, + Instruction: "You are a test assistant", + Model: slowModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + return agent +} + +func newCancelTestAgentWithToolsFinalAnswer(t *testing.T, name string) *ChatModelAgent { + t.Helper() + toolName := name + "_tool" + finalModel := &cancelTestChatModel{ + delayNs: int64(10 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "final response from " + name, + }, + startedChan: make(chan struct{}, 1), + doneChan: make(chan struct{}, 1), + } + + st := newSlowTool(toolName, 10*time.Millisecond, "tool result") + + agent, err := NewChatModelAgent(context.Background(), &ChatModelAgentConfig{ + Name: name, + Description: "Test agent " + name, + Instruction: "You are a test assistant", + Model: finalModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + return agent +} + +func TestWithCancel_SequentialAgent(t *testing.T) { + ctx := context.Background() + + t.Run("CancelImmediate_DuringSecondAgent", func(t *testing.T) { + // The first agent completes quickly. The second agent takes a long time. + // Cancel during the second agent's model call. + agent1Started := make(chan struct{}, 1) + agent2Started := make(chan struct{}, 1) + + agent1 := newCancelTestAgent(t, "fast_agent", 50*time.Millisecond, agent1Started) + agent2 := newCancelTestAgent(t, "slow_agent", 5*time.Second, agent2Started) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq_agent", + Description: "Sequential test", + SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + // Wait for second agent to start + select { + case <-agent2Started: + case <-time.After(10 * time.Second): + t.Fatal("Second agent did not start") + } + + time.Sleep(50 * time.Millisecond) + + // Cancel should NOT return ErrExecutionEnded (the bug before the fix) + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err, "Cancel during second agent should succeed, not return ErrExecutionEnded") + + drainEventsAndAssertCancelError(t, iter) + }) +} + +func TestWithCancel_LoopAgent(t *testing.T) { + ctx := context.Background() + + t.Run("CancelImmediate_DuringIteration", func(t *testing.T) { + // Agent in a loop. Cancel during second iteration's model call. + modelStarted := make(chan struct{}, 10) + + slowModel := &cancelTestChatModel{ + delayNs: int64(3 * time.Second), + response: &schema.Message{ + Role: schema.Assistant, + Content: "loop response", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 10), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "loop_inner", + Description: "Inner loop agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop_agent", + Description: "Loop test", + SubAgents: []Agent{agent}, + MaxIterations: 10, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: loopAgent, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + // Wait for first iteration's model call to start + select { + case <-modelStarted: + case <-time.After(10 * time.Second): + t.Fatal("Model did not start") + } + + time.Sleep(50 * time.Millisecond) + + // Cancel should succeed + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err, "Cancel during loop iteration should succeed") + + drainAndAssertCancelError(t, iter) + }) +} + +func TestWithCancel_ParallelAgent(t *testing.T) { + ctx := context.Background() + + t.Run("CancelImmediate_InterruptsAllBranches", func(t *testing.T) { + agent1Started := make(chan struct{}, 1) + agent2Started := make(chan struct{}, 1) + + // Both agents have long delays, so cancel should interrupt both. + agent1 := newCancelTestAgent(t, "par_agent1", 5*time.Second, agent1Started) + agent2 := newCancelTestAgent(t, "par_agent2", 5*time.Second, agent2Started) + + parAgent, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "par_agent", + Description: "Parallel test", + SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: parAgent, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + // Wait for both agents to start + for i := 0; i < 2; i++ { + select { + case <-agent1Started: + case <-agent2Started: + case <-time.After(10 * time.Second): + t.Fatal("Parallel agents did not start") + } + } + + time.Sleep(50 * time.Millisecond) + + start := time.Now() + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err, "Cancel during parallel agents should succeed") + + events := drainEventsAndAssertCancelError(t, iter) + elapsed := time.Since(start) + + _ = events + assert.True(t, elapsed < 3*time.Second, "Should complete quickly after cancel, elapsed: %v", elapsed) + }) +} + +func TestWithCancel_SupervisorAgent(t *testing.T) { + ctx := context.Background() + + t.Run("CancelImmediate_DuringSubAgent", func(t *testing.T) { + // Supervisor delegates to a slow sub-agent via transfer. + // Cancel during the sub-agent's model call. + supervisorModelStarted := make(chan struct{}, 1) + subAgentModelStarted := make(chan struct{}, 1) + + // The supervisor model returns a transfer_to_agent tool call + supervisorModel := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "slow_sub"}`, + }, + }, + }, + }, + } + + supervisorAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "supervisor", + Description: "Supervisor agent", + Instruction: "You are a supervisor", + Model: supervisorModel, + }) + assert.NoError(t, err) + + subAgent := newCancelTestAgent(t, "slow_sub", 5*time.Second, subAgentModelStarted) + + agentWithSubAgents, err := SetSubAgents(ctx, supervisorAgent, []Agent{subAgent}) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agentWithSubAgents, + EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + // Ignore the supervisor model start, wait for the sub-agent model + // The supervisor model is fast (simpleChatModel), so it will start and finish quickly + _ = supervisorModelStarted + select { + case <-subAgentModelStarted: + case <-time.After(10 * time.Second): + t.Fatal("Sub-agent model did not start") + } + + time.Sleep(50 * time.Millisecond) + + start := time.Now() + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err, "Cancel during sub-agent should succeed") + + drainAndAssertCancelError(t, iter) + elapsed := time.Since(start) + + assert.True(t, elapsed < 3*time.Second, "Should complete quickly after cancel, elapsed: %v", elapsed) + }) +} + +func TestFilterCancelOption(t *testing.T) { + t.Run("RemovesCancelOption", func(t *testing.T) { + cancelOpt, _ := WithCancel() + sessionOpt := WithSessionValues(map[string]any{"key": "value"}) + opts := []AgentRunOption{cancelOpt, sessionOpt} + + filtered := filterCancelOption(opts) + assert.Len(t, filtered, 1, "Should have removed the cancel option") + + // Verify the remaining option is the session option + testOpt := &options{} + filtered[0].implSpecificOptFn.(func(*options))(testOpt) + assert.NotNil(t, testOpt.sessionValues) + assert.Nil(t, testOpt.cancelCtx) + }) + + t.Run("KeepsNonCancelOptions", func(t *testing.T) { + sessionOpt := WithSessionValues(map[string]any{"key": "value"}) + callbackOpt := WithCallbacks() + opts := []AgentRunOption{sessionOpt, callbackOpt} + + filtered := filterCancelOption(opts) + assert.Len(t, filtered, 2, "Should keep all non-cancel options") + }) + + t.Run("EmptyInput", func(t *testing.T) { + filtered := filterCancelOption(nil) + assert.Nil(t, filtered) + }) +} + +func wrapIterWithMarkDone(iter *AsyncIterator[*AgentEvent], cc *cancelContext) *AsyncIterator[*AgentEvent] { + if cc == nil { + return iter + } + outIter, outGen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer cc.markDone() + defer outGen.Close() + for { + event, ok := iter.Next() + if !ok { + return + } + outGen.Send(event) + } + }() + return outIter +} + +func TestWrapIterWithMarkDone(t *testing.T) { + t.Run("MarksDoneAfterDrain", func(t *testing.T) { + cc := newCancelContext() + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + gen.Send(&AgentEvent{AgentName: "test"}) + gen.Close() + }() + + wrapped := wrapIterWithMarkDone(iter, cc) + + event, ok := wrapped.Next() + assert.True(t, ok) + assert.Equal(t, "test", event.AgentName) + + _, ok = wrapped.Next() + assert.False(t, ok) + + // markDone should have been called, so doneChan should be closed + select { + case <-cc.doneChan: + // good + case <-time.After(time.Second): + t.Fatal("doneChan was not closed after drain") + } + }) + + t.Run("NilCancelContext_PassesThrough", func(t *testing.T) { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + gen.Send(&AgentEvent{AgentName: "test"}) + gen.Close() + }() + + wrapped := wrapIterWithMarkDone(iter, nil) + assert.Equal(t, iter, wrapped, "Should return same iter when cc is nil") + }) +} + +func TestGraphInterruptFuncs_Parallel(t *testing.T) { + t.Run("MultipleGraphInterruptFuncsAllCalled", func(t *testing.T) { + cc := newCancelContext() + + var called1, called2 int32 + cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) { + atomic.AddInt32(&called1, 1) + }) + cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) { + atomic.AddInt32(&called2, 1) + }) + + // Simulate immediate cancel + cc.setMode(CancelImmediate) + atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateCancelling) + close(cc.cancelChan) + cc.sendImmediateInterrupt() + + assert.Equal(t, int32(1), atomic.LoadInt32(&called1), "First graph interrupt func should be called") + assert.Equal(t, int32(1), atomic.LoadInt32(&called2), "Second graph interrupt func should be called") + }) + + t.Run("RetroactiveFire_OnSetAfterCancel", func(t *testing.T) { + cc := newCancelContext() + + // First set up cancel state with immediate interrupt + cc.setMode(CancelImmediate) + atomic.CompareAndSwapInt32(&cc.state, stateRunning, stateCancelling) + close(cc.cancelChan) + close(cc.immediateChan) + atomic.StoreInt32(&cc.interruptSent, interruptImmediate) + + // Now register a new function - it should be retroactively fired + var called int32 + cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) { + atomic.AddInt32(&called, 1) + }) + + assert.Equal(t, int32(1), atomic.LoadInt32(&called), "setGraphInterruptFunc should retroactively fire new func") + }) +} + +// -- Tests for transition-point cancel (cancel between sub-agents) -- + +// gatedChatModel is a model that: +// - Signals doneChan when Generate completes +// - Optionally blocks on gateChan before returning (nil gateChan = no blocking) +// - Tracks call count via callCount +type gatedChatModel struct { + response *schema.Message + gateChan chan struct{} // if non-nil, blocks until closed before returning + doneChan chan struct{} // signalled after Generate completes + callCount int32 +} + +func (m *gatedChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m.callCount, 1) + if m.gateChan != nil { + select { + case <-m.gateChan: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + select { + case m.doneChan <- struct{}{}: + default: + } + return m.response, nil +} + +func (m *gatedChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + msg, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.Message{msg}), nil +} + +func (m *gatedChatModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +func TestCheckCancel_Sequential_BetweenSubAgents(t *testing.T) { + ctx := context.Background() + + // CancelAfterToolCalls fires at transition boundaries between sub-agents. + // At a transition boundary, the completed sub-agent's entire execution + // (including any tool calls) is done, satisfying the CancelAfterToolCalls + // contract — even if this particular sub-agent had no tools. + model1 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent1 done"}, + gateChan: make(chan struct{}), + doneChan: make(chan struct{}, 1), + } + model2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent2 done"}, + doneChan: make(chan struct{}, 1), + } + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", Model: model1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", Model: model2, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "sequential test", SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt) + + for atomic.LoadInt32(&model1.callCount) == 0 { + runtime.Gosched() + } + + cancelCalled, result := cancelAsync(cancelFn, WithAgentCancelMode(CancelAfterToolCalls)) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(model1.gateChan) + + assert.NoError(t, result.waitDone(t), "CancelAfterToolCalls should succeed at transition boundary") + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, int32(1), atomic.LoadInt32(&model1.callCount), "Agent1 model should be invoked") + assert.Equal(t, int32(0), atomic.LoadInt32(&model2.callCount), + "Agent2 model should NOT be invoked (CancelAfterToolCalls caught at transition)") +} + +func TestCheckCancel_Loop_BetweenIterations(t *testing.T) { + ctx := context.Background() + + // CancelAfterToolCalls fires at loop iteration boundaries. + // After the first iteration completes, any tool calls it made are done, + // satisfying the CancelAfterToolCalls contract. + mdl := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "loop iter"}, + gateChan: make(chan struct{}), + doneChan: make(chan struct{}, 10), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "loop_inner", Description: "inner", Instruction: "test", Model: mdl, + }) + assert.NoError(t, err) + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop", Description: "loop test", SubAgents: []Agent{agent}, MaxIterations: 3, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: loopAgent, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt) + + for atomic.LoadInt32(&mdl.callCount) == 0 { + runtime.Gosched() + } + + cancelCalled, result := cancelAsync(cancelFn, WithAgentCancelMode(CancelAfterToolCalls)) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(mdl.gateChan) + + assert.NoError(t, result.waitDone(t), "CancelAfterToolCalls should succeed at loop transition boundary") + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, int32(1), atomic.LoadInt32(&mdl.callCount), + "Model should be called once; second iteration caught at transition") +} + +func TestCheckCancel_Parallel_PreSpawn(t *testing.T) { + ctx := context.Background() + + // Cancel fires before Run is called. Neither model should be invoked. + model1 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "par1"}, + doneChan: make(chan struct{}, 1), + } + model2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "par2"}, + doneChan: make(chan struct{}, 1), + } + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "par1", Description: "first", Instruction: "test", Model: model1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "par2", Description: "second", Instruction: "test", Model: model2, + }) + assert.NoError(t, err) + + parAgent, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "par", Description: "parallel test", SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + // Fire cancel in goroutine (cancelFn blocks until handled) + cancelOpt, cancelFn := WithCancel() + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn() + cancelDone <- handle.Wait() + }() + // Wait for cancelChan to be closed (happens synchronously before the blocking doneChan wait) + time.Sleep(20 * time.Millisecond) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: parAgent, EnableStreaming: false, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt) + + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + cancelErr = ce + } + } + + // cancelFn should have completed + select { + case err = <-cancelDone: + assert.NoError(t, err) + case <-time.After(5 * time.Second): + t.Fatal("cancelFn did not return") + } + + assert.NotNil(t, cancelErr, "Should have CancelError") + assert.Equal(t, int32(0), atomic.LoadInt32(&model1.callCount), "First model should never be invoked") + assert.Equal(t, int32(0), atomic.LoadInt32(&model2.callCount), "Second model should never be invoked") +} + +func TestCheckCancel_Transfer_BeforeTarget(t *testing.T) { + ctx := context.Background() + + // Supervisor CMA returns a transfer action (instantly). + // Cancel fires after transfer action but before target runs. + // Target model should never be invoked. + supervisorModel := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{{ + ID: "call_1", Type: "function", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "target"}`, + }, + }}, + }, + } + targetModel := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "target done"}, + doneChan: make(chan struct{}, 1), + } + + supervisorAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "supervisor", Description: "supervisor", Instruction: "test", Model: supervisorModel, + }) + assert.NoError(t, err) + + targetAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "target", Description: "target", Instruction: "test", Model: targetModel, + }) + assert.NoError(t, err) + + agentWithSub, err := SetSubAgents(ctx, supervisorAgent, []Agent{targetAgent}) + assert.NoError(t, err) + + // Fire cancel in goroutine (cancelFn blocks until handled) + cancelOpt, cancelFn := WithCancel() + cancelDone := make(chan error, 1) + go func() { + handle, _ := cancelFn() + cancelDone <- handle.Wait() + }() + time.Sleep(20 * time.Millisecond) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agentWithSub, EnableStreaming: false, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + cancelErr = ce + } + } + + select { + case err = <-cancelDone: + assert.NoError(t, err) + case <-time.After(5 * time.Second): + t.Fatal("cancelFn did not return") + } + + assert.NotNil(t, cancelErr, "Should have CancelError") + assert.Equal(t, int32(0), atomic.LoadInt32(&targetModel.callCount), "Target model should never be invoked") +} + +func TestCheckCancel_AlreadyHandled_NoDuplicate(t *testing.T) { + ctx := context.Background() + + // In a sequential agent, if the first CMA handles the cancel (graph interrupt), + // the workflow's transition check should NOT emit a duplicate CancelError. + // Use a slow model so cancel fires during its execution (handled by CMA). + modelStarted := make(chan struct{}, 1) + model1 := &cancelTestChatModel{ + delayNs: int64(2 * time.Second), + response: &schema.Message{Role: schema.Assistant, Content: "agent1"}, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + model2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent2"}, + doneChan: make(chan struct{}, 1), + } + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", Model: model1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", Model: model2, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "sequential", SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + // Wait for model to start, then cancel during model execution + select { + case <-modelStarted: + case <-time.After(5 * time.Second): + t.Fatal("Model did not start") + } + time.Sleep(50 * time.Millisecond) + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err) + + cancelCount := 0 + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + cancelCount++ + } + } + + assert.Equal(t, 1, cancelCount, "Should have exactly one CancelError, no duplicate from workflow transition") + assert.Equal(t, int32(0), atomic.LoadInt32(&model2.callCount), "Second agent should not run") +} + +// Tests for CancelAfterChatModel/CancelAfterToolCalls in nested workflow structures. +// These verify that safe-point cancel modes propagate through the entire agent hierarchy +// and fire at whichever nested level reaches the safe-point first. + +func TestCancel_SequentialWorkflow_CancelAfterChatModel(t *testing.T) { + ctx := context.Background() + agent1Started := make(chan struct{}, 1) + + agent1 := newCancelTestAgentWithTools(t, "seq_slow", 500*time.Millisecond, agent1Started) + agent2 := newCancelTestAgentWithTools(t, "seq_fast", 50*time.Millisecond, make(chan struct{}, 1)) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq_agent", + Description: "Sequential workflow", + SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("seq-cancel-1")) + + select { + case <-agent1Started: + case <-time.After(10 * time.Second): + t.Fatal("First agent did not start") + } + + handle, contributed := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + assert.True(t, contributed, "Cancel should contribute") + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil && errors.As(event.Err, &cancelErr) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "Should have CancelError") + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + assert.NotNil(t, cancelErr.interruptSignal, "CancelError should have interrupt signal for checkpoint") + + resumeAgent1 := newCancelTestAgentWithToolsFinalAnswer(t, "seq_slow") + resumeAgent2 := newCancelTestAgentWithToolsFinalAnswer(t, "seq_fast") + + resumeSeq, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq_agent", + Description: "Sequential workflow", + SubAgents: []Agent{resumeAgent1, resumeAgent2}, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumeSeq, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, "seq-cancel-1") + assert.NoError(t, err) + assert.NotNil(t, resumeIter) + + var resumeEvents []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + assert.Nil(t, event.Err, "Should not have error during resume") + resumeEvents = append(resumeEvents, event) + } + assert.NotEmpty(t, resumeEvents, "Resume should produce events") +} + +func TestCancelImmediate_OrphanedToolGoroutine_NoPanic(t *testing.T) { + t.Run("unit_send_after_close", func(t *testing.T) { + _, gen := NewAsyncIteratorPair[*AgentEvent]() + + cc := newCancelContext() + cc.setMode(CancelImmediate) + close(cc.cancelChan) + close(cc.immediateChan) + + gen.Close() + + execCtx := &chatModelAgentExecCtx{ + generator: gen, + cancelCtx: cc, + } + + assert.NotPanics(t, func() { + execCtx.send(&AgentEvent{AgentName: "test"}) + }, "send after generator.Close must not panic") + }) + + t.Run("unit_send_after_close_without_cancel_ctx", func(t *testing.T) { + _, gen := NewAsyncIteratorPair[*AgentEvent]() + gen.Close() + + execCtx := &chatModelAgentExecCtx{ + generator: gen, + } + + assert.NotPanics(t, func() { + execCtx.send(&AgentEvent{AgentName: "test"}) + }, "send after generator.Close must not panic even without cancelCtx (trySend safety net)") + }) + + t.Run("unit_send_nil_execCtx", func(t *testing.T) { + var execCtx *chatModelAgentExecCtx + assert.NotPanics(t, func() { + execCtx.send(&AgentEvent{AgentName: "test"}) + }, "send on nil execCtx must not panic") + }) + + t.Run("unit_send_nil_generator", func(t *testing.T) { + execCtx := &chatModelAgentExecCtx{} + assert.NotPanics(t, func() { + execCtx.send(&AgentEvent{AgentName: "test"}) + }, "send with nil generator must not panic") + }) + + t.Run("unit_isImmediateCancelled_nil_cancelContext", func(t *testing.T) { + var cc *cancelContext + assert.False(t, cc.isImmediateCancelled(), "nil cancelContext should return false") + }) + + t.Run("unit_trySend_race_window", func(t *testing.T) { + _, gen := NewAsyncIteratorPair[*AgentEvent]() + cc := newCancelContext() + + gen.Close() + + execCtx := &chatModelAgentExecCtx{ + generator: gen, + cancelCtx: cc, + } + + assert.NotPanics(t, func() { + execCtx.send(&AgentEvent{AgentName: "test"}) + }, "trySend must handle the case where isImmediateCancelled is false but generator is closed") + }) + + t.Run("unit_SendEvent_after_close", func(t *testing.T) { + _, gen := NewAsyncIteratorPair[*AgentEvent]() + + cc := newCancelContext() + cc.setMode(CancelImmediate) + close(cc.cancelChan) + close(cc.immediateChan) + + gen.Close() + + execCtx := &chatModelAgentExecCtx{ + generator: gen, + cancelCtx: cc, + } + + ctx := withTypedChatModelAgentExecCtx(context.Background(), execCtx) + + assert.NotPanics(t, func() { + err := SendEvent(ctx, &AgentEvent{AgentName: "test"}) + assert.NoError(t, err) + }, "SendEvent after generator.Close must not panic") + }) + + t.Run("unit_SendEvent_no_execCtx", func(t *testing.T) { + err := SendEvent(context.Background(), &AgentEvent{AgentName: "test"}) + assert.Error(t, err, "SendEvent without execCtx should return error") + }) + + t.Run("integration_cancel_escalation_orphans_tool", func(t *testing.T) { + ctx := context.Background() + + toolStarted := make(chan struct{}, 1) + toolDone := make(chan struct{}, 1) + st := &slowToolWithSignal{ + name: "orphan_tool", + delay: 2 * time.Second, + result: "tool result", + startedChan: toolStarted, + } + + mdl := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_orphan_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "orphan_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OrphanTestAgent", + Description: "Test agent for orphaned tool goroutine panic", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + cancelOpt, cancelFn := WithCancel() + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("Use the tool")}, + }, cancelOpt) + assert.NotNil(t, iter) + + select { + case <-toolStarted: + case <-time.After(10 * time.Second): + t.Fatal("Tool did not start") + } + + timeout := 50 * time.Millisecond + handle, contributed := cancelFn( + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(timeout), + ) + assert.True(t, contributed, "Cancel should contribute") + + err = handle.Wait() + assert.True(t, err == nil || errors.Is(err, ErrCancelTimeout), + "handle.Wait should return nil or ErrCancelTimeout, got: %v", err) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + go func() { + time.Sleep(3 * time.Second) + select { + case toolDone <- struct{}{}: + default: + } + }() + + runtime.Gosched() + time.Sleep(3 * time.Second) + + select { + case <-toolDone: + default: + } + }) +} + +// -- Tests for CancelImmediate in nested agent structures -- + +func newTestChatModel(response *schema.Message, delay time.Duration) *cancelTestChatModel { + m := &cancelTestChatModel{ + response: response, + startedChan: make(chan struct{}, 1), + doneChan: make(chan struct{}, 1), + } + if delay > 0 { + m.setDelay(delay) + } + return m +} + +func newToolCallResponse(toolName string) *schema.Message { + return &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Type: "function", Function: schema.FunctionCall{Name: toolName, Arguments: `{}`}}, + }, + } +} + +func newAgentWithTool(t *testing.T, ctx context.Context, name string, mdl model.BaseChatModel, subAgent Agent) (Agent, error) { + t.Helper() + return NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: name, + Description: name, + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, subAgent)}, + }, + }, + }) +} + +func waitForChan(t *testing.T, ch <-chan struct{}, msg string) { + t.Helper() + select { + case <-ch: + case <-time.After(10 * time.Second): + t.Fatal(msg) + } +} + +func drainCancelError(t *testing.T, iter *AsyncIterator[*AgentEvent]) *CancelError { + t.Helper() + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + errors.As(event.Err, &cancelErr) + } + } + return cancelErr +} + +func drainResumeErrors(t *testing.T, iter *AsyncIterator[*AgentEvent]) []error { + t.Helper() + var errs []error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + errs = append(errs, event.Err) + } + } + return errs +} + +type cancelResult struct { + err error + contributed bool + done chan struct{} +} + +func cancelAsync(cancelFn AgentCancelFunc, opts ...AgentCancelOption) (cancelCalled chan struct{}, result *cancelResult) { + cancelCalled = make(chan struct{}) + result = &cancelResult{done: make(chan struct{})} + go func() { + handle, contributed := cancelFn(opts...) + result.contributed = contributed + close(cancelCalled) + result.err = handle.Wait() + close(result.done) + }() + return +} + +func (r *cancelResult) waitDone(t *testing.T) error { + t.Helper() + select { + case <-r.done: + return r.err + case <-time.After(10 * time.Second): + t.Fatal("cancel did not complete") + return nil + } +} + +func TestCancelImmediate_AgentTool_PreservesChildCheckpoint(t *testing.T) { + ctx := context.Background() + + leafModel := newTestChatModel( + &schema.Message{Role: schema.Assistant, Content: "leaf response"}, 2*time.Second) + leafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "leaf_agent", Description: "Leaf agent in agentTool", Model: leafModel, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "inner_seq", Description: "Inner sequential workflow", SubAgents: []Agent{leafAgent}, + }) + assert.NoError(t, err) + + rootModel := newTestChatModel(newToolCallResponse("inner_seq"), 0) + rootAgent, err := newAgentWithTool(t, ctx, "root_agent", rootModel, seqAgent) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{Agent: rootAgent, CheckPointStore: store}) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("immediate-agent-tool-1")) + + waitForChan(t, leafModel.startedChan, "Leaf agent model did not start") + + handle, contributed := cancelFn(WithRecursive()) + assert.True(t, contributed) + assert.NoError(t, handle.Wait()) + + cancelErr := drainCancelError(t, iter) + assert.NotNil(t, cancelErr, "Should have CancelError from CancelImmediate through agentTool") + assert.NotNil(t, cancelErr.interruptSignal) + + resumeLeaf, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "leaf_agent", Description: "Leaf agent in agentTool", + Model: newTestChatModel(&schema.Message{Role: schema.Assistant, Content: "resumed leaf"}, 0), + }) + assert.NoError(t, err) + resumeSeq, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "inner_seq", Description: "Inner sequential workflow", SubAgents: []Agent{resumeLeaf}, + }) + assert.NoError(t, err) + resumeRoot, err := newAgentWithTool(t, ctx, "root_agent", + newTestChatModel(&schema.Message{Role: schema.Assistant, Content: "resumed root"}, 0), resumeSeq) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{Agent: resumeRoot, CheckPointStore: store}) + resumeIter, err := runner2.Resume(ctx, "immediate-agent-tool-1") + assert.NoError(t, err) + assert.Empty(t, drainResumeErrors(t, resumeIter), "Resume should complete without errors") +} + +func TestCancelImmediate_ParallelWorkflow_WithAgentTool(t *testing.T) { + ctx := context.Background() + + leafModel := newTestChatModel( + &schema.Message{Role: schema.Assistant, Content: "leaf response"}, 2*time.Second) + leafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "leaf_agent", Description: "Leaf agent in agentTool", Model: leafModel, + }) + assert.NoError(t, err) + + agentWithTool, err := newAgentWithTool(t, ctx, "agent_with_tool", + newTestChatModel(newToolCallResponse("leaf_agent"), 0), leafAgent) + assert.NoError(t, err) + + simpleStarted := make(chan struct{}, 1) + simpleAgent := newCancelTestAgent(t, "simple_agent", 2*time.Second, simpleStarted) + + parAgent, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "par_agent", Description: "Parallel with agentTool and simple agent", + SubAgents: []Agent{agentWithTool, simpleAgent}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: parAgent, EnableStreaming: false}) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + waitForChan(t, leafModel.startedChan, "Leaf agent did not start") + waitForChan(t, simpleStarted, "Simple agent did not start") + + start := time.Now() + handle, _ := cancelFn() + assert.NoError(t, handle.Wait()) + + cancelErr := drainCancelError(t, iter) + elapsed := time.Since(start) + + assert.NotNil(t, cancelErr, "Should have CancelError from parallel with agentTool") + assert.True(t, elapsed < 5*time.Second, "Should complete quickly after cancel, elapsed: %v", elapsed) +} + +type cancelUnawareAgent struct { + name string + desc string + delay time.Duration + response string +} + +type multiResponseGatedModel struct { + responses []*schema.Message + gateChan chan struct{} + gateOnce bool + gated int32 + doneChan chan struct{} + callCount int32 +} + +func (m *multiResponseGatedModel) Generate(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + idx := atomic.AddInt32(&m.callCount, 1) + if m.gateChan != nil && (!m.gateOnce || atomic.CompareAndSwapInt32(&m.gated, 0, 1)) { + select { + case <-m.gateChan: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + if len(m.responses) == 0 { + return nil, fmt.Errorf("multiResponseGatedModel: no responses configured") + } + resp := m.responses[(int(idx)-1)%len(m.responses)] + if m.doneChan != nil { + select { + case m.doneChan <- struct{}{}: + default: + } + } + return resp, nil +} + +func (m *multiResponseGatedModel) Stream(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + resp, err := m.Generate(ctx, msgs, opts...) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.Message{resp}), nil +} + +func (m *multiResponseGatedModel) BindTools(tools []*schema.ToolInfo) error { return nil } + +func (a *cancelUnawareAgent) Name(_ context.Context) string { return a.name } +func (a *cancelUnawareAgent) Description(_ context.Context) string { return a.desc } + +func (a *cancelUnawareAgent) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + // Intentionally ignores ctx.Done() — simulates a custom agent that + // does not participate in the cancel protocol at all. + // Delay is kept short (relative to grace period) to avoid goroutine + // leak lasting long after the test completes. + time.Sleep(a.delay) + }() + return iter +} + +func TestCancelImmediate_CustomAgent_GracePeriodFallback(t *testing.T) { + ctx := context.Background() + + customAgent := &cancelUnawareAgent{ + name: "custom_slow", desc: "A custom agent that ignores cancel", + delay: 5 * time.Second, response: "custom response", + } + + rootModel := newTestChatModel(newToolCallResponse("custom_slow"), 0) + rootAgent, err := newAgentWithTool(t, ctx, "root_agent", rootModel, customAgent) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{Agent: rootAgent, EnableStreaming: false}) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt) + + waitForChan(t, rootModel.startedChan, "Root model did not start") + waitForChan(t, rootModel.doneChan, "Root model did not finish") + + start := time.Now() + handle, _ := cancelFn() + assert.NoError(t, handle.Wait()) + + cancelErr := drainCancelError(t, iter) + elapsed := time.Since(start) + + assert.NotNil(t, cancelErr, "Should have CancelError (from grace period fallback)") + assert.True(t, elapsed < 5*time.Second, + "Should complete within grace period + overhead, elapsed: %v", elapsed) +} + +func TestCancelImmediate_MultiLevelNesting(t *testing.T) { + ctx := context.Background() + + innerLeafModel := newTestChatModel( + &schema.Message{Role: schema.Assistant, Content: "inner leaf response"}, 2*time.Second) + innerLeafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner_leaf", Description: "Innermost leaf agent", Model: innerLeafModel, + }) + assert.NoError(t, err) + + middleAgent, err := newAgentWithTool(t, ctx, "middle_agent", + newTestChatModel(newToolCallResponse("inner_leaf"), 0), innerLeafAgent) + assert.NoError(t, err) + + rootAgent, err := newAgentWithTool(t, ctx, "root_agent", + newTestChatModel(newToolCallResponse("middle_agent"), 0), middleAgent) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{Agent: rootAgent, CheckPointStore: store}) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("multi-level-1")) + + waitForChan(t, innerLeafModel.startedChan, "Inner leaf model did not start") + + start := time.Now() + handle, contributed := cancelFn() + assert.True(t, contributed) + assert.NoError(t, handle.Wait()) + + cancelErr := drainCancelError(t, iter) + elapsed := time.Since(start) + + assert.NotNil(t, cancelErr, "Should have CancelError from multi-level nesting") + assert.NotNil(t, cancelErr.interruptSignal) + assert.True(t, elapsed < 5*time.Second, "Should complete quickly, elapsed: %v", elapsed) + + resumeInnerLeaf, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "inner_leaf", Description: "Innermost leaf agent", + Model: newTestChatModel(&schema.Message{Role: schema.Assistant, Content: "resumed inner leaf"}, 0), + }) + assert.NoError(t, err) + resumeMiddle, err := newAgentWithTool(t, ctx, "middle_agent", + newTestChatModel(&schema.Message{Role: schema.Assistant, Content: "resumed middle"}, 0), resumeInnerLeaf) + assert.NoError(t, err) + resumeRoot, err := newAgentWithTool(t, ctx, "root_agent", + newTestChatModel(&schema.Message{Role: schema.Assistant, Content: "resumed root"}, 0), resumeMiddle) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{Agent: resumeRoot, CheckPointStore: store}) + resumeIter, err := runner2.Resume(ctx, "multi-level-1") + assert.NoError(t, err) + assert.Empty(t, drainResumeErrors(t, resumeIter), "Resume should complete without errors") +} + +func TestCancelImmediate_SequentialTransitionBoundary(t *testing.T) { + ctx := context.Background() + + model1 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent1 done"}, + gateChan: make(chan struct{}), + doneChan: make(chan struct{}, 1), + } + model2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent2 done"}, + doneChan: make(chan struct{}, 1), + } + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", Model: model1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", Model: model2, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "sequential test", SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt) + + for atomic.LoadInt32(&model1.callCount) == 0 { + runtime.Gosched() + } + + cancelCalled, result := cancelAsync(cancelFn) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(model1.gateChan) + + assert.NoError(t, result.waitDone(t), "CancelImmediate should succeed at transition") + + cancelErr := drainCancelError(t, iter) + + assert.NotNil(t, cancelErr, "Should have CancelError at transition boundary") + assert.Equal(t, int32(1), atomic.LoadInt32(&model1.callCount), "Agent1 model should be invoked") + assert.Equal(t, int32(0), atomic.LoadInt32(&model2.callCount), "Agent2 model should NOT be invoked (caught at transition)") +} + +func TestCancelImmediate_LoopTransitionBoundary(t *testing.T) { + ctx := context.Background() + + mdl := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "loop iter"}, + gateChan: make(chan struct{}), + doneChan: make(chan struct{}, 10), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "loop_inner", Description: "inner", Instruction: "test", Model: mdl, + }) + assert.NoError(t, err) + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop", Description: "loop test", SubAgents: []Agent{agent}, MaxIterations: 5, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: loopAgent, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt) + + for atomic.LoadInt32(&mdl.callCount) == 0 { + runtime.Gosched() + } + + cancelCalled, result := cancelAsync(cancelFn) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(mdl.gateChan) + + assert.NoError(t, result.waitDone(t), "CancelImmediate should succeed at loop transition") + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, int32(1), atomic.LoadInt32(&mdl.callCount), + "Model should be called once; second iteration caught at transition") +} + +func TestCancelAfterChatModel_SequentialTransitionBoundary(t *testing.T) { + ctx := context.Background() + + model1 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent1 done"}, + gateChan: make(chan struct{}), + doneChan: make(chan struct{}, 1), + } + model2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent2 done"}, + doneChan: make(chan struct{}, 1), + } + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", Model: model1, + }) + assert.NoError(t, err) + + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", Model: model2, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "sequential test", SubAgents: []Agent{agent1, agent2}, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, + EnableStreaming: false, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt, WithCheckPointID("chatmodel-transition-1")) + + for atomic.LoadInt32(&model1.callCount) == 0 { + runtime.Gosched() + } + + cancelCalled, result := cancelAsync(cancelFn, WithAgentCancelMode(CancelAfterChatModel)) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(model1.gateChan) + + assert.NoError(t, result.waitDone(t), "CancelAfterChatModel should succeed at transition boundary") + + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + cancelErr = ce + } + } + + assert.NotNil(t, cancelErr, "Should have CancelError at transition boundary") + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + assert.Equal(t, int32(1), atomic.LoadInt32(&model1.callCount), "Agent1 model should be invoked") + assert.Equal(t, int32(0), atomic.LoadInt32(&model2.callCount), + "Agent2 model should NOT be invoked (CancelAfterChatModel caught at transition)") +} + +func TestCancelAfterChatModel_Sequential_Agent1CompletesCancelBeforeAgent2Resume(t *testing.T) { + ctx := context.Background() + + model1 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent1 done"}, + gateChan: make(chan struct{}), + doneChan: make(chan struct{}, 1), + } + model2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent2 done"}, + doneChan: make(chan struct{}, 1), + } + model3 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "agent3 done"}, + doneChan: make(chan struct{}, 1), + } + + agent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", Model: model1, + }) + assert.NoError(t, err) + agent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", Model: model2, + }) + assert.NoError(t, err) + agent3, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent3", Description: "third", Instruction: "test", Model: model3, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "3-agent sequential", SubAgents: []Agent{agent1, agent2, agent3}, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, CheckPointStore: store, EnableStreaming: false, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt, + WithCheckPointID("seq-transition-resume-1")) + + for atomic.LoadInt32(&model1.callCount) == 0 { + runtime.Gosched() + } + + cancelCalled, result := cancelAsync(cancelFn, WithAgentCancelMode(CancelAfterChatModel)) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(model1.gateChan) + + assert.NoError(t, result.waitDone(t)) + + cancelErr := drainCancelError(t, iter) + assert.NotNil(t, cancelErr, "Should have CancelError") + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + assert.Equal(t, int32(1), atomic.LoadInt32(&model1.callCount)) + assert.Equal(t, int32(0), atomic.LoadInt32(&model2.callCount), + "Agent2 should NOT run (cancel caught at transition after agent1)") + assert.Equal(t, int32(0), atomic.LoadInt32(&model3.callCount)) + + resumeModel2 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "resumed agent2"}, + doneChan: make(chan struct{}, 1), + } + resumeModel3 := &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "resumed agent3"}, + doneChan: make(chan struct{}, 1), + } + + resumeAgent1, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent1", Description: "first", Instruction: "test", + Model: &gatedChatModel{ + response: &schema.Message{Role: schema.Assistant, Content: "should not run"}, + doneChan: make(chan struct{}, 1), + }, + }) + assert.NoError(t, err) + resumeAgent2, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent2", Description: "second", Instruction: "test", Model: resumeModel2, + }) + assert.NoError(t, err) + resumeAgent3, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent3", Description: "third", Instruction: "test", Model: resumeModel3, + }) + assert.NoError(t, err) + + resumeSeq, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq", Description: "3-agent sequential", + SubAgents: []Agent{resumeAgent1, resumeAgent2, resumeAgent3}, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumeSeq, CheckPointStore: store, EnableStreaming: false, + }) + resumeIter, err := runner2.Resume(ctx, "seq-transition-resume-1") + assert.NoError(t, err) + assert.Empty(t, drainResumeErrors(t, resumeIter), "Resume should complete without errors") + + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeModel2.callCount), + "Agent2 should run on resume") + assert.Equal(t, int32(1), atomic.LoadInt32(&resumeModel3.callCount), + "Agent3 should run on resume") +} + +func TestCancelAfterToolCalls_LoopTransitionBoundary(t *testing.T) { + ctx := context.Background() + + // Model that returns tool calls on odd calls and no tools on even calls. + // This completes one ReAct cycle per pair of calls: + // call 1 (gated): returns tool call → tool runs → call 2: returns no tools → END + // The gate only blocks the very first call. After that, all calls proceed instantly. + mdl := &multiResponseGatedModel{ + responses: []*schema.Message{ + {Role: schema.Assistant, ToolCalls: []schema.ToolCall{{ + ID: "call_1", Type: "function", + Function: schema.FunctionCall{Name: "loop_tool", Arguments: `{"input": "test"}`}, + }}}, + {Role: schema.Assistant, Content: "iteration done"}, + }, + gateChan: make(chan struct{}), + gateOnce: true, + doneChan: make(chan struct{}, 10), + } + + st := &slowTool{ + name: "loop_tool", + delay: 10 * time.Millisecond, + result: "tool done", + startedChan: make(chan struct{}, 10), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "loop_inner", Description: "inner", Instruction: "test", Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop", Description: "loop test", SubAgents: []Agent{agent}, MaxIterations: 10, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{Agent: loopAgent, CheckPointStore: store}) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("toolcalls-loop-1")) + + // Wait for the model to be entered (blocked on gate) + for atomic.LoadInt32(&mdl.callCount) == 0 { + runtime.Gosched() + } + + // Fire cancel, wait for it to be registered, then release the gate + cancelCalled, result := cancelAsync(cancelFn, WithAgentCancelMode(CancelAfterToolCalls)) + waitForChan(t, cancelCalled, "cancelFn was not called") + close(mdl.gateChan) + + // Iteration 1 completes fully (model→tool→model-no-tools→END). + // The CancelAfterToolCalls safe-point inside ReAct fires after tool calls, + // OR the transition boundary catches it before iteration 2. + // Note: this test doesn't deterministically distinguish which path fires — + // both are semantically correct for CancelAfterToolCalls. The transition- + // boundary code path for CancelAfterToolCalls in loops is not definitively + // covered here because the ReAct safe-point may handle it first. + assert.NoError(t, result.waitDone(t)) + + cancelErr := drainCancelError(t, iter) + assert.NotNil(t, cancelErr, "Should have CancelError from CancelAfterToolCalls in loop") + assert.Equal(t, CancelAfterToolCalls, cancelErr.Info.Mode) +} + +func TestCancelContext_RecursiveGraceBoundary(t *testing.T) { + t.Run("AgentToolDescendantMarker_PropagatesToParents", func(t *testing.T) { + parent := newCancelContext() + ctx := context.Background() + child := parent.deriveAgentToolCancelContext(ctx) + grandchild := child.deriveAgentToolCancelContext(ctx) + t.Cleanup(func() { + grandchild.markDone() + child.markDone() + }) + + assert.False(t, parent.hasAgentToolDescendant()) + assert.False(t, child.hasAgentToolDescendant()) + + grandchild.markAgentToolDescendant() + + assert.True(t, grandchild.hasAgentToolDescendant()) + assert.True(t, child.hasAgentToolDescendant()) + assert.True(t, parent.hasAgentToolDescendant()) + }) +} + +func TestCancel_ParallelWorkflow_CancelAfterChatModel(t *testing.T) { + ctx := context.Background() + slowStarted := make(chan struct{}, 1) + + slowAgent := newCancelTestAgentWithTools(t, "par_slow", 1*time.Second, slowStarted) + fastAgent := newCancelTestAgentWithTools(t, "par_fast", 50*time.Millisecond, make(chan struct{}, 1)) + + parAgent, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "par_agent", + Description: "Parallel workflow", + SubAgents: []Agent{slowAgent, fastAgent}, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: parAgent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("par-cancel-1")) + + select { + case <-slowStarted: + case <-time.After(10 * time.Second): + t.Fatal("Slow agent did not start") + } + + handle, contributed := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + assert.True(t, contributed, "Cancel should contribute") + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil && errors.As(event.Err, &cancelErr) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "Should have CancelError from parallel workflow") + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + + resumeSlow := newCancelTestAgentWithToolsFinalAnswer(t, "par_slow") + resumeFast := newCancelTestAgentWithToolsFinalAnswer(t, "par_fast") + + resumePar, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "par_agent", + Description: "Parallel workflow", + SubAgents: []Agent{resumeSlow, resumeFast}, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumePar, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, "par-cancel-1") + assert.NoError(t, err) + assert.NotNil(t, resumeIter) + + var resumeErrors []error + for { + event, ok := resumeIter.Next() + if !ok { + break + } + if event.Err != nil { + resumeErrors = append(resumeErrors, event.Err) + } + } + assert.Empty(t, resumeErrors, "Resume should complete without errors") +} + +func TestCancel_LoopWorkflow_CancelAfterChatModel(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 10) + + agent := newCancelTestAgentWithTools(t, "loop_inner", 500*time.Millisecond, modelStarted) + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop_agent", + Description: "Loop workflow", + SubAgents: []Agent{agent}, + MaxIterations: 10, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: loopAgent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("loop-cancel-1")) + + select { + case <-modelStarted: + case <-time.After(10 * time.Second): + t.Fatal("Model did not start") + } + + handle, contributed := cancelFn(WithAgentCancelMode(CancelAfterChatModel)) + assert.True(t, contributed, "Cancel should contribute") + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil && errors.As(event.Err, &cancelErr) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "Should have CancelError from loop workflow") + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + + resumeAgent := newCancelTestAgentWithToolsFinalAnswer(t, "loop_inner") + + resumeLoop, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop_agent", + Description: "Loop workflow", + SubAgents: []Agent{resumeAgent}, + MaxIterations: 10, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumeLoop, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, "loop-cancel-1") + assert.NoError(t, err) + assert.NotNil(t, resumeIter) + + var resumeEvents []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + assert.Nil(t, event.Err, "Should not have error during resume") + resumeEvents = append(resumeEvents, event) + } + assert.NotEmpty(t, resumeEvents, "Resume should produce events") +} + +func TestCancel_NestedWorkflow_AgentTool_CancelAfterChatModel(t *testing.T) { + // Structure: Runner -> RootCMA (with tools) -> agentTool -> flowAgent -> seqWorkflow -> LeafCMA + ctx := context.Background() + leafStarted := make(chan struct{}, 1) + + leafModel := &cancelTestChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "leaf response", + }, + startedChan: leafStarted, + doneChan: make(chan struct{}, 1), + } + leafModel.setDelay(500 * time.Millisecond) + + leafAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "leaf_agent", + Description: "Leaf agent in workflow", + Model: leafModel, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "inner_seq", + Description: "Inner sequential workflow", + SubAgents: []Agent{leafAgent}, + }) + assert.NoError(t, err) + + rootModel := &cancelTestChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "inner_seq", + Arguments: `{}`, + }, + }, + }, + }, + startedChan: make(chan struct{}, 1), + doneChan: make(chan struct{}, 1), + } + rootAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "root_agent", + Description: "Root agent", + Model: rootModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, seqAgent)}, + }, + }, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: rootAgent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, cancelOpt, WithCheckPointID("nested-cancel-1")) + + select { + case <-leafStarted: + case <-time.After(10 * time.Second): + t.Fatal("Leaf agent model did not start") + } + + handle, contributed := cancelFn(WithAgentCancelMode(CancelAfterChatModel), WithRecursive()) + assert.True(t, contributed, "Cancel should contribute") + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil && errors.As(event.Err, &cancelErr) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "Should have CancelError from deeply nested workflow") + assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode) + assert.NotNil(t, cancelErr.interruptSignal, "CancelError should carry interrupt signal through agent tree") + + // Phase 2: Resume from checkpoint — new instances to avoid data races + resumeLeafModel := &cancelTestChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "resumed leaf response", + }, + startedChan: make(chan struct{}, 1), + doneChan: make(chan struct{}, 1), + } + resumeLeaf, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "leaf_agent", + Description: "Leaf agent in workflow", + Model: resumeLeafModel, + }) + assert.NoError(t, err) + + resumeSeq, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "inner_seq", + Description: "Inner sequential workflow", + SubAgents: []Agent{resumeLeaf}, + }) + assert.NoError(t, err) + + resumeRootModel := &cancelTestChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "resumed root response", + }, + startedChan: make(chan struct{}, 1), + doneChan: make(chan struct{}, 1), + } + resumeRoot, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "root_agent", + Description: "Root agent", + Model: resumeRootModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, resumeSeq)}, + }, + }, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumeRoot, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, "nested-cancel-1") + assert.NoError(t, err) + assert.NotNil(t, resumeIter) + + var resumeErrors []error + for { + event, ok := resumeIter.Next() + if !ok { + break + } + if event.Err != nil { + resumeErrors = append(resumeErrors, event.Err) + } + } + assert.Empty(t, resumeErrors, "Resume should complete without errors") +} + +func TestCancel_CancelAfterToolCalls_InSequentialWorkflow(t *testing.T) { + ctx := context.Background() + toolStarted := make(chan struct{}, 1) + + st := &slowTool{ + name: "slow_tool", + delay: 200 * time.Millisecond, + result: "tool done", + startedChan: toolStarted, + } + + modelWithToolCall := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "slow_tool", + Arguments: `{"input": "test"}`, + }, + }, + }, + }, + } + + agentWithTools, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent_with_tools", + Description: "Agent with slow tool", + Model: modelWithToolCall, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{st}, + }, + }, + }) + assert.NoError(t, err) + + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq_agent", + Description: "Sequential workflow with tool agent", + SubAgents: []Agent{agentWithTools}, + }) + assert.NoError(t, err) + + store := newCancelTestStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: seqAgent, + CheckPointStore: store, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("Use the tool")}, cancelOpt, WithCheckPointID("tool-cancel-1")) + + select { + case <-toolStarted: + case <-time.After(10 * time.Second): + t.Fatal("Tool did not start") + } + + // Cancel after tool calls — should wait for the tool to finish, then cancel + handle, contributed := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + assert.True(t, contributed, "Cancel should contribute") + err = handle.Wait() + assert.NoError(t, err) + + hasCancelError := false + var cancelErr *CancelError + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil && errors.As(event.Err, &cancelErr) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "Should have CancelError after tool calls complete") + assert.Equal(t, CancelAfterToolCalls, cancelErr.Info.Mode) + + // Phase 2: Resume from checkpoint — new instances + resumeTool := &slowTool{ + name: "slow_tool", + delay: 50 * time.Millisecond, + result: "resumed tool done", + startedChan: make(chan struct{}, 1), + } + + resumeModel := &simpleChatModel{ + response: &schema.Message{ + Role: schema.Assistant, + Content: "resumed response after tool", + }, + } + + resumeAgentWithTools, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "agent_with_tools", + Description: "Agent with slow tool", + Model: resumeModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{resumeTool}, + }, + }, + }) + assert.NoError(t, err) + + resumeSeq, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "seq_agent", + Description: "Sequential workflow with tool agent", + SubAgents: []Agent{resumeAgentWithTools}, + }) + assert.NoError(t, err) + + runner2 := NewRunner(ctx, RunnerConfig{ + Agent: resumeSeq, + CheckPointStore: store, + }) + + resumeIter, err := runner2.Resume(ctx, "tool-cancel-1") + assert.NoError(t, err) + assert.NotNil(t, resumeIter) + + var resumeEvents []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + assert.Nil(t, event.Err, "Should not have error during resume") + resumeEvents = append(resumeEvents, event) + } + assert.NotEmpty(t, resumeEvents, "Resume should produce events") +} + +// TestCancel_SafePointNeverFires_ErrExecutionEnded verifies the waitForCompletion +// path where a safe-point cancel is submitted while the agent is running, but +// the agent finishes without hitting the requested safe-point (e.g. +// CancelAfterToolCalls on an agent with no tool calls). The cancel CAS succeeds +// (stateRunning → stateCancelling), but the agent completes normally (markDone → +// stateDone), so waitForCompletion returns ErrExecutionEnded. +func TestCancel_SafePointNeverFires_ErrExecutionEnded(t *testing.T) { + ctx := context.Background() + + gate := make(chan struct{}) + done := make(chan struct{}, 1) + + m := &gatedChatModel{ + gateChan: gate, + doneChan: done, + response: &schema.Message{ + Role: schema.Assistant, + Content: "Final answer, no tool calls", + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "NoToolAgent", + Description: "Agent with no tools", + Instruction: "You are a test assistant", + Model: m, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + }) + + cancelOpt, cancelFn := WithCancel() + iter := runner.Run(ctx, []Message{schema.UserMessage("hello")}, cancelOpt) + + // Wait a moment for the agent to enter Generate and block on gateChan. + runtime.Gosched() + time.Sleep(50 * time.Millisecond) + + // Submit a safe-point cancel for tool calls. The agent has no tools, + // so this safe-point will never fire. + handle, submitted := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + assert.True(t, submitted) + + // Let the model complete. The agent finishes without hitting the tool + // calls safe-point → markDone → stateDone → waitForCompletion returns + // ErrExecutionEnded. + close(gate) + + waitErr := handle.Wait() + assert.ErrorIs(t, waitErr, ErrExecutionEnded) + + for { + _, ok := iter.Next() + if !ok { + break + } + } +} + +// TestBuildCancelFunc_StateDoneUnderLock exercises the race-condition path +// in buildCancelFunc where the state transitions to stateDone between the +// lockless check and the locked check (cancel.go L732-734). +func TestBuildCancelFunc_StateDoneUnderLock(t *testing.T) { + cc := newCancelContext() + cancelFn := cc.buildCancelFunc() + + // Hold cancelMu so the cancel func blocks when it tries to acquire the lock. + cc.cancelMu.Lock() + + type result struct { + handle *CancelHandle + ok bool + } + ch := make(chan result, 1) + + go func() { + h, ok := cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + ch <- result{h, ok} + }() + + // Give the goroutine time to reach the Lock() call. + runtime.Gosched() + time.Sleep(20 * time.Millisecond) + + // Transition to stateDone while the cancel goroutine is blocked on the lock. + cc.markDone() + + // Release the lock. The cancel func resumes and finds stateDone. + cc.cancelMu.Unlock() + + r := <-ch + assert.False(t, r.ok, "cancel should not be accepted when execution already done") + assert.ErrorIs(t, r.handle.Wait(), ErrExecutionEnded) +} + +// TestBuildCancelFunc_CASFailStateDone exercises the race-condition path +// in buildCancelFunc where the CAS on stateRunning→stateCancelling fails +// because markDone transitioned stateRunning→stateDone concurrently +// (cancel.go L742-743). +func TestBuildCancelFunc_CASFailStateDone(t *testing.T) { + // Exercises cancel.go L742-743: CAS(stateRunning→stateCancelling) fails + // because markDone transitions stateRunning→stateDone concurrently. + // + // The window between the state check (L738) and CAS (L739) is extremely + // tight. We maximize the chance by having the cancel goroutine block on + // cancelMu, then racing markDone with the lock release. + hit := false + for i := 0; i < 100000 && !hit; i++ { + cc := newCancelContext() + cancelFn := cc.buildCancelFunc() + + // Hold cancelMu so the cancel goroutine blocks at L725. + cc.cancelMu.Lock() + + cancelDone := make(chan struct{}) + var h *CancelHandle + var ok bool + + go func() { + defer close(cancelDone) + h, ok = cancelFn(WithAgentCancelMode(CancelAfterToolCalls)) + }() + + // Let the cancel goroutine reach the Lock() call. + runtime.Gosched() + + // Release lock and fire markDone concurrently. The cancel goroutine + // will acquire the lock and race with markDone on the CAS. + go cc.markDone() + cc.cancelMu.Unlock() + + <-cancelDone + + if !ok && errors.Is(h.Wait(), ErrExecutionEnded) { + hit = true + } + } + if hit { + t.Log("Successfully hit CAS-fail → stateDone path") + } else { + t.Log("CAS race path not triggered (L743 remains a theoretical race edge)") + } +} diff --git a/adk/chatmodel.go b/adk/chatmodel.go new file mode 100644 index 0000000..37a55f1 --- /dev/null +++ b/adk/chatmodel.go @@ -0,0 +1,1714 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + "math" + "runtime/debug" + "strings" + "sync" + "sync/atomic" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +var _ ResumableAgent = &TypedChatModelAgent[*schema.Message]{} +var _ TypedResumableAgent[*schema.AgenticMessage] = &TypedChatModelAgent[*schema.AgenticMessage]{} + +type typedChatModelAgentExecCtx[M MessageType] struct { + runtimeReturnDirectly map[string]bool + generator *AsyncGenerator[*TypedAgentEvent[M]] + cancelCtx *cancelContext + + failoverLastSuccessModel model.BaseModel[M] + + // suppressEventSend prevents eventSenderModel from emitting AgentEvents for the current + // Generate call. Set to true before each rejected retry attempt and reset to false after. + // Invariant: any code path that emits model output events MUST check this flag. + suppressEventSend bool + retryVerdictSignal *retryVerdictSignal + + afterToolCallsHook func(ctx context.Context) error +} + +func (e *typedChatModelAgentExecCtx[M]) send(event *TypedAgentEvent[M]) { + if e == nil || e.generator == nil { + return + } + if e.cancelCtx != nil && e.cancelCtx.isImmediateCancelled() { + return + } + e.generator.trySend(event) +} + +type chatModelAgentExecCtx = typedChatModelAgentExecCtx[*schema.Message] + +type typedChatModelAgentExecCtxKey[M MessageType] struct{} + +func withTypedChatModelAgentExecCtx[M MessageType](ctx context.Context, execCtx *typedChatModelAgentExecCtx[M]) context.Context { + return context.WithValue(ctx, typedChatModelAgentExecCtxKey[M]{}, execCtx) +} + +func getTypedChatModelAgentExecCtx[M MessageType](ctx context.Context) *typedChatModelAgentExecCtx[M] { + if v := ctx.Value(typedChatModelAgentExecCtxKey[M]{}); v != nil { + return v.(*typedChatModelAgentExecCtx[M]) + } + return nil +} + +type chatModelAgentRunOptions struct { + chatModelOptions []model.Option + toolOptions []tool.Option + agentToolOptions map[string][]AgentRunOption + + historyModifier func(context.Context, []Message) []Message + + afterToolCallsHook func(ctx context.Context) error +} + +// WithChatModelOptions sets options for the underlying chat model. +func WithChatModelOptions(opts []model.Option) AgentRunOption { + return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) { + t.chatModelOptions = opts + }) +} + +// WithToolOptions sets options for tools used by the chat model agent. +func WithToolOptions(opts []tool.Option) AgentRunOption { + return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) { + t.toolOptions = opts + }) +} + +// WithAgentToolRunOptions specifies per-tool run options for the agent. +func WithAgentToolRunOptions(opts map[string][]AgentRunOption) AgentRunOption { + return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) { + t.agentToolOptions = opts + }) +} + +// WithHistoryModifier sets a function to modify history during resume. +// Deprecated: use ResumeWithData and ChatModelAgentResumeData instead. +func WithHistoryModifier(f func(context.Context, []Message) []Message) AgentRunOption { + return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) { + t.historyModifier = f + }) +} + +// WithAfterToolCallsHook registers a per-run hook that fires synchronously after +// all tool calls in a react iteration complete, before the next ChatModel call. +// +// This is suitable for TurnLoop Push+Preempt patterns where the pushed item +// must be visible to the next turn's GenInput. +func WithAfterToolCallsHook(fn func(ctx context.Context) error) AgentRunOption { + return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) { + t.afterToolCallsHook = fn + }) +} + +type ToolsConfig struct { + compose.ToolsNodeConfig + + // ReturnDirectly specifies tools that cause the agent to return immediately when called. + // The map keys are tool names indicate whether the tool should trigger immediate return. + ReturnDirectly map[string]bool + + // EmitInternalEvents indicates whether internal events from agentTool should be emitted + // to the parent agent's AsyncGenerator, allowing real-time streaming of nested agent output + // to the end-user via Runner. + // + // Note that these forwarded events are NOT recorded in the parent agent's runSession. + // They are only emitted to the end-user and have no effect on the parent agent's state + // or checkpoint. + // + // Action Scoping: + // Actions emitted by the inner agent are scoped to the agent tool boundary: + // - Interrupted: Propagated via CompositeInterrupt to allow proper interrupt/resume + // - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool + EmitInternalEvents bool +} + +// TypedGenModelInput transforms the agent's system instruction and user input into model input +// messages ([]M). This is the primary customization point for controlling what the model sees. +// The default implementation prepends a system message (if instruction is non-empty), +// followed by the user's input messages. +type TypedGenModelInput[M MessageType] func(ctx context.Context, instruction string, input *TypedAgentInput[M]) ([]M, error) + +// GenModelInput transforms agent instructions and input into a format suitable for the model. +type GenModelInput = TypedGenModelInput[*schema.Message] + +func defaultGenModelInput(ctx context.Context, instruction string, input *AgentInput) ([]Message, error) { + msgs := make([]Message, 0, len(input.Messages)+1) + + if instruction != "" { + sp := schema.SystemMessage(instruction) + + vs := GetSessionValues(ctx) + if len(vs) > 0 { + ct := prompt.FromMessages(schema.FString, sp) + ms, err := ct.Format(ctx, vs) + if err != nil { + return nil, fmt.Errorf("defaultGenModelInput: failed to format instruction using FString template. "+ + "This formatting is triggered automatically when SessionValues are present. "+ + "If your instruction contains literal curly braces (e.g., JSON), provide a custom GenModelInput that uses another format. If you are using "+ + "SessionValues for purposes other than instruction formatting, provide a custom GenModelInput that does no formatting at all: %w", err) + } + + sp = ms[0] + } + + msgs = append(msgs, sp) + } + + msgs = append(msgs, input.Messages...) + + return msgs, nil +} + +func newDefaultGenModelInput[M MessageType]() TypedGenModelInput[M] { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(GenModelInput(defaultGenModelInput)).(TypedGenModelInput[M]) + case *schema.AgenticMessage: + return any(TypedGenModelInput[*schema.AgenticMessage](func(_ context.Context, instruction string, input *TypedAgentInput[*schema.AgenticMessage]) ([]*schema.AgenticMessage, error) { + msgs := make([]*schema.AgenticMessage, 0, len(input.Messages)+1) + if instruction != "" { + msgs = append(msgs, schema.SystemAgenticMessage(instruction)) + } + msgs = append(msgs, input.Messages...) + return msgs, nil + })).(TypedGenModelInput[M]) + default: + panic("unreachable: unknown MessageType") + } +} + +// TypedChatModelAgentState represents the state of a chat model agent during conversation. +// This is the primary state type for both TypedChatModelAgentMiddleware and AgentMiddleware callbacks. +type TypedChatModelAgentState[M MessageType] struct { + // Messages contains all messages in the current conversation session. + Messages []M + + // ToolInfos contains the tool definitions passed to the model via model.WithTools. + // BeforeModelRewriteState handlers can read and modify this field to control which tools + // the model sees on each call. + ToolInfos []*schema.ToolInfo + + // DeferredToolInfos contains tool definitions for server-side deferred retrieval, + // passed to the model via model.WithDeferredTools. These tools are not included in the + // immediate tool list but can be discovered by the model through its native search capability. + // Nil when not in use. + DeferredToolInfos []*schema.ToolInfo +} + +// ChatModelAgentState is the default state type using *schema.Message. +type ChatModelAgentState = TypedChatModelAgentState[*schema.Message] + +// Deprecated: Use ChatModelAgentMiddleware (interface-based Handlers) instead. +// AgentMiddleware will be removed in a future release. +// +// AgentMiddleware provides hooks to customize agent behavior at various stages of execution. +type AgentMiddleware struct { + // AdditionalInstruction adds supplementary text to the agent's system instruction. + // This instruction is concatenated with the base instruction before each chat model call. + AdditionalInstruction string + + // AdditionalTools adds supplementary tools to the agent's available toolset. + // These tools are combined with the tools configured for the agent. + AdditionalTools []tool.BaseTool + + // BeforeChatModel is called before each ChatModel invocation, allowing modification of the agent state. + BeforeChatModel func(context.Context, *ChatModelAgentState) error + + // AfterChatModel is called after each ChatModel invocation, allowing modification of the agent state. + AfterChatModel func(context.Context, *ChatModelAgentState) error + + // WrapToolCall wraps tool calls with custom middleware logic. + // Each middleware contains Invokable and/or Streamable functions for tool calls. + WrapToolCall compose.ToolMiddleware +} + +// TypedChatModelAgentConfig is the generic configuration for ChatModelAgent. +type TypedChatModelAgentConfig[M MessageType] struct { + // Name of the agent. Better be unique across all agents. + // Optional. If empty, the agent can still run standalone but cannot be used as + // a sub-agent tool via NewAgentTool (which requires a non-empty Name). + Name string + // Description of the agent's capabilities. + // Helps other agents determine whether to transfer tasks to this agent. + // Optional. If empty, the agent can still run standalone but cannot be used as + // a sub-agent tool via NewAgentTool (which requires a non-empty Description). + Description string + // Instruction used as the system prompt for this agent. + // Optional. If empty, no system prompt will be used. + // Supports f-string placeholders for session values in default GenModelInput, for example: + // "You are a helpful assistant. The current time is {Time}. The current user is {User}." + // These placeholders will be replaced with session values for "Time" and "User". + Instruction string + + // Model is the chat model used by the agent. + // If your ChatModelAgent uses any tools, this model must support the model.WithTools + // call option, as that's how ChatModelAgent configures the model with tool information. + Model model.BaseModel[M] + + ToolsConfig ToolsConfig + + // GenModelInput transforms instructions and input messages into the model's input format. + // Optional. Defaults to defaultGenModelInput which combines instruction and messages. + GenModelInput TypedGenModelInput[M] + + // Exit defines the tool used to terminate the agent process. + // Optional. If nil, no Exit Action will be generated. + // You can use the provided 'ExitTool' implementation directly. + // + // NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven + // to be more effective empirically. Consider using ChatModelAgent with AgentTool + // or DeepAgent instead for most multi-agent scenarios. + Exit tool.BaseTool + + // OutputKey stores the agent's response in the session. + // Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content). + // + // NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven + // to be more effective empirically. Consider using ChatModelAgent with AgentTool + // or DeepAgent instead for most multi-agent scenarios. + OutputKey string + + // MaxIterations defines the upper limit of ChatModel generation cycles. + // The agent will terminate with an error if this limit is exceeded. + // Optional. Defaults to 20. + MaxIterations int + + // Deprecated: Use Handlers instead. Middlewares will be removed in a future release. + // Handlers provides a more flexible interface-based approach for extending agent behavior. + Middlewares []AgentMiddleware + + // Handlers configures interface-based handlers for extending agent behavior. + // Unlike Middlewares (struct-based), Handlers allow users to: + // - Add custom methods to their handler implementations + // - Return modified context from handler methods + // - Centralize configuration in struct fields instead of closures + // + // Handlers are processed after Middlewares, in registration order. + // See ChatModelAgentMiddleware documentation for when to use Handlers vs Middlewares. + // + // Execution Order (relative to AgentMiddleware and ToolsConfig): + // + // Model call lifecycle (outermost to innermost wrapper chain): + // 1. AgentMiddleware.BeforeChatModel (hook, runs before model call) + // 2. ChatModelAgentMiddleware.BeforeModelRewriteState (hook, can modify state before model call) + // 3. failoverModelWrapper (internal - failover between models, if configured) + // 4. retryModelWrapper (internal - retries on failure, if configured) + // 5. eventSenderModelWrapper (internal - sends model response events) + // 6. ChatModelAgentMiddleware.WrapModel (wrapper, first registered is outermost) + // 7. callbackInjectionModelWrapper (internal - injects callbacks if not enabled; when failover is enabled, this is handled per-model inside failoverProxyModel instead) + // 8. failoverProxyModel (internal - dispatches to selected failover model, if configured) / Model.Generate/Stream + // 9. ChatModelAgentMiddleware.AfterModelRewriteState (hook, can modify state after model call) + // 10. AgentMiddleware.AfterChatModel (hook, runs after model call) + // + // Custom Event Sender Position: + // By default, events are sent after all user middlewares (WrapModel) have processed the output, + // containing the modified messages. To send events with original (unmodified) output, pass + // NewEventSenderModelWrapper as a Handler after the modifying middleware: + // + // agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + // Handlers: []adk.ChatModelAgentMiddleware{ + // myCustomHandler, // First registered = outermost wrapper + // adk.NewEventSenderModelWrapper(), // Last registered = innermost, events sent with original output + // }, + // }) + // + // Handler order: first registered is outermost. So [A, B, C] becomes A(B(C(model))). + // EventSenderModelWrapper sends events in post-processing, so placing it innermost + // means it receives the original model output before outer handlers modify it. + // + // When EventSenderModelWrapper is detected in Handlers, the framework skips + // the default event sender to avoid duplicate events. + // + // Tool call lifecycle (outermost to innermost): + // 1. eventSenderToolWrapper (internal ToolMiddleware - sends tool result events after all processing) + // 2. ToolsConfig.ToolCallMiddlewares (ToolMiddleware) + // 3. AgentMiddleware.WrapToolCall (ToolMiddleware) + // 4. ChatModelAgentMiddleware.WrapToolCall (wrapper, first registered is outermost) + // 5. callbackInjectedToolCall (internal - injects callbacks if tool doesn't handle them) + // 6. Tool.InvokableRun/StreamableRun + // + // Custom Tool Event Sender Position: + // By default, tool result events are emitted by an internal event sender placed before + // all user middlewares (outermost), so events reflect the fully processed tool output. + // To control exactly where in the handler chain tool events are emitted, pass + // NewEventSenderToolWrapper() as one of the Handlers. Its position determines which + // middlewares' effects are visible in the emitted event: + // + // agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + // Handlers: []adk.ChatModelAgentMiddleware{ + // loggingHandler, // Outermost: sees event-sender output + // adk.NewEventSenderToolWrapper(), // Events reflect output from handlers below + // sanitizationHandler, // Innermost: runs first, modifies tool output + // }, + // }) + // + // Handler order: first registered is outermost. So [A, B, C] wraps as A(B(C(tool))). + // The event sender captures tool output in post-processing, so its position controls + // which handlers' modifications are included in the emitted events. + // + // When NewEventSenderToolWrapper is detected in Handlers, the framework skips + // the default event sender to avoid duplicate events. + // + // Tool List Modification: + // + // There are two ways to modify the tool list: + // + // 1. In BeforeAgent: Modify ChatModelAgentContext.Tools ([]tool.BaseTool) directly. This affects + // both the tool info list passed to ChatModel AND the actual tools available for + // execution. Changes persist for the entire agent run. + // + // 2. In BeforeModelRewriteState: Modify state.ToolInfos and state.DeferredToolInfos directly. + // This affects the tool info list passed to ChatModel for this and all subsequent model + // calls (changes are persisted in state). This is the recommended approach for dynamic + // tool filtering/selection based on conversation context. + // + // Modifying tools in WrapModel (e.g. via model.WithTools) is discouraged: changes there + // are NOT persisted in state, only affect a single model call, and break prompt cache. + Handlers []TypedChatModelAgentMiddleware[M] + + // ModelRetryConfig configures retry behavior for the ChatModel. + // When set, the agent will automatically retry failed ChatModel calls + // based on the configured policy. + // Optional. If nil, no retry will be performed. + ModelRetryConfig *TypedModelRetryConfig[M] + + // ModelFailoverConfig configures failover behavior for the ChatModel. + // When set, the agent will first try the last successful model (initially the configured Model), + // and on failure, call GetFailoverModel to select alternate models. + // Model field is still required as it serves as the initial model. + // Optional. If nil, no failover will be performed. + ModelFailoverConfig *ModelFailoverConfig[M] +} + +type ChatModelAgentConfig = TypedChatModelAgentConfig[*schema.Message] + +// TypedChatModelAgent is a chat model-backed agent parameterized by message type. +// +// For M = *schema.Message, the full ReAct loop (model → tool calls → model) is used. +// For M = *schema.AgenticMessage, a single-shot chain is used since agentic models +// handle tool calling internally. Cancel monitoring and retry on the model stream +// are not yet supported for agentic models. +type TypedChatModelAgent[M MessageType] struct { + name string + description string + instruction string + + model model.BaseModel[M] + toolsConfig ToolsConfig + + genModelInput TypedGenModelInput[M] + + outputKey string + maxIterations int + + subAgents []TypedAgent[M] + parentAgent TypedAgent[M] + + disallowTransferToParent bool + + exit tool.BaseTool + + handlers []TypedChatModelAgentMiddleware[M] + middlewares []AgentMiddleware + + modelRetryConfig *TypedModelRetryConfig[M] + modelFailoverConfig *ModelFailoverConfig[M] + + once sync.Once + run typedRunFunc[M] + frozen uint32 + exeCtx *execContext +} + +type ChatModelAgent = TypedChatModelAgent[*schema.Message] + +// typedRunParams holds the parameters for a typedRunFunc invocation. +type typedRunParams[M MessageType] struct { + input *TypedAgentInput[M] + generator *AsyncGenerator[*TypedAgentEvent[M]] + store *bridgeStore + instruction string + returnDirectly map[string]bool + cancelCtx *cancelContext + cancelCtxOwned bool + composeOpts []compose.Option + + afterToolCallsHook func(ctx context.Context) error +} + +type typedRunFunc[M MessageType] func(ctx context.Context, p *typedRunParams[M]) + +func resolveRunCancelContext(ctx context.Context, o *options) (*cancelContext, bool) { + inherited := getCancelContext(ctx) + if o.cancelCtx != nil { + return o.cancelCtx, o.cancelCtx != inherited + } + return inherited, false +} + +// NewChatModelAgent creates a new ChatModelAgent with the given config. +func NewChatModelAgent(ctx context.Context, config *ChatModelAgentConfig) (*ChatModelAgent, error) { + return NewTypedChatModelAgent[*schema.Message](ctx, config) +} + +// NewTypedChatModelAgent creates a new TypedChatModelAgent with the given config. +func NewTypedChatModelAgent[M MessageType](ctx context.Context, config *TypedChatModelAgentConfig[M]) (*TypedChatModelAgent[M], error) { + if config.ModelFailoverConfig != nil { + if config.ModelFailoverConfig.GetFailoverModel == nil { + return nil, errors.New("ModelFailoverConfig.GetFailoverModel is required when ModelFailoverConfig is set") + } + + // ShouldFailover is required when ModelFailoverConfig is set + if config.ModelFailoverConfig.ShouldFailover == nil { + return nil, errors.New("ModelFailoverConfig.ShouldFailover is required when ModelFailoverConfig is set") + } + } + + if config.Model == nil { + return nil, errors.New("agent 'Model' is required") + } + + var genInput TypedGenModelInput[M] + if config.GenModelInput != nil { + genInput = config.GenModelInput + } else { + genInput = newDefaultGenModelInput[M]() + } + + tc := config.ToolsConfig + + // Tool call middleware execution order (outermost to innermost): + // 1. eventSenderToolWrapper (internal - sends tool result events after all modifications) + // 2. User-provided ToolsConfig.ToolCallMiddlewares (original order preserved) + // 3. Middlewares' WrapToolCall (in registration order) + // 4. ChatModelAgentMiddleware.WrapToolCall (in registration order) + // 5. callbackInjectedToolCall (internal - injects callbacks if tool doesn't handle them) + if !hasUserEventSenderToolWrapper(config.Handlers) { + defaultToolEventSender := handlersToToolMiddlewares([]TypedChatModelAgentMiddleware[M]{newTypedEventSenderToolWrapper[M]()}) + tc.ToolCallMiddlewares = append(defaultToolEventSender, tc.ToolCallMiddlewares...) + } + tc.ToolCallMiddlewares = append(tc.ToolCallMiddlewares, collectToolMiddlewaresFromMiddlewares(config.Middlewares)...) + + // Cancel monitoring middleware (innermost — close to the tool endpoint). + // This allows early abort of the raw tool result stream when immediateChan fires + // (CancelImmediate or timeout escalation), while requiring outer wrappers to + // propagate stream errors such as ErrStreamCanceled without swallowing them. + cancelToolHandler := &cancelMonitoredToolHandler{} + tc.ToolCallMiddlewares = append(tc.ToolCallMiddlewares, compose.ToolMiddleware{ + Streamable: cancelToolHandler.WrapStreamableToolCall, + EnhancedStreamable: cancelToolHandler.WrapEnhancedStreamableToolCall, + }) + + return &TypedChatModelAgent[M]{ + name: config.Name, + description: config.Description, + instruction: config.Instruction, + model: config.Model, + toolsConfig: tc, + genModelInput: genInput, + exit: config.Exit, + outputKey: config.OutputKey, + maxIterations: config.MaxIterations, + handlers: config.Handlers, + middlewares: config.Middlewares, + modelRetryConfig: config.ModelRetryConfig, + modelFailoverConfig: config.ModelFailoverConfig, + }, nil +} + +func collectToolMiddlewaresFromMiddlewares(mws []AgentMiddleware) []compose.ToolMiddleware { + var middlewares []compose.ToolMiddleware + for _, m := range mws { + if m.WrapToolCall.Invokable == nil && m.WrapToolCall.Streamable == nil && m.WrapToolCall.EnhancedStreamable == nil && m.WrapToolCall.EnhancedInvokable == nil { + continue + } + middlewares = append(middlewares, m.WrapToolCall) + } + return middlewares +} + +const ( + TransferToAgentToolName = "transfer_to_agent" + TransferToAgentToolDesc = "Transfer the question to another agent." + TransferToAgentToolDescChinese = "将问题移交给其他 Agent。" +) + +var ( + toolInfoTransferToAgent = &schema.ToolInfo{ + Name: TransferToAgentToolName, + + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "agent_name": { + Desc: "the name of the agent to transfer to", + Required: true, + Type: schema.String, + }, + }), + } + + ToolInfoExit = &schema.ToolInfo{ + Name: "exit", + Desc: "Exit the agent process and return the final result.", + + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "final_result": { + Desc: "the final result to return", + Required: true, + Type: schema.String, + }, + }), + } +) + +type ExitTool struct{} + +func (et ExitTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return ToolInfoExit, nil +} + +func (et ExitTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + type exitParams struct { + FinalResult string `json:"final_result"` + } + + params := &exitParams{} + err := sonic.UnmarshalString(argumentsInJSON, params) + if err != nil { + return "", err + } + + err = SendToolGenAction(ctx, "exit", NewExitAction()) + if err != nil { + return "", err + } + + return params.FinalResult, nil +} + +type transferToAgent struct{} + +func (tta transferToAgent) Info(_ context.Context) (*schema.ToolInfo, error) { + desc := internal.SelectPrompt(internal.I18nPrompts{ + English: TransferToAgentToolDesc, + Chinese: TransferToAgentToolDescChinese, + }) + info := *toolInfoTransferToAgent + info.Desc = desc + return &info, nil +} + +func transferToAgentToolOutput(destName string) string { + tpl := internal.SelectPrompt(internal.I18nPrompts{ + English: "successfully transferred to agent [%s]", + Chinese: "成功移交任务至 agent [%s]", + }) + return fmt.Sprintf(tpl, destName) +} + +func (tta transferToAgent) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + type transferParams struct { + AgentName string `json:"agent_name"` + } + + params := &transferParams{} + err := sonic.UnmarshalString(argumentsInJSON, params) + if err != nil { + return "", err + } + + err = SendToolGenAction(ctx, TransferToAgentToolName, NewTransferToAgentAction(params.AgentName)) + if err != nil { + return "", err + } + + return transferToAgentToolOutput(params.AgentName), nil +} + +func (a *TypedChatModelAgent[M]) Name(_ context.Context) string { + return a.name +} + +func (a *TypedChatModelAgent[M]) Description(_ context.Context) string { + return a.description +} + +func (a *TypedChatModelAgent[M]) GetType() string { + return "ChatModel" +} + +// OnSetSubAgents implements OnSubAgents. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func (a *TypedChatModelAgent[M]) OnSetSubAgents(_ context.Context, subAgents []TypedAgent[M]) error { + if atomic.LoadUint32(&a.frozen) == 1 { + return errors.New("agent has been frozen after run") + } + + if len(a.subAgents) > 0 { + return errors.New("agent's sub-agents has already been set") + } + + a.subAgents = subAgents + return nil +} + +// OnSetAsSubAgent implements OnSubAgents. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func (a *TypedChatModelAgent[M]) OnSetAsSubAgent(_ context.Context, parent TypedAgent[M]) error { + if atomic.LoadUint32(&a.frozen) == 1 { + return errors.New("agent has been frozen after run") + } + + if a.parentAgent != nil { + return errors.New("agent has already been set as a sub-agent of another agent") + } + + a.parentAgent = parent + return nil +} + +// OnDisallowTransferToParent implements OnSubAgents. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func (a *TypedChatModelAgent[M]) OnDisallowTransferToParent(_ context.Context) error { + if atomic.LoadUint32(&a.frozen) == 1 { + return errors.New("agent has been frozen after run") + } + + a.disallowTransferToParent = true + + return nil +} + +type ChatModelAgentInterruptInfo struct { + Info *compose.InterruptInfo + Data []byte +} + +func init() { + schema.RegisterName[*ChatModelAgentInterruptInfo]("_eino_adk_chat_model_agent_interrupt_info") +} + +func extractTextContent[M MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return v.Content + case *schema.AgenticMessage: + var texts []string + for _, block := range v.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeAssistantGenText && block.AssistantGenText != nil { + texts = append(texts, block.AssistantGenText.Text) + } + } + return strings.Join(texts, "\n") + default: + return "" + } +} + +func setOutputToSession[M MessageType](ctx context.Context, msg M, msgStream *schema.StreamReader[M], outputKey string) error { + if !isNilMessage(msg) { + AddSessionValue(ctx, outputKey, extractTextContent(msg)) + return nil + } + + concatenated, err := concatMessageStream(msgStream) + if err != nil { + return err + } + + AddSessionValue(ctx, outputKey, extractTextContent(concatenated)) + return nil +} + +func typedErrFunc[M MessageType](err error) typedRunFunc[M] { + return func(ctx context.Context, p *typedRunParams[M]) { + p.generator.Send(&TypedAgentEvent[M]{Err: err}) + } +} + +// ChatModelAgentResumeData holds data that can be provided to a ChatModelAgent during a resume operation +// to modify its behavior. It is provided via the adk.ResumeWithData function. +type ChatModelAgentResumeData struct { + // HistoryModifier is a function that can transform the agent's message history before it is sent to the model. + // This allows for adding new information or context upon resumption. + HistoryModifier func(ctx context.Context, history []Message) []Message +} + +type execContext struct { + instruction string + toolsNodeConf compose.ToolsNodeConfig + returnDirectly map[string]bool + + toolInfos []*schema.ToolInfo + unwrappedTools []tool.BaseTool + + toolSearchTool *schema.ToolInfo // set by BeforeAgent when the model supports native tool search + + rebuildGraph bool // whether needs to instantiate a new graph because of topology changes due to tool modifications + toolUpdated bool // whether needs to pass a compose.WithToolList option to ToolsNode due to tool list change +} + +func (a *TypedChatModelAgent[M]) applyBeforeAgent(ctx context.Context, ec *execContext) (context.Context, *execContext, error) { + runCtx := &ChatModelAgentContext{ + Instruction: ec.instruction, + Tools: cloneSlice(ec.unwrappedTools), + ReturnDirectly: copyMap(ec.returnDirectly), + } + + var err error + for i, handler := range a.handlers { + ctx, runCtx, err = handler.BeforeAgent(ctx, runCtx) + if err != nil { + return ctx, nil, fmt.Errorf("handler[%d] (%T) BeforeAgent failed: %w", i, handler, err) + } + } + + toolsNodeConf := ec.toolsNodeConf + toolsNodeConf.Tools = runCtx.Tools + toolsNodeConf.ToolCallMiddlewares = cloneSlice(ec.toolsNodeConf.ToolCallMiddlewares) + + runtimeEC := &execContext{ + instruction: runCtx.Instruction, + toolsNodeConf: toolsNodeConf, + returnDirectly: runCtx.ReturnDirectly, + toolSearchTool: runCtx.ToolSearchTool, + toolUpdated: true, + rebuildGraph: (len(ec.toolsNodeConf.Tools) == 0 && len(runCtx.Tools) > 0) || + (len(ec.returnDirectly) == 0 && len(runCtx.ReturnDirectly) > 0), + } + + toolInfos, err := genToolInfos(ctx, &runtimeEC.toolsNodeConf) + if err != nil { + return ctx, nil, err + } + + runtimeEC.toolInfos = toolInfos + + return ctx, runtimeEC, nil +} + +func (a *TypedChatModelAgent[M]) applyAfterAgent(ctx context.Context) (context.Context, error) { + if len(a.handlers) == 0 { + return ctx, nil + } + + var state TypedChatModelAgentState[M] + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + state.Messages = st.Messages + state.ToolInfos = st.ToolInfos + state.DeferredToolInfos = st.DeferredToolInfos + return nil + }) + + var err error + for i, handler := range a.handlers { + ctx, err = handler.AfterAgent(ctx, &state) + if err != nil { + return ctx, fmt.Errorf("handler[%d] (%T) AfterAgent failed: %w", i, handler, err) + } + } + return ctx, nil +} + +func (a *TypedChatModelAgent[M]) prepareExecContext(ctx context.Context) (*execContext, error) { + instruction := a.instruction + toolsNodeConf := a.toolsConfig.ToolsNodeConfig + toolsNodeConf.Tools = cloneSlice(a.toolsConfig.Tools) + toolsNodeConf.ToolCallMiddlewares = cloneSlice(a.toolsConfig.ToolCallMiddlewares) + returnDirectly := copyMap(a.toolsConfig.ReturnDirectly) + + transferToAgents := a.subAgents + if a.parentAgent != nil && !a.disallowTransferToParent { + transferToAgents = append(transferToAgents, a.parentAgent) + } + + if len(transferToAgents) > 0 { + transferInstruction := genTransferToAgentInstruction(ctx, transferToAgents) + instruction = concatInstructions(instruction, transferInstruction) + + toolsNodeConf.Tools = append(toolsNodeConf.Tools, &transferToAgent{}) + returnDirectly[TransferToAgentToolName] = true + } + + if a.exit != nil { + toolsNodeConf.Tools = append(toolsNodeConf.Tools, a.exit) + exitInfo, err := a.exit.Info(ctx) + if err != nil { + return nil, err + } + returnDirectly[exitInfo.Name] = true + } + + for _, m := range a.middlewares { + if m.AdditionalInstruction != "" { + instruction = concatInstructions(instruction, m.AdditionalInstruction) + } + toolsNodeConf.Tools = append(toolsNodeConf.Tools, m.AdditionalTools...) + } + + unwrappedTools := cloneSlice(toolsNodeConf.Tools) + + handlerMiddlewares := handlersToToolMiddlewares(a.handlers) + toolsNodeConf.ToolCallMiddlewares = append(toolsNodeConf.ToolCallMiddlewares, handlerMiddlewares...) + + toolInfos, err := genToolInfos(ctx, &toolsNodeConf) + if err != nil { + return nil, err + } + + return &execContext{ + instruction: instruction, + toolsNodeConf: toolsNodeConf, + returnDirectly: returnDirectly, + toolInfos: toolInfos, + unwrappedTools: unwrappedTools, + }, nil +} + +// handleRunFuncError is the common error handler for buildNoToolsRunFunc and buildReActRunFunc. +// It handles compose interrupts (both cancel-triggered and business) +// and generic errors, sending the appropriate event to the generator. +func (a *TypedChatModelAgent[M]) handleRunFuncError( + ctx context.Context, + err error, + cancelCtx *cancelContext, + cancelCtxOwned bool, + store *bridgeStore, + generator *AsyncGenerator[*TypedAgentEvent[M]], +) { + info, ok := compose.ExtractInterruptInfo(err) + if ok { + if cancelCtx != nil { + if !cancelCtx.shouldCancel() { + // Note: there is a benign TOCTOU window here. Between shouldCancel() + // returning false and markDone() executing, a concurrent cancel could + // transition stateRunning→stateCancelling. markDone() then does + // stateCancelling→stateDone, and the cancel func receives + // ErrExecutionEnded (execution finished before cancel took effect). + cancelCtx.markDone() + } + } + + data, existed, sErr := store.Get(ctx, bridgeCheckpointID) + if sErr != nil { + generator.Send(&TypedAgentEvent[M]{AgentName: a.name, Err: fmt.Errorf("failed to get interrupt info: %w", sErr)}) + return + } + if !existed { + generator.Send(&TypedAgentEvent[M]{AgentName: a.name, Err: fmt.Errorf("interrupt occurred but checkpoint data is missing")}) + return + } + + is := FromInterruptContexts(info.InterruptContexts) + event := TypedCompositeInterrupt[M](ctx, info, data, is) + event.Action.Interrupted.Data = &ChatModelAgentInterruptInfo{ + Info: info, + Data: data, + } + event.AgentName = a.name + generator.Send(event) + return + } + + if cancelCtxOwned && cancelCtx != nil { + cancelCtx.markDone() + } + generator.Send(&TypedAgentEvent[M]{Err: err}) +} + +type typedNoToolsInput[M MessageType] struct { + input *TypedAgentInput[M] + instruction string +} + +func appendModelToChain[I, O any, M MessageType](chain *compose.Chain[I, O], m model.BaseModel[M]) { + var zero M + switch any(zero).(type) { + case *schema.Message: + chain.AppendChatModel(any(m).(model.BaseChatModel)) + case *schema.AgenticMessage: + chain.AppendAgenticModel(any(m).(model.AgenticModel)) + } +} + +func (a *TypedChatModelAgent[M]) buildNoToolsRunFunc(_ context.Context) (typedRunFunc[M], error) { + return func(ctx context.Context, p *typedRunParams[M]) { + cancelCtx := p.cancelCtx + ctx = withCancelContext(ctx, cancelCtx) + + wrappedModel := buildModelWrappers(a.model, &typedModelWrapperConfig[M]{ + handlers: a.handlers, + middlewares: a.middlewares, + retryConfig: a.modelRetryConfig, + failoverConfig: a.modelFailoverConfig, + cancelContext: cancelCtx, + }) + + chain := compose.NewChain[typedNoToolsInput[M], M]( + compose.WithGenLocalState(func(ctx context.Context) (state *typedState[M]) { + return &typedState[M]{} + })) + + chain.AppendLambda(compose.InvokableLambda(func(ctx context.Context, in typedNoToolsInput[M]) ([]M, error) { + messages, err := a.genModelInput(ctx, in.instruction, in.input) + if err != nil { + return nil, err + } + if err := compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.Messages = append(st.Messages, messages...) + return nil + }); err != nil { + return nil, err + } + return messages, nil + })) + + appendModelToChain(chain, wrappedModel) + + if len(a.handlers) > 0 { + chain.AppendLambda(compose.InvokableLambda(func(ctx context.Context, msg M) (M, error) { + _, err := a.applyAfterAgent(ctx) + return msg, err + })) + } + + var compileOptions []compose.GraphCompileOption + compileOptions = append(compileOptions, + compose.WithGraphName(a.name), + compose.WithCheckPointStore(p.store), + compose.WithSerializer(&gobSerializer{})) + + if cancelCtx != nil { + var interrupt func(...compose.GraphInterruptOption) + ctx, interrupt = compose.WithGraphInterrupt(ctx) + cancelCtx.setGraphInterruptFunc(interrupt) + } + + r, err := chain.Compile(ctx, compileOptions...) + if err != nil { + p.generator.Send(&TypedAgentEvent[M]{Err: err}) + return + } + + ctx = withTypedChatModelAgentExecCtx(ctx, &typedChatModelAgentExecCtx[M]{ + generator: p.generator, + cancelCtx: cancelCtx, + failoverLastSuccessModel: a.model, + }) + + // Pre-execution cancel check + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode() == CancelImmediate || atomic.LoadInt32(&cancelCtx.escalated) == 1 { + cancelErr, ok := cancelCtx.createAndMarkCancelHandled() + if !ok { + return + } + p.generator.Send(&TypedAgentEvent[M]{Err: cancelErr}) + return + } + } + + in := typedNoToolsInput[M]{input: p.input, instruction: p.instruction} + + var msg M + var msgStream *schema.StreamReader[M] + if p.input.EnableStreaming { + msgStream, err = r.Stream(ctx, in, p.composeOpts...) + } else { + msg, err = r.Invoke(ctx, in, p.composeOpts...) + } + + if err == nil { + if a.outputKey != "" { + err = setOutputToSession(ctx, msg, msgStream, a.outputKey) + if err != nil { + p.generator.Send(&TypedAgentEvent[M]{Err: err}) + } + } else if msgStream != nil { + msgStream.Close() + } + return + } + + a.handleRunFuncError(ctx, err, cancelCtx, p.cancelCtxOwned, p.store, p.generator) + }, nil +} + +func (a *TypedChatModelAgent[M]) buildReActRunFunc(ctx context.Context, bc *execContext) (typedRunFunc[M], error) { + var zero M + switch any(zero).(type) { + case *schema.Message: + return a.buildMessageReActRunFunc(ctx, bc) + case *schema.AgenticMessage: + // single-shot: agentic models handle tool calling internally + return a.buildAgenticReActRunFunc(ctx, bc) + default: + return nil, fmt.Errorf("unsupported message type %T for ReAct run mode", zero) + } +} + +type reactRunInput struct { + input *AgentInput + instruction string +} + +func (a *TypedChatModelAgent[M]) buildMessageReActRunFunc(_ context.Context, bc *execContext) (typedRunFunc[M], error) { + // safe: only called when M = *schema.Message (guarded by type switch in buildReActRunFunc) + msgModel := any(a.model).(model.BaseChatModel) + msgHandlers := any(a.handlers).([]ChatModelAgentMiddleware) + genModelInputFn := any(a.genModelInput).(GenModelInput) + msgConf := &reactConfig{ + model: msgModel, + toolsConfig: &bc.toolsNodeConf, + modelWrapperConf: &modelWrapperConfig{ + handlers: msgHandlers, + middlewares: a.middlewares, + retryConfig: any(a.modelRetryConfig).(*ModelRetryConfig), + failoverConfig: any(a.modelFailoverConfig).(*ModelFailoverConfig[*schema.Message]), + toolInfos: bc.toolInfos, + }, + toolsReturnDirectly: bc.returnDirectly, + agentName: a.name, + maxIterations: a.maxIterations, + } + if len(a.handlers) > 0 { + msgAgent := any(a).(*TypedChatModelAgent[*schema.Message]) + msgConf.afterAgentFunc = func(ctx context.Context, msg *schema.Message) (*schema.Message, error) { + _, err := msgAgent.applyAfterAgent(ctx) + return msg, err + } + } + + return func(ctx context.Context, p *typedRunParams[M]) { + mp := any(p).(*typedRunParams[*schema.Message]) + cancelCtx := mp.cancelCtx + msgConf.cancelCtx = cancelCtx + if msgConf.modelWrapperConf != nil { + msgConf.modelWrapperConf.cancelContext = cancelCtx + } + ctx = withCancelContext(ctx, cancelCtx) + + g, err := newReact(ctx, msgConf) + if err != nil { + mp.generator.Send(&AgentEvent{Err: err}) + return + } + + chain := compose.NewChain[reactRunInput, Message](). + AppendLambda( + compose.InvokableLambda(func(ctx context.Context, in reactRunInput) (*reactInput, error) { + messages, genErr := genModelInputFn(ctx, in.instruction, in.input) + if genErr != nil { + return nil, genErr + } + return &reactInput{ + Messages: messages, + }, nil + }), + ). + AppendGraph(g, compose.WithNodeName("ReAct"), compose.WithGraphCompileOptions(compose.WithMaxRunSteps(math.MaxInt))) + + var compileOptions []compose.GraphCompileOption + compileOptions = append(compileOptions, + compose.WithGraphName(a.name), + compose.WithCheckPointStore(mp.store), + compose.WithSerializer(&gobSerializer{}), + compose.WithMaxRunSteps(math.MaxInt)) + + if cancelCtx != nil { + var interrupt func(...compose.GraphInterruptOption) + ctx, interrupt = compose.WithGraphInterrupt(ctx) + cancelCtx.setGraphInterruptFunc(interrupt) + } + + runnable, err_ := chain.Compile(ctx, compileOptions...) + if err_ != nil { + mp.generator.Send(&AgentEvent{Err: err_}) + return + } + + ctx = withTypedChatModelAgentExecCtx(ctx, &chatModelAgentExecCtx{ + runtimeReturnDirectly: mp.returnDirectly, + generator: mp.generator, + cancelCtx: cancelCtx, + failoverLastSuccessModel: msgModel, + afterToolCallsHook: mp.afterToolCallsHook, + }) + + // Pre-execution cancel check + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode() == CancelImmediate || atomic.LoadInt32(&cancelCtx.escalated) == 1 { + cancelErr, ok := cancelCtx.createAndMarkCancelHandled() + if !ok { + return + } + mp.generator.Send(&AgentEvent{Err: cancelErr}) + return + } + } + + in := reactRunInput{ + input: mp.input, + instruction: mp.instruction, + } + + var runOpts []compose.Option + runOpts = append(runOpts, mp.composeOpts...) + if a.toolsConfig.EmitInternalEvents { + runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withAgentToolEventGenerator(mp.generator)))) + } + if mp.input.EnableStreaming { + runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withAgentToolEnableStreaming(true)))) + } + + var msg Message + var msgStream MessageStream + if mp.input.EnableStreaming { + msgStream, err_ = runnable.Stream(ctx, in, runOpts...) + } else { + msg, err_ = runnable.Invoke(ctx, in, runOpts...) + } + + if err_ == nil { + if a.outputKey != "" { + err_ = setOutputToSession[*schema.Message](ctx, msg, msgStream, a.outputKey) + if err_ != nil { + mp.generator.Send(&AgentEvent{Err: err_}) + } + } else if msgStream != nil { + msgStream.Close() + } + + return + } + + a.handleRunFuncError(ctx, err_, cancelCtx, mp.cancelCtxOwned, mp.store, p.generator) + }, nil +} + +type agenticReactRunInput struct { + input *TypedAgentInput[*schema.AgenticMessage] + instruction string +} + +func (a *TypedChatModelAgent[M]) buildAgenticReActRunFunc(_ context.Context, bc *execContext) (typedRunFunc[M], error) { + agenticModel := any(a.model).(model.AgenticModel) + agenticHandlers := any(a.handlers).([]TypedChatModelAgentMiddleware[*schema.AgenticMessage]) + genModelInputFn := any(a.genModelInput).(TypedGenModelInput[*schema.AgenticMessage]) + agenticConf := &agenticReactConfig{ + model: agenticModel, + toolsConfig: &bc.toolsNodeConf, + modelWrapperConf: &typedModelWrapperConfig[*schema.AgenticMessage]{ + handlers: agenticHandlers, + middlewares: a.middlewares, + retryConfig: any(a.modelRetryConfig).(*TypedModelRetryConfig[*schema.AgenticMessage]), + failoverConfig: any(a.modelFailoverConfig).(*ModelFailoverConfig[*schema.AgenticMessage]), + toolInfos: bc.toolInfos, + }, + toolsReturnDirectly: bc.returnDirectly, + agentName: a.name, + maxIterations: a.maxIterations, + } + if len(a.handlers) > 0 { + agenticAgent := any(a).(*TypedChatModelAgent[*schema.AgenticMessage]) + agenticConf.afterAgentFunc = func(ctx context.Context, msg *schema.AgenticMessage) (*schema.AgenticMessage, error) { + _, err := agenticAgent.applyAfterAgent(ctx) + return msg, err + } + } + + return func(ctx context.Context, p *typedRunParams[M]) { + ap := any(p).(*typedRunParams[*schema.AgenticMessage]) + cancelCtx := ap.cancelCtx + agenticConf.cancelCtx = cancelCtx + if agenticConf.modelWrapperConf != nil { + agenticConf.modelWrapperConf.cancelContext = cancelCtx + } + ctx = withCancelContext(ctx, cancelCtx) + + g, err := newAgenticReact(ctx, agenticConf) + if err != nil { + ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err}) + return + } + + chain := compose.NewChain[agenticReactRunInput, *schema.AgenticMessage](). + AppendLambda( + compose.InvokableLambda(func(ctx context.Context, in agenticReactRunInput) (*agenticReactInput, error) { + messages, genErr := genModelInputFn(ctx, in.instruction, in.input) + if genErr != nil { + return nil, genErr + } + return &agenticReactInput{ + Messages: messages, + }, nil + }), + ). + AppendGraph(g, compose.WithNodeName("ReAct"), compose.WithGraphCompileOptions(compose.WithMaxRunSteps(math.MaxInt))) + + var compileOptions []compose.GraphCompileOption + compileOptions = append(compileOptions, + compose.WithGraphName(a.name), + compose.WithCheckPointStore(ap.store), + compose.WithSerializer(&gobSerializer{}), + compose.WithMaxRunSteps(math.MaxInt)) + + if cancelCtx != nil { + var interrupt func(...compose.GraphInterruptOption) + ctx, interrupt = compose.WithGraphInterrupt(ctx) + cancelCtx.setGraphInterruptFunc(interrupt) + } + + runnable, err_ := chain.Compile(ctx, compileOptions...) + if err_ != nil { + ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err_}) + return + } + + ctx = withTypedChatModelAgentExecCtx(ctx, &typedChatModelAgentExecCtx[*schema.AgenticMessage]{ + runtimeReturnDirectly: ap.returnDirectly, + generator: ap.generator, + cancelCtx: cancelCtx, + failoverLastSuccessModel: agenticModel, + afterToolCallsHook: ap.afterToolCallsHook, + }) + + // Pre-execution cancel check + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode() == CancelImmediate || atomic.LoadInt32(&cancelCtx.escalated) == 1 { + cancelErr, ok := cancelCtx.createAndMarkCancelHandled() + if !ok { + return + } + ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: cancelErr}) + return + } + } + + in := agenticReactRunInput{input: ap.input, instruction: ap.instruction} + + var runOpts []compose.Option + runOpts = append(runOpts, ap.composeOpts...) + if a.toolsConfig.EmitInternalEvents { + runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withTypedAgentToolEventGenerator[*schema.AgenticMessage](ap.generator)))) + } + if ap.input.EnableStreaming { + runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withAgentToolEnableStreaming(true)))) + } + + var msg *schema.AgenticMessage + var msgStream *schema.StreamReader[*schema.AgenticMessage] + if ap.input.EnableStreaming { + msgStream, err_ = runnable.Stream(ctx, in, runOpts...) + } else { + msg, err_ = runnable.Invoke(ctx, in, runOpts...) + } + + if err_ == nil { + if a.outputKey != "" { + err_ = setOutputToSession(ctx, msg, msgStream, a.outputKey) + if err_ != nil { + ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err_}) + } + } else if msgStream != nil { + msgStream.Close() + } + + return + } + + a.handleRunFuncError(ctx, err_, cancelCtx, ap.cancelCtxOwned, ap.store, p.generator) + }, nil +} + +func (a *TypedChatModelAgent[M]) buildRunFunc(ctx context.Context) typedRunFunc[M] { + a.once.Do(func() { + ec, err := a.prepareExecContext(ctx) + if err != nil { + a.run = typedErrFunc[M](err) + return + } + + a.exeCtx = ec + + if len(ec.toolsNodeConf.Tools) == 0 { + var run typedRunFunc[M] + run, err = a.buildNoToolsRunFunc(ctx) + if err != nil { + a.run = typedErrFunc[M](err) + return + } + a.run = run + return + } + + var run typedRunFunc[M] + run, err = a.buildReActRunFunc(ctx, ec) + if err != nil { + a.run = typedErrFunc[M](err) + return + } + a.run = run + }) + + atomic.StoreUint32(&a.frozen, 1) + + return a.run +} + +func (a *TypedChatModelAgent[M]) getRunFunc(ctx context.Context) (context.Context, typedRunFunc[M], *execContext, error) { + defaultRun := a.buildRunFunc(ctx) + bc := a.exeCtx + + if bc == nil { + return ctx, defaultRun, bc, nil + } + + if len(a.handlers) == 0 { + runtimeBC := &execContext{ + instruction: bc.instruction, + toolsNodeConf: bc.toolsNodeConf, + returnDirectly: bc.returnDirectly, + toolInfos: bc.toolInfos, + } + return ctx, defaultRun, runtimeBC, nil + } + + ctx, runtimeBC, err := a.applyBeforeAgent(ctx, bc) + if err != nil { + return ctx, nil, nil, err + } + + if !runtimeBC.rebuildGraph { + return ctx, defaultRun, runtimeBC, nil + } + + var tempRun typedRunFunc[M] + if len(runtimeBC.toolsNodeConf.Tools) == 0 { + tempRun, err = a.buildNoToolsRunFunc(ctx) + if err != nil { + return ctx, nil, nil, err + } + } else { + tempRun, err = a.buildReActRunFunc(ctx, runtimeBC) + if err != nil { + return ctx, nil, nil, err + } + } + + return ctx, tempRun, runtimeBC, nil +} + +func (a *TypedChatModelAgent[M]) Run(ctx context.Context, input *TypedAgentInput[M], opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + + o := getCommonOptions(nil, opts...) + cancelCtx, cancelCtxOwned := resolveRunCancelContext(ctx, o) + + ctx, run, bc, err := a.getRunFunc(ctx) + if err != nil { + go func() { + if cancelCtxOwned && cancelCtx != nil { + defer cancelCtx.markDone() + } + generator.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("ChatModelAgent getRunFunc error: %w", err)}) + generator.Close() + }() + return iterator + } + + co := getComposeOptions(opts) + co = append(co, compose.WithCheckPointID(bridgeCheckpointID)) + runOps := GetImplSpecificOptions[chatModelAgentRunOptions](nil, opts...) + + if bc != nil { + if len(bc.toolInfos) > 0 { + co = append(co, compose.WithChatModelOption(model.WithTools(bc.toolInfos))) + } + if bc.toolSearchTool != nil { + co = append(co, compose.WithChatModelOption(model.WithToolSearchTool(bc.toolSearchTool))) + } + if bc.toolUpdated { + co = append(co, compose.WithToolsNodeOption(compose.WithToolList(bc.toolsNodeConf.Tools...))) + } + } + + go func() { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&TypedAgentEvent[M]{Err: e}) + } + + generator.Close() + }() + + var ( + instruction string + returnDirectly map[string]bool + ) + + if bc != nil { + instruction = bc.instruction + returnDirectly = bc.returnDirectly + } + + run(ctx, &typedRunParams[M]{ + input: input, + generator: generator, + store: newBridgeStore(), + instruction: instruction, + returnDirectly: returnDirectly, + cancelCtx: cancelCtx, + cancelCtxOwned: cancelCtxOwned, + composeOpts: co, + afterToolCallsHook: runOps.afterToolCallsHook, + }) + }() + + if cancelCtxOwned { + return wrapIterWithCancelCtx(iterator, cancelCtx) + } + return iterator +} + +func (a *TypedChatModelAgent[M]) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + + o := getCommonOptions(nil, opts...) + cancelCtx, cancelCtxOwned := resolveRunCancelContext(ctx, o) + + ctx, run, bc, err := a.getRunFunc(ctx) + if err != nil { + go func() { + if cancelCtxOwned && cancelCtx != nil { + defer cancelCtx.markDone() + } + generator.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("ChatModelAgent getRunFunc error: %w", err)}) + generator.Close() + }() + return iterator + } + + co := getComposeOptions(opts) + co = append(co, compose.WithCheckPointID(bridgeCheckpointID)) + resumeRunOps := GetImplSpecificOptions[chatModelAgentRunOptions](nil, opts...) + + if bc != nil { + if len(bc.toolInfos) > 0 { + co = append(co, compose.WithChatModelOption(model.WithTools(bc.toolInfos))) + } + if bc.toolSearchTool != nil { + co = append(co, compose.WithChatModelOption(model.WithToolSearchTool(bc.toolSearchTool))) + } + if bc.toolUpdated { + co = append(co, compose.WithToolsNodeOption(compose.WithToolList(bc.toolsNodeConf.Tools...))) + } + } + + if info == nil { + panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but info is nil", a.Name(ctx))) + } + + if info.InterruptState == nil { + panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but has no state", a.Name(ctx))) + } + + stateByte, ok := info.InterruptState.([]byte) + if !ok { + panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but has invalid interrupt state type: %T", + a.Name(ctx), info.InterruptState)) + } + + // Migrate legacy checkpoints before resume. + // This covers both: + // - v0.7.*: state is stored as a struct wire type (stateV07) under the legacy name. + // - v0.8.0-v0.8.3: state is stored as a GobEncoder payload under the same legacy name and must + // be routed to a GobDecode-compatible compat type via byte-patching. + // The result is re-encoded so the resume path always operates on the current *State. + stateByte, err = preprocessComposeCheckpoint(stateByte) + if err != nil { + go func() { + generator.Send(&TypedAgentEvent[M]{Err: err}) + generator.Close() + }() + return iterator + } + + var historyModifier func(ctx context.Context, history []Message) []Message + if info.ResumeData != nil { + resumeData, ok := info.ResumeData.(*ChatModelAgentResumeData) + if !ok { + panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but has invalid resume data type: %T", + a.Name(ctx), info.ResumeData)) + } + historyModifier = resumeData.HistoryModifier + } + + if historyModifier != nil { + co = append(co, compose.WithStateModifier(func(ctx context.Context, path compose.NodePath, state any) error { + s, ok := state.(*State) + if !ok { + return nil + } + s.Messages = historyModifier(ctx, s.Messages) + return nil + })) + } + + go func() { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&TypedAgentEvent[M]{Err: e}) + } + + generator.Close() + }() + + var ( + instruction string + returnDirectly map[string]bool + ) + + if bc != nil { + instruction = bc.instruction + returnDirectly = bc.returnDirectly + } + + run(ctx, &typedRunParams[M]{ + input: &TypedAgentInput[M]{EnableStreaming: info.EnableStreaming}, + generator: generator, + store: newResumeBridgeStore(bridgeCheckpointID, stateByte), + instruction: instruction, + returnDirectly: returnDirectly, + cancelCtx: cancelCtx, + cancelCtxOwned: cancelCtxOwned, + composeOpts: co, + afterToolCallsHook: resumeRunOps.afterToolCallsHook, + }) + }() + + if cancelCtxOwned { + return wrapIterWithCancelCtx(iterator, cancelCtx) + } + return iterator +} + +func getComposeOptions(opts []AgentRunOption) []compose.Option { + o := GetImplSpecificOptions[chatModelAgentRunOptions](nil, opts...) + var co []compose.Option + if len(o.chatModelOptions) > 0 { + co = append(co, compose.WithChatModelOption(o.chatModelOptions...)) + } + var to []tool.Option + if len(o.toolOptions) > 0 { + to = append(to, o.toolOptions...) + } + for toolName, atos := range o.agentToolOptions { + to = append(to, withAgentToolOptions(toolName, atos)) + } + if len(to) > 0 { + co = append(co, compose.WithToolsNodeOption(compose.WithToolOption(to...))) + } + if o.historyModifier != nil { + co = append(co, compose.WithStateModifier(func(ctx context.Context, path compose.NodePath, state any) error { + s, ok := state.(*State) + if !ok { + return fmt.Errorf("unexpected state type: %T, expected: %T", state, &State{}) + } + s.Messages = o.historyModifier(ctx, s.Messages) + return nil + })) + } + return co +} + +type gobSerializer struct{} + +func (g *gobSerializer) Marshal(v any) ([]byte, error) { + buf := new(bytes.Buffer) + err := gob.NewEncoder(buf).Encode(v) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (g *gobSerializer) Unmarshal(data []byte, v any) error { + buf := bytes.NewBuffer(data) + return gob.NewDecoder(buf).Decode(v) +} + +// preprocessComposeCheckpoint migrates legacy compose checkpoints to the current format. +// It handles the v0.8.0-v0.8.3 format: +// - gob name "_eino_adk_state_v080_" (already byte-patched by preprocessADKCheckpoint +// from "_eino_adk_react_state"), opaque-bytes wire format → decoded as *stateV080 +// +// v0.7 checkpoints need no migration — State is now a plain struct registered under the +// same gob name, and gob handles missing fields gracefully. +// +// Fast path: if the legacy name is not present, skip entirely. +func preprocessComposeCheckpoint(data []byte) ([]byte, error) { + const lenPrefixedCompatName = "\x15" + stateGobNameV080 + if bytes.Contains(data, []byte(lenPrefixedCompatName)) { + // v0.8.0-v0.8.3: already byte-patched by preprocessADKCheckpoint; decode as *stateV080. + migrated, err := compose.MigrateCheckpointState(data, &gobSerializer{}, func(state any) (any, bool, error) { + sc, ok := state.(*stateV080) + if !ok { + return state, false, nil + } + return stateV080ToState(sc), true, nil + }) + if err != nil { + return nil, fmt.Errorf("failed to migrate v0.8.0-v0.8.3 compose checkpoint: %w", err) + } + return migrated, nil + } + + return data, nil +} diff --git a/adk/chatmodel_retry_test.go b/adk/chatmodel_retry_test.go new file mode 100644 index 0000000..e7ef159 --- /dev/null +++ b/adk/chatmodel_retry_test.go @@ -0,0 +1,3208 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "io" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +var errRetryAble = errors.New("retry-able error") +var errNonRetryAble = errors.New("non-retry-able error") + +var instantBackoff = func(_ context.Context, _ int) time.Duration { return time.Millisecond } + +type agentEvent struct { + Err error + Output *AgentOutput + StreamContent string +} + +func drainAgentEvents(t *testing.T, iterator *AsyncIterator[*AgentEvent]) []agentEvent { + t.Helper() + var events []agentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, agentEvent{Err: event.Err, Output: event.Output}) + } + return events +} + +func drainStreamingAgentEvents(t *testing.T, iterator *AsyncIterator[*AgentEvent]) (events []agentEvent, streamTermErrs []error) { + t.Helper() + for { + event, ok := iterator.Next() + if !ok { + break + } + ae := agentEvent{Err: event.Err, Output: event.Output} + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + var chunks []string + for { + msg, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + streamTermErrs = append(streamTermErrs, recvErr) + break + } + if msg != nil { + chunks = append(chunks, msg.Content) + } + } + ae.StreamContent = strings.Join(chunks, "") + } + } + events = append(events, ae) + } + return events, streamTermErrs +} + +func TestChatModelAgentRetry_NoTools_DirectError_Generate(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 3 { + return nil, errRetryAble + } + return schema.AssistantMessage("Success after retry", nil), nil + }).Times(3) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.Equal(t, "Success after retry", event.Output.MessageOutput.Message.Content) + + _, ok = iterator.Next() + assert.False(t, ok) + assert.Equal(t, int32(3), atomic.LoadInt32(&callCount)) +} + +func TestChatModelAgentRetry_NoTools_DirectError_Stream(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + if count < 2 { + return nil, errRetryAble + } + return schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Success", nil), + }), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + _, ok = iterator.Next() + assert.False(t, ok) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) +} + +type streamErrorModel struct { + callCount int32 + failAtChunk int + maxFailures int + tools []*schema.ToolInfo + returnTool bool +} + +func (m *streamErrorModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("Generated", nil), nil +} + +func (m *streamErrorModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&m.callCount, 1) + + sr, sw := schema.Pipe[*schema.Message](10) + go func() { + defer sw.Close() + for i := 0; i < 5; i++ { + if i == m.failAtChunk && int(count) <= m.maxFailures { + sw.Send(nil, errRetryAble) + return + } + if m.returnTool && i == 0 { + sw.Send(schema.AssistantMessage("", []schema.ToolCall{{ + ID: "call-1", + Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}, + }}), nil) + } else { + sw.Send(schema.AssistantMessage("chunk", nil), nil) + } + } + }() + return sr, nil +} + +func (m *streamErrorModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + m.tools = tools + return m, nil +} + +func TestChatModelAgentRetry_StreamError(t *testing.T) { + t.Run("WithTools", func(t *testing.T) { + ctx := context.Background() + + m := &streamErrorModel{ + failAtChunk: 2, + maxFailures: 2, + returnTool: false, + } + + config := &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: m, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&fakeToolForTest{tarCount: 0}}, + }, + }, + } + + agent, err := NewChatModelAgent(ctx, config) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 3, len(events)) + + var streamErrEventCount int + var errs []error + for i, event := range events { + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming { + sr := event.Output.MessageOutput.MessageStream + for { + msg, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + streamErrEventCount++ + errs = append(errs, err) + t.Logf("event %d: err: %v", i, err) + break + } + t.Logf("event %d: %v", i, msg.Content) + } + } + } + + assert.Equal(t, 2, streamErrEventCount) + assert.Equal(t, 2, len(errs)) + var willRetryErr *WillRetryError + assert.True(t, errors.As(errs[0], &willRetryErr)) + assert.True(t, errors.As(errs[1], &willRetryErr)) + assert.Equal(t, int32(3), atomic.LoadInt32(&m.callCount)) + }) + + t.Run("NoTools", func(t *testing.T) { + ctx := context.Background() + + m := &streamErrorModel{ + failAtChunk: 2, + maxFailures: 2, + returnTool: false, + } + + config := &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: m, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + } + + agent, err := NewChatModelAgent(ctx, config) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 3, len(events)) + + var streamErrEventCount int + var errs []error + for i, event := range events { + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming { + sr := event.Output.MessageOutput.MessageStream + for { + msg, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + streamErrEventCount++ + errs = append(errs, err) + t.Logf("event %d: err: %v", i, err) + break + } + t.Logf("event %d: %v", i, msg.Content) + } + } + } + + assert.Equal(t, 2, streamErrEventCount) + assert.Equal(t, 2, len(errs)) + var willRetryErr *WillRetryError + assert.True(t, errors.As(errs[0], &willRetryErr)) + assert.True(t, errors.As(errs[1], &willRetryErr)) + assert.Equal(t, int32(3), atomic.LoadInt32(&m.callCount)) + }) +} + +func TestChatModelAgentRetry_WithTools_DirectError_Generate(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 2 { + return nil, errRetryAble + } + return schema.AssistantMessage("Success after retry", nil), nil + }).Times(2) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + fakeTool := &fakeToolForTest{tarCount: 0} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.Equal(t, "Success after retry", event.Output.MessageOutput.Message.Content) + + _, ok = iterator.Next() + assert.False(t, ok) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) +} + +func TestChatModelAgentRetry_NonRetryableError(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errNonRetryAble).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.True(t, errors.Is(event.Err, errNonRetryAble)) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +type inputCapturingModel struct { + capturedInputs [][]Message +} + +func (m *inputCapturingModel) Generate(_ context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) { + m.capturedInputs = append(m.capturedInputs, input) + return schema.AssistantMessage("Response from capturing model", nil), nil +} + +func (m *inputCapturingModel) Stream(_ context.Context, input []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + m.capturedInputs = append(m.capturedInputs, input) + return schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Response from capturing model", nil), + }), nil +} + +func (m *inputCapturingModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +func TestChatModelAgentRetry_MaxRetriesExhausted(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errRetryAble).Times(4) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.True(t, errors.Is(event.Err, ErrExceedMaxRetries)) + var retryErr *RetryExhaustedError + assert.True(t, errors.As(event.Err, &retryErr)) + assert.True(t, errors.Is(retryErr.LastErr, errRetryAble)) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestChatModelAgentRetry_BackoffFunction(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var backoffCalls []int + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 3 { + return nil, errRetryAble + } + return schema.AssistantMessage("Success", nil), nil + }).Times(3) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + BackoffFunc: func(ctx context.Context, attempt int) time.Duration { + backoffCalls = append(backoffCalls, attempt) + return time.Millisecond + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + + _, ok = iterator.Next() + assert.False(t, ok) + + assert.Equal(t, []int{1, 2}, backoffCalls) +} + +func TestChatModelAgentRetry_NoRetryConfig(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errRetryAble).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent without retry config", + Instruction: "You are a helpful assistant.", + Model: cm, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.True(t, errors.Is(event.Err, errRetryAble)) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestChatModelAgentRetry_WithTools_NonRetryAbleStreamError(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errNonRetryAble).Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + fakeTool := &fakeToolForTest{tarCount: 0} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.True(t, errors.Is(event.Err, errNonRetryAble)) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +type nonRetryAbleStreamErrorModel struct { + tools []*schema.ToolInfo +} + +func (m *nonRetryAbleStreamErrorModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("Generated", nil), nil +} + +func (m *nonRetryAbleStreamErrorModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("chunk1", nil), nil) + sw.Send(nil, errNonRetryAble) + }() + return sr, nil +} + +func (m *nonRetryAbleStreamErrorModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + m.tools = tools + return m, nil +} + +func TestChatModelAgentRetry_NoTools_NonRetryAbleStreamError(t *testing.T) { + ctx := context.Background() + + m := &nonRetryAbleStreamErrorModel{} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent for retry functionality", + Instruction: "You are a helpful assistant.", + Model: m, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 2, len(events)) + + event0 := events[0] + assert.NotNil(t, event0.Output) + assert.NotNil(t, event0.Output.MessageOutput) + assert.True(t, event0.Output.MessageOutput.IsStreaming) + sr := event0.Output.MessageOutput.MessageStream + var streamErr error + for { + _, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + streamErr = err + break + } + } + assert.NotNil(t, streamErr) + assert.True(t, errors.Is(streamErr, errNonRetryAble), "Stream error should be the original error") + + event1 := events[1] + assert.NotNil(t, event1.Err) + assert.True(t, errors.Is(event1.Err, errNonRetryAble)) +} + +func TestDefaultBackoff(t *testing.T) { + ctx := context.Background() + + d1 := defaultBackoff(ctx, 1) + d2 := defaultBackoff(ctx, 2) + d3 := defaultBackoff(ctx, 3) + + t.Logf("Backoff delays: d1=%v, d2=%v, d3=%v", d1, d2, d3) + + assert.True(t, d1 >= 100*time.Millisecond && d1 < 150*time.Millisecond, + "First retry should be ~100ms + jitter (0-50ms), got %v", d1) + assert.True(t, d2 >= 200*time.Millisecond && d2 < 300*time.Millisecond, + "Second retry should be ~200ms + jitter (0-100ms), got %v", d2) + assert.True(t, d3 >= 400*time.Millisecond && d3 < 600*time.Millisecond, + "Third retry should be ~400ms + jitter (0-200ms), got %v", d3) + + d10 := defaultBackoff(ctx, 10) + t.Logf("Backoff delay for attempt 10: %v", d10) + assert.True(t, d10 >= 10*time.Second && d10 <= 15*time.Second, + "Delay should be capped at 10s + jitter (0-5s), got %v", d10) + + d100 := defaultBackoff(ctx, 100) + t.Logf("Backoff delay for attempt 100: %v", d100) + assert.True(t, d100 >= 10*time.Second && d100 <= 15*time.Second, + "Delay should still be capped at 10s + jitter for very high attempts, got %v", d100) +} + +type customError struct { + code int + msg string +} + +func (e *customError) Error() string { + return e.msg +} + +func TestWillRetryError_Unwrap(t *testing.T) { + originalErr := &customError{code: 500, msg: "internal error"} + willRetry := &WillRetryError{ErrStr: originalErr.Error(), RetryAttempt: 1, err: originalErr} + + assert.True(t, errors.Is(willRetry, originalErr)) + + var targetErr *customError + assert.True(t, errors.As(willRetry, &targetErr)) + assert.Equal(t, 500, targetErr.code) + assert.Equal(t, "internal error", targetErr.msg) +} + +func TestChatModelAgentRetry_DefaultIsRetryAble(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 2 { + return nil, errors.New("any error should be retried") + } + return schema.AssistantMessage("Success", nil), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTestAgent", + Description: "Test agent with default IsRetryAble", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.Equal(t, "Success", event.Output.MessageOutput.Message.Content) + + _, ok = iterator.Next() + assert.False(t, ok) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) +} + +func TestSequentialWorkflow_RetryAbleStreamError_SuccessfulRetry(t *testing.T) { + ctx := context.Background() + + retryModel := &streamErrorModel{ + failAtChunk: 2, + maxFailures: 2, + } + + agentA, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AgentA", + Description: "Agent A with retry that emits stream errors then succeeds", + Instruction: "You are agent A.", + Model: retryModel, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + capturingModel := &inputCapturingModel{} + agentB, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AgentB", + Description: "Agent B that captures input", + Instruction: "You are agent B.", + Model: capturingModel, + }) + assert.NoError(t, err) + + sequentialAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "SequentialAgent", + Description: "Sequential agent A->B", + SubAgents: []Agent{agentA, agentB}, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + ctx, _ = initRunCtx(ctx, sequentialAgent.Name(ctx), input) + iterator := sequentialAgent.Run(ctx, input) + + var willRetryErrCount int + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming { + sr := event.Output.MessageOutput.MessageStream + for { + _, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + var retryErr *WillRetryError + if errors.As(err, &retryErr) { + willRetryErrCount++ + } + break + } + } + } + } + + assert.Equal(t, 2, willRetryErrCount, "End-user should receive 2 WillRetryError events") + assert.Equal(t, 1, len(capturingModel.capturedInputs), "Agent B should be called exactly once") + + successorInput := capturingModel.capturedInputs[0] + var hasSuccessfulMessage bool + for _, msg := range successorInput { + if strings.Contains(msg.Content, "chunkchunkchunkchunkchunk") { + hasSuccessfulMessage = true + break + } + } + assert.True(t, hasSuccessfulMessage, "Agent B should receive the successful message from Agent A") + + for _, msg := range successorInput { + assert.NotContains(t, msg.Content, "retry-able error", "Agent B should not receive failed stream messages") + } +} + +type streamErrorModelNoRetry struct { + callCount int32 +} + +func (m *streamErrorModelNoRetry) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("Generated", nil), nil +} + +func (m *streamErrorModelNoRetry) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m.callCount, 1) + sr, sw := schema.Pipe[*schema.Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("chunk1", nil), nil) + sw.Send(schema.AssistantMessage("chunk2", nil), nil) + sw.Send(nil, errRetryAble) + }() + return sr, nil +} + +func (m *streamErrorModelNoRetry) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +func TestSequentialWorkflow_NonRetryAbleStreamError_StopsFlow(t *testing.T) { + ctx := context.Background() + + nonRetryModel := &nonRetryAbleStreamErrorModel{} + + agentA, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AgentA", + Description: "Agent A that emits non-retryable stream error", + Instruction: "You are agent A.", + Model: nonRetryModel, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + }) + assert.NoError(t, err) + + capturingModel := &inputCapturingModel{} + agentB, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AgentB", + Description: "Agent B that captures input", + Instruction: "You are agent B.", + Model: capturingModel, + }) + assert.NoError(t, err) + + sequentialAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "SequentialAgent", + Description: "Sequential agent A->B", + SubAgents: []Agent{agentA, agentB}, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + ctx, _ = initRunCtx(ctx, sequentialAgent.Name(ctx), input) + iterator := sequentialAgent.Run(ctx, input) + + var streamErrFound bool + var finalErrEvent *AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Err != nil { + finalErrEvent = event + } + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming { + sr := event.Output.MessageOutput.MessageStream + for { + _, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + streamErrFound = true + assert.True(t, errors.Is(err, errNonRetryAble), "Stream error should be the original error") + break + } + } + } + } + + assert.True(t, streamErrFound, "End-user should receive stream error") + assert.NotNil(t, finalErrEvent, "Should receive a final error event") + assert.True(t, errors.Is(finalErrEvent.Err, errNonRetryAble), "Final error should be the non-retryable error") + assert.Equal(t, 0, len(capturingModel.capturedInputs), "Agent B should NOT be called due to error") +} + +func TestSequentialWorkflow_NoRetryConfig_StreamError_StopsFlow(t *testing.T) { + ctx := context.Background() + + noRetryModel := &streamErrorModelNoRetry{} + + agentA, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AgentA", + Description: "Agent A without retry config that emits stream error", + Instruction: "You are agent A.", + Model: noRetryModel, + }) + assert.NoError(t, err) + + capturingModel := &inputCapturingModel{} + agentB, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AgentB", + Description: "Agent B that captures input", + Instruction: "You are agent B.", + Model: capturingModel, + }) + assert.NoError(t, err) + + sequentialAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "SequentialAgent", + Description: "Sequential agent A->B", + SubAgents: []Agent{agentA, agentB}, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + ctx, _ = initRunCtx(ctx, sequentialAgent.Name(ctx), input) + iterator := sequentialAgent.Run(ctx, input) + + var streamErrFound bool + var finalErrEvent *AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Err != nil { + finalErrEvent = event + } + if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming { + sr := event.Output.MessageOutput.MessageStream + for { + _, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + streamErrFound = true + assert.True(t, errors.Is(err, errRetryAble), "Stream error should be the original error") + break + } + } + } + } + + assert.True(t, streamErrFound, "End-user should receive stream error") + assert.NotNil(t, finalErrEvent, "Should receive a final error event") + assert.True(t, errors.Is(finalErrEvent.Err, errRetryAble), "Final error should be the original error") + assert.Equal(t, 0, len(capturingModel.capturedInputs), "Agent B should NOT be called due to error") + assert.Equal(t, int32(1), atomic.LoadInt32(&noRetryModel.callCount), "Model should only be called once (no retry)") +} + +// failThenToolCallStreamModel is a ChatModel that: +// - First Stream() call: yields a partial chunk then fails with a retryable error mid-stream. +// - Second Stream() call (retry): yields a tool-call message (success). +// - Third Generate() call (after tool result): yields a final assistant message. +// +// This exercises the path where the eventSenderModel copies the first stream, +// wraps its error as WillRetryError, and sends it as an event to the session. +// The retryModelWrapper then retries, gets a clean stream with a tool call, +// the tool interrupts, and checkpoint save needs to gob-encode the session +// (which still contains the unconsumed WillRetryError event stream). +type failThenToolCallStreamModel struct { + streamCallCount int32 + genCallCount int32 +} + +func (m *failThenToolCallStreamModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m.genCallCount, 1) + return schema.AssistantMessage("final answer", nil), nil +} + +func (m *failThenToolCallStreamModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&m.streamCallCount, 1) + + sr, sw := schema.Pipe[*schema.Message](10) + go func() { + defer sw.Close() + if count == 1 { + // First call: yield a partial chunk then fail. + sw.Send(schema.AssistantMessage("partial", nil), nil) + sw.Send(nil, errRetryAble) + return + } + // Second call (retry): yield a tool-call message. + sw.Send(schema.AssistantMessage("", []schema.ToolCall{{ + ID: "call-1", + Function: schema.FunctionCall{ + Name: "interrupt_tool", + Arguments: `{}`, + }, + }}), nil) + }() + return sr, nil +} + +func (m *failThenToolCallStreamModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +// interruptToolForRetryTest is a tool that always interrupts. +type interruptToolForRetryTest struct{} + +func (t *interruptToolForRetryTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "interrupt_tool", + Desc: "tool that interrupts", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string"}, + }), + }, nil +} + +func (t *interruptToolForRetryTest) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + return "", tool.Interrupt(ctx, "interrupted by tool") +} + +// TestCheckpointSave_WillRetryError_StreamNotConsumed verifies that checkpoint +// saving succeeds when the session contains an event with an unconsumed stream +// that ends with WillRetryError. +// +// Scenario: +// 1. ChatModelAgent with retry (MaxRetries=1) and a tool that always interrupts +// 2. Model.Stream() #1 yields "partial" then errRetryAble mid-stream +// → eventSenderModel copies the stream, wraps the error as WillRetryError, +// sends the event to the session (stream NOT consumed by anyone yet) +// → retryModelWrapper detects error on its copy, retries +// 3. Model.Stream() #2 succeeds with a tool-call message +// 4. Tool executes → interrupts +// 5. Runner.handleIter sees the interrupt → saveCheckPoint → gob encodes runSession +// 6. The session has the WillRetryError event with an unconsumed stream +// → agentEventWrapper.GobEncode proactively consumes the stream via +// getMessageFromWrappedEvent, so MessageVariant.GobEncode sees an error-free +// array and succeeds +func TestCheckpointSave_WillRetryError_StreamNotConsumed(t *testing.T) { + ctx := context.Background() + + mdl := &failThenToolCallStreamModel{} + itool := &interruptToolForRetryTest{} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Agent for checkpoint stream error test", + Instruction: "You are a test agent.", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{itool}, + }, + }, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + IsRetryAble: func(_ context.Context, err error) bool { + return errors.Is(err, errRetryAble) + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + store := newMyStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, + []Message{schema.UserMessage("hello")}, + WithCheckPointID("ckpt-1"), + ) + + for { + event, ok := iter.Next() + if !ok { + break + } + + if event.Err != nil { + t.Logf("event error: %v", event.Err) + } + } + + // Verify the checkpoint was saved successfully. + _, exists, _ := store.Get(ctx, "ckpt-1") + assert.True(t, exists, "checkpoint should be saved successfully; "+ + "if this fails, the WillRetryError stream in the session caused gob encoding to fail") + + // Sanity: the model should have been called twice for Stream (fail + retry). + assert.Equal(t, int32(2), atomic.LoadInt32(&mdl.streamCallCount), + "model should be called twice: first fail, then retry success") +} + +func TestChatModelAgentRetry_ShouldRetry_RejectMessage_Stream(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + r, w := schema.Pipe[*schema.Message](1) + go func() { + if count < 2 { + _ = w.Send(schema.AssistantMessage("bad stream content", nil), nil) + } else { + _ = w.Send(schema.AssistantMessage("good stream content", nil), nil) + } + w.Close() + }() + return r, nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ShouldRetryStreamTestAgent", + Description: "Test ShouldRetry message rejection in stream mode", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{Retry: true} + } + if retryCtx.OutputMessage != nil && strings.Contains(retryCtx.OutputMessage.Content, "bad") { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events, _ := drainStreamingAgentEvents(t, iterator) + var foundGoodContent bool + for _, e := range events { + if e.StreamContent == "good stream content" { + foundGoodContent = true + } + } + require.True(t, foundGoodContent, "should have received good stream content") + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) +} + +func TestShouldRetry_Generate(t *testing.T) { + t.Run("RetryContext_Fields", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 2 { + return schema.AssistantMessage("bad", nil), nil + } + return schema.AssistantMessage("good", nil), nil + }).Times(2) + + var capturedContexts []*RetryContext + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryContextFieldsAgent", + Description: "Test that RetryContext fields are correctly populated", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + capturedContexts = append(capturedContexts, retryCtx) + if retryCtx.OutputMessage != nil && retryCtx.OutputMessage.Content == "bad" { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + for { + event, ok := iterator.Next() + if !ok { + break + } + _ = event + } + + assert.Len(t, capturedContexts, 2, "ShouldRetry should be called twice") + + assert.Equal(t, 1, capturedContexts[0].RetryAttempt) + assert.Len(t, capturedContexts[0].InputMessages, 2) + assert.True(t, len(capturedContexts[0].Options) > 0, "should have default options") + assert.Equal(t, "bad", capturedContexts[0].OutputMessage.Content) + assert.Nil(t, capturedContexts[0].Err) + + assert.Equal(t, 2, capturedContexts[1].RetryAttempt) + assert.Equal(t, "good", capturedContexts[1].OutputMessage.Content) + assert.Nil(t, capturedContexts[1].Err) + }) + + t.Run("RewriteError_OnMessage", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("unrecoverable bad message", nil), nil).Times(1) + + fatalErr := errors.New("fatal: unrecoverable model output") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RewriteErrorTestAgent", + Description: "Test ShouldRetry RewriteError on message", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && strings.Contains(retryCtx.OutputMessage.Content, "unrecoverable") { + return &RetryDecision{ + Retry: false, + RewriteError: fatalErr, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + events := drainAgentEvents(t, iterator) + require.NotEmpty(t, events) + foundErr := false + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, fatalErr) { + foundErr = true + } + } + require.True(t, foundErr, "should have received the fatal rewrite error") + }) + + t.Run("RewriteError_OnError", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + origErr := errors.New("original transient error") + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, origErr).Times(1) + + wrappedErr := errors.New("wrapped: original transient error with more context") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RewriteErrorOnErrorTestAgent", + Description: "Test ShouldRetry RewriteError replacing original error", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{ + Retry: false, + RewriteError: wrappedErr, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + events := drainAgentEvents(t, iterator) + require.NotEmpty(t, events) + foundErr := false + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, wrappedErr) { + foundErr = true + } + } + require.True(t, foundErr, "should have received the wrapped rewrite error") + }) + + t.Run("AdditionalOptions", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + var capturedOpts [][]model.Option + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + capturedOpts = append(capturedOpts, opts) + if count < 2 { + return nil, errRetryAble + } + return schema.AssistantMessage("success", nil), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "AdditionalOptionsTestAgent", + Description: "Test ShouldRetry AdditionalOptions", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{ + Retry: true, + AdditionalOptions: []model.Option{model.WithMaxTokens(8192)}, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + assert.Equal(t, 2, len(capturedOpts)) + assert.Equal(t, len(capturedOpts[0])+1, len(capturedOpts[1])) + }) + + t.Run("ModifiedInputMessages_NoPersist", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + var capturedInputs [][]*schema.Message + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + inputCopy := make([]*schema.Message, len(input)) + copy(inputCopy, input) + capturedInputs = append(capturedInputs, inputCopy) + if count < 2 { + return nil, errRetryAble + } + return schema.AssistantMessage("success", nil), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ModifiedInputNoPersistAgent", + Description: "Test ShouldRetry ModifiedInputMessages without persistence", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{ + Retry: true, + ModifiedInputMessages: []*schema.Message{ + schema.SystemMessage("compressed instruction"), + schema.UserMessage("Hello"), + }, + PersistModifiedInputMessages: false, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + assert.Equal(t, 2, len(capturedInputs)) + assert.Equal(t, "compressed instruction", capturedInputs[1][0].Content, "second call should use modified input") + }) + + t.Run("Backoff", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 2 { + return nil, errRetryAble + } + return schema.AssistantMessage("success", nil), nil + }).Times(2) + + customBackoff := 50 * time.Millisecond + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "BackoffTestAgent", + Description: "Test ShouldRetry custom Backoff in decision", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{ + Retry: true, + Backoff: customBackoff, + } + } + return &RetryDecision{Retry: false} + }, + }, + }) + assert.NoError(t, err) + + start := time.Now() + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + elapsed := time.Since(start) + assert.True(t, elapsed >= customBackoff && elapsed < customBackoff+200*time.Millisecond, "expected backoff ~%v, got %v", customBackoff, elapsed) + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + }) + + t.Run("SuppressFlag_Rejected_NoEvent", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count == 1 { + return schema.AssistantMessage("bad", nil), nil + } + return schema.AssistantMessage("good", nil), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "SuppressRejected", + Description: "Test suppress flag rejects first then accepts", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && retryCtx.OutputMessage.Content == "bad" { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + var msgEvents []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + msgEvents = append(msgEvents, event) + } + } + assert.Equal(t, 1, len(msgEvents), "should have exactly 1 message event (suppressed rejected)") + assert.Equal(t, "good", msgEvents[0].Output.MessageOutput.Message.Content) + }) + + t.Run("SuppressFlag_AllRejected_NoEvents", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("always bad", nil), nil).Times(3) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "SuppressAllRejected", + Description: "Test suppress flag all rejected no events", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: true} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + events := drainAgentEvents(t, iterator) + var msgEventCount int + var foundExhaustedErr bool + for _, e := range events { + if e.Output != nil && e.Output.MessageOutput != nil { + msgEventCount++ + } + if e.Err != nil && errors.Is(e.Err, ErrExceedMaxRetries) { + foundExhaustedErr = true + } + } + assert.Equal(t, 0, msgEventCount, "no message events should be emitted when all are rejected") + require.True(t, foundExhaustedErr, "final event should have RetryExhaustedError") + }) + + t.Run("SuppressFlag_Accepted_FirstAttempt", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("perfect", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "SuppressAcceptedFirst", + Description: "Test suppress flag accepted first attempt", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + var msgEvents []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + msgEvents = append(msgEvents, event) + } + } + assert.Equal(t, 1, len(msgEvents), "should have exactly 1 event") + assert.Equal(t, "perfect", msgEvents[0].Output.MessageOutput.Message.Content) + }) + + t.Run("ContextCanceled_DuringSleep", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&callCount, 1) + return nil, errors.New("transient") + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ContextCancelDuringSleep", + Description: "Test context cancellation during backoff sleep", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 5, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: true} + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 10 * time.Second }, + }, + }) + require.NoError(t, err) + + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + + start := time.Now() + iterator := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + }) + events := drainAgentEvents(t, iterator) + elapsed := time.Since(start) + + require.True(t, elapsed < 2*time.Second, "should not block for full backoff; elapsed: %v", elapsed) + assert.Equal(t, int32(1), atomic.LoadInt32(&callCount)) + + var foundCtxErr bool + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, context.Canceled) { + foundCtxErr = true + } + } + require.True(t, foundCtxErr, "should have received context.Canceled error") + }) +} + +func TestShouldRetry_Stream(t *testing.T) { + t.Run("ErrorRetry", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + streamErr := errors.New("stream unavailable") + var callCount int32 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + if count < 2 { + return nil, streamErr + } + r, w := schema.Pipe[*schema.Message](1) + go func() { + _ = w.Send(schema.AssistantMessage("recovered stream", nil), nil) + w.Close() + }() + return r, nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamErrorRetryAgent", + Description: "Test ShouldRetry when Stream returns error (nil stream)", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events, _ := drainStreamingAgentEvents(t, iterator) + var foundContent bool + for _, e := range events { + if e.StreamContent == "recovered stream" { + foundContent = true + } + } + require.True(t, foundContent, "should have received recovered stream content after error retry") + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + }) + + t.Run("ErrorRewrite", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + streamErr := errors.New("model overloaded") + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, streamErr).Times(1) + + fatalErr := errors.New("fatal: model overloaded, aborting") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamErrorRewriteAgent", + Description: "Test ShouldRetry RewriteError when Stream returns error", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil && strings.Contains(retryCtx.Err.Error(), "overloaded") { + return &RetryDecision{ + Retry: false, + RewriteError: fatalErr, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events := drainAgentEvents(t, iterator) + require.NotEmpty(t, events) + foundErr := false + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, fatalErr) { + foundErr = true + } + } + require.True(t, foundErr, "should have received the fatal rewrite error from stream") + }) + + t.Run("RewriteError_OnMessage", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + r, w := schema.Pipe[*schema.Message](1) + go func() { + _ = w.Send(schema.AssistantMessage("hallucinated garbage output", nil), nil) + w.Close() + }() + return r, nil + }).Times(1) + + fatalErr := errors.New("fatal: hallucinated output detected") + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamRewriteOnMessageAgent", + Description: "Test ShouldRetry RewriteError on successful stream with bad content", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && strings.Contains(retryCtx.OutputMessage.Content, "hallucinated") { + return &RetryDecision{ + Retry: false, + RewriteError: fatalErr, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events := drainAgentEvents(t, iterator) + require.NotEmpty(t, events) + foundErr := false + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, fatalErr) { + foundErr = true + } + } + require.True(t, foundErr, "should have received fatal rewrite error from stream message inspection") + }) + + t.Run("PartialStreamError", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + partialErr := errors.New("connection reset mid-stream") + var callCount int32 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + r, w := schema.Pipe[*schema.Message](1) + go func() { + _ = w.Send(schema.AssistantMessage("partial chunk", nil), nil) + if count < 2 { + w.Send(nil, partialErr) + } else { + _ = w.Send(schema.AssistantMessage(" complete", nil), nil) + w.Close() + } + }() + return r, nil + }).Times(2) + + var capturedContexts []*RetryContext + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamPartialErrorAgent", + Description: "Test ShouldRetry when stream has partial content then error", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + capturedContexts = append(capturedContexts, retryCtx) + if retryCtx.Err != nil { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + for { + _, err := mo.MessageStream.Recv() + if err != nil { + break + } + } + } + } + } + + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + assert.Equal(t, 2, len(capturedContexts)) + assert.NotNil(t, capturedContexts[0].Err, "first attempt should have stream error") + assert.NotNil(t, capturedContexts[0].OutputMessage, "first attempt should have partial message despite error") + assert.Equal(t, "partial chunk", capturedContexts[0].OutputMessage.Content) + }) + + t.Run("ModifiedInputsAndOptions_WithPersist", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + var capturedInputs [][]*schema.Message + var capturedOptLens []int + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + inputCopy := make([]*schema.Message, len(input)) + copy(inputCopy, input) + capturedInputs = append(capturedInputs, inputCopy) + capturedOptLens = append(capturedOptLens, len(opts)) + + r, w := schema.Pipe[*schema.Message](1) + go func() { + if count < 2 { + _ = w.Send(schema.AssistantMessage("too long response exceeds limit", nil), nil) + } else { + _ = w.Send(schema.AssistantMessage("good response", nil), nil) + } + w.Close() + }() + return r, nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamModifiedInputsPersistAgent", + Description: "Test ShouldRetry with ModifiedInputMessages (persist) and AdditionalOptions in stream mode", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && strings.Contains(retryCtx.OutputMessage.Content, "too long") { + return &RetryDecision{ + Retry: true, + ModifiedInputMessages: []*schema.Message{ + schema.SystemMessage("compressed instruction"), + schema.UserMessage("summarized history"), + }, + PersistModifiedInputMessages: true, + AdditionalOptions: []model.Option{model.WithMaxTokens(16384)}, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events, _ := drainStreamingAgentEvents(t, iterator) + var foundGood bool + for _, e := range events { + if e.StreamContent == "good response" { + foundGood = true + } + } + + require.True(t, foundGood, "should have received good response after retry with modified inputs") + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + assert.Equal(t, 2, len(capturedInputs)) + assert.Equal(t, "compressed instruction", capturedInputs[1][0].Content, "second call should use modified input") + assert.Equal(t, "summarized history", capturedInputs[1][1].Content) + assert.Equal(t, capturedOptLens[0]+1, capturedOptLens[1]) + }) + + t.Run("VerdictSignal_CleanStream_Rejected", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + if count == 1 { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("bad", nil)}), nil + } + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("good", nil)}), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "VerdictCleanRejected", + Description: "Test verdict signal on clean stream rejected", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && retryCtx.OutputMessage.Content == "bad" { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + var streamEvents []int + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + idx := len(streamEvents) + streamEvents = append(streamEvents, idx) + var lastErr error + for { + _, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + lastErr = recvErr + break + } + } + if idx == 0 { + var willRetryErr *WillRetryError + assert.True(t, errors.As(lastErr, &willRetryErr), "first stream should end with WillRetryError") + } else { + assert.ErrorIs(t, lastErr, io.EOF, "second stream should end with io.EOF") + } + } + } + } + assert.Equal(t, 2, len(streamEvents), "should have exactly 2 stream events") + assert.Equal(t, int32(2), atomic.LoadInt32(&callCount)) + }) + + t.Run("VerdictSignal_StreamError_Rejected", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + streamErr := errors.New("mid-stream error") + var callCount int32 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&callCount, 1) + if count == 1 { + r, w := schema.Pipe[*schema.Message](1) + go func() { + _ = w.Send(schema.AssistantMessage("partial", nil), nil) + w.Send(nil, streamErr) + }() + return r, nil + } + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("good", nil)}), nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "VerdictStreamErrorRejected", + Description: "Test verdict signal on stream error rejected", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + var firstEventHasWillRetry bool + var eventCount int + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + eventCount++ + for { + _, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + if eventCount == 1 { + var willRetryErr *WillRetryError + if errors.As(recvErr, &willRetryErr) { + firstEventHasWillRetry = true + } + } + break + } + } + } + } + } + assert.True(t, firstEventHasWillRetry, "first event stream should end with WillRetryError via errWrapper path") + assert.Equal(t, 2, eventCount, "should have 2 stream events") + }) + + t.Run("VerdictSignal_Accepted_FirstAttempt", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("perfect", nil)}), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "VerdictAcceptedFirst", + Description: "Test verdict signal accepted first attempt", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + var eventCount int + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + eventCount++ + var lastErr error + for { + _, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + lastErr = recvErr + break + } + } + assert.ErrorIs(t, lastErr, io.EOF, "accepted stream should end with io.EOF") + } + } + } + assert.Equal(t, 1, eventCount, "should have exactly 1 event") + }) + + t.Run("VerdictSignal_AllRejected_Exhausted", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("always bad", nil)}), nil + }).Times(3) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "VerdictAllRejected", + Description: "Test verdict signal all rejected exhausted", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: true} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events, streamTermErrs := drainStreamingAgentEvents(t, iterator) + var willRetryCount int + var foundExhaustedErr bool + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, ErrExceedMaxRetries) { + foundExhaustedErr = true + } + } + for _, termErr := range streamTermErrs { + var willRetryErr *WillRetryError + if errors.As(termErr, &willRetryErr) { + willRetryCount++ + } + } + assert.Equal(t, 3, willRetryCount, "all 3 stream events should end with WillRetryError") + require.True(t, foundExhaustedErr, "final error should be RetryExhaustedError") + }) + + t.Run("ShouldRetry_Panics_VerdictStillSent", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("trigger panic", nil)}), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ShouldRetryPanicsAgent", + Description: "Test that ShouldRetry panic sends verdict signal and does not deadlock", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + panic("deliberate panic in ShouldRetry") + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + + done := make(chan struct{}) + var events []agentEvent + go func() { + defer close(done) + iterator := agent.Run(ctx, input) + events = drainAgentEvents(t, iterator) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("test deadlocked — verdict signal was not sent after ShouldRetry panic") + } + require.NotEmpty(t, events) + var foundPanicErr bool + for _, e := range events { + if e.Err != nil && strings.Contains(e.Err.Error(), "panic") { + foundPanicErr = true + } + } + assert.True(t, foundPanicErr, "should have received a panic error event") + }) +} + +func TestErrStreamCanceled(t *testing.T) { + t.Run("Stream_ShouldRetry_NeverRetried", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + r, w := schema.Pipe[*schema.Message](1) + go func() { + _ = w.Send(schema.AssistantMessage("partial", nil), nil) + w.Send(nil, ErrStreamCanceled) + }() + return r, nil + }).Times(1) + + var shouldRetryCalled int32 + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamCanceledShouldRetry", + Description: "Test ErrStreamCanceled never retried with ShouldRetry", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + atomic.AddInt32(&shouldRetryCalled, 1) + return &RetryDecision{Retry: true} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + for { + _, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + break + } + } + } + } + } + assert.Equal(t, int32(0), atomic.LoadInt32(&shouldRetryCalled), "ShouldRetry should never be called for ErrStreamCanceled") + }) + + t.Run("Stream_LegacyIsRetryAble_NeverRetried", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + r, w := schema.Pipe[*schema.Message](1) + go func() { + _ = w.Send(schema.AssistantMessage("partial", nil), nil) + w.Send(nil, ErrStreamCanceled) + }() + return r, nil + }).Times(1) + + var isRetryAbleCalled int32 + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamCanceledLegacy", + Description: "Test ErrStreamCanceled never retried with legacy IsRetryAble", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(_ context.Context, err error) bool { + atomic.AddInt32(&isRetryAbleCalled, 1) + return true + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + for { + _, recvErr := mo.MessageStream.Recv() + if recvErr != nil { + break + } + } + } + } + } + assert.Equal(t, int32(0), atomic.LoadInt32(&isRetryAbleCalled), "IsRetryAble should never be called for ErrStreamCanceled") + }) + + t.Run("Generate_ShouldRetry_NeverRetried", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, ErrStreamCanceled).Times(1) + + var shouldRetryCalled int32 + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "GenCanceledShouldRetry", + Description: "Test ErrStreamCanceled in Generate never retried", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + atomic.AddInt32(&shouldRetryCalled, 1) + return &RetryDecision{Retry: true} + }, + BackoffFunc: instantBackoff, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + for { + _, ok := iterator.Next() + if !ok { + break + } + } + assert.Equal(t, int32(0), atomic.LoadInt32(&shouldRetryCalled), "ShouldRetry should never be called for ErrStreamCanceled") + }) +} + +func TestAttack_ShouldRetry_NilDecisionOnEveryCall(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("ok", nil), nil).Times(1) + + var shouldRetryCalls int32 + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "NilDecisionAgent", + Description: "ShouldRetry always returns nil — should accept on first call", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + atomic.AddInt32(&shouldRetryCalls, 1) + return nil + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + iterator := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("Hello")}}) + events := drainAgentEvents(t, iterator) + + require.NotEmpty(t, events) + assert.Equal(t, int32(1), atomic.LoadInt32(&shouldRetryCalls)) + var foundOK bool + for _, e := range events { + if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.Message != nil { + if e.Output.MessageOutput.Message.Content == "ok" { + foundOK = true + } + } + } + assert.True(t, foundOK, "nil decision should accept the message as-is") +} + +func TestAttack_ShouldRetry_MaxRetriesZero_RejectFirstAttempt(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("bad", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "MaxZeroRejectAgent", + Description: "MaxRetries=0 with ShouldRetry rejecting — should exhaust immediately", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 0, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: true} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + iterator := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("Hello")}}) + events := drainAgentEvents(t, iterator) + + var foundExhausted bool + for _, e := range events { + if e.Err != nil { + var exhaustedErr *RetryExhaustedError + if errors.As(e.Err, &exhaustedErr) { + foundExhausted = true + } + } + } + assert.True(t, foundExhausted, "MaxRetries=0 with Retry:true should produce RetryExhaustedError") +} + +func TestAttack_ShouldRetry_RetryTrueWithRewriteError_IgnoresRewrite(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count == 1 { + return nil, errors.New("transient") + } + return schema.AssistantMessage("success", nil), nil + }).Times(2) + + rewriteErr := errors.New("this should be ignored") + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryTrueRewriteAgent", + Description: "Retry=true with RewriteError should ignore the rewrite", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{Retry: true, RewriteError: rewriteErr} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + iterator := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("Hello")}}) + events := drainAgentEvents(t, iterator) + + var foundSuccess bool + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, rewriteErr) { + t.Fatal("RewriteError should be ignored when Retry=true") + } + if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.Message != nil { + if e.Output.MessageOutput.Message.Content == "success" { + foundSuccess = true + } + } + } + assert.True(t, foundSuccess, "should eventually succeed after retry, ignoring RewriteError") +} + +func TestAttack_ShouldRetry_OptionsAccumulateAcrossRetries(t *testing.T) { + ctx := context.Background() + + var capturedOpts [][]model.Option + var callCount int32 + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + capturedOpts = append(capturedOpts, opts) + if count <= 2 { + return nil, errors.New("needs retry") + } + return schema.AssistantMessage("done", nil), nil + }).Times(3) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OptsAccumulateAgent", + Description: "Verify options accumulate across retries", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 5, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{ + Retry: true, + AdditionalOptions: []model.Option{model.WithMaxTokens(100 * retryCtx.RetryAttempt)}, + } + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + iterator := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("Hello")}}) + drainAgentEvents(t, iterator) + + require.Len(t, capturedOpts, 3) + assert.True(t, len(capturedOpts[1]) > len(capturedOpts[0]), + "second call should have more options than first (accumulated AdditionalOptions)") + assert.True(t, len(capturedOpts[2]) > len(capturedOpts[1]), + "third call should have more options than second (accumulated AdditionalOptions)") +} + +func TestAttack_ShouldRetry_Stream_NilDecisionAccepts(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("stream ok", nil)}), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamNilDecisionAgent", + Description: "ShouldRetry returns nil in stream mode — should accept", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return nil + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + + events, streamTermErrs := drainStreamingAgentEvents(t, iterator) + var foundStreamContent bool + for _, e := range events { + if e.StreamContent == "stream ok" { + foundStreamContent = true + } + } + assert.True(t, foundStreamContent, "nil decision should accept the stream") + for _, termErr := range streamTermErrs { + assert.Equal(t, io.EOF, termErr, "stream should terminate with clean EOF, not error") + } +} + +func TestAttack_ShouldRetry_Stream_MaxRetriesZero_Exhausted(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("rejected", nil)}), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamMaxZeroAgent", + Description: "Stream mode with MaxRetries=0 rejecting — should exhaust immediately", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 0, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: true} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + + done := make(chan struct{}) + var events []agentEvent + go func() { + defer close(done) + iterator := agent.Run(ctx, input) + events, _ = drainStreamingAgentEvents(t, iterator) + }() + + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("test deadlocked — Stream MaxRetries=0 with reject should not hang") + } + + var foundExhausted bool + for _, e := range events { + if e.Err != nil { + var exhaustedErr *RetryExhaustedError + if errors.As(e.Err, &exhaustedErr) { + foundExhausted = true + } + } + } + assert.True(t, foundExhausted, "MaxRetries=0 stream reject should produce RetryExhaustedError") +} + +func TestAttack_ShouldRetry_Stream_RewriteErrorOnCleanStream(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("looks good but bad", nil)}), nil + }).Times(1) + + fatalErr := errors.New("fatal: content policy violation") + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "StreamRewriteCleanAgent", + Description: "Stream returns cleanly but ShouldRetry rewrites to error", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: false, RewriteError: fatalErr} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + + done := make(chan struct{}) + var events []agentEvent + go func() { + defer close(done) + iterator := agent.Run(ctx, input) + events, _ = drainStreamingAgentEvents(t, iterator) + }() + + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("test deadlocked") + } + + var foundFatal bool + for _, e := range events { + if e.Err != nil && errors.Is(e.Err, fatalErr) { + foundFatal = true + } + } + assert.True(t, foundFatal, "RewriteError on clean stream should propagate the fatal error") +} + +func TestAttack_ShouldRetry_ConcatMessagesFails_EmptyStream(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + r, w := schema.Pipe[*schema.Message](1) + w.Close() + return r, nil + }).Times(1) + + var capturedCtx *RetryContext + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "EmptyStreamAgent", + Description: "Stream returns zero chunks — both OutputMessage and Err should be nil", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + capturedCtx = retryCtx + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + + done := make(chan struct{}) + go func() { + defer close(done) + iterator := agent.Run(ctx, input) + drainStreamingAgentEvents(t, iterator) + }() + + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("test deadlocked on empty stream") + } + + require.NotNil(t, capturedCtx) + assert.NotNil(t, capturedCtx.OutputMessage, "empty stream should have non-nil OutputMessage from ConcatMessages") + assert.Nil(t, capturedCtx.Err, "empty stream should have nil Err") +} + +func TestAttack_ShouldRetry_Stream_MidStreamError_VerdictDoubleRead(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + midStreamErr := errors.New("mid-stream transient error") + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + r, w := schema.Pipe[*schema.Message](1) + go func() { + defer w.Close() + _ = w.Send(schema.AssistantMessage("chunk1", nil), nil) + _ = w.Send(nil, midStreamErr) + }() + return r, nil + }).Times(2) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "DoubleReadBugAgent", + Description: "Reproduces signal.ch double-read when event stream hits mid-stream error then EOF", + Instruction: "You are a helpful assistant.", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: instantBackoff, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + EnableStreaming: true, + } + + done := make(chan struct{}) + go func() { + defer close(done) + iterator := agent.Run(ctx, input) + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil { + mo := event.Output.MessageOutput + if mo.IsStreaming && mo.MessageStream != nil { + for { + _, recvErr := mo.MessageStream.Recv() + if recvErr == io.EOF { + break + } + } + } + } + } + }() + + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("goroutine leak: onEOF blocked on signal.ch after errWrapper already drained the verdict") + } +} + +type rejectReasonTestModel struct { + streamFn func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) +} + +func (m *rejectReasonTestModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("generated", nil), nil +} + +func (m *rejectReasonTestModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return m.streamFn(ctx, input, opts...) +} + +func TestRejectReason_StreamPath(t *testing.T) { + ctx := context.Background() + + type rejectInfo struct { + Reason string + Attempt int + } + + streamErr := errors.New("bad output") + var streamCallCount int32 + + m := &rejectReasonTestModel{ + streamFn: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + n := atomic.AddInt32(&streamCallCount, 1) + if n == 1 { + return streamWithMidError( + []*schema.Message{schema.AssistantMessage("rejected chunk", nil)}, + streamErr, + ), nil + } + sr, sw := schema.Pipe[*schema.Message](1) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("accepted", nil), nil) + }() + return sr, nil + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "reject-reason-agent", + Description: "test reject reason", + Instruction: "test", + Model: m, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(_ context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.Err != nil { + return &RetryDecision{ + Retry: true, + RejectReason: rejectInfo{ + Reason: "output quality too low", + Attempt: retryCtx.RetryAttempt, + }, + } + } + return nil + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return time.Millisecond }, + }, + }) + require.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("hello")}, + EnableStreaming: true, + } + ctx, _ = initRunCtx(ctx, agent.Name(ctx), input) + iter := agent.Run(ctx, input) + + var capturedRejectReasons []any + var finalContent string + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + continue + } + if ev.Output != nil && ev.Output.MessageOutput != nil && ev.Output.MessageOutput.IsStreaming { + sr := ev.Output.MessageOutput.MessageStream + for { + chunk, recvErr := sr.Recv() + if recvErr != nil { + var willRetry *WillRetryError + if errors.As(recvErr, &willRetry) { + capturedRejectReasons = append(capturedRejectReasons, willRetry.RejectReason()) + } + break + } + if chunk != nil { + finalContent = chunk.Content + } + } + } + } + + assert.Contains(t, finalContent, "accepted") + require.NotEmpty(t, capturedRejectReasons, "should have at least one WillRetryError with RejectReason from stream Recv()") + for _, reason := range capturedRejectReasons { + require.NotNil(t, reason) + ri, ok := reason.(rejectInfo) + require.True(t, ok, "RejectReason should be rejectInfo type, got %T", reason) + assert.Equal(t, "output quality too low", ri.Reason) + assert.Equal(t, 1, ri.Attempt) + } +} + +func TestWillRetryError_RejectReason(t *testing.T) { + t.Run("nil when not set", func(t *testing.T) { + wrErr := &WillRetryError{ErrStr: "test", RetryAttempt: 1, err: errors.New("test")} + assert.Nil(t, wrErr.RejectReason(), "RejectReason should be nil when not set") + }) + + t.Run("returns value when set", func(t *testing.T) { + reason := map[string]string{"key": "value"} + wrErr := &WillRetryError{ + ErrStr: "rejected", + RetryAttempt: 2, + rejectReason: reason, + err: errors.New("inner"), + } + assert.Equal(t, reason, wrErr.RejectReason()) + assert.Equal(t, "rejected", wrErr.Error()) + assert.Equal(t, 2, wrErr.RetryAttempt) + }) +} diff --git a/adk/chatmodel_test.go b/adk/chatmodel_test.go new file mode 100644 index 0000000..2c92064 --- /dev/null +++ b/adk/chatmodel_test.go @@ -0,0 +1,2417 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +// TestChatModelAgentRun tests the Run method of ChatModelAgent +func TestChatModelAgentRun(t *testing.T) { + // Basic test for Run method + t.Run("BasicFunctionality", func(t *testing.T) { + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil). + Times(1) + + // Create a ChatModelAgent + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Hello, who are you?"), + }, + } + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + + // Verify the message content + msg := event.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.Equal(t, "Hello, I am an AI assistant.", msg.Content) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) + + t.Run("BasicChatModelWithAgentMiddleware", func(t *testing.T) { + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil). + Times(1) + + afterChatModelExecuted := false + + // Create a ChatModelAgent + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + Middlewares: []AgentMiddleware{ + { + BeforeChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + state.Messages = append(state.Messages, schema.UserMessage("m")) + return nil + }, + AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + assert.Len(t, state.Messages, 4) + afterChatModelExecuted = true + return nil + }, + }, + }, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Hello, who are you?"), + }, + } + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + + // Verify the message content + msg := event.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.Equal(t, "Hello, I am an AI assistant.", msg.Content) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + + assert.True(t, afterChatModelExecuted) + }) + + t.Run("AfterChatModel_NoTools_ModifyDoesNotAffectEvent", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("original content", nil), nil). + Times(1) + + var capturedMessages []*schema.Message + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for AfterChatModel NoTools scenario", + Instruction: "You are a helpful assistant.", + Model: cm, + Middlewares: []AgentMiddleware{ + { + AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + capturedMessages = make([]*schema.Message, len(state.Messages)) + copy(capturedMessages, state.Messages) + state.Messages = append(state.Messages, schema.AssistantMessage("appended content", nil)) + return nil + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.Equal(t, "original content", msg.Content) + + _, ok = iterator.Next() + assert.False(t, ok) + + assert.Len(t, capturedMessages, 3) + }) + + t.Run("AfterChatModel_ReAct_ModifyAffectsFlow", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("first response with tool call", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}}, + }), nil + } + return schema.AssistantMessage("final response", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + toolCalled := false + testTool := &fakeToolForTest{tarCount: 0} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for AfterChatModel ReAct scenario", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + lastMsg := state.Messages[len(state.Messages)-1] + if len(lastMsg.ToolCalls) > 0 { + toolCalled = true + state.Messages[len(state.Messages)-1] = schema.AssistantMessage("modified to remove tool call", nil) + } + return nil + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.True(t, toolCalled) + assert.Equal(t, 1, generateCount) + + assert.Equal(t, 1, len(events)) + event := events[0] + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.Equal(t, "first response with tool call", event.Output.MessageOutput.Message.Content) + assert.Len(t, event.Output.MessageOutput.Message.ToolCalls, 1) + }) + + t.Run("AfterChatModel_ReAct_AppendToolCall_AffectsFlow", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("first response no tool", nil), nil + } + return schema.AssistantMessage("final response", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + testTool := &fakeToolForTest{tarCount: 0} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for AfterChatModel ReAct append tool call", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + if generateCount == 1 { + state.Messages[len(state.Messages)-1] = schema.AssistantMessage("modified with tool call", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: "test_tool", Arguments: "{}"}}, + }) + } + return nil + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 2, generateCount) + + assert.Equal(t, 3, len(events)) + + event0 := events[0] + assert.NotNil(t, event0.Output) + assert.NotNil(t, event0.Output.MessageOutput) + assert.Equal(t, "first response no tool", event0.Output.MessageOutput.Message.Content) + assert.Empty(t, event0.Output.MessageOutput.Message.ToolCalls) + + event2 := events[2] + assert.NotNil(t, event2.Output) + assert.NotNil(t, event2.Output.MessageOutput) + assert.Equal(t, "final response", event2.Output.MessageOutput.Message.Content) + }) + + // Test with streaming output + t.Run("StreamOutput", func(t *testing.T) { + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create a stream reader for the mock response + sr := schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Hello", nil), + schema.AssistantMessage(", I am", nil), + schema.AssistantMessage(" an AI assistant.", nil), + }) + + // Set up expectations for the mock model + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(sr, nil). + Times(1) + + // Create a ChatModelAgent + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent with streaming enabled + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello, who are you?")}, + EnableStreaming: true, + } + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) + + // Test error handling + t.Run("ErrorHandling", func(t *testing.T) { + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model to return an error + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errors.New("model error")). + Times(1) + + // Create a ChatModelAgent + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello, who are you?")}, + } + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator, should contain an error + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.Contains(t, event.Err.Error(), "model error") + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) + + // Test with tools + t.Run("WithTools", func(t *testing.T) { + ctx := context.Background() + + // Create a fake tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 1, + } + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }}), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Task completed", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create a ChatModelAgent with tools + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Use the test tool")}, + } + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get events from the iterator + // First event should be the model output with tool call + event1, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event1) + assert.Nil(t, event1.Err) + assert.NotNil(t, event1.Output) + assert.NotNil(t, event1.Output.MessageOutput) + assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role) + + // Second event should be the tool output + event2, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event2) + assert.Nil(t, event2.Err) + assert.NotNil(t, event2.Output) + assert.NotNil(t, event2.Output.MessageOutput) + assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role) + + // Third event should be the final model output + event3, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event3) + assert.Nil(t, event3.Err) + assert.NotNil(t, event3.Output) + assert.NotNil(t, event3.Output.MessageOutput) + assert.Equal(t, schema.Assistant, event3.Output.MessageOutput.Role) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) + + t.Run("WrapToolCall_ToolMiddleware", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + fakeTool := &fakeToolForTest{tarCount: 1} + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + var capturedToolName string + var capturedArgs string + middlewareInvoked := false + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test WrapToolCall middleware", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + WrapToolCall: compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + middlewareInvoked = true + capturedToolName = input.Name + capturedArgs = input.Arguments + return next(ctx, input) + } + }, + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.True(t, middlewareInvoked, "WrapToolCall middleware should have been invoked") + assert.Equal(t, info.Name, capturedToolName) + assert.Equal(t, `{"name":"test"}`, capturedArgs) + assert.Equal(t, 3, len(events)) + assert.Equal(t, schema.Assistant, events[0].Output.MessageOutput.Role) + assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) + assert.Equal(t, schema.Assistant, events[2].Output.MessageOutput.Role) + assert.Equal(t, "done", events[2].Output.MessageOutput.Message.Content) + }) + + t.Run("WrapToolCall_MultipleMiddlewares_Order", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + fakeTool := &fakeToolForTest{tarCount: 1} + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + var order []string + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test WrapToolCall middleware order", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + WrapToolCall: compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + order = append(order, "mw1_before") + out, err := next(ctx, input) + order = append(order, "mw1_after") + return out, err + } + }, + }, + }, + { + WrapToolCall: compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + order = append(order, "mw2_before") + out, err := next(ctx, input) + order = append(order, "mw2_after") + return out, err + } + }, + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + } + iterator := agent.Run(ctx, input) + + for { + _, ok := iterator.Next() + if !ok { + break + } + } + + // First registered middleware is outermost + assert.Equal(t, []string{"mw1_before", "mw2_before", "mw2_after", "mw1_after"}, order) + }) + + t.Run("WrapToolCall_ModifyResult", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + fakeTool := &fakeToolForTest{tarCount: 1} + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test WrapToolCall modify result", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + WrapToolCall: compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + out, err := next(ctx, input) + if err != nil { + return out, err + } + out.Result = "modified: " + out.Result + return out, nil + } + }, + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 3, len(events)) + // Tool result event should have the modified content + toolEvent := events[1] + assert.Equal(t, schema.Tool, toolEvent.Output.MessageOutput.Role) + assert.Contains(t, toolEvent.Output.MessageOutput.Message.Content, "modified:") + }) + + t.Run("WrapToolCall_StreamableMiddleware", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + streamTool := &fakeStreamToolForTest{tarCount: 1} + info, err := streamTool.Info(ctx) + assert.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{"name":"test"}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + middlewareInvoked := false + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test Streamable WrapToolCall middleware", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{streamTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + WrapToolCall: compose.ToolMiddleware{ + Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + middlewareInvoked = true + return next(ctx, input) + } + }, + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.True(t, middlewareInvoked, "Streamable WrapToolCall middleware should have been invoked") + assert.Equal(t, 3, len(events)) + assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) + }) + + t.Run("WrapToolCall_EnhancedInvokableMiddleware", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + enhTool := &fakeEnhancedInvokableToolForTest{} + info, err := enhTool.Info(ctx) + assert.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + middlewareInvoked := false + var capturedToolName string + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test EnhancedInvokable WrapToolCall middleware", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{enhTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + WrapToolCall: compose.ToolMiddleware{ + EnhancedInvokable: func(next compose.EnhancedInvokableToolEndpoint) compose.EnhancedInvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + middlewareInvoked = true + capturedToolName = input.Name + return next(ctx, input) + } + }, + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.True(t, middlewareInvoked, "EnhancedInvokable WrapToolCall middleware should have been invoked") + assert.Equal(t, info.Name, capturedToolName) + assert.Equal(t, 3, len(events)) + assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) + }) + + t.Run("WrapToolCall_EnhancedStreamableMiddleware", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + enhStreamTool := &fakeEnhancedStreamableToolForTest{} + info, err := enhStreamTool.Info(ctx) + assert.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: info.Name, Arguments: `{}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + middlewareInvoked := false + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test EnhancedStreamable WrapToolCall middleware", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{enhStreamTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + WrapToolCall: compose.ToolMiddleware{ + EnhancedStreamable: func(next compose.EnhancedStreamableToolEndpoint) compose.EnhancedStreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + middlewareInvoked = true + return next(ctx, input) + } + }, + }, + }, + }, + }) + assert.NoError(t, err) + + input := &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + } + iterator := agent.Run(ctx, input) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.True(t, middlewareInvoked, "EnhancedStreamable WrapToolCall middleware should have been invoked") + assert.Equal(t, 3, len(events)) + assert.Equal(t, schema.Tool, events[1].Output.MessageOutput.Role) + }) +} + +// TestExitTool tests the Exit tool functionality +func TestExitTool(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock chat model + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + // First call: model generates a message with Exit tool call + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("I'll exit with a final result", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: "exit", + Arguments: `{"final_result": "This is the final result"}`}, + }, + }), nil). + Times(1) + + // Model should implement WithTools + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create an agent with the Exit tool + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with Exit tool", + Instruction: "You are a helpful assistant.", + Model: cm, + Exit: &ExitTool{}, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Please exit with a final result"), + }, + } + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // First event: model output with tool call + event1, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event1) + assert.Nil(t, event1.Err) + assert.NotNil(t, event1.Output) + assert.NotNil(t, event1.Output.MessageOutput) + assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role) + + // Second event: tool output (Exit) + event2, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event2) + assert.Nil(t, event2.Err) + assert.NotNil(t, event2.Output) + assert.NotNil(t, event2.Output.MessageOutput) + assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role) + + // Verify the action is Exit + assert.NotNil(t, event2.Action) + assert.True(t, event2.Action.Exit) + + // Verify the final result + assert.Equal(t, "This is the final result", event2.Output.MessageOutput.Message.Content) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestParallelReturnDirectlyToolCall(t *testing.T) { + ctx := context.Background() + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock chat model + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + // First call: model generates a message with Exit tool call + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("I'll exit with a final result", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{Name: "tool1"}, + }, + { + ID: "tool-call-2", + Function: schema.FunctionCall{Name: "tool2"}, + }, + { + ID: "tool-call-3", + Function: schema.FunctionCall{Name: "tool3"}, + }, + }), nil). + Times(1) + + // Model should implement WithTools + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create an agent with the Exit tool + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with Exit tool", + Instruction: "You are a helpful assistant.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{ + &myTool{name: "tool1", desc: "tool1", waitTime: time.Millisecond}, + &myTool{name: "tool2", desc: "tool2", waitTime: 10 * time.Millisecond}, + &myTool{name: "tool3", desc: "tool3", waitTime: 100 * time.Millisecond}, + }, + }, + ReturnDirectly: map[string]bool{ + "tool1": true, + }, + }, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + }) + iter := r.Query(ctx, "") + times := 0 + for { + e, ok := iter.Next() + if !ok { + assert.Equal(t, 4, times) + break + } + if times == 3 { + assert.Equal(t, "tool1", e.Output.MessageOutput.Message.ToolName) + } + times++ + } +} + +func TestConcurrentSameToolSendToolGenActionUsesToolCallID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("tools", []schema.ToolCall{ + {ID: "id1", Function: schema.FunctionCall{Name: "action_tool", Arguments: "A"}}, + {ID: "id2", Function: schema.FunctionCall{Name: "action_tool", Arguments: "B"}}, + }), nil). + Times(1) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil). + Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Agent with action tool", + Instruction: "", + Model: cm, + ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{actionTool{}}}}, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}) + seen := map[string]bool{} + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.Message != nil && e.Output.MessageOutput.Message.Role == schema.Tool { + if e.Action != nil && e.Action.CustomizedAction != nil { + if s, ok := e.Action.CustomizedAction.(string); ok { + seen[s] = true + } + } + } + } + assert.True(t, seen["A"]) + assert.True(t, seen["B"]) +} + +type myTool struct { + name string + desc string + waitTime time.Duration +} + +func (m *myTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: m.name, + Desc: m.desc, + }, nil +} + +func (m *myTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + time.Sleep(m.waitTime) + return "success", nil +} + +type actionTool struct{} + +func (a actionTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "action_tool", Desc: "action tool"}, nil +} + +func (a actionTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + _ = SendToolGenAction(ctx, "action_tool", &AgentAction{CustomizedAction: argumentsInJSON}) + return "ok", nil +} + +type streamActionTool struct{} + +func (s streamActionTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "action_tool_stream", Desc: "action stream tool"}, nil +} + +func (s streamActionTool) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) { + _ = SendToolGenAction(ctx, "action_tool_stream", &AgentAction{CustomizedAction: argumentsInJSON}) + sr, sw := schema.Pipe[string](1) + go func() { + defer sw.Close() + _ = sw.Send("o", nil) + _ = sw.Send("k", nil) + }() + return sr, nil +} + +type legacyStreamActionTool struct{} + +func (s legacyStreamActionTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "legacy_action_tool_stream", Desc: "legacy action stream tool"}, nil +} + +func (s legacyStreamActionTool) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) { + _ = compose.ProcessState(ctx, func(ctx context.Context, st *State) error { + st.setToolGenAction("legacy_action_tool_stream", &AgentAction{CustomizedAction: argumentsInJSON}) + return nil + }) + sr, sw := schema.Pipe[string](1) + go func() { + defer sw.Close() + _ = sw.Send("o", nil) + _ = sw.Send("k", nil) + }() + return sr, nil +} + +// TestChatModelAgentOutputKey tests the outputKey configuration and setOutputToSession function +func TestChatModelAgentOutputKey(t *testing.T) { + // Test outputKey configuration - stores output in session + t.Run("OutputKeyStoresInSession", func(t *testing.T) { + for i := 0; i < 1000; i++ { + + } + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello, I am an AI assistant.", nil), nil). + Times(1) + + // Create a ChatModelAgent with outputKey configured + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + OutputKey: "agent_output", // This should store output in session + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Initialize a run context to enable session storage + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Hello, who are you?"), + }, + } + ctx, runCtx := initRunCtx(ctx, "TestAgent", input) + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Run the agent + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + + // Verify the message content + msg := event.Output.MessageOutput.Message + assert.Equal(t, "Hello, I am an AI assistant.", msg.Content) + + // Verify that the output was stored in the session + time.AfterFunc(100*time.Millisecond, func() { + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "agent_output") + assert.Equal(t, "Hello, I am an AI assistant.", sessionValues["agent_output"]) + }) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) + + // Test outputKey configuration with streaming - stores concatenated output in session + t.Run("OutputKeyWithStreamingStoresInSession", func(t *testing.T) { + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create a stream reader for the mock response + sr := schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Hello", nil), + schema.AssistantMessage(", I am", nil), + schema.AssistantMessage(" an AI assistant.", nil), + }) + + // Set up expectations for the mock model + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(sr, nil). + Times(1) + + // Create a ChatModelAgent with outputKey configured + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for unit testing", + Instruction: "You are a helpful assistant.", + Model: cm, + OutputKey: "agent_output", // This should store concatenated stream in session + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Initialize a run context to enable session storage + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Hello, who are you?")}, + EnableStreaming: true, + } + ctx, runCtx := initRunCtx(ctx, "TestAgent", input) + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Run the agent + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + time.AfterFunc(100*time.Millisecond, func() { + // Verify that the concatenated output was stored in the session + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "agent_output") + assert.Equal(t, "Hello, I am an AI assistant.", sessionValues["agent_output"]) + }) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) + + // Test setOutputToSession function directly - regular message + t.Run("SetOutputToSessionRegularMessage", func(t *testing.T) { + ctx := context.Background() + + // Initialize a run context to enable session storage + input := &AgentInput{ + Messages: []Message{schema.UserMessage("test")}, + } + ctx, runCtx := initRunCtx(ctx, "TestAgent", input) + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Test with a regular message + msg := schema.AssistantMessage("Test response", nil) + err := setOutputToSession(ctx, msg, nil, "test_output") + assert.NoError(t, err) + + // Verify the message content was stored + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "test_output") + assert.Equal(t, "Test response", sessionValues["test_output"]) + }) + + // Test setOutputToSession function directly - streaming message + t.Run("SetOutputToSessionStreamingMessage", func(t *testing.T) { + ctx := context.Background() + + // Initialize a run context to enable session storage + input := &AgentInput{ + Messages: []Message{schema.UserMessage("test")}, + } + ctx, runCtx := initRunCtx(ctx, "TestAgent", input) + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Test with a streaming message + sr := schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Stream", nil), + schema.AssistantMessage(" response", nil), + schema.AssistantMessage(" content", nil), + }) + err := setOutputToSession(ctx, nil, sr, "test_output") + assert.NoError(t, err) + + // Verify the concatenated stream content was stored + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "test_output") + assert.Equal(t, "Stream response content", sessionValues["test_output"]) + }) + + // Test setOutputToSession function directly - error case + t.Run("SetOutputToSessionErrorCase", func(t *testing.T) { + ctx := context.Background() + + // Initialize a run context to enable session storage + input := &AgentInput{ + Messages: []Message{schema.UserMessage("test")}, + } + ctx, runCtx := initRunCtx(ctx, "TestAgent", input) + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Test with an invalid stream (simulate error) + // Create a stream that will fail when concatenated + sr := schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("test", nil), + }) + // Close the stream to simulate an error condition + sr.Close() + + // This should return an error because the stream is closed + err := setOutputToSession(ctx, nil, sr, "test_output") + // Note: The actual behavior may vary depending on the stream implementation + // Some streams may not error when closed, so we'll accept either outcome + if err != nil { + // If there's an error, verify nothing was stored + sessionValues := GetSessionValues(ctx) + assert.NotContains(t, sessionValues, "test_output") + } else { + // If no error, verify the content was stored + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "test_output") + assert.Equal(t, "test", sessionValues["test_output"]) + } + }) + + // Test outputKey with React workflow (tools enabled) + t.Run("OutputKeyWithReactWorkflow", func(t *testing.T) { + ctx := context.Background() + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create a simple tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 1, + } + + // Set up expectations for the mock model - it will generate a final response + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Final response from React workflow", nil), nil). + Times(1) + // Model should implement WithTools + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create a ChatModelAgent with outputKey and tools configured + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent with tools", + Instruction: "You are a helpful assistant.", + Model: cm, + OutputKey: "agent_output", + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + }) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Initialize a run context to enable session storage + input := &AgentInput{ + Messages: []Message{schema.UserMessage("Use the tool")}, + } + ctx, runCtx := initRunCtx(ctx, "TestAgent", input) + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Run the agent + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.Nil(t, event.Err) + + // Verify the message content + msg := event.Output.MessageOutput.Message + assert.Equal(t, "Final response from React workflow", msg.Content) + + // Verify that the output was stored in the session + time.AfterFunc(time.Millisecond*10, func() { + sessionValues := GetSessionValues(ctx) + assert.Contains(t, sessionValues, "agent_output") + assert.Equal(t, "Final response from React workflow", sessionValues["agent_output"]) + }) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) + }) +} + +func TestConcurrentSameStreamToolSendToolGenActionUsesToolCallID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("tools", []schema.ToolCall{ + {ID: "sid1", Function: schema.FunctionCall{Name: "action_tool_stream", Arguments: "SA"}}, + {ID: "sid2", Function: schema.FunctionCall{Name: "action_tool_stream", Arguments: "SB"}}, + }), nil). + Times(1) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil). + Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Agent with stream action tool", + Instruction: "", + Model: cm, + ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{streamActionTool{}}}}, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}) + seen := map[string]bool{} + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Output != nil && e.Output.MessageOutput != nil { + if e.Output.MessageOutput.IsStreaming { + if e.Action != nil && e.Action.CustomizedAction != nil { + if s, ok := e.Action.CustomizedAction.(string); ok { + seen[s] = true + } + } + } + } + } + assert.True(t, seen["SA"]) + assert.True(t, seen["SB"]) +} + +func TestStreamToolLegacyNameKeyFallback(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("tools", []schema.ToolCall{ + {ID: "lsid1", Function: schema.FunctionCall{Name: "legacy_action_tool_stream", Arguments: "LA"}}, + }), nil). + Times(1) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil). + Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Agent with legacy stream action tool", + Instruction: "", + Model: cm, + ToolsConfig: ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{legacyStreamActionTool{}}}}, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("go")}}) + found := false + for { + e, ok := iter.Next() + if !ok { + break + } + if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.IsStreaming { + if e.Action != nil && e.Action.CustomizedAction != nil { + if s, ok := e.Action.CustomizedAction.(string); ok { + found = (s == "LA") + } + } + } + } + assert.True(t, found) +} + +func TestChatModelAgent_ToolResultMiddleware_EmitsFinalResult(t *testing.T) { + originalResult := "original_result" + modifiedResult := "modified_by_middleware" + + resultModifyingMiddleware := compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + output, err := next(ctx, input) + if err != nil { + return nil, err + } + output.Result = modifiedResult + return output, nil + } + }, + Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + output, err := next(ctx, input) + if err != nil { + return nil, err + } + output.Result = schema.StreamReaderFromArray([]string{modifiedResult}) + return output, nil + } + }, + } + + t.Run("Invoke", func(t *testing.T) { + ctx := context.Background() + testTool := &simpleToolForMiddlewareTest{name: "test_tool", result: originalResult} + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + info, err := testTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"input": "test"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "test_agent", + Description: "test agent with middleware", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware}, + }, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false, CheckPointStore: newBridgeStore()}) + it := r.Run(ctx, []Message{schema.UserMessage("call the tool")}) + + var toolResultEvents []*AgentEvent + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil && + ev.Output.MessageOutput.Message != nil && + ev.Output.MessageOutput.Message.Role == schema.Tool { + toolResultEvents = append(toolResultEvents, ev) + } + } + + assert.NotEmpty(t, toolResultEvents, "should have at least one tool result event") + for _, ev := range toolResultEvents { + assert.Equal(t, modifiedResult, ev.Output.MessageOutput.Message.Content, + "tool result event should contain the middleware-modified result, not the original") + assert.NotEqual(t, originalResult, ev.Output.MessageOutput.Message.Content, + "tool result event should NOT contain the original result") + } + }) + + t.Run("Stream", func(t *testing.T) { + ctx := context.Background() + testTool := &simpleToolForMiddlewareTest{name: "test_tool_stream", result: originalResult} + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + info, err := testTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"input": "test"}`, + }, + }, + }), + }), nil). + Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("final response", nil), + }), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "test_agent", + Description: "test agent with middleware", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware}, + }, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true, CheckPointStore: newBridgeStore()}) + it := r.Run(ctx, []Message{schema.UserMessage("call the tool")}) + + var toolResultContents []string + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output != nil && ev.Output.MessageOutput != nil { + if ev.Output.MessageOutput.Message != nil && + ev.Output.MessageOutput.Message.Role == schema.Tool { + toolResultContents = append(toolResultContents, ev.Output.MessageOutput.Message.Content) + } + if ev.Output.MessageOutput.IsStreaming && + ev.Output.MessageOutput.MessageStream != nil && + ev.Output.MessageOutput.Role == schema.Tool { + var msgs []*schema.Message + for { + msg, err := ev.Output.MessageOutput.MessageStream.Recv() + if err != nil { + break + } + msgs = append(msgs, msg) + } + if len(msgs) > 0 { + concated, err := schema.ConcatMessages(msgs) + if err == nil { + toolResultContents = append(toolResultContents, concated.Content) + } + } + } + } + } + + assert.NotEmpty(t, toolResultContents, "should have at least one tool result event") + for _, content := range toolResultContents { + assert.Equal(t, modifiedResult, content, + "tool result event should contain the middleware-modified result, not the original") + assert.NotEqual(t, originalResult, content, + "tool result event should NOT contain the original result") + } + }) +} + +type simpleToolForMiddlewareTest struct { + name string + result string +} + +func (s *simpleToolForMiddlewareTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: s.name, + Desc: "simple tool", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "input": { + Desc: "input", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (s *simpleToolForMiddlewareTest) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return s.result, nil +} + +func (s *simpleToolForMiddlewareTest) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) { + return schema.StreamReaderFromArray([]string{s.result}), nil +} + +func TestGetComposeOptions(t *testing.T) { + t.Run("WithChatModelOptions", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var capturedTemperature float32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + options := model.GetCommonOptions(&model.Options{}, opts...) + if options.Temperature != nil { + capturedTemperature = *options.Temperature + } + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + }) + assert.NoError(t, err) + + temp := float32(0.7) + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + WithChatModelOptions([]model.Option{model.WithTemperature(temp)})) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, temp, capturedTemperature, "Temperature should be passed through WithChatModelOptions") + }) + + t.Run("WithToolOptions", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var toolOptionsCaptured bool + testTool := &toolOptionCapturingTool{ + name: "test_tool", + onRun: func(opts []tool.Option) { + if len(opts) > 0 { + toolOptionsCaptured = true + } + }, + } + info, _ := testTool.Info(ctx) + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil).Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + WithToolOptions([]tool.Option{testToolOption("test_value")})) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, toolOptionsCaptured, "Tool options should be passed through WithToolOptions") + }) + +} + +type toolOptionCapturingTool struct { + name string + onRun func(opts []tool.Option) +} + +func (t *toolOptionCapturingTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: t.name + " description"}, nil +} + +func (t *toolOptionCapturingTool) InvokableRun(_ context.Context, _ string, opts ...tool.Option) (string, error) { + if t.onRun != nil { + t.onRun(opts) + } + return t.name + " result", nil +} + +type testToolOptions struct { + value string +} + +func testToolOption(value string) tool.Option { + return tool.WrapImplSpecificOptFn(func(o *testToolOptions) { + o.value = value + }) +} + +type errorTool struct { + infoErr error +} + +func (e *errorTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return nil, e.infoErr +} + +func (e *errorTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return "", nil +} + +func TestChatModelAgent_PrepareExecContextError(t *testing.T) { + t.Run("Run_WithToolInfoError_ReturnsError", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + expectedErr := errors.New("tool info error") + errTool := &errorTool{infoErr: expectedErr} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{errTool}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + event, ok := iter.Next() + assert.True(t, ok) + assert.NotNil(t, event.Err) + assert.Contains(t, event.Err.Error(), "tool info error") + + _, ok = iter.Next() + assert.False(t, ok) + }) + + t.Run("Resume_WithToolInfoError_ReturnsError", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + expectedErr := errors.New("tool info error for resume") + errTool := &errorTool{infoErr: expectedErr} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{errTool}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Resume(ctx, &ResumeInfo{ + InterruptState: []byte("dummy"), + EnableStreaming: false, + }) + + event, ok := iter.Next() + assert.True(t, ok) + assert.NotNil(t, event.Err) + assert.Contains(t, event.Err.Error(), "tool info error for resume") + + _, ok = iter.Next() + assert.False(t, ok) + }) +} + +// fakeEnhancedInvokableToolForTest implements tool.EnhancedInvokableTool for testing. +type fakeEnhancedInvokableToolForTest struct{} + +func (t *fakeEnhancedInvokableToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "test_enhanced_invokable_tool", + Desc: "test enhanced invokable tool", + }, nil +} + +func (t *fakeEnhancedInvokableToolForTest) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "enhanced invokable result"}, + }, + }, nil +} + +// fakeEnhancedStreamableToolForTest implements tool.EnhancedStreamableTool for testing. +type fakeEnhancedStreamableToolForTest struct{} + +func (t *fakeEnhancedStreamableToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "test_enhanced_streamable_tool", + Desc: "test enhanced streamable tool", + }, nil +} + +func (t *fakeEnhancedStreamableToolForTest) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr := schema.StreamReaderFromArray([]*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "enhanced streamable result"}}}, + }) + return sr, nil +} + +func TestPreprocessComposeCheckpoint_MigrateErrorIsReturned(t *testing.T) { + in := []byte("prefix\u0015" + stateGobNameV080 + "suffix") + _, err := preprocessComposeCheckpoint(in) + assert.Error(t, err) +} + +func TestNewChatModelAgent_FailoverConfigValidation(t *testing.T) { + ctx := context.Background() + cm := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("ok", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil + }, + } + + t.Run("missing GetFailoverModel", func(t *testing.T) { + _, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: cm, + ModelFailoverConfig: &ModelFailoverConfig[*schema.Message]{ + ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, + }, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "ModelFailoverConfig.GetFailoverModel") + }) + + t.Run("missing ShouldFailover", func(t *testing.T) { + _, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "test", + Model: cm, + ModelFailoverConfig: &ModelFailoverConfig[*schema.Message]{ + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return cm, nil, nil + }, + }, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "ModelFailoverConfig.ShouldFailover") + }) +} + +// aliasCaptureTool captures the raw arguments JSON received by the tool. +type aliasCaptureTool struct { + name string + params map[string]*schema.ParameterInfo + receivedArgs string +} + +func (t *aliasCaptureTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: t.name + " tool", + ParamsOneOf: schema.NewParamsOneOfByParams(t.params), + }, nil +} + +func (t *aliasCaptureTool) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + t.receivedArgs = argumentsInJSON + return "ok", nil +} + +func TestToolAliasesPropagation(t *testing.T) { + t.Run("prepareExecContext_propagates_ToolAliases", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + + captureTool := &aliasCaptureTool{ + name: "grep", + params: map[string]*schema.ParameterInfo{ + "pattern": {Type: schema.String, Desc: "regex pattern"}, + "path": {Type: schema.String, Desc: "search path"}, + }, + } + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "grep", + Arguments: `{"grep_content": "TODO", "path": "/src"}`, + }, + }, + }, + }, nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "test", + Instruction: "test", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{captureTool}, + ToolAliases: map[string]compose.ToolAliasConfig{ + "grep": { + ArgumentsAliases: map[string][]string{ + "pattern": {"grep_content"}, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search for TODOs")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called") + var args map[string]any + err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) + require.NoError(t, err) + assert.Equal(t, "TODO", args["pattern"], "alias 'grep_content' should be remapped to 'pattern'") + assert.NotContains(t, args, "grep_content", "alias key should not be present after remapping") + assert.Equal(t, "/src", args["path"]) + }) + + t.Run("applyBeforeAgent_preserves_ToolAliases_when_handler_modifies_tools", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + + captureTool := &aliasCaptureTool{ + name: "grep", + params: map[string]*schema.ParameterInfo{ + "pattern": {Type: schema.String, Desc: "regex pattern"}, + }, + } + + extraTool := &aliasCaptureTool{ + name: "extra_tool", + params: map[string]*schema.ParameterInfo{ + "input": {Type: schema.String}, + }, + } + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "grep", + Arguments: `{"grep_content": "FIXME"}`, + }, + }, + }, + }, nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + handler := &testToolsHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + tools: []tool.BaseTool{extraTool}, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "test", + Instruction: "test", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{captureTool}, + ToolAliases: map[string]compose.ToolAliasConfig{ + "grep": { + ArgumentsAliases: map[string][]string{ + "pattern": {"grep_content"}, + }, + }, + }, + }, + }, + Handlers: []ChatModelAgentMiddleware{handler}, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search for FIXMEs")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called") + var args map[string]any + err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) + require.NoError(t, err) + assert.Equal(t, "FIXME", args["pattern"], "alias 'grep_content' should be remapped to 'pattern' even after handler rebuild") + assert.NotContains(t, args, "grep_content", "alias key should not be present after remapping") + }) + + t.Run("name_alias_propagated_through_prepareExecContext", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + + captureTool := &aliasCaptureTool{ + name: "grep", + params: map[string]*schema.ParameterInfo{ + "pattern": {Type: schema.String}, + }, + } + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "search_content", + Arguments: `{"pattern": "TODO"}`, + }, + }, + }, + }, nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "test", + Instruction: "test", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{captureTool}, + ToolAliases: map[string]compose.ToolAliasConfig{ + "grep": { + NameAliases: []string{"search_content"}, + }, + }, + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("search")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + require.NotEmpty(t, captureTool.receivedArgs, "tool should have been called via name alias 'search_content'") + var args map[string]any + err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) + require.NoError(t, err) + assert.Equal(t, "TODO", args["pattern"]) + }) + + t.Run("handler_adds_tool_matching_preexisting_ToolAliases_with_no_initial_tools", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + + captureTool := &aliasCaptureTool{ + name: "grep", + params: map[string]*schema.ParameterInfo{ + "pattern": {Type: schema.String, Desc: "regex pattern"}, + }, + } + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "grep", + Arguments: `{"grep_content": "BUG"}`, + }, + }, + }, + }, nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + handler := &testToolsHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + tools: []tool.BaseTool{captureTool}, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "test", + Instruction: "test", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + ToolAliases: map[string]compose.ToolAliasConfig{ + "grep": { + ArgumentsAliases: map[string][]string{ + "pattern": {"grep_content"}, + }, + }, + }, + }, + }, + Handlers: []ChatModelAgentMiddleware{handler}, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("find bugs")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + require.NotEmpty(t, captureTool.receivedArgs, "tool added by handler should have been called") + var args map[string]any + err = json.Unmarshal([]byte(captureTool.receivedArgs), &args) + require.NoError(t, err) + assert.Equal(t, "BUG", args["pattern"], "alias 'grep_content' should be remapped to 'pattern' for handler-added tool") + assert.NotContains(t, args, "grep_content") + }) +} diff --git a/adk/config.go b/adk/config.go new file mode 100644 index 0000000..67ead86 --- /dev/null +++ b/adk/config.go @@ -0,0 +1,35 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import "github.com/cloudwego/eino/adk/internal" + +// Language represents the language setting for the ADK built-in prompts. +type Language = internal.Language + +const ( + // LanguageEnglish represents English language. + LanguageEnglish Language = internal.LanguageEnglish + // LanguageChinese represents Chinese language. + LanguageChinese Language = internal.LanguageChinese +) + +// SetLanguage sets the language for the ADK built-in prompts. +// The default language is English if not explicitly set. +func SetLanguage(lang Language) error { + return internal.SetLanguage(lang) +} diff --git a/adk/deterministic_transfer.go b/adk/deterministic_transfer.go new file mode 100644 index 0000000..ce5b200 --- /dev/null +++ b/adk/deterministic_transfer.go @@ -0,0 +1,301 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "runtime/debug" + "sync" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +func init() { + schema.RegisterName[*deterministicTransferState]("_eino_adk_deterministic_transfer_state") +} + +type deterministicTransferState struct { + EventList []*agentEventWrapper +} + +// AgentWithDeterministicTransferTo wraps an agent to transfer to given agents deterministically. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func AgentWithDeterministicTransferTo(_ context.Context, config *DeterministicTransferConfig) Agent { + if ra, ok := config.Agent.(ResumableAgent); ok { + return &resumableAgentWithDeterministicTransferTo{ + agent: ra, + toAgentNames: config.ToAgentNames, + } + } + return &agentWithDeterministicTransferTo{ + agent: config.Agent, + toAgentNames: config.ToAgentNames, + } +} + +type agentWithDeterministicTransferTo struct { + agent Agent + toAgentNames []string +} + +func (a *agentWithDeterministicTransferTo) Description(ctx context.Context) string { + return a.agent.Description(ctx) +} + +func (a *agentWithDeterministicTransferTo) Name(ctx context.Context) string { + return a.agent.Name(ctx) +} + +func (a *agentWithDeterministicTransferTo) GetType() string { + if typer, ok := a.agent.(components.Typer); ok { + return typer.GetType() + } + return "DeterministicTransfer" +} + +func (a *agentWithDeterministicTransferTo) Run(ctx context.Context, + input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + + if fa, ok := a.agent.(*flowAgent); ok { + return runFlowAgentWithIsolatedSession(ctx, fa, input, a.toAgentNames, options...) + } + + aIter := a.agent.Run(ctx, input, options...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + go forwardEventsAndAppendTransfer(aIter, generator, a.toAgentNames) + + return iterator +} + +type resumableAgentWithDeterministicTransferTo struct { + agent ResumableAgent + toAgentNames []string +} + +func (a *resumableAgentWithDeterministicTransferTo) Description(ctx context.Context) string { + return a.agent.Description(ctx) +} + +func (a *resumableAgentWithDeterministicTransferTo) Name(ctx context.Context) string { + return a.agent.Name(ctx) +} + +func (a *resumableAgentWithDeterministicTransferTo) GetType() string { + if typer, ok := a.agent.(components.Typer); ok { + return typer.GetType() + } + return "DeterministicTransfer" +} + +func (a *resumableAgentWithDeterministicTransferTo) Run(ctx context.Context, + input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + + if fa, ok := a.agent.(*flowAgent); ok { + return runFlowAgentWithIsolatedSession(ctx, fa, input, a.toAgentNames, options...) + } + + aIter := a.agent.Run(ctx, input, options...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + go forwardEventsAndAppendTransfer(aIter, generator, a.toAgentNames) + + return iterator +} + +func (a *resumableAgentWithDeterministicTransferTo) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + if fa, ok := a.agent.(*flowAgent); ok { + return resumeFlowAgentWithIsolatedSession(ctx, fa, info, a.toAgentNames, opts...) + } + + aIter := a.agent.Resume(ctx, info, opts...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + go forwardEventsAndAppendTransfer(aIter, generator, a.toAgentNames) + + return iterator +} + +func forwardEventsAndAppendTransfer(iter *AsyncIterator[*AgentEvent], + generator *AsyncGenerator[*AgentEvent], toAgentNames []string) { + + defer func() { + if panicErr := recover(); panicErr != nil { + generator.Send(&AgentEvent{Err: safe.NewPanicErr(panicErr, debug.Stack())}) + } + generator.Close() + }() + + var lastEvent *AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + generator.Send(event) + lastEvent = event + } + + if lastEvent != nil && lastEvent.Action != nil && (lastEvent.Action.Interrupted != nil || lastEvent.Action.Exit) { + return + } + + sendTransferEvents(generator, toAgentNames) +} + +func runFlowAgentWithIsolatedSession(ctx context.Context, fa *flowAgent, input *AgentInput, + toAgentNames []string, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + + parentSession := getSession(ctx) + parentRunCtx := getRunCtx(ctx) + + isolatedSession := &runSession{ + Values: parentSession.Values, + valuesMtx: parentSession.valuesMtx, + } + if isolatedSession.valuesMtx == nil { + isolatedSession.valuesMtx = &sync.Mutex{} + } + if isolatedSession.Values == nil { + isolatedSession.Values = make(map[string]any) + } + + ctx = setRunCtx(ctx, &runContext{ + Session: isolatedSession, + RootInput: parentRunCtx.RootInput, + RunPath: parentRunCtx.RunPath, + }) + + iter := fa.Run(ctx, input, options...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + go handleFlowAgentEvents(ctx, iter, generator, isolatedSession, parentSession, toAgentNames) + + return iterator +} + +func resumeFlowAgentWithIsolatedSession(ctx context.Context, fa *flowAgent, info *ResumeInfo, + toAgentNames []string, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + + state, ok := info.InterruptState.(*deterministicTransferState) + if !ok || state == nil { + return genErrorIter(errors.New("invalid interrupt state for flowAgent resume in deterministic transfer")) + } + + parentSession := getSession(ctx) + parentRunCtx := getRunCtx(ctx) + + isolatedSession := &runSession{ + Values: parentSession.Values, + valuesMtx: parentSession.valuesMtx, + Events: state.EventList, + } + if isolatedSession.valuesMtx == nil { + isolatedSession.valuesMtx = &sync.Mutex{} + } + if isolatedSession.Values == nil { + isolatedSession.Values = make(map[string]any) + } + + ctx = setRunCtx(ctx, &runContext{ + Session: isolatedSession, + RootInput: parentRunCtx.RootInput, + RunPath: parentRunCtx.RunPath, + }) + + iter := fa.Resume(ctx, info, opts...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + go handleFlowAgentEvents(ctx, iter, generator, isolatedSession, parentSession, toAgentNames) + + return iterator +} + +func handleFlowAgentEvents(ctx context.Context, iter *AsyncIterator[*AgentEvent], + generator *AsyncGenerator[*AgentEvent], isolatedSession, parentSession *runSession, toAgentNames []string) { + + defer func() { + if panicErr := recover(); panicErr != nil { + generator.Send(&AgentEvent{Err: safe.NewPanicErr(panicErr, debug.Stack())}) + } + generator.Close() + }() + + var lastEvent *AgentEvent + + for { + event, ok := iter.Next() + if !ok { + break + } + + if parentSession != nil && (event.Action == nil || event.Action.Interrupted == nil) { + copied := copyTypedAgentEvent(event) + setAutomaticClose(copied) + setAutomaticClose(event) + parentSession.addEvent(copied) + } + + if event.Action != nil && event.Action.internalInterrupted != nil { + lastEvent = event + continue + } + + generator.Send(event) + lastEvent = event + } + + if lastEvent != nil && lastEvent.Action != nil { + if lastEvent.Action.internalInterrupted != nil { + events := isolatedSession.getEvents() + state := &deterministicTransferState{EventList: events} + compositeEvent := CompositeInterrupt(ctx, "deterministic transfer wrapper interrupted", + state, lastEvent.Action.internalInterrupted) + generator.Send(compositeEvent) + return + } + + if lastEvent.Action.Exit { + return + } + } + + sendTransferEvents(generator, toAgentNames) +} + +func sendTransferEvents(generator *AsyncGenerator[*AgentEvent], toAgentNames []string) { + for _, toAgentName := range toAgentNames { + aMsg, tMsg := GenTransferMessages(context.Background(), toAgentName) + + aEvent := EventFromMessage(aMsg, nil, schema.Assistant, "") + generator.Send(aEvent) + + tEvent := EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName) + tEvent.Action = &AgentAction{ + TransferToAgent: &TransferToAgentAction{ + DestAgentName: toAgentName, + }, + } + generator.Send(tEvent) + } +} diff --git a/adk/deterministic_transfer_test.go b/adk/deterministic_transfer_test.go new file mode 100644 index 0000000..da135da --- /dev/null +++ b/adk/deterministic_transfer_test.go @@ -0,0 +1,731 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +type dtTestStore struct { + data map[string][]byte +} + +func newDTTestStore() *dtTestStore { + return &dtTestStore{data: make(map[string][]byte)} +} + +func (s *dtTestStore) Set(_ context.Context, key string, value []byte) error { + s.data[key] = value + return nil +} + +func (s *dtTestStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.data[key] + return v, ok, nil +} + +type dtTestAgent struct { + name string + runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] + resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] +} + +func (a *dtTestAgent) Name(_ context.Context) string { + return a.name +} + +func (a *dtTestAgent) Description(_ context.Context) string { + return a.name + " description" +} + +func (a *dtTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return a.runFn(ctx, input, options...) +} + +func (a *dtTestAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + if a.resumeFn != nil { + return a.resumeFn(ctx, info, opts...) + } + return a.runFn(ctx, &AgentInput{}, opts...) +} + +func TestDeterministicTransferFlowAgentInterruptResume(t *testing.T) { + ctx := context.Background() + store := newDTTestStore() + + interruptData := "interrupt_data" + var runCount int + + innerAgent := &dtTestAgent{ + name: "inner", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + runCount++ + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + gen.Send(EventFromMessage(schema.AssistantMessage("before interrupt", nil), nil, schema.Assistant, "")) + intEvent := Interrupt(ctx, interruptData) + intEvent.Action.Interrupted.Data = interruptData + gen.Send(intEvent) + }() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + runCount++ + + assert.True(t, info.WasInterrupted, "innerAgent resumeFn: should be interrupted") + assert.True(t, info.IsResumeTarget, "innerAgent resumeFn: should be resume target") + + runCtx := getRunCtx(ctx) + assert.NotNil(t, runCtx, "innerAgent resumeFn: runCtx should not be nil") + assert.NotNil(t, runCtx.Session, "innerAgent resumeFn: runCtx.Session should not be nil") + + var agentEvents []*AgentEvent + for _, ev := range runCtx.Session.Events { + if ev.AgentEvent != nil { + agentEvents = append(agentEvents, ev.AgentEvent) + } + } + + assert.Len(t, agentEvents, 1, "innerAgent resumeFn: should have exactly 1 agent event") + if len(agentEvents) == 1 { + ev := agentEvents[0] + assert.Equal(t, "inner", ev.AgentName, "innerAgent resumeFn: event should be from inner agent") + assert.Equal(t, "before interrupt", ev.Output.MessageOutput.Message.Content, "innerAgent resumeFn: event content should be 'before interrupt'") + assert.Len(t, ev.RunPath, 2, "innerAgent resumeFn: RunPath should have 2 steps (outer agent, inner agent)") + if len(ev.RunPath) == 2 { + assert.Equal(t, "outer", ev.RunPath[0].agentName, "innerAgent resumeFn: RunPath[0] should be outer agent") + assert.Equal(t, "inner", ev.RunPath[1].agentName, "innerAgent resumeFn: RunPath[1] should be inner agent") + } + } + + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + gen.Send(EventFromMessage(schema.AssistantMessage("after resume", nil), nil, schema.Assistant, "")) + }() + return iter + }, + } + + innerFlowAgent := toFlowAgent(ctx, innerAgent) + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: innerFlowAgent, + ToAgentNames: []string{"next_agent"}, + }) + + outerAgent := &dtTestAgent{ + name: "outer", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return wrapped.Run(ctx, input, options...) + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.WasInterrupted, "outerAgent resumeFn: should be interrupted") + + runCtx := getRunCtx(ctx) + assert.NotNil(t, runCtx, "outerAgent resumeFn: runCtx should not be nil") + assert.NotNil(t, runCtx.Session, "outerAgent resumeFn: runCtx.Session should not be nil") + + var agentEvents []*AgentEvent + for _, ev := range runCtx.Session.Events { + if ev.AgentEvent != nil { + agentEvents = append(agentEvents, ev.AgentEvent) + } + } + + assert.Len(t, agentEvents, 1, "outerAgent resumeFn: should have exactly 1 agent event") + if len(agentEvents) == 1 { + ev := agentEvents[0] + assert.Equal(t, "inner", ev.AgentName, "outerAgent resumeFn: event should be from inner agent (preserved original)") + assert.Equal(t, "before interrupt", ev.Output.MessageOutput.Message.Content, "outerAgent resumeFn: event content should be 'before interrupt'") + assert.Len(t, ev.RunPath, 2, "outerAgent resumeFn: RunPath should have 2 steps") + if len(ev.RunPath) == 2 { + assert.Equal(t, "outer", ev.RunPath[0].agentName, "outerAgent resumeFn: RunPath[0] should be outer agent") + assert.Equal(t, "inner", ev.RunPath[1].agentName, "outerAgent resumeFn: RunPath[1] should be inner agent") + } + } + + ra := wrapped.(ResumableAgent) + return ra.Resume(ctx, info, opts...) + }, + } + + outerFlowAgent := toFlowAgent(ctx, outerAgent) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: outerFlowAgent, + EnableStreaming: true, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1")) + + var events []*AgentEvent + var interrupted bool + var interruptEvent *AgentEvent + for { + ev, ok := iter.Next() + if !ok { + break + } + events = append(events, ev) + if ev.Action != nil && ev.Action.Interrupted != nil { + interrupted = true + interruptEvent = ev + } + } + + assert.Equal(t, 1, runCount, "run should have been called once") + assert.True(t, interrupted, "should have interrupted") + assert.Greater(t, len(events), 0, "should have events") + if interruptEvent == nil { + t.Fatal("should have interrupt event") + } + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts, "should have interrupt contexts") + + _, exists, err := store.Get(ctx, "cp1") + assert.NoError(t, err) + assert.True(t, exists, "checkpoint should have been saved") + + var hasDeterministicTransferContext bool + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + t.Logf("InterruptContext: ID=%s, Info=%v, IsRootCause=%v, Addr=%v", intCtx.ID, intCtx.Info, intCtx.IsRootCause, intCtx.Address) + if intCtx.Info == "deterministic transfer wrapper interrupted" { + hasDeterministicTransferContext = true + } + for parent := intCtx.Parent; parent != nil; parent = parent.Parent { + t.Logf(" Parent: ID=%s, Info=%v, Addr=%v", parent.ID, parent.Info, parent.Address) + if parent.Info == "deterministic transfer wrapper interrupted" { + hasDeterministicTransferContext = true + } + } + } + assert.True(t, hasDeterministicTransferContext, "should have deterministic transfer interrupt context") + + var rootCauseID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + rootCauseID = intCtx.ID + break + } + } + assert.NotEmpty(t, rootCauseID, "should have root cause interrupt ID") + + resumeIter, err := runner.ResumeWithParams(ctx, "cp1", &ResumeParams{ + Targets: map[string]any{rootCauseID: nil}, + }) + assert.NoError(t, err) + + var resumeEvents []*AgentEvent + var resumeErr error + var hasTransfer bool + for { + ev, ok := resumeIter.Next() + if !ok { + break + } + + if ev.Err != nil { + resumeErr = ev.Err + } + if ev.Action != nil && ev.Action.TransferToAgent != nil { + hasTransfer = true + } + resumeEvents = append(resumeEvents, ev) + } + + assert.Equal(t, 2, runCount, "inner agent should be called twice (once for initial, once for resume)") + assert.NotEmpty(t, resumeEvents, "should have resume events") + assert.True(t, hasTransfer, "should have transfer action after resume") + assert.Error(t, resumeErr, "transfer should fail because next_agent doesn't exist") + assert.Contains(t, resumeErr.Error(), "next_agent", "error should mention the missing agent") +} + +func TestDeterministicTransferRunPathPreserved(t *testing.T) { + ctx := context.Background() + store := newDTTestStore() + + var collectedRunPaths [][]RunStep + + innerAgent := &dtTestAgent{ + name: "inner", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("from inner", nil), nil, schema.Assistant, "") + gen.Send(ev) + }() + return iter + }, + } + + innerFlowAgent := toFlowAgent(ctx, innerAgent) + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: innerFlowAgent, + ToAgentNames: []string{}, + }) + + outerAgent := &dtTestAgent{ + name: "outer", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + innerIter := wrapped.Run(ctx, input, options...) + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + for { + ev, ok := innerIter.Next() + if !ok { + break + } + collectedRunPaths = append(collectedRunPaths, ev.RunPath) + gen.Send(ev) + } + }() + return iter + }, + } + + outerFlowAgent := toFlowAgent(ctx, outerAgent) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: outerFlowAgent, + EnableStreaming: true, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1")) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.NotEmpty(t, collectedRunPaths, "should have collected RunPaths") + for _, rp := range collectedRunPaths { + assert.Len(t, rp, 2, "RunPath should have 2 steps (outer agent, inner agent)") + if len(rp) == 2 { + assert.Equal(t, "outer", rp[0].agentName, "RunPath[0] should be outer agent") + assert.Equal(t, "inner", rp[1].agentName, "RunPath[1] should be inner agent") + } + } +} + +func TestDeterministicTransferExitSkipsTransfer(t *testing.T) { + ctx := context.Background() + store := newDTTestStore() + + innerAgent := &dtTestAgent{ + name: "inner", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("inner exits", nil), nil, schema.Assistant, "") + ev.Action = &AgentAction{Exit: true} + gen.Send(ev) + }() + return iter + }, + } + + innerFlowAgent := toFlowAgent(ctx, innerAgent) + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: innerFlowAgent, + ToAgentNames: []string{"next_agent"}, + }) + + var outerSawExit bool + var transferGenerated bool + + outerAgent := &dtTestAgent{ + name: "outer", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + innerIter := wrapped.Run(ctx, input, options...) + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + for { + ev, ok := innerIter.Next() + if !ok { + break + } + if ev.Action != nil && ev.Action.Exit { + outerSawExit = true + } + if ev.Action != nil && ev.Action.TransferToAgent != nil { + transferGenerated = true + } + gen.Send(ev) + } + }() + return iter + }, + } + + outerFlowAgent := toFlowAgent(ctx, outerAgent) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: outerFlowAgent, + EnableStreaming: true, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1")) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, outerSawExit, "outer should see exit event from inner") + assert.False(t, transferGenerated, "transfer should not be generated when inner exits") +} + +type nonFlowTestAgent struct { + name string + runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] + resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] +} + +func (a *nonFlowTestAgent) Name(_ context.Context) string { + return a.name +} + +func (a *nonFlowTestAgent) Description(_ context.Context) string { + return a.name + " description" +} + +func (a *nonFlowTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return a.runFn(ctx, input, options...) +} + +func (a *nonFlowTestAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + if a.resumeFn != nil { + return a.resumeFn(ctx, info, opts...) + } + return a.runFn(ctx, &AgentInput{}, opts...) +} + +type nonResumableTestAgent struct { + name string + runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] +} + +func (a *nonResumableTestAgent) Name(_ context.Context) string { + return a.name +} + +func (a *nonResumableTestAgent) Description(_ context.Context) string { + return a.name + " description" +} + +func (a *nonResumableTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return a.runFn(ctx, input, options...) +} + +func TestDeterministicTransferNonFlowAgent_ExitSkipsTransfer(t *testing.T) { + ctx := context.Background() + + agent := &nonFlowTestAgent{ + name: "test_agent", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("exiting", nil), nil, schema.Assistant, "") + ev.Action = &AgentAction{Exit: true} + gen.Send(ev) + }() + return iter + }, + } + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: agent, + ToAgentNames: []string{"next_agent"}, + }) + + iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + var events []*AgentEvent + var sawExit bool + var sawTransfer bool + for { + ev, ok := iter.Next() + if !ok { + break + } + events = append(events, ev) + if ev.Action != nil && ev.Action.Exit { + sawExit = true + } + if ev.Action != nil && ev.Action.TransferToAgent != nil { + sawTransfer = true + } + } + + assert.True(t, sawExit, "should see exit event") + assert.False(t, sawTransfer, "should NOT see transfer when exit is last event") + assert.Len(t, events, 1, "should have exactly 1 event (exit)") +} + +func TestDeterministicTransferNonFlowAgent_AppendsTransfer(t *testing.T) { + ctx := context.Background() + + agent := &nonFlowTestAgent{ + name: "test_agent", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("normal output", nil), nil, schema.Assistant, "") + gen.Send(ev) + }() + return iter + }, + } + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: agent, + ToAgentNames: []string{"next_agent"}, + }) + + iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + var events []*AgentEvent + var sawTransfer bool + var transferTarget string + for { + ev, ok := iter.Next() + if !ok { + break + } + events = append(events, ev) + if ev.Action != nil && ev.Action.TransferToAgent != nil { + sawTransfer = true + transferTarget = ev.Action.TransferToAgent.DestAgentName + } + } + + assert.True(t, sawTransfer, "should see transfer event after normal completion") + assert.Equal(t, "next_agent", transferTarget, "transfer target should be next_agent") + assert.Greater(t, len(events), 1, "should have more than 1 event (output + transfer messages)") +} + +func TestDeterministicTransferNonFlowAgent_InterruptSkipsTransfer(t *testing.T) { + ctx := context.Background() + + agent := &nonFlowTestAgent{ + name: "test_agent", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := &AgentEvent{ + Action: &AgentAction{ + Interrupted: &InterruptInfo{Data: "test interrupt"}, + }, + } + gen.Send(ev) + }() + return iter + }, + } + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: agent, + ToAgentNames: []string{"next_agent"}, + }) + + iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + var events []*AgentEvent + var sawInterrupt bool + var sawTransfer bool + for { + ev, ok := iter.Next() + if !ok { + break + } + events = append(events, ev) + if ev.Action != nil && ev.Action.Interrupted != nil { + sawInterrupt = true + } + if ev.Action != nil && ev.Action.TransferToAgent != nil { + sawTransfer = true + } + } + + assert.True(t, sawInterrupt, "should see interrupt event") + assert.False(t, sawTransfer, "should NOT see transfer when interrupted") + assert.Len(t, events, 1, "should have exactly 1 event (interrupt)") +} + +func TestDeterministicTransferNonFlowAgent_Resume(t *testing.T) { + ctx := context.Background() + + var resumeCalled bool + agent := &nonFlowTestAgent{ + name: "test_agent", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("from run", nil), nil, schema.Assistant, "") + gen.Send(ev) + }() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + resumeCalled = true + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("from resume", nil), nil, schema.Assistant, "") + gen.Send(ev) + }() + return iter + }, + } + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: agent, + ToAgentNames: []string{"next_agent"}, + }) + + ra, ok := wrapped.(ResumableAgent) + assert.True(t, ok, "wrapped agent should be ResumableAgent") + + iter := ra.Resume(ctx, &ResumeInfo{WasInterrupted: true}) + + var sawTransfer bool + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Action != nil && ev.Action.TransferToAgent != nil { + sawTransfer = true + } + } + + assert.True(t, resumeCalled, "resume should have been called on inner agent") + assert.True(t, sawTransfer, "should see transfer event after resume completion") +} + +func TestDeterministicTransferFlowAgent_ResumeWithInvalidState(t *testing.T) { + ctx := context.Background() + + innerAgent := &dtTestAgent{ + name: "inner", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + gen.Send(EventFromMessage(schema.AssistantMessage("test", nil), nil, schema.Assistant, "")) + }() + return iter + }, + } + + innerFlowAgent := toFlowAgent(ctx, innerAgent) + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: innerFlowAgent, + ToAgentNames: []string{"next_agent"}, + }) + + ra, ok := wrapped.(ResumableAgent) + assert.True(t, ok, "wrapped flowAgent should be ResumableAgent") + + iter := ra.Resume(ctx, &ResumeInfo{ + WasInterrupted: true, + InterruptState: nil, + }) + + var gotError bool + var errorMsg string + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + gotError = true + errorMsg = ev.Err.Error() + } + } + + assert.True(t, gotError, "should get error for invalid state") + assert.Contains(t, errorMsg, "invalid interrupt state", "error should mention invalid state") +} + +func TestDeterministicTransferNonResumableAgent(t *testing.T) { + ctx := context.Background() + + agent := &nonResumableTestAgent{ + name: "non_resumable", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + ev := EventFromMessage(schema.AssistantMessage("output", nil), nil, schema.Assistant, "") + gen.Send(ev) + }() + return iter + }, + } + + wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{ + Agent: agent, + ToAgentNames: []string{"next_agent"}, + }) + + _, isResumable := wrapped.(ResumableAgent) + assert.False(t, isResumable, "wrapped non-resumable agent should NOT be ResumableAgent") + + assert.Equal(t, "non_resumable", wrapped.Name(ctx), "Name should delegate to inner agent") + assert.Equal(t, "non_resumable description", wrapped.Description(ctx), "Description should delegate to inner agent") + + iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + var sawTransfer bool + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Action != nil && ev.Action.TransferToAgent != nil { + sawTransfer = true + } + } + + assert.True(t, sawTransfer, "should see transfer event") +} diff --git a/adk/failover_chatmodel.go b/adk/failover_chatmodel.go new file mode 100644 index 0000000..f12d890 --- /dev/null +++ b/adk/failover_chatmodel.go @@ -0,0 +1,490 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "io" + "log" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type failoverCurrentModelKey struct{} + +func typedSetFailoverCurrentModel[M MessageType](ctx context.Context, currentModel model.BaseModel[M]) context.Context { + return context.WithValue(ctx, failoverCurrentModelKey{}, currentModel) +} + +func typedGetFailoverCurrentModel[M MessageType](ctx context.Context) (model.BaseModel[M], bool) { + m, ok := ctx.Value(failoverCurrentModelKey{}).(model.BaseModel[M]) + return m, ok +} + +type failoverHasMoreAttemptsKey struct{} + +// withFailoverHasMoreAttempts sets a flag in context indicating whether additional failover +// attempts remain after the current one. This is read by buildErrWrapper to decide whether +// stream errors should be wrapped as WillRetryError. +func withFailoverHasMoreAttempts(ctx context.Context, hasMore bool) context.Context { + return context.WithValue(ctx, failoverHasMoreAttemptsKey{}, hasMore) +} + +// getFailoverHasMoreAttempts returns true if the current failover attempt has more attempts +// after it, false otherwise (including when no failover context is present). +func getFailoverHasMoreAttempts(ctx context.Context) bool { + v, _ := ctx.Value(failoverHasMoreAttemptsKey{}).(bool) + return v +} + +type typedFailoverProxyModel[M MessageType] struct { +} + +func (m *typedFailoverProxyModel[M]) prepareTarget(ctx context.Context) (model.BaseModel[M], error) { + target, ok := typedGetFailoverCurrentModel[M](ctx) + if !ok { + return nil, errors.New("failover current model not found in context") + } + + if !components.IsCallbacksEnabled(target) { + target = typedCallbackInjectionModelWrapper[M]{}.wrapModel(target) + } + + return target, nil +} + +func (m *typedFailoverProxyModel[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + target, err := m.prepareTarget(ctx) + if err != nil { + var zero M + return zero, err + } + + return target.Generate(ctx, input, opts...) +} + +func (m *typedFailoverProxyModel[M]) Stream(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + target, err := m.prepareTarget(ctx) + if err != nil { + return nil, err + } + + return target.Stream(ctx, input, opts...) +} + +func (m *typedFailoverProxyModel[M]) IsCallbacksEnabled() bool { + return true +} + +func (m *typedFailoverProxyModel[M]) GetType() string { + return "FailoverProxyModel" +} + +type failoverProxyModel = typedFailoverProxyModel[*schema.Message] + +// FailoverContext contains context information during failover process. +type FailoverContext[M MessageType] struct { + // FailoverAttempt is the current failover attempt number, starting from 1. + FailoverAttempt uint + + // InputMessages is the original input messages before any transformation. + InputMessages []M + + // LastOutputMessage is the output message from the last failed attempt. + // May be nil if no output was produced. For streaming, this may be a partial message + // already received before the stream error. + LastOutputMessage M + + // LastErr is the error from the last failed attempt that triggered this failover. + // + // Note: When ModelRetryConfig is also configured, LastErr will be a *RetryExhaustedError + // (if retries were exhausted) rather than the original model error. The original error + // can be retrieved via RetryExhaustedError.LastErr. + LastErr error +} + +// ModelFailoverConfig configures failover behavior for ChatModel. +// When configured, each ChatModel call first tries the last successful model (initially the configured Model), +// and if that fails, calls GetFailoverModel to select alternate models. +type ModelFailoverConfig[M MessageType] struct { + // MaxRetries specifies the maximum number of failover attempts. + // + // When failover is triggered, GetFailoverModel will be called up to MaxRetries times + // (FailoverAttempt starts from 1). If GetFailoverModel returns an error, failover + // stops immediately and that error is returned. + // + // A value of 0 means no failover (GetFailoverModel will not be called). + // A value of 1 means GetFailoverModel may be called once. + // + // Note: if lastSuccessModel is set (from a previous successful call), it will be tried + // first before calling GetFailoverModel. + MaxRetries uint + + // ShouldFailover determines whether to fail over to the next model when an error occurs. + // It receives the output message (may be nil/zero if no output is available) and the error (non-nil on failure). + // For streaming errors, outputMessage can carry a partial message accumulated before the error. + // + // Note: When ModelRetryConfig is also configured, outputErr will be a *RetryExhaustedError + // (if retries were exhausted) rather than the original model error. Use errors.As to extract + // the RetryExhaustedError and access RetryExhaustedError.LastErr for the original error. + // + // Note: When the context itself is cancelled (ctx.Err() != nil), failover will stop immediately + // regardless of this function. However, if the model returns context.Canceled or context.DeadlineExceeded + // as an error while the context is still active, this function will still be called. + // Should not be nil when ModelFailoverConfig is set. + // Return true to fail over to the next model, false to stop and return the current result/error. + ShouldFailover func(ctx context.Context, outputMessage M, outputErr error) bool + + // GetFailoverModel is called when a model call fails and ShouldFailover returns true. + // It selects the next model to use for the failover attempt and optionally transforms input messages. + // It receives the failover context containing attempt number (starting from 1), original input, and last result. + // Return values: + // - failoverModel: The model to use for this failover attempt. + // - failoverModelInputMessages: The transformed input messages for the failover model. If nil, will use original input. + // - failoverErr: If non-nil, failover stops and this error is returned. + // Should not be nil when ModelFailoverConfig is set via ChatModelAgentConfig. + GetFailoverModel func(ctx context.Context, failoverCtx *FailoverContext[M]) ( + failoverModel model.BaseModel[M], failoverModelInputMessages []M, failoverErr error) +} + +func typedGetFailoverLastSuccessModel[M MessageType](ctx context.Context) model.BaseModel[M] { + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + if execCtx == nil { + return nil + } + return execCtx.failoverLastSuccessModel +} + +func typedSetFailoverLastSuccessModel[M MessageType](ctx context.Context, m model.BaseModel[M]) { + if execCtx := getTypedChatModelAgentExecCtx[M](ctx); execCtx != nil { + execCtx.failoverLastSuccessModel = m + } +} + +type failoverModelWrapper[M MessageType] struct { + config *ModelFailoverConfig[M] + inner model.BaseModel[M] +} + +func newFailoverModelWrapper[M MessageType](inner model.BaseModel[M], config *ModelFailoverConfig[M]) *failoverModelWrapper[M] { + return &failoverModelWrapper[M]{ + config: config, + inner: inner, + } +} + +func (f *failoverModelWrapper[M]) needFailover(ctx context.Context, outputMessage M, outputErr error) bool { + if ctx.Err() != nil { + return false + } + + if _, ok := compose.ExtractInterruptInfo(outputErr); ok { + return false + } + + // ErrStreamCanceled means the caller voluntarily abandoned the stream; + // never retry or fail over in this case. + if errors.Is(outputErr, ErrStreamCanceled) { + return false + } + + // ShouldFailover is validated at agent construction; nil here indicates a programmer error. + return f.config.ShouldFailover(ctx, outputMessage, outputErr) +} + +func (f *failoverModelWrapper[M]) getFailoverModel(ctx context.Context, failoverCtx *FailoverContext[M]) (model.BaseModel[M], []M, error) { + currentModel, msgs, err := f.config.GetFailoverModel(ctx, failoverCtx) + if err != nil { + return nil, nil, err + } + if currentModel == nil { + return nil, nil, nil + } + return currentModel, msgs, nil +} + +func (f *failoverModelWrapper[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + // Defensive: GetFailoverModel is validated non-nil at agent construction. + if f.config.GetFailoverModel == nil { + return f.inner.Generate(ctx, input, opts...) + } + + var lastOutputMessage M + var lastErr error + + // Try lastSuccessModel first if available. + if lastSuccess := typedGetFailoverLastSuccessModel[M](ctx); lastSuccess != nil { + if err := ctx.Err(); err != nil { + var zero M + return zero, err + } + + modelCtx := typedSetFailoverCurrentModel(ctx, lastSuccess) + modelCtx = withFailoverHasMoreAttempts(modelCtx, f.config.MaxRetries > 0) + result, err := f.inner.Generate(modelCtx, input, opts...) + if err == nil { + return result, nil + } + + lastOutputMessage = result + lastErr = err + + if !f.needFailover(ctx, result, err) { + return result, err + } + + log.Printf("failover ChatModel.Generate lastSuccessModel failed: %v", err) + } + + for attempt := uint(1); attempt <= f.config.MaxRetries; attempt++ { + if err := ctx.Err(); err != nil { + var zero M + return zero, err + } + + failoverCtx := &FailoverContext[M]{ + FailoverAttempt: attempt, + InputMessages: input, + LastOutputMessage: lastOutputMessage, + LastErr: lastErr, + } + + currentModel, currentInput, err := f.getFailoverModel(ctx, failoverCtx) + if err != nil { + var zero M + return zero, err + } + if currentModel == nil { + var zero M + return zero, fmt.Errorf("failover GetFailoverModel returned nil model at attempt %d", attempt) + } + + if currentInput == nil { + currentInput = input + } + + modelCtx := typedSetFailoverCurrentModel(ctx, currentModel) + modelCtx = withFailoverHasMoreAttempts(modelCtx, attempt < f.config.MaxRetries) + result, err := f.inner.Generate(modelCtx, currentInput, opts...) + lastOutputMessage = result + lastErr = err + + if err == nil { + typedSetFailoverLastSuccessModel(ctx, currentModel) + return result, nil + } + + if !f.needFailover(ctx, result, err) { + return result, err + } + + if attempt < f.config.MaxRetries { + log.Printf("failover ChatModel.Generate attempt %d failed: %v", attempt, err) + } + } + + return lastOutputMessage, lastErr +} + +func (f *failoverModelWrapper[M]) Stream(ctx context.Context, input []M, opts ...model.Option) ( + *schema.StreamReader[M], error) { + // Defensive: GetFailoverModel is validated non-nil at agent construction. + if f.config.GetFailoverModel == nil { + return f.inner.Stream(ctx, input, opts...) + } + + var lastOutputMessage M + var lastErr error + + // Try lastSuccessModel first if available. + if lastSuccess := typedGetFailoverLastSuccessModel[M](ctx); lastSuccess != nil { + if err := ctx.Err(); err != nil { + return nil, err + } + + modelCtx := typedSetFailoverCurrentModel(ctx, lastSuccess) + modelCtx = withFailoverHasMoreAttempts(modelCtx, f.config.MaxRetries > 0) + stream, err := f.inner.Stream(modelCtx, input, opts...) + if err != nil { + lastErr = err + var zero M + if !f.needFailover(ctx, zero, err) { + return nil, err + } + log.Printf("failover ChatModel.Stream lastSuccessModel failed: %v", err) + } else { + copies := stream.Copy(2) + checkCopy := copies[0] + returnCopy := copies[1] + + outMsg, streamErr := typedConsumeStream(checkCopy) + if streamErr != nil { + lastOutputMessage = outMsg + lastErr = streamErr + returnCopy.Close() + + if !f.needFailover(ctx, outMsg, streamErr) { + return nil, streamErr + } + log.Printf("failover ChatModel.Stream lastSuccessModel failed: %v", streamErr) + } else { + return returnCopy, nil + } + } + } + + for attempt := uint(1); attempt <= f.config.MaxRetries; attempt++ { + if err := ctx.Err(); err != nil { + return nil, err + } + + failoverCtx := &FailoverContext[M]{ + FailoverAttempt: attempt, + InputMessages: input, + LastOutputMessage: lastOutputMessage, + LastErr: lastErr, + } + + currentModel, currentInput, err := f.getFailoverModel(ctx, failoverCtx) + if err != nil { + return nil, err + } + if currentModel == nil { + return nil, fmt.Errorf("failover GetFailoverModel returned nil model at attempt %d", attempt) + } + + if currentInput == nil { + currentInput = input + } + + modelCtx := typedSetFailoverCurrentModel(ctx, currentModel) + modelCtx = withFailoverHasMoreAttempts(modelCtx, attempt < f.config.MaxRetries) + stream, err := f.inner.Stream(modelCtx, currentInput, opts...) + if err != nil { + lastErr = err + var zero M + lastOutputMessage = zero + + if !f.needFailover(ctx, zero, err) { + return nil, err + } + + if attempt < f.config.MaxRetries { + log.Printf("failover ChatModel.Stream attempt %d failed: %v", attempt, err) + } + continue + } + + // The stream returned by f.inner.Stream is already Copy'd by the inner eventSender layer: one + // copy is forwarded to the client in real time via events. Therefore consuming a copy here does + // NOT block client-side streaming. + // + // We Copy the stream into two readers: + // - checkCopy: consumed synchronously to surface mid-stream errors and decide whether to fail over. + // - returnCopy: returned to the caller (stateModelWrapper), which also consumes synchronously to + // build state (AfterModelRewriteState), so waiting here adds no extra latency. + // + // If checkCopy errors and failover is allowed, we close returnCopy and retry with the next model. + // Otherwise we return returnCopy. + // + // NOTE on duplicate events during failover: when a retry happens, events from the failed attempt + // may already have been emitted to the client, and the retry will emit a new stream. Client-side + // handlers are expected to handle multiple rounds (e.g., reset on retry or deduplicate by attempt + // metadata). + copies := stream.Copy(2) + checkCopy := copies[0] + returnCopy := copies[1] + + outMsg, streamErr := typedConsumeStream(checkCopy) + if streamErr != nil { + lastOutputMessage = outMsg + lastErr = streamErr + returnCopy.Close() + + if !f.needFailover(ctx, outMsg, streamErr) { + return nil, streamErr + } + + if attempt < f.config.MaxRetries { + log.Printf("failover ChatModel.Stream attempt %d failed: %v", attempt, streamErr) + } + continue + } + + typedSetFailoverLastSuccessModel(ctx, currentModel) + return returnCopy, nil + } + + return nil, lastErr +} + +func typedConsumeStream[M MessageType](stream *schema.StreamReader[M]) (M, error) { + var zero M + defer stream.Close() + + switch s := any(stream).(type) { + case *schema.StreamReader[*schema.Message]: + chunks := make([]*schema.Message, 0) + for { + chunk, err := s.Recv() + if err == io.EOF { + break + } + if err != nil { + msg, _ := schema.ConcatMessages(chunks) + if msg != nil { + return any(msg).(M), err + } + return zero, err + } + chunks = append(chunks, chunk) + } + msg, _ := schema.ConcatMessages(chunks) + if msg != nil { + return any(msg).(M), nil + } + return zero, nil + case *schema.StreamReader[*schema.AgenticMessage]: + chunks := make([]*schema.AgenticMessage, 0) + for { + chunk, err := s.Recv() + if err == io.EOF { + break + } + if err != nil { + msg, _ := schema.ConcatAgenticMessages(chunks) + if msg != nil { + return any(msg).(M), err + } + return zero, err + } + chunks = append(chunks, chunk) + } + msg, _ := schema.ConcatAgenticMessages(chunks) + if msg != nil { + return any(msg).(M), nil + } + return zero, nil + default: + panic("unreachable: unknown MessageType") + } +} diff --git a/adk/failover_chatmodel_test.go b/adk/failover_chatmodel_test.go new file mode 100644 index 0000000..8b8ca57 --- /dev/null +++ b/adk/failover_chatmodel_test.go @@ -0,0 +1,742 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "io" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +type fakeChatModel struct { + generate func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) + stream func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) + callbacksEnabled bool +} + +func (m *fakeChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return m.generate(ctx, input, opts...) +} + +func (m *fakeChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return m.stream(ctx, input, opts...) +} + +func (m *fakeChatModel) IsCallbacksEnabled() bool { + return m.callbacksEnabled +} + +func drainMessageStream(sr *schema.StreamReader[*schema.Message]) ([]*schema.Message, error) { + defer sr.Close() + var out []*schema.Message + for { + chunk, err := sr.Recv() + if err == io.EOF { + return out, nil + } + if err != nil { + return out, err + } + out = append(out, chunk) + } +} + +func streamWithMidError(chunks []*schema.Message, err error) *schema.StreamReader[*schema.Message] { + sr, sw := schema.Pipe[*schema.Message](2) + go func() { + defer sw.Close() + for _, c := range chunks { + sw.Send(c, nil) + } + sw.Send(nil, err) + }() + return sr +} + +func streamWithMidErrorControlled(chunks []*schema.Message, err error, firstSent chan struct{}, release chan struct{}) *schema.StreamReader[*schema.Message] { + sr, sw := schema.Pipe[*schema.Message](2) + go func() { + defer sw.Close() + for i, c := range chunks { + sw.Send(c, nil) + if i == 0 && firstSent != nil { + close(firstSent) + if release != nil { + <-release + } + } + } + sw.Send(nil, err) + }() + return sr +} + +func TestFailoverCurrentModelContext(t *testing.T) { + t.Run("set and get", func(t *testing.T) { + ctx := context.Background() + m := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("ok", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil + }, + } + ctx = typedSetFailoverCurrentModel[*schema.Message](ctx, m) + got, ok := typedGetFailoverCurrentModel[*schema.Message](ctx) + require.True(t, ok) + require.Same(t, m, got) + }) + + t.Run("wrong type", func(t *testing.T) { + ctx := context.WithValue(context.Background(), failoverCurrentModelKey{}, "bad") + _, ok := typedGetFailoverCurrentModel[*schema.Message](ctx) + require.False(t, ok) + }) + + t.Run("missing", func(t *testing.T) { + _, ok := typedGetFailoverCurrentModel[*schema.Message](context.Background()) + require.False(t, ok) + }) +} + +func TestFailoverProxyModel(t *testing.T) { + t.Run("generate missing context", func(t *testing.T) { + p := &failoverProxyModel{} + _, err := p.Generate(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + }) + + t.Run("stream missing context", func(t *testing.T) { + p := &failoverProxyModel{} + _, err := p.Stream(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + }) + + t.Run("generate routes to current model", func(t *testing.T) { + var called int32 + target := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&called, 1) + return schema.AssistantMessage("routed", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("routed", nil)}), nil + }, + } + ctx := typedSetFailoverCurrentModel[*schema.Message](context.Background(), target) + p := &failoverProxyModel{} + msg, err := p.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "routed", msg.Content) + require.Equal(t, int32(1), atomic.LoadInt32(&called)) + }) +} + +func TestFailoverModelWrapper_Generate(t *testing.T) { + t.Run("delegates when GetFailoverModel nil", func(t *testing.T) { + var called int32 + inner := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&called, 1) + return schema.AssistantMessage("inner", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("inner", nil)}), nil + }, + } + w := newFailoverModelWrapper[*schema.Message](inner, &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 2, + ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, + GetFailoverModel: nil, + }) + msg, err := w.Generate(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "inner", msg.Content) + require.Equal(t, int32(1), atomic.LoadInt32(&called)) + }) + + t.Run("failover to second model", func(t *testing.T) { + wantErr := errors.New("first failed") + var shouldCalls int32 + var m1Calls int32 + var m2Calls int32 + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, wantErr + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + }, + } + m2 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m2Calls, 1) + return schema.AssistantMessage("ok", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + return errors.Is(err, wantErr) + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.Equal(t, uint(1), failoverCtx.FailoverAttempt) + return m2, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + msg, err := w.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "ok", msg.Content) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("canceled error delegates to ShouldFailover", func(t *testing.T) { + var shouldCalls int32 + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, context.Canceled + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 5, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + // User decides to stop on canceled error + return !errors.Is(err, context.Canceled) + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m1, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := w.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.ErrorIs(t, err, context.Canceled) + // ShouldFailover is called once and returns false, stopping failover + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("stops when GetFailoverModel returns error", func(t *testing.T) { + wantErr := errors.New("get model failed") + var called int32 + inner := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&called, 1) + return schema.AssistantMessage("unused", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 3, + ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return nil, nil, wantErr + }, + } + + w := newFailoverModelWrapper[*schema.Message](inner, cfg) + _, err := w.Generate(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.ErrorIs(t, err, wantErr) + require.Equal(t, int32(0), atomic.LoadInt32(&called)) + }) + + t.Run("stops when GetFailoverModel returns nil model", func(t *testing.T) { + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return nil, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + msg, err := w.Generate(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.Nil(t, msg) + require.Error(t, err) + require.ErrorContains(t, err, "GetFailoverModel returned nil model") + }) +} + +func TestFailoverModelWrapper_Stream(t *testing.T) { + t.Run("returns stream when first attempt succeeds", func(t *testing.T) { + var shouldCalls int32 + in := schema.UserMessage("hi") + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, input []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + require.Len(t, input, 1) + require.Same(t, in, input[0]) + return schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("a", nil), + schema.AssistantMessage("b", nil), + }), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 0, + ShouldFailover: func(context.Context, *schema.Message, error) bool { + atomic.AddInt32(&shouldCalls, 1) + return false + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m1, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := w.Stream(ctx, []*schema.Message{in}) + require.NoError(t, err) + msgs, err := drainMessageStream(sr) + require.NoError(t, err) + require.Len(t, msgs, 2) + require.Equal(t, "a", msgs[0].Content) + require.Equal(t, "b", msgs[1].Content) + require.Equal(t, int32(0), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("failover when Stream returns error immediately", func(t *testing.T) { + wantErr := errors.New("stream init failed") + var shouldCalls int32 + var m1Calls int32 + var m2Calls int32 + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return nil, wantErr + }, + } + m2 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + return errors.Is(err, wantErr) + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.Equal(t, uint(1), failoverCtx.FailoverAttempt) + return m2, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := w.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + msgs, err := drainMessageStream(sr) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.Equal(t, "ok", msgs[0].Content) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("failover when stream errors mid-way", func(t *testing.T) { + streamErr := errors.New("mid error") + var shouldCalls int32 + var seenOutput atomic.Value + var m1Calls int32 + var m2Calls int32 + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return streamWithMidError([]*schema.Message{ + schema.AssistantMessage("p1", nil), + schema.AssistantMessage("p2", nil), + }, streamErr), nil + }, + } + m2 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("final", nil)}), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, out *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + if errors.Is(err, streamErr) && out != nil { + seenOutput.Store(out.Content) + } + return errors.Is(err, streamErr) + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.Equal(t, uint(1), failoverCtx.FailoverAttempt) + return m2, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := w.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + msgs, err := drainMessageStream(sr) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.Equal(t, "final", msgs[0].Content) + require.Equal(t, "p1p2", seenOutput.Load()) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("stop when ShouldFailover returns false for mid-way error", func(t *testing.T) { + streamErr := errors.New("mid error") + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return streamWithMidError([]*schema.Message{schema.AssistantMessage("p", nil)}, streamErr), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 3, + ShouldFailover: func(context.Context, *schema.Message, error) bool { + return false + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m1, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := w.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Nil(t, sr) + require.ErrorIs(t, err, streamErr) + }) + + t.Run("canceled mid-way error delegates to ShouldFailover", func(t *testing.T) { + var shouldCalls int32 + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return streamWithMidError([]*schema.Message{schema.AssistantMessage("p", nil)}, context.Canceled), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 3, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + // User decides to stop on canceled error + return !errors.Is(err, context.Canceled) + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m1, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := w.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Nil(t, sr) + require.ErrorIs(t, err, context.Canceled) + // ShouldFailover is called once and returns false, stopping failover + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("stop when Stream returns error immediately and ShouldFailover returns false", func(t *testing.T) { + wantErr := errors.New("stream init failed") + var shouldCalls int32 + var m1Calls int32 + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return nil, wantErr + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 3, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + require.ErrorIs(t, err, wantErr) + return false + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m1, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := w.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Nil(t, sr) + require.ErrorIs(t, err, wantErr) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) + }) + + t.Run("stops when GetFailoverModel returns nil model", func(t *testing.T) { + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return nil, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + sr, err := w.Stream(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.Nil(t, sr) + require.Error(t, err) + require.ErrorContains(t, err, "GetFailoverModel returned nil model") + }) + + t.Run("stops when GetFailoverModel returns error", func(t *testing.T) { + wantErr := errors.New("get model failed") + var called int32 + inner := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&called, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("unused", nil)}), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 3, + ShouldFailover: func(context.Context, *schema.Message, error) bool { return true }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return nil, nil, wantErr + }, + } + + w := newFailoverModelWrapper[*schema.Message](inner, cfg) + sr, err := w.Stream(context.Background(), []*schema.Message{schema.UserMessage("hi")}) + require.Nil(t, sr) + require.ErrorIs(t, err, wantErr) + require.Equal(t, int32(0), atomic.LoadInt32(&called)) + }) + + t.Run("stops when ctx canceled during mid-way error handling", func(t *testing.T) { + midErr := errors.New("mid error") + var shouldCalls int32 + var m1Calls int32 + var m2Calls int32 + firstSent := make(chan struct{}) + release := make(chan struct{}) + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return streamWithMidErrorControlled( + []*schema.Message{schema.AssistantMessage("p", nil)}, + midErr, + firstSent, + release, + ), nil + }, + } + m2 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("unused", nil)}), nil + }, + } + + cfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(context.Context, *schema.Message, error) bool { + atomic.AddInt32(&shouldCalls, 1) + return true + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.Equal(t, uint(1), failoverCtx.FailoverAttempt) + return m2, nil, nil + }, + } + + w := newFailoverModelWrapper[*schema.Message](&failoverProxyModel{}, cfg) + baseCtx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + ctx, cancel := context.WithCancel(baseCtx) + type result struct { + sr *schema.StreamReader[*schema.Message] + err error + } + ch := make(chan result, 1) + go func() { + sr, err := w.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + ch <- result{sr: sr, err: err} + }() + + <-firstSent + cancel() + close(release) + + res := <-ch + if res.sr != nil { + res.sr.Close() + } + require.Nil(t, res.sr) + require.ErrorIs(t, res.err, midErr) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(0), atomic.LoadInt32(&m2Calls)) + require.Equal(t, int32(0), atomic.LoadInt32(&shouldCalls)) + }) +} + +func TestTypedConsumeStream_EmptyAgenticStream(t *testing.T) { + sr, sw := schema.Pipe[*schema.AgenticMessage](1) + sw.Close() + + msg, err := typedConsumeStream(sr) + assert.Nil(t, err, "empty stream should not return error") + assert.NotNil(t, msg, "empty stream should return non-nil message from ConcatAgenticMessages") +} + +func TestTypedConsumeStream_AgenticMidStreamError(t *testing.T) { + midErr := errors.New("mid-stream failure") + sr := streamWithMidErrorAgentic( + []*schema.AgenticMessage{agenticChunk("chunk1"), agenticChunk("chunk2")}, + midErr, + ) + + msg, err := typedConsumeStream(sr) + assert.ErrorIs(t, err, midErr, "should return the mid-stream error") + assert.NotNil(t, msg, "should return concatenated partial message from received chunks") +} + +func streamWithMidErrorAgentic(chunks []*schema.AgenticMessage, err error) *schema.StreamReader[*schema.AgenticMessage] { + sr, sw := schema.Pipe[*schema.AgenticMessage](len(chunks) + 1) + go func() { + defer sw.Close() + for _, c := range chunks { + sw.Send(c, nil) + } + sw.Send(nil, err) + }() + return sr +} + +func agenticChunk(text string) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: text}), + }, + } +} diff --git a/adk/filesystem/backend.go b/adk/filesystem/backend.go new file mode 100644 index 0000000..62ebee8 --- /dev/null +++ b/adk/filesystem/backend.go @@ -0,0 +1,304 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package filesystem provides file system operations. +package filesystem + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// FileInfo represents basic file metadata information. +type FileInfo struct { + // Path is the path of the file or directory, which can be a filename, relative path, or absolute path. + Path string + + // IsDir indicates whether the entry is a directory. + // true for directories, false for regular files. + IsDir bool + + // Size is the file size in bytes. + // For directories, this value may be 0 or platform-dependent. + Size int64 + + // ModifiedAt is the last modification time in ISO 8601 format. + // Example: "2025-01-15T10:30:00Z" + ModifiedAt string +} + +// GrepMatch represents a single pattern match result. +type GrepMatch struct { + Content string + + // Path is the file path where the match was found. + Path string + + // Line is the 1-based line number of the match. + Line int +} + +// LsInfoRequest contains parameters for listing file information. +type LsInfoRequest struct { + // Path specifies the directory path to list. + Path string +} + +// ReadRequest contains parameters for reading file content. +type ReadRequest struct { + // FilePath is the path to the file to be read. + FilePath string + + // Offset specifies the starting line number (1-based) for reading. + // Line 1 is the first line of the file. + // Use this when the file is too large to read at once. + // Defaults to 1 (start from the first line). + // Values < 1 will be treated as 1. + Offset int + + // Limit specifies the maximum number of lines to read. + // When Limit is 0 (default), the entire file content is returned. + Limit int +} + +// MultiModalReadRequest extends ReadRequest with parameters only applicable +// to MultiModalReader implementations (e.g. PDF page ranges). +type MultiModalReadRequest struct { + ReadRequest + + // Pages specifies the page range for PDF files (e.g. "1-5", "3", "10-20"). + Pages string +} + +// GrepRequest contains parameters for searching file content. +type GrepRequest struct { + // ===== Search Parameters ===== + + // Pattern is the search pattern, supports full regular expression syntax. + // Uses ripgrep syntax (not grep). Examples: + // - "log.*Error" matches lines with "log" followed by "Error" + // - "function\\s+\\w+" matches "function" followed by whitespace and word characters + // - Literal braces need escaping: "interface\\{\\}" matches "interface{}" + Pattern string + + // Path is an optional directory path to limit the search scope. + Path string + + // ===== File Filtering ===== + + // Glob is an optional pattern to filter the files to be searched. + // It filters by file path, not content. If empty, no files are filtered. + // Supports standard glob wildcards: + // - `*` matches any characters except path separators. + // - `**` matches any directories recursively. + // - `?` matches a single character. + // - `[abc]` matches one character from the set. + Glob string + + // FileType is the file type filter, e.g., "js", "py", "rust". + // More efficient than Glob for standard file types. + FileType string + + // ===== Search Options ===== + + // CaseInsensitive enables case insensitive search. + CaseInsensitive bool + + // EnableMultiline enables multiline mode where patterns can span lines. + // Default: false (patterns match within single lines only). + EnableMultiline bool + + // ===== Context Display (Content mode only) ===== + + // AfterLines shows N lines after each match. + // Only applicable when OutputMode is "content". + // Values <= 0 are treated as unset. + AfterLines int + + // BeforeLines shows N lines before each match. + // Only applicable when OutputMode is "content". + // Values <= 0 are treated as unset. + BeforeLines int +} + +// GlobInfoRequest contains parameters for glob pattern matching. +type GlobInfoRequest struct { + // Pattern is the glob expression used to match file paths. + // It supports standard glob syntax: + // - `*` matches any characters except path separators. + // - `**` matches any directories recursively. + // - `?` matches a single character. + // - `[abc]` matches one character from the set. + Pattern string + + // Path is the base directory from which to start the search. + Path string +} + +// WriteRequest contains parameters for writing file content. +type WriteRequest struct { + // FilePath is the path of the file to write. + FilePath string + + // Content is the data to be written to the file. + Content string +} + +// EditRequest contains parameters for editing file content. +type EditRequest struct { + // FilePath is the path of the file to edit. + FilePath string + + // OldString is the exact string to be replaced. It must be non-empty and will be matched literally, including whitespace. + OldString string + + // NewString is the string that will replace OldString. + // It must be different from OldString. + // An empty string can be used to effectively delete OldString. + NewString string + + // ReplaceAll controls the replacement behavior. + // If true, all occurrences of OldString are replaced. + // If false, the operation fails unless OldString appears exactly once in the file. + ReplaceAll bool +} + +// FileContentPartType defines the type of a multimodal file content part. +type FileContentPartType string + +const ( + // FileContentPartTypeImage represents an image part (e.g. PNG, JPG). + FileContentPartTypeImage FileContentPartType = "image" + // FileContentPartTypePDF represents a file part (e.g. PDF). + FileContentPartTypePDF FileContentPartType = "pdf" +) + +// FileContentPart represents a multimodal part of file content. +// Data holds raw bytes; encoding (e.g. base64) is handled by the consumer. +type FileContentPart struct { + // Type is the kind of content this part represents. + // Required. + Type FileContentPartType + + // MIMEType is the MIME type of the content (e.g. "image/png", "application/pdf"). + // Required. + MIMEType string + + // Data is the raw binary content. + // Required. + Data []byte +} + +// FileContent holds the result of a Read operation. +type FileContent struct { + // Content holds the plain text content of the file. + Content string +} + +// MultiFileContent holds the result of a MultiModalRead operation. +// +// FileContent and Parts are mutually exclusive (one-of): +// - Set FileContent for plain text results (same as a normal Read). +// - Set Parts for multimodal results (images, PDFs, etc.). +// +// When Parts is non-empty, FileContent is ignored. +type MultiFileContent struct { + *FileContent + + // Parts holds multimodal output parts (e.g. image, PDF). + Parts []FileContentPart +} + +// MultiModalReader is an optional extension interface for Backend. +// Backends that implement this interface support multimodal file reading, +// returning structured parts (images, PDFs) instead of plain text. +// +// For large file handling, there are two approaches to control output size: +// - Implement size control within MultiModalRead (e.g. reject files exceeding a threshold, +// downsample images, or limit PDF page counts at the backend level). +// - Use ToolMiddleware's EnhancedInvokable to customize result transformation, +// or use the built-in reduction middleware with configurable policies. +type MultiModalReader interface { + MultiModalRead(ctx context.Context, req *MultiModalReadRequest) (*MultiFileContent, error) +} + +// Backend is a pluggable, unified file backend protocol interface. +// +// All methods use struct-based parameters to allow future extensibility +// without breaking backward compatibility. +type Backend interface { + // LsInfo lists file information under the given path. + // + // Returns: + // - []FileInfo: List of matching file information + // - error: Error if the operation fails + LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error) + + // Read reads file content with support for line-based offset and limit. + // + // Returns: + // - string: The file content read + // - error: Error if file does not exist or read fails + Read(ctx context.Context, req *ReadRequest) (*FileContent, error) + + // GrepRaw searches for content matching the specified pattern in files. + // + // Returns: + // - []GrepMatch: List of all matching results + // - error: Error if the search fails + GrepRaw(ctx context.Context, req *GrepRequest) ([]GrepMatch, error) + + // GlobInfo returns file information matching the glob pattern. + // + // Returns: + // - []FileInfo: List of matching file information + // - error: Error if the pattern is invalid or operation fails + GlobInfo(ctx context.Context, req *GlobInfoRequest) ([]FileInfo, error) + + // Write creates or updates file content. + // + // Returns: + // - error: Error if the write operation fails + Write(ctx context.Context, req *WriteRequest) error + + // Edit replaces string occurrences in a file. + // + // Returns: + // - error: Error if file does not exist, OldString is empty, or OldString is not found + Edit(ctx context.Context, req *EditRequest) error +} + +// ExecuteRequest contains parameters for executing a command. +type ExecuteRequest struct { + Command string // The command to execute + RunInBackendGround bool +} + +// ExecuteResponse contains the response result of command execution. +type ExecuteResponse struct { + Output string // Command output content + ExitCode *int // Command exit code + Truncated bool // Whether the output was truncated +} + +type Shell interface { + Execute(ctx context.Context, input *ExecuteRequest) (result *ExecuteResponse, err error) +} + +type StreamingShell interface { + ExecuteStreaming(ctx context.Context, input *ExecuteRequest) (result *schema.StreamReader[*ExecuteResponse], err error) +} diff --git a/adk/filesystem/backend_inmemory.go b/adk/filesystem/backend_inmemory.go new file mode 100644 index 0000000..1a81181 --- /dev/null +++ b/adk/filesystem/backend_inmemory.go @@ -0,0 +1,774 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +import ( + "context" + "fmt" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/bmatcuk/doublestar/v4" +) + +type fileEntry struct { + content string + modifiedAt time.Time +} + +// InMemoryBackend is an in-memory implementation of the Backend interface. +// It stores files in a map and is safe for concurrent use. +type InMemoryBackend struct { + mu sync.RWMutex + files map[string]*fileEntry +} + +// NewInMemoryBackend creates a new in-memory backend. +func NewInMemoryBackend() *InMemoryBackend { + return &InMemoryBackend{ + files: make(map[string]*fileEntry), + } +} + +// LsInfo lists file information under the given path. +func (b *InMemoryBackend) LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + // Normalize path + path := normalizePath(req.Path) + + var result []FileInfo + seen := make(map[string]bool) + dirInfo := make(map[string]*FileInfo) + + for filePath, entry := range b.files { + normalizedFilePath := normalizePath(filePath) + + // Check if file is under the given path + if path == "/" || strings.HasPrefix(normalizedFilePath, path+"/") || normalizedFilePath == path { + // For directory listing, we want to show immediate children + relativePath := strings.TrimPrefix(normalizedFilePath, path) + relativePath = strings.TrimPrefix(relativePath, "/") + + if relativePath == "" { + // The path itself is a file + if !seen[normalizedFilePath] { + result = append(result, FileInfo{ + Path: filepath.Base(normalizedFilePath), + IsDir: false, + Size: int64(len(entry.content)), + ModifiedAt: entry.modifiedAt.Format(time.RFC3339Nano), + }) + seen[normalizedFilePath] = true + } + continue + } + + // Get the first segment (immediate child) + parts := strings.SplitN(relativePath, "/", 2) + if len(parts) > 0 { + childPath := path + if path != "/" { + childPath += "/" + } + childPath += parts[0] + + isDir := len(parts) > 1 + if !seen[childPath] { + if isDir { + dirInfo[childPath] = &FileInfo{ + Path: parts[0], + IsDir: true, + Size: 0, + ModifiedAt: entry.modifiedAt.Format(time.RFC3339Nano), + } + } else { + result = append(result, FileInfo{ + Path: parts[0], + IsDir: false, + Size: int64(len(entry.content)), + ModifiedAt: entry.modifiedAt.Format(time.RFC3339Nano), + }) + } + seen[childPath] = true + } else if isDir { + if info, ok := dirInfo[childPath]; ok { + if entry.modifiedAt.After(mustParseTime(info.ModifiedAt)) { + info.ModifiedAt = entry.modifiedAt.Format(time.RFC3339Nano) + } + } + } + } + } + } + + for _, info := range dirInfo { + result = append(result, *info) + } + + return result, nil +} + +func mustParseTime(s string) time.Time { + t, _ := time.Parse(time.RFC3339Nano, s) + return t +} + +// Read reads file content with offset and limit. +func (b *InMemoryBackend) Read(ctx context.Context, req *ReadRequest) (*FileContent, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + filePath := normalizePath(req.FilePath) + + entry, exists := b.files[filePath] + if !exists { + return nil, fmt.Errorf("file not found: %s", filePath) + } + + // Convert 1-based offset to 0-based index; values < 1 default to line 1 + offset := req.Offset - 1 + if offset < 0 { + offset = 0 + } + limit := req.Limit + + content := entry.content + + // Fast path: no offset and no limit — return as-is + if offset == 0 && limit <= 0 { + return &FileContent{Content: content}, nil + } + + // Fast path: no offset, content fits within limit — return as-is + if offset == 0 { + lineCount := strings.Count(content, "\n") + 1 + if lineCount <= limit { + return &FileContent{Content: content}, nil + } + } + + // Skip `offset` lines by scanning for newlines directly + start := 0 + for i := 0; i < offset; i++ { + idx := strings.IndexByte(content[start:], '\n') + if idx == -1 { + // offset exceeds total lines + return &FileContent{}, nil + } + start += idx + 1 + } + + // No limit: return everything from start + if limit <= 0 { + return &FileContent{Content: content[start:]}, nil + } + + // Find the end position after `limit` lines + end := start + for i := 0; i < limit; i++ { + idx := strings.IndexByte(content[end:], '\n') + if idx == -1 { + // Reached the end of content + return &FileContent{Content: content[start:]}, nil + } + end += idx + 1 + } + + // Trim the trailing newline from the last included line + return &FileContent{Content: content[start : end-1]}, nil +} + +// GrepRaw returns matches for the given pattern. +func (b *InMemoryBackend) GrepRaw(ctx context.Context, req *GrepRequest) ([]GrepMatch, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + if req.Pattern == "" { + return nil, fmt.Errorf("pattern cannot be empty") + } + + re, err := b.compilePattern(req) + if err != nil { + return nil, err + } + + searchPath := "/" + if req.Path != "" { + searchPath = normalizePath(req.Path) + } + + filteredFiles, err := b.filterFiles(searchPath, req) + if err != nil { + return nil, err + } + + if len(filteredFiles) == 0 { + return []GrepMatch{}, nil + } + + if len(filteredFiles) == 1 { + collector := newGrepCollector() + entry := b.files[filteredFiles[0]] + collector.processFile(filteredFiles[0], entry.content, re, req) + return collector.buildResults(b, req) + } + + matches, err := b.grepFilesInParallel(filteredFiles, re, req) + if err != nil { + return nil, err + } + + if req.BeforeLines > 0 || req.AfterLines > 0 { + matches = b.applyContext(matches, req) + } + + return matches, nil +} + +func (b *InMemoryBackend) grepFilesInParallel(filteredFiles []string, re *regexp.Regexp, req *GrepRequest) ([]GrepMatch, error) { + numWorkers := len(filteredFiles) + if numWorkers > 10 { + numWorkers = 10 + } + + type fileTask struct { + path string + content string + } + + tasks := make(chan fileTask, len(filteredFiles)) + results := make(chan []GrepMatch, len(filteredFiles)) + errChan := make(chan error, numWorkers) + + var wg sync.WaitGroup + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + errChan <- fmt.Errorf("worker %d panic: %v", workerID, r) + } + }() + + collector := newGrepCollector() + for task := range tasks { + fileMatches := collector.findMatches(task.path, task.content, re, req) + if len(fileMatches) > 0 { + results <- fileMatches + } + } + }(i) + } + + for _, filePath := range filteredFiles { + entry := b.files[filePath] + tasks <- fileTask{ + path: filePath, + content: entry.content, + } + } + close(tasks) + + go func() { + wg.Wait() + close(results) + close(errChan) + }() + + var allMatches []GrepMatch + var errs []error + + for { + select { + case matches, ok := <-results: + if !ok { + results = nil + } else { + allMatches = append(allMatches, matches...) + } + case err, ok := <-errChan: + if !ok { + errChan = nil + } else if err != nil { + errs = append(errs, err) + } + } + + if results == nil && errChan == nil { + break + } + } + + if len(errs) > 0 { + return nil, fmt.Errorf("grep failed with %d error(s): %v", len(errs), errs[0]) + } + + return allMatches, nil +} + +func (b *InMemoryBackend) compilePattern(req *GrepRequest) (*regexp.Regexp, error) { + pattern := req.Pattern + if req.CaseInsensitive { + pattern = "(?i)" + pattern + } + re, err := regexp.Compile(pattern) + if err != nil { + return nil, fmt.Errorf("invalid regex pattern: %w", err) + } + return re, nil +} + +func (b *InMemoryBackend) filterFiles(searchPath string, req *GrepRequest) ([]string, error) { + var candidateFiles []string + + for filePath := range b.files { + normalizedFilePath := normalizePath(filePath) + + if searchPath != "/" && !strings.HasPrefix(normalizedFilePath, searchPath+"/") && normalizedFilePath != searchPath { + continue + } + + candidateFiles = append(candidateFiles, normalizedFilePath) + } + + if req.Glob != "" { + filtered, err := b.filterByGlob(candidateFiles, searchPath, req.Glob) + if err != nil { + return nil, err + } + candidateFiles = filtered + } + + if req.FileType != "" { + candidateFiles = b.filterByFileType(candidateFiles, req.FileType) + } + + return candidateFiles, nil +} + +func (b *InMemoryBackend) filterByGlob(files []string, searchPath string, globPattern string) ([]string, error) { + var result []string + + for _, filePath := range files { + var matchPath string + if strings.Contains(globPattern, "/") || strings.Contains(globPattern, "**") { + if searchPath == "/" { + matchPath = strings.TrimPrefix(filePath, "/") + } else { + matchPath = strings.TrimPrefix(filePath, searchPath+"/") + } + } else { + matchPath = filepath.Base(filePath) + } + + matched, err := doublestar.Match(globPattern, matchPath) + if err != nil { + return nil, fmt.Errorf("invalid glob pattern: %w", err) + } + if matched { + result = append(result, filePath) + } + } + + return result, nil +} + +func (b *InMemoryBackend) filterByFileType(files []string, fileType string) []string { + var result []string + + for _, filePath := range files { + ext := strings.TrimPrefix(filepath.Ext(filePath), ".") + if matchFileType(ext, fileType) { + result = append(result, filePath) + } + } + + return result +} + +// matchFileType checks if the file extension matches the given file type. +func matchFileType(ext, fileType string) bool { + typeMap := map[string][]string{ + "ada": {"adb", "ads"}, + "agda": {"agda", "lagda"}, + "aidl": {"aidl"}, + "amake": {"bp", "mk"}, + "asciidoc": {"adoc", "asc", "asciidoc"}, + "asm": {"S", "asm", "s"}, + "asp": {"ascx", "asp", "aspx"}, + "ats": {"ats", "dats", "hats", "sats"}, + "avro": {"avdl", "avpr", "avsc"}, + "awk": {"awk"}, + "bat": {"bat"}, + "bazel": {"BUILD", "bazel", "bzl"}, + "bitbake": {"bb", "bbappend", "bbclass", "conf", "inc"}, + "c": {"c", "h", "H", "cats"}, + "cabal": {"cabal"}, + "cbor": {"cbor"}, + "ceylon": {"ceylon"}, + "clojure": {"clj", "cljc", "cljs", "cljx"}, + "cmake": {"cmake"}, + "coffeescript": {"coffee"}, + "config": {"cfg", "conf", "config", "ini"}, + "coq": {"v"}, + "cpp": {"C", "cc", "cpp", "cxx", "c++", "h", "hh", "hpp", "hxx", "h++", "inl"}, + "crystal": {"cr", "ecr"}, + "cs": {"cs"}, + "csharp": {"cs"}, + "cshtml": {"cshtml"}, + "css": {"css", "scss", "sass", "less"}, + "csv": {"csv"}, + "cuda": {"cu", "cuh"}, + "cython": {"pxd", "pxi", "pyx"}, + "d": {"d"}, + "dart": {"dart"}, + "devicetree": {"dts", "dtsi"}, + "dhall": {"dhall"}, + "diff": {"diff", "patch"}, + "docker": {"dockerfile"}, + "go": {"go"}, + "groovy": {"gradle", "groovy"}, + "haskell": {"c2hs", "cpphs", "hs", "hsc", "lhs"}, + "html": {"ejs", "htm", "html"}, + "java": {"java", "jsp", "jspx", "properties"}, + "js": {"cjs", "js", "jsx", "mjs", "vue"}, + "json": {"json", "sarif"}, + "jsonl": {"jsonl"}, + "julia": {"jl"}, + "jupyter": {"ipynb", "jpynb"}, + "kotlin": {"kt", "kts"}, + "less": {"less"}, + "lua": {"lua"}, + "make": {"mak", "mk"}, + "markdown": {"markdown", "md", "mdown", "mdwn", "mdx", "mkd", "mkdn"}, + "md": {"markdown", "md", "mdown", "mdwn", "mdx", "mkd", "mkdn"}, + "matlab": {"m"}, + "ocaml": {"ml", "mli", "mll", "mly"}, + "perl": {"PL", "perl", "pl", "plh", "plx", "pm", "t"}, + "php": {"php", "php3", "php4", "php5", "php7", "php8", "pht", "phtml"}, + "python": {"py", "pyi"}, + "py": {"py", "pyi"}, + "ruby": {"gemspec", "rb", "rbw"}, + "rust": {"rs"}, + "sass": {"sass", "scss"}, + "scala": {"sbt", "scala"}, + "sh": {"bash", "sh", "zsh"}, + "sql": {"psql", "sql"}, + "swift": {"swift"}, + "toml": {"toml"}, + "ts": {"cts", "mts", "ts", "tsx"}, + "typescript": {"cts", "mts", "ts", "tsx"}, + "txt": {"txt"}, + "vue": {"vue"}, + "xml": {"dtd", "xml", "xsd", "xsl", "xslt"}, + "yaml": {"yaml", "yml"}, + "zig": {"zig"}, + } + + if exts, ok := typeMap[fileType]; ok { + for _, e := range exts { + if ext == e { + return true + } + } + } + return ext == fileType +} + +// applyContext adds context lines around matches. +func (b *InMemoryBackend) applyContext(matches []GrepMatch, req *GrepRequest) []GrepMatch { + if len(matches) == 0 { + return matches + } + + beforeLines := 0 + afterLines := 0 + + if req.BeforeLines > 0 { + beforeLines = req.BeforeLines + } + if req.AfterLines > 0 { + afterLines = req.AfterLines + } + + if beforeLines <= 0 && afterLines <= 0 { + return matches + } + + // Group matches by file path for efficient processing + matchesByFile := make(map[string][]GrepMatch) + fileOrder := make([]string, 0) + seenFiles := make(map[string]bool) + + for _, match := range matches { + if !seenFiles[match.Path] { + fileOrder = append(fileOrder, match.Path) + seenFiles[match.Path] = true + } + matchesByFile[match.Path] = append(matchesByFile[match.Path], match) + } + + var result []GrepMatch + + // Process each file once + for _, filePath := range fileOrder { + fileMatches := matchesByFile[filePath] + + // Get file content once per file + b.mu.RLock() + entry, exists := b.files[filePath] + b.mu.RUnlock() + + if !exists { + // If file doesn't exist, keep original matches + result = append(result, fileMatches...) + continue + } + + lines := strings.Split(entry.content, "\n") + processedLines := make(map[int]bool) + + // Process all matches for this file + for _, match := range fileMatches { + startLine := match.Line - beforeLines + if startLine < 1 { + startLine = 1 + } + + endLine := match.Line + afterLines + if endLine > len(lines) { + endLine = len(lines) + } + + for lineNum := startLine; lineNum <= endLine; lineNum++ { + if !processedLines[lineNum] { + processedLines[lineNum] = true + result = append(result, GrepMatch{ + Path: filePath, + Line: lineNum, + Content: lines[lineNum-1], + }) + } + } + } + } + + return result +} + +// GlobInfo returns file info entries matching the glob pattern. +func (b *InMemoryBackend) GlobInfo(ctx context.Context, req *GlobInfoRequest) ([]FileInfo, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + basePath := normalizePath(req.Path) + isAbsolutePattern := strings.HasPrefix(req.Pattern, "/") + + var result []FileInfo + + for filePath, entry := range b.files { + normalizedFilePath := normalizePath(filePath) + + var matchPath string + var resultPath string + + if isAbsolutePattern { + matchPath = normalizedFilePath + resultPath = normalizedFilePath + } else { + if basePath != "/" && !strings.HasPrefix(normalizedFilePath, basePath+"/") && normalizedFilePath != basePath { + continue + } + + if basePath == "/" { + matchPath = strings.TrimPrefix(normalizedFilePath, "/") + } else { + matchPath = strings.TrimPrefix(normalizedFilePath, basePath+"/") + } + resultPath = matchPath + } + + matched, err := doublestar.Match(req.Pattern, matchPath) + if err != nil { + return nil, fmt.Errorf("invalid glob pattern: %w", err) + } + + if matched { + result = append(result, FileInfo{ + Path: resultPath, + IsDir: false, + Size: int64(len(entry.content)), + ModifiedAt: entry.modifiedAt.Format(time.RFC3339Nano), + }) + } + } + + return result, nil +} + +// Write creates or overwrites file content. +func (b *InMemoryBackend) Write(ctx context.Context, req *WriteRequest) error { + b.mu.Lock() + defer b.mu.Unlock() + + filePath := normalizePath(req.FilePath) + b.files[filePath] = &fileEntry{ + content: req.Content, + modifiedAt: time.Now(), + } + + return nil +} + +// Edit replaces string occurrences in a file. +func (b *InMemoryBackend) Edit(ctx context.Context, req *EditRequest) error { + b.mu.Lock() + defer b.mu.Unlock() + + filePath := normalizePath(req.FilePath) + + entry, exists := b.files[filePath] + if !exists { + return fmt.Errorf("file not found: %s", filePath) + } + + if req.OldString == "" { + return fmt.Errorf("oldString must be non-empty") + } + + content := entry.content + if !strings.Contains(content, req.OldString) { + return fmt.Errorf("oldString not found in file: %s", filePath) + } + + if !req.ReplaceAll { + firstIndex := strings.Index(content, req.OldString) + if firstIndex != -1 { + // Check if there's another occurrence after the first one + if strings.Contains(content[firstIndex+len(req.OldString):], req.OldString) { + return fmt.Errorf("multiple occurrences of oldString found in file %s, but ReplaceAll is false", filePath) + } + } + } + + var newContent string + if req.ReplaceAll { + newContent = strings.ReplaceAll(content, req.OldString, req.NewString) + } else { + newContent = strings.Replace(content, req.OldString, req.NewString, 1) + } + + b.files[filePath] = &fileEntry{ + content: newContent, + modifiedAt: time.Now(), + } + + return nil +} + +// normalizePath normalizes a file path by ensuring it starts with "/" and removing trailing slashes. +func normalizePath(path string) string { + if path == "" { + return "/" + } + + // Ensure path starts with "/" + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + return filepath.Clean(path) +} + +type grepCollector struct { + allMatches []GrepMatch +} + +func newGrepCollector() *grepCollector { + return &grepCollector{ + allMatches: []GrepMatch{}, + } +} + +func (c *grepCollector) processFile(filePath, content string, re *regexp.Regexp, req *GrepRequest) { + fileMatches := c.findMatches(filePath, content, re, req) + if len(fileMatches) > 0 { + c.allMatches = append(c.allMatches, fileMatches...) + } +} + +func (c *grepCollector) findMatches(filePath, content string, re *regexp.Regexp, req *GrepRequest) []GrepMatch { + if req.EnableMultiline { + return c.findMultilineMatches(filePath, content, re) + } + return c.findSingleLineMatches(filePath, content, re) +} + +func (c *grepCollector) findMultilineMatches(filePath, content string, re *regexp.Regexp) []GrepMatch { + var fileMatches []GrepMatch + matches := re.FindAllStringIndex(content, -1) + lines := strings.Split(content, "\n") + + for _, match := range matches { + matchStart := match[0] + matchEnd := match[1] + startLineNum := 1 + strings.Count(content[:matchStart], "\n") + endLineNum := 1 + strings.Count(content[:matchEnd], "\n") + + for lineNum := startLineNum; lineNum <= endLineNum && lineNum <= len(lines); lineNum++ { + fileMatches = append(fileMatches, GrepMatch{ + Path: filePath, + Line: lineNum, + Content: lines[lineNum-1], + }) + } + } + return fileMatches +} + +func (c *grepCollector) findSingleLineMatches(filePath, content string, re *regexp.Regexp) []GrepMatch { + var fileMatches []GrepMatch + lines := strings.Split(content, "\n") + for lineNum, line := range lines { + if re.MatchString(line) { + fileMatches = append(fileMatches, GrepMatch{ + Path: filePath, + Line: lineNum + 1, + Content: line, + }) + } + } + return fileMatches +} + +func (c *grepCollector) buildResults(b *InMemoryBackend, req *GrepRequest) ([]GrepMatch, error) { + return c.buildContentResult(b, req), nil +} + +func (c *grepCollector) buildContentResult(b *InMemoryBackend, req *GrepRequest) []GrepMatch { + results := c.allMatches + if req.BeforeLines > 0 || req.AfterLines > 0 { + results = b.applyContext(c.allMatches, req) + } + return results +} diff --git a/adk/filesystem/backend_inmemory_test.go b/adk/filesystem/backend_inmemory_test.go new file mode 100644 index 0000000..fae9bf0 --- /dev/null +++ b/adk/filesystem/backend_inmemory_test.go @@ -0,0 +1,2441 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +import ( + "context" + "fmt" + "path/filepath" + "strconv" + "strings" + "testing" + "time" +) + +func TestInMemoryBackend_WriteAndRead(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + // Test Write + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "line1\nline2\nline3\nline4\nline5", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + // Test Read - full content + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Limit: 100, + }) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + expected := "line1\nline2\nline3\nline4\nline5" + if content.Content != expected { + t.Errorf("Read content mismatch. Expected: %q, Got: %q", expected, content.Content) + } + + // Test Read - with offset and limit + content, err = backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Offset: 1, + Limit: 2, + }) + if err != nil { + t.Fatalf("Read with offset failed: %v", err) + } + expected = "line1\nline2" + if content.Content != expected { + t.Errorf("Read with offset content mismatch. Expected: %q, Got: %q", expected, content.Content) + } + + // Test Read - non-existent file + _, err = backend.Read(ctx, &ReadRequest{ + FilePath: "/nonexistent.txt", + Limit: 10, + }) + if err == nil { + t.Error("Expected error for non-existent file, got nil") + } +} + +func TestInMemoryBackend_LsInfo(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + // Create some files + backend.Write(ctx, &WriteRequest{ + FilePath: "/file1.txt", + Content: "content1", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/file2.txt", + Content: "content2", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file3.txt", + Content: "content3", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/subdir/file4.txt", + Content: "content4", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir2/file5.txt", + Content: "content5", + }) + + // Test LsInfo - root + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos) != 4 { // file1.txt, file2.txt, dir1, dir2 + t.Errorf("Expected 4 items in root, got %d", len(infos)) + } + + // Test LsInfo - specific directory + infos, err = backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"}) + if err != nil { + t.Fatalf("LsInfo for /dir1 failed: %v", err) + } + if len(infos) != 2 { // file3.txt, subdir + t.Errorf("Expected 2 items in /dir1, got %d", len(infos)) + } +} + +func TestInMemoryBackend_Edit(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + // Create a file + backend.Write(ctx, &WriteRequest{ + FilePath: "/edit.txt", + Content: "hello world\nhello again\nhello world", + }) + + // Test Edit - report error if old string occurs + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/edit.txt", + OldString: "hello", + NewString: "hi", + ReplaceAll: false, + }) + if err == nil { + t.Fatal("should have failed") + } + + // Test Edit - replace all occurrences + backend.Write(ctx, &WriteRequest{ + FilePath: "/edit2.txt", + Content: "hello world\nhello again\nhello world", + }) + err = backend.Edit(ctx, &EditRequest{ + FilePath: "/edit2.txt", + OldString: "hello", + NewString: "hi", + ReplaceAll: true, + }) + if err != nil { + t.Fatalf("Edit (replace all) failed: %v", err) + } + + content, _ := backend.Read(ctx, &ReadRequest{ + FilePath: "/edit2.txt", + Limit: 100, + }) + expected := "hi world\nhi again\nhi world" + if content.Content != expected { + t.Errorf("Edit (replace all) content mismatch. Expected: %q, Got: %q", expected, content.Content) + } + + // Test Edit - non-existent file + err = backend.Edit(ctx, &EditRequest{ + FilePath: "/nonexistent.txt", + OldString: "old", + NewString: "new", + ReplaceAll: false, + }) + if err == nil { + t.Error("Expected error for non-existent file, got nil") + } + + // Test Edit - empty oldString + err = backend.Edit(ctx, &EditRequest{ + FilePath: "/edit.txt", + OldString: "", + NewString: "new", + ReplaceAll: false, + }) + if err == nil { + t.Error("Expected error for empty oldString, got nil") + } +} + +func TestInMemoryBackend_LsInfo_PathIsFilename(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/file1.txt", + Content: "content1", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/file2.txt", + Content: "content2", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file3.txt", + Content: "content3", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/subdir/file4.txt", + Content: "content4", + }) + + t.Run("RootDirectory", func(t *testing.T) { + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + for _, info := range infos { + if strings.Contains(info.Path, "/") { + t.Errorf("Path should be filename only, got: %s", info.Path) + } + if info.IsDir { + if info.Path != "dir1" { + t.Errorf("Expected directory name 'dir1', got: %s", info.Path) + } + } else { + if info.Path != "file1.txt" && info.Path != "file2.txt" { + t.Errorf("Expected filename 'file1.txt' or 'file2.txt', got: %s", info.Path) + } + } + } + }) + + t.Run("Subdirectory", func(t *testing.T) { + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + for _, info := range infos { + if strings.Contains(info.Path, "/") { + t.Errorf("Path should be filename only, got: %s", info.Path) + } + if info.IsDir { + if info.Path != "subdir" { + t.Errorf("Expected directory name 'subdir', got: %s", info.Path) + } + } else { + if info.Path != "file3.txt" { + t.Errorf("Expected filename 'file3.txt', got: %s", info.Path) + } + } + } + }) + + t.Run("NestedSubdirectory", func(t *testing.T) { + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1/subdir"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + if len(infos) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos)) + } + + info := infos[0] + if info.Path != "file4.txt" { + t.Errorf("Expected filename 'file4.txt', got: %s", info.Path) + } + if strings.Contains(info.Path, "/") { + t.Errorf("Path should be filename only, got: %s", info.Path) + } + }) +} + +func TestInMemoryBackend_GlobInfo(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + // Create some files + backend.Write(ctx, &WriteRequest{ + FilePath: "/file1.txt", + Content: "content1", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/file2.py", + Content: "content2", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file3.txt", + Content: "content3", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file4.py", + Content: "content4", + }) + + // Test GlobInfo - match .txt files in root only + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "*.txt", + Path: "/", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + if len(infos) != 1 { // only file1.txt in root + t.Errorf("Expected 1 .txt file in root, got %d", len(infos)) + } + if infos[0].Path != "file1.txt" { + t.Errorf("Expected relative path 'file1.txt', got %s", infos[0].Path) + } + + // Test GlobInfo - match all .py files in dir1 + infos, err = backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "*.py", + Path: "/dir1", + }) + if err != nil { + t.Fatalf("GlobInfo for /dir1 failed: %v", err) + } + if len(infos) != 1 { // file4.py + t.Errorf("Expected 1 .py file in /dir1, got %d", len(infos)) + } + if infos[0].Path != "file4.py" { + t.Errorf("Expected relative path 'file4.py', got %s", infos[0].Path) + } +} + +func TestInMemoryBackend_GlobInfo_RelativePath(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/Users/bytedance/Desktop/github/eino/file1.go", + Content: "content1", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/Users/bytedance/Desktop/github/openai-go/paginationmanual_test.go", + Content: "content2", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/Users/bytedance/Desktop/github/openai-go/paginationauto_test.go", + Content: "content3", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/Users/bytedance/Desktop/other/test.go", + Content: "content4", + }) + + t.Run("GlobFromRootWithPattern", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "**/*.go", + Path: "/Users/bytedance/Desktop/github", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 3 { + t.Fatalf("Expected 3 .go files, got %d", len(infos)) + } + + expectedPaths := map[string]bool{ + "eino/file1.go": false, + "openai-go/paginationmanual_test.go": false, + "openai-go/paginationauto_test.go": false, + } + + for _, info := range infos { + if _, exists := expectedPaths[info.Path]; exists { + expectedPaths[info.Path] = true + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + + for path, found := range expectedPaths { + if !found { + t.Errorf("Expected path not found: %s", path) + } + } + }) + + t.Run("GlobFromSubdirectory", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "*.go", + Path: "/Users/bytedance/Desktop/github/openai-go", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 2 { + t.Fatalf("Expected 2 .go files, got %d", len(infos)) + } + + expectedPaths := map[string]bool{ + "paginationmanual_test.go": false, + "paginationauto_test.go": false, + } + + for _, info := range infos { + if _, exists := expectedPaths[info.Path]; exists { + expectedPaths[info.Path] = true + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + + for path, found := range expectedPaths { + if !found { + t.Errorf("Expected path not found: %s", path) + } + } + }) + + t.Run("GlobFromRootWithAbsolutePattern", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "/Users/bytedance/Desktop/github/**/*.go", + Path: "/", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + expected := map[string]bool{ + "/Users/bytedance/Desktop/github/eino/file1.go": false, + "/Users/bytedance/Desktop/github/openai-go/paginationmanual_test.go": false, + "/Users/bytedance/Desktop/github/openai-go/paginationauto_test.go": false, + } + for _, info := range infos { + if _, ok := expected[info.Path]; ok { + expected[info.Path] = true + } + } + for path, found := range expected { + if !found { + t.Errorf("Expected absolute path not found: %s", path) + } + } + }) + + t.Run("GlobRecursiveWithRelativePattern", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "**/*.go", + Path: "/Users/bytedance/Desktop/github", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 3 { + t.Fatalf("Expected 3 .go files with ** pattern, got %d", len(infos)) + } + + expected := map[string]bool{ + "eino/file1.go": false, + "openai-go/paginationmanual_test.go": false, + "openai-go/paginationauto_test.go": false, + } + + for _, info := range infos { + if _, ok := expected[info.Path]; ok { + expected[info.Path] = true + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + + for path, found := range expected { + if !found { + t.Errorf("Expected relative path not found: %s", path) + } + } + }) +} + +func TestInMemoryBackend_GlobInfo_RecursivePattern(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/src/main.go", + Content: "main", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/src/utils/helper.go", + Content: "helper", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/src/utils/deep/nested.go", + Content: "nested", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/test/test.go", + Content: "test", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/README.md", + Content: "readme", + }) + + t.Run("DoubleStarMatchesAllSubdirectories", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "**/*.go", + Path: "/project", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 4 { + t.Fatalf("Expected 4 .go files, got %d", len(infos)) + } + + expected := map[string]bool{ + "src/main.go": false, + "src/utils/helper.go": false, + "src/utils/deep/nested.go": false, + "test/test.go": false, + } + + for _, info := range infos { + if _, ok := expected[info.Path]; ok { + expected[info.Path] = true + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + + for path, found := range expected { + if !found { + t.Errorf("Expected path not found: %s", path) + } + } + }) + + t.Run("DoubleStarInMiddleOfPattern", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "src/**/*.go", + Path: "/project", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 3 { + t.Fatalf("Expected 3 .go files under src/, got %d", len(infos)) + } + + expected := map[string]bool{ + "src/main.go": false, + "src/utils/helper.go": false, + "src/utils/deep/nested.go": false, + } + + for _, info := range infos { + if _, ok := expected[info.Path]; ok { + expected[info.Path] = true + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + + for path, found := range expected { + if !found { + t.Errorf("Expected path not found: %s", path) + } + } + }) + + t.Run("DoubleStarAtEnd", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "src/**", + Path: "/project", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 3 { + t.Fatalf("Expected 3 files under src/, got %d", len(infos)) + } + + expected := map[string]bool{ + "src/main.go": false, + "src/utils/helper.go": false, + "src/utils/deep/nested.go": false, + } + + for _, info := range infos { + if _, ok := expected[info.Path]; ok { + expected[info.Path] = true + } + } + + for path, found := range expected { + if !found { + t.Errorf("Expected path not found: %s", path) + } + } + }) + + t.Run("AbsolutePatternWithDoubleStarRecursive", func(t *testing.T) { + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "/project/**/*.go", + Path: "/", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 4 { + t.Fatalf("Expected 4 .go files, got %d", len(infos)) + } + + expected := map[string]bool{ + "/project/src/main.go": false, + "/project/src/utils/helper.go": false, + "/project/src/utils/deep/nested.go": false, + "/project/test/test.go": false, + } + + for _, info := range infos { + if _, ok := expected[info.Path]; ok { + expected[info.Path] = true + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + + for path, found := range expected { + if !found { + t.Errorf("Expected absolute path not found: %s", path) + } + } + }) +} + +func TestInMemoryBackend_Concurrent(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + // Test concurrent writes and reads + done := make(chan bool) + for i := 0; i < 10; i++ { + go func(n int) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/concurrent.txt", + Content: "content", + }) + backend.Read(ctx, &ReadRequest{ + FilePath: "/concurrent.txt", + Limit: 10, + }) + done <- true + }(i) + } + + for i := 0; i < 10; i++ { + <-done + } +} + +func TestInMemoryBackend_LsInfo_FileInfoMetadata(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + t.Run("FileMetadata", func(t *testing.T) { + content := "hello world" + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: content, + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + if len(infos) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos)) + } + + info := infos[0] + if info.Path != "test.txt" { + t.Errorf("Expected path test.txt, got %s", info.Path) + } + if info.IsDir { + t.Error("Expected IsDir to be false for file") + } + if info.Size != int64(len(content)) { + t.Errorf("Expected size %d, got %d", len(content), info.Size) + } + if info.ModifiedAt == "" { + t.Error("Expected ModifiedAt to be non-empty") + } + _, err = time.Parse(time.RFC3339Nano, info.ModifiedAt) + if err != nil { + t.Errorf("ModifiedAt is not valid RFC3339 format: %v", err) + } + }) + + t.Run("DirectoryMetadata", func(t *testing.T) { + backend := NewInMemoryBackend() + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file1.txt", + Content: "content1", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + if len(infos) != 1 { + t.Fatalf("Expected 1 directory, got %d", len(infos)) + } + + info := infos[0] + if info.Path != "dir1" { + t.Errorf("Expected path dir1, got %s", info.Path) + } + if !info.IsDir { + t.Error("Expected IsDir to be true for directory") + } + if info.Size != 0 { + t.Errorf("Expected size 0 for directory, got %d", info.Size) + } + if info.ModifiedAt == "" { + t.Error("Expected ModifiedAt to be non-empty for directory") + } + }) + + t.Run("MixedFilesAndDirectories", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{ + FilePath: "/file1.txt", + Content: "content1", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file2.txt", + Content: "content2", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/subdir/file3.txt", + Content: "content3", + }) + + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + if len(infos) != 2 { + t.Fatalf("Expected 2 items (file1.txt, dir1), got %d", len(infos)) + } + + fileCount := 0 + dirCount := 0 + for _, info := range infos { + if info.IsDir { + dirCount++ + if info.Path != "dir1" { + t.Errorf("Expected directory path dir1, got %s", info.Path) + } + } else { + fileCount++ + if info.Path != "file1.txt" { + t.Errorf("Expected file path file1.txt, got %s", info.Path) + } + if info.Size != int64(len("content1")) { + t.Errorf("Expected file size %d, got %d", len("content1"), info.Size) + } + } + } + + if fileCount != 1 { + t.Errorf("Expected 1 file, got %d", fileCount) + } + if dirCount != 1 { + t.Errorf("Expected 1 directory, got %d", dirCount) + } + }) + + t.Run("SubdirectoryListing", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file1.txt", + Content: "short", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/subdir/file2.txt", + Content: "longer content here", + }) + + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + if len(infos) != 2 { + t.Fatalf("Expected 2 items (file1.txt, subdir), got %d", len(infos)) + } + + for _, info := range infos { + if info.Path == "file1.txt" { + if info.IsDir { + t.Error("Expected file1.txt to be a file") + } + if info.Size != int64(len("short")) { + t.Errorf("Expected size %d, got %d", len("short"), info.Size) + } + } else if info.Path == "subdir" { + if !info.IsDir { + t.Error("Expected subdir to be a directory") + } + } else { + t.Errorf("Unexpected path: %s", info.Path) + } + } + }) + + t.Run("DirectoryModifiedAtUsesLatestFile", func(t *testing.T) { + backend := NewInMemoryBackend() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file1.txt", + Content: "content1", + }) + time.Sleep(10 * time.Millisecond) + + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir1/file2.txt", + Content: "content2", + }) + + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + + if len(infos) != 1 { + t.Fatalf("Expected 1 directory, got %d", len(infos)) + } + + dirInfo := infos[0] + if !dirInfo.IsDir { + t.Fatal("Expected directory") + } + + dirModTime, _ := time.Parse(time.RFC3339Nano, dirInfo.ModifiedAt) + + subInfos, _ := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"}) + var latestFileTime time.Time + for _, info := range subInfos { + fileTime, _ := time.Parse(time.RFC3339Nano, info.ModifiedAt) + if fileTime.After(latestFileTime) { + latestFileTime = fileTime + } + } + + if !dirModTime.Equal(latestFileTime) && dirModTime.Before(latestFileTime) { + t.Logf("Directory mod time: %v, Latest file time: %v", dirModTime, latestFileTime) + } + }) +} + +func TestInMemoryBackend_GlobInfo_FileInfoMetadata(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + t.Run("BasicMetadata", func(t *testing.T) { + content := "test content" + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: content, + }) + + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "*.txt", + Path: "/", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos)) + } + + info := infos[0] + if info.Path != "test.txt" { + t.Errorf("Expected path test.txt, got %s", info.Path) + } + if info.IsDir { + t.Error("Expected IsDir to be false") + } + if info.Size != int64(len(content)) { + t.Errorf("Expected size %d, got %d", len(content), info.Size) + } + if info.ModifiedAt == "" { + t.Error("Expected ModifiedAt to be non-empty") + } + }) + + t.Run("MultipleFilesMetadata", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{ + FilePath: "/file1.txt", + Content: "short", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/file2.txt", + Content: "much longer content", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/file3.py", + Content: "python", + }) + + infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{ + Pattern: "*.txt", + Path: "/", + }) + if err != nil { + t.Fatalf("GlobInfo failed: %v", err) + } + + if len(infos) != 2 { + t.Fatalf("Expected 2 .txt files, got %d", len(infos)) + } + + for _, info := range infos { + if info.IsDir { + t.Errorf("Expected IsDir to be false for %s", info.Path) + } + if info.Size <= 0 { + t.Errorf("Expected positive size for %s, got %d", info.Path, info.Size) + } + if info.ModifiedAt == "" { + t.Errorf("Expected ModifiedAt to be non-empty for %s", info.Path) + } + } + }) +} + +func TestInMemoryBackend_WriteAndEdit_ModifiedAt(t *testing.T) { + ctx := context.Background() + + t.Run("WriteUpdatesModifiedAt", func(t *testing.T) { + backend := NewInMemoryBackend() + beforeWrite := time.Now() + time.Sleep(1 * time.Millisecond) + + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "initial content", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + time.Sleep(1 * time.Millisecond) + afterWrite := time.Now() + + infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos)) + } + + modTime, err := time.Parse(time.RFC3339Nano, infos[0].ModifiedAt) + if err != nil { + t.Fatalf("Failed to parse ModifiedAt: %v", err) + } + + if modTime.Before(beforeWrite) || modTime.After(afterWrite) { + t.Errorf("ModifiedAt %v should be between %v and %v", modTime, beforeWrite, afterWrite) + } + }) + + t.Run("EditUpdatesModifiedAt", func(t *testing.T) { + backend := NewInMemoryBackend() + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/edit.txt", + Content: "hello world", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + infos1, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos1) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos1)) + } + modTime1, err := time.Parse(time.RFC3339Nano, infos1[0].ModifiedAt) + if err != nil { + t.Fatalf("Failed to parse ModifiedAt: %v", err) + } + + time.Sleep(10 * time.Millisecond) + + err = backend.Edit(ctx, &EditRequest{ + FilePath: "/edit.txt", + OldString: "hello", + NewString: "hi", + ReplaceAll: true, + }) + if err != nil { + t.Fatalf("Edit failed: %v", err) + } + + infos2, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos2) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos2)) + } + modTime2, err := time.Parse(time.RFC3339Nano, infos2[0].ModifiedAt) + if err != nil { + t.Fatalf("Failed to parse ModifiedAt: %v", err) + } + + if !modTime2.After(modTime1) { + t.Errorf("ModifiedAt should be updated after edit. Before: %v, After: %v", modTime1, modTime2) + } + }) + + t.Run("OverwriteUpdatesModifiedAt", func(t *testing.T) { + backend := NewInMemoryBackend() + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/overwrite.txt", + Content: "original", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + infos1, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos1) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos1)) + } + modTime1, err := time.Parse(time.RFC3339Nano, infos1[0].ModifiedAt) + if err != nil { + t.Fatalf("Failed to parse ModifiedAt: %v", err) + } + + time.Sleep(10 * time.Millisecond) + + err = backend.Write(ctx, &WriteRequest{ + FilePath: "/overwrite.txt", + Content: "new content", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + infos2, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos2) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos2)) + } + modTime2, err := time.Parse(time.RFC3339Nano, infos2[0].ModifiedAt) + if err != nil { + t.Fatalf("Failed to parse ModifiedAt: %v", err) + } + + if !modTime2.After(modTime1) { + t.Errorf("ModifiedAt should be updated after overwrite. Before: %v, After: %v", modTime1, modTime2) + } + }) + + t.Run("SizeUpdatesAfterEdit", func(t *testing.T) { + backend := NewInMemoryBackend() + err := backend.Write(ctx, &WriteRequest{ + FilePath: "/size.txt", + Content: "hello", + }) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + infos1, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos1) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos1)) + } + size1 := infos1[0].Size + + err = backend.Edit(ctx, &EditRequest{ + FilePath: "/size.txt", + OldString: "hello", + NewString: "hello world", + ReplaceAll: true, + }) + if err != nil { + t.Fatalf("Edit failed: %v", err) + } + + infos2, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"}) + if err != nil { + t.Fatalf("LsInfo failed: %v", err) + } + if len(infos2) != 1 { + t.Fatalf("Expected 1 file, got %d", len(infos2)) + } + size2 := infos2[0].Size + + if size2 <= size1 { + t.Errorf("Size should increase after edit. Before: %d, After: %d", size1, size2) + } + if size2 != int64(len("hello world")) { + t.Errorf("Expected size %d, got %d", len("hello world"), size2) + } + }) +} + +func TestInMemoryBackend_Read_EdgeCases(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "line1\nline2\nline3", + }) + + t.Run("negative offset should be treated as zero", func(t *testing.T) { + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Offset: -5, + Limit: 2, + }) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + expected := "line1\nline2" + if content.Content != expected { + t.Errorf("Expected: %q, Got: %q", expected, content.Content) + } + }) + + t.Run("offset exceeds file length", func(t *testing.T) { + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Offset: 100, + Limit: 10, + }) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + if content.Content != "" { + t.Errorf("Expected empty content, got: %q", content.Content) + } + }) + + t.Run("zero or negative limit should use default 200", func(t *testing.T) { + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Offset: 0, + Limit: 0, + }) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + lines := strings.Split(content.Content, "\n") + if len(lines) != 3 { + t.Errorf("Expected 3 lines, got %d", len(lines)) + } + }) + + t.Run("limit exceeds remaining lines", func(t *testing.T) { + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Offset: 1, + Limit: 100, + }) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + lines := strings.Split(content.Content, "\n") + if len(lines) != 3 { + t.Errorf("Expected 3 lines, got %d", len(lines)) + } + }) +} + +func TestInMemoryBackend_Edit_EdgeCases(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + t.Run("edit non-existent file", func(t *testing.T) { + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/nonexistent.txt", + OldString: "old", + NewString: "new", + }) + if err == nil { + t.Error("Expected error for non-existent file") + } + if !strings.Contains(err.Error(), "not found") { + t.Errorf("Expected 'not found' error, got: %v", err) + } + }) + + t.Run("empty oldString", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "content", + }) + + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/test.txt", + OldString: "", + NewString: "new", + }) + if err == nil { + t.Error("Expected error for empty oldString") + } + if !strings.Contains(err.Error(), "non-empty") { + t.Errorf("Expected 'non-empty' error, got: %v", err) + } + }) + + t.Run("oldString not found", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "hello world", + }) + + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/test.txt", + OldString: "notfound", + NewString: "new", + }) + if err == nil { + t.Error("Expected error when oldString not found") + } + if !strings.Contains(err.Error(), "not found in file") { + t.Errorf("Expected 'not found in file' error, got: %v", err) + } + }) + + t.Run("multiple occurrences with ReplaceAll false", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "foo bar foo baz", + }) + + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/test.txt", + OldString: "foo", + NewString: "FOO", + ReplaceAll: false, + }) + if err == nil { + t.Error("Expected error for multiple occurrences with ReplaceAll=false") + } + if !strings.Contains(err.Error(), "multiple occurrences") { + t.Errorf("Expected 'multiple occurrences' error, got: %v", err) + } + }) + + t.Run("single occurrence with ReplaceAll false", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "foo bar baz", + }) + + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/test.txt", + OldString: "foo", + NewString: "FOO", + ReplaceAll: false, + }) + if err != nil { + t.Fatalf("Edit failed: %v", err) + } + + content, _ := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Limit: 100, + }) + if !strings.Contains(content.Content, "FOO") { + t.Error("Expected content to contain 'FOO'") + } + }) + + t.Run("ReplaceAll replaces all occurrences", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.txt", + Content: "foo bar foo baz foo", + }) + + err := backend.Edit(ctx, &EditRequest{ + FilePath: "/test.txt", + OldString: "foo", + NewString: "FOO", + ReplaceAll: true, + }) + if err != nil { + t.Fatalf("Edit failed: %v", err) + } + + content, _ := backend.Read(ctx, &ReadRequest{ + FilePath: "/test.txt", + Limit: 100, + }) + if strings.Contains(content.Content, "foo") { + t.Error("Expected all 'foo' to be replaced") + } + fooCount := strings.Count(content.Content, "FOO") + if fooCount != 3 { + t.Errorf("Expected 3 occurrences of 'FOO', got %d", fooCount) + } + }) +} + +func TestInMemoryBackend_NormalizePath(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + t.Run("paths are normalized on write", func(t *testing.T) { + testCases := []struct { + inputPath string + normalizedPath string + }{ + {"test.txt", "/test.txt"}, + {"/test.txt", "/test.txt"}, + {"//test.txt", "/test.txt"}, + {"/dir//file.txt", "/dir/file.txt"}, + {"/dir/../file.txt", "/file.txt"}, + } + + for _, tc := range testCases { + backend.Write(ctx, &WriteRequest{ + FilePath: tc.inputPath, + Content: "content", + }) + + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: tc.normalizedPath, + Limit: 10, + }) + if err != nil { + t.Errorf("Failed to read normalized path %s (from %s): %v", tc.normalizedPath, tc.inputPath, err) + } + if !strings.Contains(content.Content, "content") { + t.Errorf("Content not found for normalized path %s (from %s)", tc.normalizedPath, tc.inputPath) + } + } + }) +} + +func TestInMemoryBackend_MatchFileType(t *testing.T) { + testCases := []struct { + ext string + fileType string + expected bool + }{ + {"go", "go", true}, + {"py", "python", true}, + {"py", "py", true}, + {"js", "js", true}, + {"ts", "typescript", true}, + {"ts", "ts", true}, + {"cpp", "cpp", true}, + {"c", "c", true}, + {"h", "c", true}, + {"md", "markdown", true}, + {"txt", "txt", true}, + {"go", "python", false}, + {"js", "typescript", false}, + {"unknown", "go", false}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s matches %s", tc.ext, tc.fileType), func(t *testing.T) { + result := matchFileType(tc.ext, tc.fileType) + if result != tc.expected { + t.Errorf("matchFileType(%q, %q) = %v, expected %v", tc.ext, tc.fileType, result, tc.expected) + } + }) + } +} + +func TestInMemoryBackend_GrepRaw(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.go", + Content: "package main\nfunc main() {\n\tlog.Error(\"error\")\n\tfmt.Println(\"hello\")\n}", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/test.py", + Content: "def hello():\n print('error')\n print('world')", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/dir/file.go", + Content: "package test\nfunc TestError() {\n\tlog.Error(\"test error\")\n}", + }) + + t.Run("basic pattern search", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "error", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 3 { + t.Errorf("Expected 2 matches, got %d", len(matches)) + } + }) + + t.Run("empty pattern error", func(t *testing.T) { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "", + }) + if err == nil { + t.Error("Expected error for empty pattern") + } + if !strings.Contains(err.Error(), "cannot be empty") { + t.Errorf("Expected 'cannot be empty' error, got: %v", err) + } + }) + + t.Run("invalid regex pattern", func(t *testing.T) { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "[invalid", + }) + if err == nil { + t.Error("Expected error for invalid regex") + } + if !strings.Contains(err.Error(), "invalid regex") { + t.Errorf("Expected 'invalid regex' error, got: %v", err) + } + }) + + t.Run("case sensitive search", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "Error", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 3 { + t.Errorf("Expected 2 matches, got %d", len(matches)) + } + }) + + t.Run("case insensitive search", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "ERROR", + CaseInsensitive: true, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 3 { + t.Errorf("Expected at least 2 matches, got %d", len(matches)) + } + }) + + t.Run("filter by file type", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "error", + FileType: "go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasSuffix(match.Path, ".go") { + t.Errorf("Expected only .go files, got: %s", match.Path) + } + } + }) + + t.Run("filter by glob pattern", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "Error", + Glob: "*.go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasSuffix(match.Path, ".go") { + t.Errorf("Expected only .go files, got: %s", match.Path) + } + } + }) + + t.Run("invalid glob pattern", func(t *testing.T) { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "error", + Glob: "[invalid", + }) + if err == nil { + t.Error("Expected error for invalid glob pattern") + } + if !strings.Contains(err.Error(), "invalid glob") { + t.Errorf("Expected 'invalid glob' error, got: %v", err) + } + }) + + t.Run("search in specific path", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "Error", + Path: "/dir", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasPrefix(match.Path, "/dir") { + t.Errorf("Expected matches only from /dir, got: %s", match.Path) + } + } + }) + + t.Run("search with non-existent path", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "error", + Path: "/nonexistent", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 0 { + t.Errorf("Expected 0 matches for non-existent path, got %d", len(matches)) + } + }) + + t.Run("regex pattern matching", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "log\\..*Error", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 1 { + t.Errorf("Expected at least 1 match, got %d", len(matches)) + } + }) + + t.Run("no matches found", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "nonexistent_pattern_xyz", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 0 { + t.Errorf("Expected 0 matches, got %d", len(matches)) + } + }) + + t.Run("match line numbers", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "log\\.Error", + FileType: "go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if match.Line <= 0 { + t.Errorf("Expected positive line number, got %d", match.Line) + } + } + }) + + t.Run("match content is returned", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "package main", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 1 { + t.Fatal("Expected at least 1 match") + } + found := false + for _, match := range matches { + if strings.Contains(match.Content, "package main") { + found = true + break + } + } + if !found { + t.Error("Expected match content to contain 'package main'") + } + }) +} + +func TestInMemoryBackend_GrepRaw_WithContext(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/context.txt", + Content: "line1\nline2\ntarget line\nline4\nline5\nline6", + }) + + t.Run("with before context", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "target", + BeforeLines: 2, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 3 { + t.Errorf("Expected at least 3 matches (2 before + target), got %d", len(matches)) + } + }) + + t.Run("with after context", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "target", + AfterLines: 2, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 3 { + t.Errorf("Expected at least 3 matches (target + 2 after), got %d", len(matches)) + } + }) + + t.Run("with both before and after context", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "target", + BeforeLines: 1, + AfterLines: 1, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 3 { + t.Errorf("Expected at least 3 matches (1 before + target + 1 after), got %d", len(matches)) + } + }) + + t.Run("context at file boundaries", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/boundary.txt", + Content: "first line target\nsecond line", + }) + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "target", + Path: "/boundary.txt", + BeforeLines: 5, + AfterLines: 5, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) == 0 { + t.Error("Expected at least 1 match") + } + }) + + t.Run("zero context lines", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "target", + BeforeLines: 0, + AfterLines: 0, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 1 { + t.Error("Expected at least 1 match") + } + }) + + t.Run("negative context lines treated as zero", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "target", + BeforeLines: -5, + AfterLines: -5, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) < 1 { + t.Error("Expected at least 1 match") + } + }) +} + +func TestInMemoryBackend_GrepRaw_Multiline(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/multiline.txt", + Content: "start\nmiddle line\nend", + }) + + t.Run("single line mode (default)", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "start.*end", + EnableMultiline: false, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 0 { + t.Errorf("Expected 0 matches in single-line mode, got %d", len(matches)) + } + }) + + t.Run("multiline mode enabled", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "start[\\s\\S]*end", + EnableMultiline: true, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) == 0 { + t.Error("Expected matches in multiline mode") + } + }) + + t.Run("multiline with multiple matches", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/multiline2.txt", + Content: "block1 start\nblock1 middle\nblock1 end\n\nblock2 start\nblock2 end", + }) + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "start[\\s\\S]*?end", + Path: "/multiline2.txt", + EnableMultiline: true, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) == 0 { + t.Error("Expected matches in multiline mode") + } + }) + + t.Run("multiline with multiple matches v2", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/multiline3.txt", + Content: ` +const a = 1; +function calculateTotal( + items, + discount +) { + return items.reduce((sum, item) => sum + item.price, 0); +} + +const b = 2; + +/* + * This is a comment + * spanning multiple lines + */ + +class UserService { + constructor(db) { + this.db = db; + } +} +`, + }) + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "function calculateTotal\\([^\\)]*\\)", + Path: "/multiline3.txt", + EnableMultiline: true, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) == 0 { + t.Error("Expected matches in multiline mode") + } + + foundLastLine := false + for _, match := range matches { + if match.Line == 6 && strings.Contains(match.Content, ") {") { + foundLastLine = true + break + } + } + if !foundLastLine { + t.Error("Expected to find line 5 with ') {' in content") + for _, match := range matches { + t.Logf("Line %d: %s", match.Line, match.Content) + } + } + }) + +} + +func TestInMemoryBackend_GrepRaw_EmptyFiles(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + t.Run("search in empty file", func(t *testing.T) { + backend.Write(ctx, &WriteRequest{ + FilePath: "/empty.txt", + Content: "", + }) + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "anything", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 0 { + t.Errorf("Expected 0 matches in empty file, got %d", len(matches)) + } + }) + + t.Run("search with no files", func(t *testing.T) { + emptyBackend := NewInMemoryBackend() + matches, err := emptyBackend.GrepRaw(ctx, &GrepRequest{ + Pattern: "anything", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 0 { + t.Errorf("Expected 0 matches with no files, got %d", len(matches)) + } + }) +} + +func TestInMemoryBackend_GrepRaw_SpecialCharacters(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/special.txt", + Content: "interface{}\nmap[string]int\nfunc() error\n$variable\n*pointer", + }) + + t.Run("match curly braces", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "interface\\{\\}", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 1 { + t.Errorf("Expected 1 match, got %d", len(matches)) + } + }) + + t.Run("match square brackets", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "map\\[.*\\]", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 1 { + t.Errorf("Expected 1 match, got %d", len(matches)) + } + }) + + t.Run("match parentheses", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "func\\(\\)", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 1 { + t.Errorf("Expected 1 match, got %d", len(matches)) + } + }) + + t.Run("match dollar sign", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "\\$variable", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 1 { + t.Errorf("Expected 1 match, got %d", len(matches)) + } + }) + + t.Run("match asterisk", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "\\*pointer", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 1 { + t.Errorf("Expected 1 match, got %d", len(matches)) + } + }) +} + +func TestInMemoryBackend_GrepRaw_Concurrent(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + for i := 0; i < 10; i++ { + backend.Write(ctx, &WriteRequest{ + FilePath: fmt.Sprintf("/file%d.txt", i), + Content: fmt.Sprintf("content%d with error message", i), + }) + } + + t.Run("concurrent grep operations", func(t *testing.T) { + done := make(chan bool) + for i := 0; i < 10; i++ { + go func() { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "error", + }) + if err != nil { + t.Errorf("Concurrent GrepRaw failed: %v", err) + } + done <- true + }() + } + + for i := 0; i < 10; i++ { + <-done + } + }) + + t.Run("parallel file processing", func(t *testing.T) { + backend := NewInMemoryBackend() + for i := 0; i < 100; i++ { + backend.Write(ctx, &WriteRequest{ + FilePath: fmt.Sprintf("/large/file%d.go", i), + Content: fmt.Sprintf("package main\nimport \"log\"\nfunc test%d() {\n\tlog.Error(\"error %d\")\n}", i, i), + }) + } + + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "log\\.Error", + FileType: "go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 100 { + t.Errorf("Expected 100 matches, got %d", len(matches)) + } + }) + + t.Run("single file no parallelism", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{ + FilePath: "/single.txt", + Content: "error line 1\nerror line 2\nerror line 3", + }) + + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "error", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 3 { + t.Errorf("Expected 3 matches, got %d", len(matches)) + } + }) + + t.Run("empty files list", func(t *testing.T) { + backend := NewInMemoryBackend() + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "anything", + Path: "/nonexistent", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) != 0 { + t.Errorf("Expected 0 matches, got %d", len(matches)) + } + }) + + t.Run("concurrent operations are safe", func(t *testing.T) { + backend := NewInMemoryBackend() + for i := 0; i < 20; i++ { + backend.Write(ctx, &WriteRequest{ + FilePath: fmt.Sprintf("/concurrent/file%d.txt", i), + Content: fmt.Sprintf("line1\nline2\npattern%d\nline4", i), + }) + } + + done := make(chan error, 5) + for i := 0; i < 5; i++ { + go func(id int) { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "pattern\\d+", + }) + done <- err + }(i) + } + + for i := 0; i < 5; i++ { + if err := <-done; err != nil { + t.Errorf("Concurrent operation %d failed: %v", i, err) + } + } + }) +} + +func BenchmarkInMemoryBackend_GrepRaw(b *testing.B) { + backend := NewInMemoryBackend() + ctx := context.Background() + + for i := 0; i < 100; i++ { + content := fmt.Sprintf(`package main + +import ( + "fmt" + "log" +) + +func process%d() error { + log.Error("processing error %d") + fmt.Println("hello world") + return nil +} + +func calculate%d(x, y int) int { + return x + y +} +`, i, i, i) + backend.Write(ctx, &WriteRequest{ + FilePath: fmt.Sprintf("/project/src/file%d.go", i), + Content: content, + }) + } + + b.Run("parallel_grep", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "log\\.Error", + FileType: "go", + }) + if err != nil { + b.Fatalf("GrepRaw failed: %v", err) + } + } + }) + + b.Run("with_glob_filter", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "Error", + Glob: "**/*.go", + }) + if err != nil { + b.Fatalf("GrepRaw failed: %v", err) + } + } + }) + + b.Run("case_insensitive", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "ERROR", + CaseInsensitive: true, + }) + if err != nil { + b.Fatalf("GrepRaw failed: %v", err) + } + } + }) +} + +func TestInMemoryBackend_GrepRaw_ComplexScenarios(t *testing.T) { + backend := NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/src/main.go", + Content: "package main\nimport \"log\"\nfunc main() {\n\tlog.Error(\"error\")\n}", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/src/utils/helper.go", + Content: "package utils\nfunc Helper() error {\n\treturn nil\n}", + }) + backend.Write(ctx, &WriteRequest{ + FilePath: "/project/test/main_test.go", + Content: "package main\nimport \"testing\"\nfunc TestMain(t *testing.T) {\n}", + }) + + t.Run("combine path and file type filters", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "package", + Path: "/project/src", + FileType: "go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasPrefix(match.Path, "/project/src") { + t.Errorf("Expected path to start with /project/src, got: %s", match.Path) + } + if !strings.HasSuffix(match.Path, ".go") { + t.Errorf("Expected .go file, got: %s", match.Path) + } + } + }) + + t.Run("complex regex with case insensitive", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "func\\s+\\w+", + CaseInsensitive: true, + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + if len(matches) == 0 { + t.Error("Expected at least 1 match for function declarations") + } + }) + + t.Run("glob with directory structure", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "package", + Glob: "*_test.go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasSuffix(match.Path, "_test.go") { + t.Errorf("Expected test file, got: %s", match.Path) + } + } + }) + + t.Run("glob with recursive pattern", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "package", + Glob: "**/*.go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasSuffix(match.Path, ".go") { + t.Errorf("Expected .go file, got: %s", match.Path) + } + } + if len(matches) == 0 { + t.Error("Expected at least 1 match for **/*.go pattern") + } + }) + + t.Run("glob with path prefix", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "package", + Glob: "src/**/*.go", + Path: "/project", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if !strings.HasPrefix(match.Path, "/project/src") { + t.Errorf("Expected path to start with /project/src, got: %s", match.Path) + } + if !strings.HasSuffix(match.Path, ".go") { + t.Errorf("Expected .go file, got: %s", match.Path) + } + } + }) + + t.Run("glob simple filename pattern", func(t *testing.T) { + matches, err := backend.GrepRaw(ctx, &GrepRequest{ + Pattern: "package", + Glob: "main.go", + }) + if err != nil { + t.Fatalf("GrepRaw failed: %v", err) + } + for _, match := range matches { + if filepath.Base(match.Path) != "main.go" { + t.Errorf("Expected filename 'main.go', got: %s", match.Path) + } + } + }) +} + +func TestInMemoryBackend_Read_Scenarios(t *testing.T) { + ctx := context.Background() + + t.Run("empty file returns empty content", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/empty.txt", Content: ""}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/empty.txt"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "" { + t.Errorf("expected empty content, got %q", content.Content) + } + }) + + t.Run("single-line file without newline", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/single.txt", Content: "hello"}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/single.txt"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "hello" { + t.Errorf("expected %q, got %q", "hello", content.Content) + } + }) + + t.Run("offset 0 and offset 1 both start from first line", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"}) + + c0, _ := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 0, Limit: 1}) + c1, _ := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 1, Limit: 1}) + if c0.Content != c1.Content { + t.Errorf("Offset=0 (%q) and Offset=1 (%q) should return the same first line", c0.Content, c1.Content) + } + if c0.Content != "a" { + t.Errorf("expected first line %q, got %q", "a", c0.Content) + } + }) + + t.Run("file with trailing newline preserves trailing empty line", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/trail.txt", Content: "line1\nline2\n"}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/trail.txt"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "line1\nline2\n" { + t.Errorf("expected %q, got %q", "line1\nline2\n", content.Content) + } + lines := strings.Split(content.Content, "\n") + if len(lines) != 3 { // ["line1", "line2", ""] + t.Errorf("expected 3 elements from split, got %d", len(lines)) + } + }) + + t.Run("offset exactly at last line", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"}) + + // Offset=3 (1-based) → last line "c" + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 3, Limit: 10}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "c" { + t.Errorf("expected %q, got %q", "c", content.Content) + } + }) + + t.Run("offset one beyond last line returns empty", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 4, Limit: 10}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "" { + t.Errorf("expected empty content, got %q", content.Content) + } + }) + + t.Run("limit=1 reads exactly one line", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"}) + + for i, expected := range []string{"a", "b", "c"} { + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: i + 1, Limit: 1}) + if err != nil { + t.Fatalf("line %d: unexpected error: %v", i+1, err) + } + if content.Content != expected { + t.Errorf("line %d: expected %q, got %q", i+1, expected, content.Content) + } + } + }) + + t.Run("sliding window reads consecutive ranges correctly", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "l1\nl2\nl3\nl4\nl5"}) + + tests := []struct { + offset int + limit int + expected string + }{ + {1, 2, "l1\nl2"}, + {2, 2, "l2\nl3"}, + {3, 2, "l3\nl4"}, + {4, 2, "l4\nl5"}, + {5, 2, "l5"}, + } + for _, tt := range tests { + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: tt.offset, Limit: tt.limit}) + if err != nil { + t.Fatalf("offset=%d limit=%d: unexpected error: %v", tt.offset, tt.limit, err) + } + if content.Content != tt.expected { + t.Errorf("offset=%d limit=%d: expected %q, got %q", tt.offset, tt.limit, tt.expected, content.Content) + } + } + }) + + t.Run("file with only newlines", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/newlines.txt", Content: "\n\n\n"}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/newlines.txt", Offset: 2, Limit: 1}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Line 2 is an empty string between two newlines + if content.Content != "" { + t.Errorf("expected empty line content, got %q", content.Content) + } + }) +} + +func TestInMemoryBackend_Read_NoLimit(t *testing.T) { + ctx := context.Background() + + t.Run("limit=0 reads all lines", func(t *testing.T) { + backend := NewInMemoryBackend() + fullContent := "line1\nline2\nline3\nline4\nline5" + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: fullContent}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Limit: 0}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != fullContent { + t.Errorf("expected %q, got %q", fullContent, content.Content) + } + }) + + t.Run("negative limit reads all lines", func(t *testing.T) { + backend := NewInMemoryBackend() + fullContent := "a\nb\nc" + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: fullContent}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Limit: -1}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != fullContent { + t.Errorf("expected %q, got %q", fullContent, content.Content) + } + }) + + t.Run("limit=0 with offset reads from offset to end", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc\nd\ne"}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 3, Limit: 0}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "c\nd\ne" { + t.Errorf("expected %q, got %q", "c\nd\ne", content.Content) + } + }) + + t.Run("limit=0 with offset beyond content returns empty", func(t *testing.T) { + backend := NewInMemoryBackend() + backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb"}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 10, Limit: 0}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != "" { + t.Errorf("expected empty content, got %q", content.Content) + } + }) + + t.Run("limit=0 reads file with more than 2000 lines", func(t *testing.T) { + backend := NewInMemoryBackend() + var b strings.Builder + totalLines := 2500 + for i := 1; i <= totalLines; i++ { + if i > 1 { + b.WriteString("\n") + } + b.WriteString("line" + strconv.Itoa(i)) + } + fullContent := b.String() + backend.Write(ctx, &WriteRequest{FilePath: "/big.txt", Content: fullContent}) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: "/big.txt", Limit: 0}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if content.Content != fullContent { + t.Errorf("expected all %d lines, got %d lines", + totalLines, strings.Count(content.Content, "\n")+1) + } + }) +} diff --git a/adk/flow.go b/adk/flow.go new file mode 100644 index 0000000..e84fb84 --- /dev/null +++ b/adk/flow.go @@ -0,0 +1,809 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "log" + "runtime/debug" + "strings" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/compose" + icb "github.com/cloudwego/eino/internal/callbacks" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +type HistoryEntry struct { + IsUserInput bool + AgentName string + Message Message +} + +type HistoryRewriter func(ctx context.Context, entries []*HistoryEntry) ([]Message, error) + +type flowAgent struct { + Agent + + subAgents []*flowAgent + parentAgent *flowAgent + + disallowTransferToParent bool + historyRewriter HistoryRewriter + + checkPointStore compose.CheckPointStore +} + +func (a *flowAgent) deepCopy() *flowAgent { + ret := &flowAgent{ + Agent: a.Agent, + subAgents: make([]*flowAgent, 0, len(a.subAgents)), + parentAgent: a.parentAgent, + disallowTransferToParent: a.disallowTransferToParent, + historyRewriter: a.historyRewriter, + checkPointStore: a.checkPointStore, + } + + for _, sa := range a.subAgents { + ret.subAgents = append(ret.subAgents, sa.deepCopy()) + } + return ret +} + +// SetSubAgents sets sub-agents for the given agent and returns the updated agent. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func SetSubAgents(ctx context.Context, agent Agent, subAgents []Agent) (ResumableAgent, error) { + return setSubAgents(ctx, agent, subAgents) +} + +type AgentOption func(options *flowAgent) + +// WithDisallowTransferToParent prevents a sub-agent from transferring to its parent. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func WithDisallowTransferToParent() AgentOption { + return func(fa *flowAgent) { + fa.disallowTransferToParent = true + } +} + +// WithHistoryRewriter sets a rewriter to transform conversation history +// during agent transfers. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func WithHistoryRewriter(h HistoryRewriter) AgentOption { + return func(fa *flowAgent) { + fa.historyRewriter = h + } +} + +func toFlowAgent(ctx context.Context, agent Agent, opts ...AgentOption) *flowAgent { + var fa *flowAgent + var ok bool + if fa, ok = agent.(*flowAgent); !ok { + fa = &flowAgent{Agent: agent} + } else { + fa = fa.deepCopy() + } + for _, opt := range opts { + opt(fa) + } + + if fa.historyRewriter == nil { + fa.historyRewriter = buildDefaultHistoryRewriter(agent.Name(ctx)) + } + + return fa +} + +// AgentWithOptions wraps an agent with flow-specific options and returns it. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func AgentWithOptions(ctx context.Context, agent Agent, opts ...AgentOption) Agent { + return toFlowAgent(ctx, agent, opts...) +} + +func setSubAgents(ctx context.Context, agent Agent, subAgents []Agent) (*flowAgent, error) { + fa := toFlowAgent(ctx, agent) + + if len(fa.subAgents) > 0 { + return nil, errors.New("agent's sub-agents has already been set") + } + + if onAgent, ok_ := fa.Agent.(OnSubAgents); ok_ { + err := onAgent.OnSetSubAgents(ctx, subAgents) + if err != nil { + return nil, err + } + } + + for _, s := range subAgents { + fsa := toFlowAgent(ctx, s) + + if fsa.parentAgent != nil { + return nil, errors.New("agent has already been set as a sub-agent of another agent") + } + + fsa.parentAgent = fa + if onAgent, ok__ := fsa.Agent.(OnSubAgents); ok__ { + err := onAgent.OnSetAsSubAgent(ctx, agent) + if err != nil { + return nil, err + } + + if fsa.disallowTransferToParent { + err = onAgent.OnDisallowTransferToParent(ctx) + if err != nil { + return nil, err + } + } + } + + fa.subAgents = append(fa.subAgents, fsa) + } + + return fa, nil +} + +func (a *flowAgent) getAgent(ctx context.Context, name string) *flowAgent { + for _, subAgent := range a.subAgents { + if subAgent.Name(ctx) == name { + return subAgent + } + } + + if a.parentAgent != nil && a.parentAgent.Name(ctx) == name { + return a.parentAgent + } + + return nil +} + +func rewriteMessage(msg Message, agentName string) Message { + var sb strings.Builder + sb.WriteString("For context:") + if msg.Role == schema.Assistant { + if msg.Content != "" { + sb.WriteString(fmt.Sprintf(" [%s] said: %s.", agentName, msg.Content)) + } + if len(msg.ToolCalls) > 0 { + for i := range msg.ToolCalls { + f := msg.ToolCalls[i].Function + sb.WriteString(fmt.Sprintf(" [%s] called tool: `%s` with arguments: %s.", + agentName, f.Name, f.Arguments)) + } + } + } else if msg.Role == schema.Tool && msg.Content != "" { + sb.WriteString(fmt.Sprintf(" [%s] `%s` tool returned result: %s.", + agentName, msg.ToolName, msg.Content)) + } + + rewritten := schema.UserMessage(sb.String()) + if msg.MultiContent != nil { + rewritten.MultiContent = append([]schema.ChatMessagePart{}, msg.MultiContent...) //nolint:staticcheck // backward compat with deprecated MultiContent field + } + if msg.UserInputMultiContent != nil { + rewritten.UserInputMultiContent = append([]schema.MessageInputPart{}, msg.UserInputMultiContent...) + } + + // Convert AssistantGenMultiContent to UserInputMultiContent, since the role changes to User. + // Reasoning parts have no user input equivalent and are dropped. + for _, part := range msg.AssistantGenMultiContent { + switch part.Type { + case schema.ChatMessagePartTypeText: + rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{ + Type: part.Type, + Text: part.Text, + Extra: part.Extra, + }) + case schema.ChatMessagePartTypeImageURL: + if part.Image != nil { + rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{ + Type: part.Type, + Image: &schema.MessageInputImage{MessagePartCommon: part.Image.MessagePartCommon}, + Extra: part.Extra, + }) + } + case schema.ChatMessagePartTypeAudioURL: + if part.Audio != nil { + rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{ + Type: part.Type, + Audio: &schema.MessageInputAudio{MessagePartCommon: part.Audio.MessagePartCommon}, + Extra: part.Extra, + }) + } + case schema.ChatMessagePartTypeVideoURL: + if part.Video != nil { + rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{ + Type: part.Type, + Video: &schema.MessageInputVideo{MessagePartCommon: part.Video.MessagePartCommon}, + Extra: part.Extra, + }) + } + } + } + + return rewritten +} + +func genMsg(entry *HistoryEntry, agentName string) (Message, error) { + msg := entry.Message + if entry.AgentName != agentName { + msg = rewriteMessage(msg, entry.AgentName) + } + + return msg, nil +} + +func deepCopyAgentInput(ai *AgentInput) *AgentInput { + copied := &AgentInput{ + Messages: make([]Message, len(ai.Messages)), + EnableStreaming: ai.EnableStreaming, + } + + copy(copied.Messages, ai.Messages) + + return copied +} + +func (a *flowAgent) genAgentInput(ctx context.Context, runCtx *runContext, skipTransferMessages bool) (*AgentInput, error) { + input := deepCopyAgentInput(runCtx.RootInput) + + events := runCtx.Session.getEvents() + historyEntries := make([]*HistoryEntry, 0) + + for _, m := range input.Messages { + historyEntries = append(historyEntries, &HistoryEntry{ + IsUserInput: true, + Message: m, + }) + } + + for _, event := range events { + if skipTransferMessages && event.Action != nil && event.Action.TransferToAgent != nil { + // If skipTransferMessages is true and the event contain transfer action, the message in this event won't be appended to history entries. + if event.Output != nil && + event.Output.MessageOutput != nil && + event.Output.MessageOutput.Role == schema.Tool && + len(historyEntries) > 0 { + // If the skipped message's role is Tool, remove the previous history entry as it's also a transfer message(from ChatModelAgent and GenTransferMessages). + historyEntries = historyEntries[:len(historyEntries)-1] + } + continue + } + + msg, err := getMessageFromWrappedEvent(event) + if err != nil { + var retryErr *WillRetryError + if errors.As(err, &retryErr) { + log.Printf("failed to get message from event, but will retry: %v", err) + continue + } + return nil, err + } + + if msg == nil { + continue + } + + historyEntries = append(historyEntries, &HistoryEntry{ + AgentName: event.AgentName, + Message: msg, + }) + } + + messages, err := a.historyRewriter(ctx, historyEntries) + if err != nil { + return nil, err + } + input.Messages = messages + + return input, nil +} + +func buildDefaultHistoryRewriter(agentName string) HistoryRewriter { + return func(ctx context.Context, entries []*HistoryEntry) ([]Message, error) { + messages := make([]Message, 0, len(entries)) + var err error + for _, entry := range entries { + msg := entry.Message + if !entry.IsUserInput { + msg, err = genMsg(entry, agentName) + if err != nil { + return nil, fmt.Errorf("gen agent input failed: %w", err) + } + } + + if msg != nil { + messages = append(messages, msg) + } + } + + return messages, nil + } +} + +func (a *flowAgent) Run(ctx context.Context, input *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + agentName := a.Name(ctx) + + var runCtx *runContext + ctx, runCtx = initRunCtx(ctx, agentName, input) + ctx = AppendAddressSegment(ctx, AddressSegmentAgent, agentName) + + o := getCommonOptions(nil, opts...) + cancelCtx := o.cancelCtx + + processedInput, err := a.genAgentInput(ctx, runCtx, o.skipTransferMessages) + if err != nil { + if cancelCtx != nil { + cancelCtx.markDone() + } + cbInput := &AgentCallbackInput{Input: input} + ctx = callbacks.OnStart(ctx, cbInput) + return wrapIterWithOnEnd(ctx, genErrorIter(err)) + } + + ctxForSubAgents := ctx + + agentType := getAgentType(a.Agent) + ctx = initAgentCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...) + cbInput := &AgentCallbackInput{Input: processedInput} + ctx = callbacks.OnStart(ctx, cbInput) + + input = processedInput + + if wf, ok := a.Agent.(*workflowAgent); ok { + ctx = withCancelContext(ctx, cancelCtx) + filteredOpts := filterCancelOption(filterCallbackHandlersForNestedAgents(agentName, opts)) + iter := wf.Run(ctx, input, filteredOpts...) + iter = wrapIterWithCancelCtx(iter, cancelCtx) + return wrapIterWithOnEnd(ctx, iter) + } + + aIter := a.Agent.Run(withCancelContext(ctx, cancelCtx), input, filterOptions(agentName, opts)...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), runCtx, aIter, generator, filterCancelOption(opts)...) + + return wrapIterWithCancelCtx(iterator, cancelCtx) +} + +func (a *flowAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + agentName := a.Name(ctx) + + ctx, info = buildResumeInfo(ctx, agentName, info) + + ctxForSubAgents := ctx + + o := getCommonOptions(nil, opts...) + cancelCtx := o.cancelCtx + + agentType := getAgentType(a.Agent) + ctx = initAgentCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...) + cbInput := &AgentCallbackInput{ResumeInfo: info} + ctx = callbacks.OnStart(ctx, cbInput) + + if info.WasInterrupted { + if ra, ok := a.Agent.(ResumableAgent); ok { + if _, ok := ra.(*workflowAgent); ok { + ctx = withCancelContext(ctx, cancelCtx) + filteredOpts := filterCancelOption(filterCallbackHandlersForNestedAgents(agentName, opts)) + aIter := ra.Resume(ctx, info, filteredOpts...) + aIter = wrapIterWithCancelCtx(aIter, cancelCtx) + return wrapIterWithOnEnd(ctx, aIter) + } + + aIter := ra.Resume(withCancelContext(ctx, cancelCtx), info, opts...) + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), getRunCtx(ctxForSubAgents), aIter, generator, filterCancelOption(opts)...) + return wrapIterWithCancelCtx(iterator, cancelCtx) + } + + if cancelCtx != nil { + cancelCtx.markDone() + } + return wrapIterWithOnEnd(ctx, genErrorIter(fmt.Errorf("failed to resume agent: agent '%s' is an interrupt point "+ + "but is not a ResumableAgent", agentName))) + } + + nextAgentName, err := getNextResumeAgent(ctx, info) + if err != nil { + if cancelCtx != nil { + cancelCtx.markDone() + } + return wrapIterWithOnEnd(ctx, genErrorIter(err)) + } + + subAgent := a.getAgent(ctxForSubAgents, nextAgentName) + if subAgent == nil { + if len(a.subAgents) == 0 { + if ra, ok := a.Agent.(ResumableAgent); ok { + ctx = withCancelContext(ctx, cancelCtx) + innerIter := ra.Resume(ctx, info, filterCancelOption(opts)...) + return wrapIterWithCancelCtx(wrapIterWithOnEnd(ctx, innerIter), cancelCtx) + } + return wrapIterWithOnEnd(ctx, genErrorIter(fmt.Errorf( + "failed to resume agent: agent '%s' (type %T) has no sub-agents and does not implement ResumableAgent interface. "+ + "To support resume, your custom agent wrapper must implement the ResumableAgent interface", agentName, a.Agent))) + } + if cancelCtx != nil { + cancelCtx.markDone() + } + return wrapIterWithOnEnd(ctx, genErrorIter(fmt.Errorf("failed to resume agent: sub-agent '%s' not found in agent '%s'", nextAgentName, agentName))) + } + + ctxForSubAgents = withCancelContext(ctxForSubAgents, cancelCtx) + innerIter := subAgent.Resume(ctxForSubAgents, info, filterCancelOption(opts)...) + return wrapIterWithCancelCtx(wrapIterWithOnEnd(ctx, innerIter), cancelCtx) +} + +// DeterministicTransferConfig is the configuration for AgentWithDeterministicTransferTo. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +type DeterministicTransferConfig struct { + Agent Agent + ToAgentNames []string +} + +func (a *flowAgent) run( + ctx context.Context, + ctxForSubAgents context.Context, + runCtx *runContext, + aIter *AsyncIterator[*AgentEvent], + generator *AsyncGenerator[*AgentEvent], + opts ...AgentRunOption) { + + cbIter, cbGen := NewAsyncIteratorPair[*AgentEvent]() + + cbOutput := &AgentCallbackOutput{Events: cbIter} + icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyAgentCallbackOutput), callbacks.TimingOnEnd, false) + + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&AgentEvent{Err: e}) + } + + cbGen.Close() + generator.Close() + }() + + var lastAction *AgentAction + for { + event, ok := aIter.Next() + if !ok { + break + } + + // RunPath ownership: the eino framework sets RunPath exactly once. + // If event.RunPath is already set (e.g., by agentTool), we don't modify it. + // If event.RunPath is nil/empty, we set it to the current runCtx.RunPath. + // This ensures RunPath is set exactly once and not duplicated. + if len(event.RunPath) == 0 { + event.AgentName = a.Name(ctx) + event.RunPath = runCtx.RunPath + } + // Recording policy: exact RunPath match (non-interrupt) indicates events belonging to this agent execution. + // This prevents parent recording of child/tool-internal emissions. + if (event.Action == nil || event.Action.Interrupted == nil) && exactRunPathMatch(runCtx.RunPath, event.RunPath) { + // copy the event so that the copied event's stream is exclusive for any potential consumer + // copy before adding to session because once added to session it's stream could be consumed by genAgentInput at any time + // interrupt action are not added to session, because ALL information contained in it + // is either presented to end-user, or made available to agents through other means + copied := copyTypedAgentEvent(event) + setAutomaticClose(copied) + setAutomaticClose(event) + runCtx.Session.addEvent(copied) + } + // Action gating uses exact run-path match as well: + // only actions originating from this agent execution (not child/tool runs) + // should influence parent control flow (exit/transfer/interrupt). + if exactRunPathMatch(runCtx.RunPath, event.RunPath) { + lastAction = event.Action + } + copied := copyTypedAgentEvent(event) + setAutomaticClose(copied) + setAutomaticClose(event) + cbGen.Send(copied) + generator.Send(event) + } + + var destName string + if lastAction != nil { + if lastAction.Interrupted != nil { + return + } + if lastAction.Exit { + return + } + + if lastAction.TransferToAgent != nil { + destName = lastAction.TransferToAgent.DestAgentName + } + } + + // handle transferring to another agent + if destName != "" { + agentToRun := a.getAgent(ctxForSubAgents, destName) + if agentToRun == nil { + e := fmt.Errorf("transfer failed: agent '%s' not found when transferring from '%s'", + destName, a.Name(ctxForSubAgents)) + generator.Send(&AgentEvent{Err: e}) + return + } + + subAIter := agentToRun.Run(ctxForSubAgents, nil /*subagents get input from runCtx*/, opts...) + for { + subEvent, ok_ := subAIter.Next() + if !ok_ { + break + } + + setAutomaticClose(subEvent) + generator.Send(subEvent) + } + } +} + +func exactRunPathMatch(aPath, bPath []RunStep) bool { + if len(aPath) != len(bPath) { + return false + } + for i := range aPath { + if !aPath[i].Equals(bPath[i]) { + return false + } + } + return true +} + +func wrapIterWithOnEnd(ctx context.Context, iter *AsyncIterator[*AgentEvent]) *AsyncIterator[*AgentEvent] { + cbIter, cbGen := NewAsyncIteratorPair[*AgentEvent]() + cbOutput := &AgentCallbackOutput{Events: cbIter} + icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyAgentCallbackOutput), callbacks.TimingOnEnd, false) + + outIter, outGen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer func() { + cbGen.Close() + outGen.Close() + }() + for { + event, ok := iter.Next() + if !ok { + break + } + copied := copyTypedAgentEvent(event) + cbGen.Send(copied) + outGen.Send(event) + } + }() + return outIter +} + +// --------------------------------------------------------------------------- +// Typed wrapper for the agentic path (TypedAgent[*schema.AgenticMessage]). +// +// typedFlowAgent is a minimal wrapper used exclusively by TypedRunner and +// AgentTool to execute a TypedAgent[*schema.AgenticMessage]. It handles +// callbacks, event recording, and run-path tracking. Transfer, sub-agent +// orchestration, and history rewriting are handled solely by the concrete +// flowAgent (the *schema.Message path). +// --------------------------------------------------------------------------- + +type typedFlowAgent[M MessageType] struct { + TypedAgent[M] + + checkPointStore compose.CheckPointStore +} + +func toTypedFlowAgent[M MessageType](agent TypedAgent[M]) *typedFlowAgent[M] { + if fa, ok := agent.(*typedFlowAgent[M]); ok { + return fa + } + return &typedFlowAgent[M]{TypedAgent: agent} +} + +func getTypedAgentType[M MessageType](agent TypedAgent[M]) string { + if msgAgent, ok := any(agent).(Agent); ok { + return getAgentType(msgAgent) + } + if typer, ok := any(agent).(interface{ GetType() string }); ok { + return typer.GetType() + } + return "" +} + +func (a *typedFlowAgent[M]) Run(ctx context.Context, input *TypedAgentInput[M], opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + agentName := a.Name(ctx) + + var runCtx *runContext + ctx, runCtx = initTypedRunCtx(ctx, agentName, input) + ctx = AppendAddressSegment(ctx, AddressSegmentAgent, agentName) + + o := getCommonOptions(nil, opts...) + cancelCtx := o.cancelCtx + + ctxForSubAgents := ctx + + agentType := getTypedAgentType(a.TypedAgent) + ctx = initAgenticCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...) + cbInput := &TypedAgentCallbackInput[*schema.AgenticMessage]{Input: any(input).(*TypedAgentInput[*schema.AgenticMessage])} + ctx = callbacks.OnStart(ctx, cbInput) + + aIter := a.TypedAgent.Run(withCancelContext(ctx, cancelCtx), input, filterOptions(agentName, opts)...) + + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + + go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), runCtx, aIter, generator, filterCancelOption(opts)...) + + return wrapIterWithCancelCtx(iterator, cancelCtx) +} + +func (a *typedFlowAgent[M]) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + agentName := a.Name(ctx) + + ctx, info = buildResumeInfo(ctx, agentName, info) + + ctxForSubAgents := ctx + + o := getCommonOptions(nil, opts...) + cancelCtx := o.cancelCtx + + agentType := getTypedAgentType(a.TypedAgent) + ctx = initAgenticCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...) + cbInput := &TypedAgentCallbackInput[*schema.AgenticMessage]{ResumeInfo: info} + ctx = callbacks.OnStart(ctx, cbInput) + + if info.WasInterrupted { + if ra, ok := a.TypedAgent.(TypedResumableAgent[M]); ok { + aIter := ra.Resume(withCancelContext(ctx, cancelCtx), info, opts...) + + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), getRunCtx(ctxForSubAgents), aIter, generator, filterCancelOption(opts)...) + return wrapIterWithCancelCtx(iterator, cancelCtx) + } + + if cancelCtx != nil { + cancelCtx.markDone() + } + return typedErrorIterWithOnEnd[M](ctx, fmt.Errorf("failed to resume agent: agent '%s' is an interrupt point "+ + "but is not a ResumableAgent", agentName)) + } + + _, err := getNextResumeAgent(ctx, info) + if err != nil { + if cancelCtx != nil { + cancelCtx.markDone() + } + return typedErrorIterWithOnEnd[M](ctx, err) + } + + if ra, ok := a.TypedAgent.(TypedResumableAgent[M]); ok { + ctx = withCancelContext(ctx, cancelCtx) + innerIter := ra.Resume(ctx, info, filterCancelOption(opts)...) + return wrapIterWithCancelCtx(typedWrapIterWithOnEnd(ctx, innerIter), cancelCtx) + } + return typedErrorIterWithOnEnd[M](ctx, fmt.Errorf( + "failed to resume agent: agent '%s' (type %T) does not implement ResumableAgent interface. "+ + "To support resume, your custom agent wrapper must implement the ResumableAgent interface", agentName, a.TypedAgent)) +} + +func (a *typedFlowAgent[M]) run( + ctx context.Context, + _ context.Context, + runCtx *runContext, + aIter *AsyncIterator[*TypedAgentEvent[M]], + generator *AsyncGenerator[*TypedAgentEvent[M]], + _ ...AgentRunOption) { + + agenticCbIter, agenticCbGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + cbOutput := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: agenticCbIter} + icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyTypedCallbackOutput[*schema.AgenticMessage]), callbacks.TimingOnEnd, false) + + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&TypedAgentEvent[M]{Err: e}) + } + + agenticCbGen.Close() + generator.Close() + }() + + for { + event, ok := aIter.Next() + if !ok { + break + } + + if len(event.RunPath) == 0 { + event.AgentName = a.Name(ctx) + event.RunPath = runCtx.RunPath + } + if (event.Action == nil || event.Action.Interrupted == nil) && exactRunPathMatch(runCtx.RunPath, event.RunPath) { + copied := copyTypedAgentEvent(event) + typedSetAutomaticClose(copied) + typedSetAutomaticClose(event) + addTypedEvent(runCtx.Session, copied) + } + + agenticCopied := copyTypedAgentEvent(event) + typedSetAutomaticClose(agenticCopied) + typedSetAutomaticClose(event) + agenticCbGen.Send(any(agenticCopied).(*TypedAgentEvent[*schema.AgenticMessage])) + generator.Send(event) + } +} + +func wrapAgenticIterWithOnEnd(ctx context.Context, iter *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]]) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + cbIter, cbGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + cbOutput := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: cbIter} + icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyTypedCallbackOutput[*schema.AgenticMessage]), callbacks.TimingOnEnd, false) + + outIter, outGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer func() { + cbGen.Close() + outGen.Close() + }() + for { + event, ok := iter.Next() + if !ok { + break + } + copied := copyTypedAgentEvent(event) + cbGen.Send(copied) + outGen.Send(event) + } + }() + return outIter +} + +func genAgenticErrorIter(err error) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err}) + gen.Close() + return iter +} + +func typedWrapIterWithOnEnd[M MessageType](ctx context.Context, iter *AsyncIterator[*TypedAgentEvent[M]]) *AsyncIterator[*TypedAgentEvent[M]] { + agenticIter := any(iter).(*AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]]) + return any(wrapAgenticIterWithOnEnd(ctx, agenticIter)).(*AsyncIterator[*TypedAgentEvent[M]]) +} + +func typedErrorIterWithOnEnd[M MessageType](ctx context.Context, err error) *AsyncIterator[*TypedAgentEvent[M]] { + return typedWrapIterWithOnEnd(ctx, typedErrorIter[M](err)) +} diff --git a/adk/flow_test.go b/adk/flow_test.go new file mode 100644 index 0000000..1cb095e --- /dev/null +++ b/adk/flow_test.go @@ -0,0 +1,389 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/callbacks" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +func strPtr(s string) *string { return &s } + +func TestRewriteMessage(t *testing.T) { + imageCommon := schema.MessagePartCommon{URL: strPtr("http://img.example.com")} + audioCommon := schema.MessagePartCommon{URL: strPtr("http://audio.example.com")} + videoCommon := schema.MessagePartCommon{URL: strPtr("http://video.example.com")} + + msg := &schema.Message{ + Role: schema.Assistant, + Content: "hello", + MultiContent: []schema.ChatMessagePart{ //nolint:staticcheck // testing deprecated field backward compat + {Type: schema.ChatMessagePartTypeText, Text: "legacy"}, + }, + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "pre-existing"}, + }, + AssistantGenMultiContent: []schema.MessageOutputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "gen-text", Extra: map[string]any{"k": "v"}}, + {Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageOutputImage{MessagePartCommon: imageCommon}}, + {Type: schema.ChatMessagePartTypeAudioURL, Audio: &schema.MessageOutputAudio{MessagePartCommon: audioCommon}}, + {Type: schema.ChatMessagePartTypeVideoURL, Video: &schema.MessageOutputVideo{MessagePartCommon: videoCommon}}, + {Type: schema.ChatMessagePartTypeReasoning, Reasoning: &schema.MessageOutputReasoning{Text: "secret thoughts"}}, + }, + } + + rewritten := rewriteMessage(msg, "OtherAgent") + + assert.Equal(t, schema.User, rewritten.Role) + + // MultiContent: copied, not shared + assert.Equal(t, msg.MultiContent, rewritten.MultiContent) + rewritten.MultiContent[0].Text = "mutated" + assert.Equal(t, "legacy", msg.MultiContent[0].Text) + + // UserInputMultiContent: pre-existing entry copied, AssistantGenMultiContent appended (reasoning dropped) + assert.Len(t, rewritten.UserInputMultiContent, 5) // 1 pre-existing + 4 converted (text/image/audio/video) + + // pre-existing entry is not shared + rewritten.UserInputMultiContent[0].Text = "mutated" + assert.Equal(t, "pre-existing", msg.UserInputMultiContent[0].Text) + + // text conversion + assert.Equal(t, schema.ChatMessagePartTypeText, rewritten.UserInputMultiContent[1].Type) + assert.Equal(t, "gen-text", rewritten.UserInputMultiContent[1].Text) + assert.Equal(t, map[string]any{"k": "v"}, rewritten.UserInputMultiContent[1].Extra) + + // image conversion + assert.Equal(t, schema.ChatMessagePartTypeImageURL, rewritten.UserInputMultiContent[2].Type) + assert.Equal(t, imageCommon, rewritten.UserInputMultiContent[2].Image.MessagePartCommon) + + // audio conversion + assert.Equal(t, schema.ChatMessagePartTypeAudioURL, rewritten.UserInputMultiContent[3].Type) + assert.Equal(t, audioCommon, rewritten.UserInputMultiContent[3].Audio.MessagePartCommon) + + // video conversion + assert.Equal(t, schema.ChatMessagePartTypeVideoURL, rewritten.UserInputMultiContent[4].Type) + assert.Equal(t, videoCommon, rewritten.UserInputMultiContent[4].Video.MessagePartCommon) + + // reasoning is dropped; AssistantGenMultiContent is not set on rewritten message + assert.Empty(t, rewritten.AssistantGenMultiContent) +} + +// TestTransferToAgent tests the TransferToAgent functionality +func TestTransferToAgent(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create mock models for parent and child agents + parentModel := mockModel.NewMockToolCallingChatModel(ctrl) + childModel := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the parent model + // First call: parent model generates a message with TransferToAgent tool call + parentModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("I'll transfer this to the child agent", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "ChildAgent"}`, + }, + }, + }), nil). + Times(1) + + // Set up expectations for the child model + // Second call: child model generates a response + childModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello from child agent", nil), nil). + Times(1) + + // Both models should implement WithTools + parentModel.EXPECT().WithTools(gomock.Any()).Return(parentModel, nil).AnyTimes() + childModel.EXPECT().WithTools(gomock.Any()).Return(childModel, nil).AnyTimes() + + // Create parent agent + parentAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ParentAgent", + Description: "Parent agent that will transfer to child", + Instruction: "You are a parent agent.", + Model: parentModel, + }) + assert.NoError(t, err) + assert.NotNil(t, parentAgent) + + // Create child agent + childAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ChildAgent", + Description: "Child agent that handles specific tasks", + Instruction: "You are a child agent.", + Model: childModel, + }) + assert.NoError(t, err) + assert.NotNil(t, childAgent) + + // Set up parent-child relationship + flowAgent, err := SetSubAgents(ctx, parentAgent, []Agent{childAgent}) + assert.NoError(t, err) + assert.NotNil(t, flowAgent) + + assert.NotNil(t, parentAgent.subAgents) + assert.NotNil(t, childAgent.parentAgent) + + // Run the parent agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Please transfer this to the child agent"), + }, + } + ctx, _ = initRunCtx(ctx, flowAgent.Name(ctx), input) + iterator := flowAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // First event: parent model output with tool call + event1, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event1) + assert.Nil(t, event1.Err) + assert.NotNil(t, event1.Output) + assert.NotNil(t, event1.Output.MessageOutput) + assert.Equal(t, schema.Assistant, event1.Output.MessageOutput.Role) + + // Second event: tool output (TransferToAgent) + event2, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event2) + assert.Nil(t, event2.Err) + assert.NotNil(t, event2.Output) + assert.NotNil(t, event2.Output.MessageOutput) + assert.Equal(t, schema.Tool, event2.Output.MessageOutput.Role) + + // Verify the action is TransferToAgent + assert.NotNil(t, event2.Action) + assert.NotNil(t, event2.Action.TransferToAgent) + assert.Equal(t, "ChildAgent", event2.Action.TransferToAgent.DestAgentName) + + // Third event: child model output + event3, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event3) + assert.Nil(t, event3.Err) + assert.NotNil(t, event3.Output) + assert.NotNil(t, event3.Output.MessageOutput) + assert.Equal(t, schema.Assistant, event3.Output.MessageOutput.Role) + + // Verify the message content from child agent + msg := event3.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.Equal(t, "Hello from child agent", msg.Content) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestResumeNonResumableWrapperError(t *testing.T) { + ctx := context.Background() + + interruptingAgent := &nonResumableFlowTestAgent{ + name: "inner", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + gen.Send(Interrupt(ctx, "please confirm")) + }() + return iter + }, + } + + innerFlowAgent := toFlowAgent(ctx, interruptingAgent) + + wrapper := &nonResumableFlowTestAgent{ + name: "wrapper", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return innerFlowAgent.Run(ctx, input, options...) + }, + } + + store := &flowTestStore{m: map[string][]byte{}} + runner := NewRunner(ctx, RunnerConfig{ + Agent: wrapper, + EnableStreaming: true, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1")) + + var interruptID string + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Action != nil && ev.Action.Interrupted != nil { + for _, intCtx := range ev.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + interruptID = intCtx.ID + } + } + } + } + assert.NotEmpty(t, interruptID, "should have an interrupt ID") + + resumeIter, err := runner.ResumeWithParams(ctx, "cp1", &ResumeParams{ + Targets: map[string]any{interruptID: nil}, + }) + assert.NoError(t, err) + + var resumeErr error + for { + ev, ok := resumeIter.Next() + if !ok { + break + } + if ev.Err != nil { + resumeErr = ev.Err + } + } + + assert.Error(t, resumeErr) + assert.Contains(t, resumeErr.Error(), "does not implement ResumableAgent interface") + assert.Contains(t, resumeErr.Error(), "custom agent wrapper must implement the ResumableAgent interface") +} + +type nonResumableFlowTestAgent struct { + name string + runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] +} + +func (a *nonResumableFlowTestAgent) Name(_ context.Context) string { + return a.name +} + +func (a *nonResumableFlowTestAgent) Description(_ context.Context) string { + return a.name + " description" +} + +func (a *nonResumableFlowTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return a.runFn(ctx, input, options...) +} + +type flowTestStore struct { + m map[string][]byte +} + +func (s *flowTestStore) Set(_ context.Context, key string, value []byte) error { + s.m[key] = value + return nil +} + +func (s *flowTestStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.m[key] + return v, ok, nil +} + +func TestTransferToAgentWithDesignatedCallback(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + parentModel := mockModel.NewMockToolCallingChatModel(ctrl) + childModel := mockModel.NewMockToolCallingChatModel(ctrl) + + parentModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("I'll transfer this to the child agent", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: TransferToAgentToolName, + Arguments: `{"agent_name": "ChildAgent"}`, + }, + }, + }), nil). + Times(1) + + childModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello from child agent", nil), nil). + Times(1) + + parentModel.EXPECT().WithTools(gomock.Any()).Return(parentModel, nil).AnyTimes() + childModel.EXPECT().WithTools(gomock.Any()).Return(childModel, nil).AnyTimes() + + parentAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ParentAgent", + Description: "Parent agent that will transfer to child", + Instruction: "You are a parent agent.", + Model: parentModel, + }) + assert.NoError(t, err) + + childAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ChildAgent", + Description: "Child agent that handles specific tasks", + Instruction: "You are a child agent.", + Model: childModel, + }) + assert.NoError(t, err) + + flowAgent, err := SetSubAgents(ctx, parentAgent, []Agent{childAgent}) + assert.NoError(t, err) + + var childCallbackCount int + var mu sync.Mutex + + handler := callbacks.NewHandlerBuilder().OnStartFn( + func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component == ComponentOfAgent && info.Name == "ChildAgent" { + mu.Lock() + childCallbackCount++ + mu.Unlock() + } + return ctx + }).Build() + + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Please transfer this to the child agent"), + }, + } + ctx, _ = initRunCtx(ctx, flowAgent.Name(ctx), input) + iterator := flowAgent.Run(ctx, input, WithCallbacks(handler).DesignateAgent("ChildAgent")) + + for { + _, ok := iterator.Next() + if !ok { + break + } + } + + assert.Equal(t, 1, childCallbackCount, "designated callback for ChildAgent should fire exactly once during transfer") +} diff --git a/adk/handler.go b/adk/handler.go new file mode 100644 index 0000000..f952441 --- /dev/null +++ b/adk/handler.go @@ -0,0 +1,451 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "encoding/gob" + "fmt" + "io" + "reflect" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// InvokableToolCallEndpoint is the function signature for invoking a tool synchronously. +// Middleware authors implement wrappers around this endpoint to add custom behavior. +type InvokableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) + +// StreamableToolCallEndpoint is the function signature for invoking a tool with streaming output. +// Middleware authors implement wrappers around this endpoint to add custom behavior. +type StreamableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) + +type EnhancedInvokableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) + +type EnhancedStreamableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) + +// ToolContext provides metadata about the tool being wrapped. +type ToolContext struct { + Name string + CallID string +} + +// ToolCallsContext contains metadata about the tool calls that just completed. +type ToolCallsContext struct { + // ToolCalls contains the tool call metadata from the model's response. + ToolCalls []ToolContext +} + +// TypedModelContext contains context information passed to WrapModel. +type TypedModelContext[M MessageType] struct { + // Tools contains the current tool list configured for the agent. + // This is populated at request time with the tools that will be sent to the model. + // + // Deprecated: Use TypedChatModelAgentState.ToolInfos in BeforeModelRewriteState instead. + // ModelContext.Tools remains populated for backward compatibility with existing WrapModel handlers, + // but new code should read and modify state.ToolInfos which is the source of truth for the model call. + Tools []*schema.ToolInfo + + // ModelRetryConfig contains the retry configuration for the model. + // This is populated at request time from the agent's ModelRetryConfig. + // Used by EventSenderModelWrapper to wrap stream errors appropriately. + ModelRetryConfig *TypedModelRetryConfig[M] + + // ModelFailoverConfig contains the failover configuration for the model. + // This is populated at request time from the agent's ModelFailoverConfig. + // Used by EventSenderModelWrapper to wrap stream errors so that failed failover + // attempts are skipped (not treated as fatal) by the flow event processor. + ModelFailoverConfig *ModelFailoverConfig[M] + + cancelContext *cancelContext +} + +// ModelContext is the default model context type using *schema.Message. +type ModelContext = TypedModelContext[*schema.Message] + +// ChatModelAgentContext contains runtime information passed to handlers before each ChatModelAgent run. +// Handlers can modify Instruction, Tools, and ReturnDirectly to customize agent behavior. +// +// This type is specific to ChatModelAgent. Other agent types may define their own context types. +type ChatModelAgentContext struct { + // Instruction is the current instruction for the Agent execution. + // It includes the instruction configured for the agent, additional instructions appended by framework + // and AgentMiddleware, and modifications applied by previous BeforeAgent handlers. + // The finalized instruction after all BeforeAgent handlers are then passed to GenModelInput, + // to be (optionally) formatted with SessionValues and converted to system message. + Instruction string + + // Tools are the raw tools (without any wrapper or tool middleware) currently configured for the Agent execution. + // They includes tools passed in AgentConfig, implicit tools added by framework such as transfer / exit tools, + // and other tools already added by middlewares. + Tools []tool.BaseTool + + // ReturnDirectly is the set of tool names currently configured to cause the Agent to return directly. + // This is based on the return directly map configured for the agent, plus any modifications + // by previous BeforeAgent handlers. + ReturnDirectly map[string]bool + + // ToolSearchTool is the tool info for the model's native tool search capability. + // When set by a BeforeAgent handler, the framework passes it to the model via model.WithToolSearchTool. + ToolSearchTool *schema.ToolInfo +} + +// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior. +// +// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built +// on top of it (e.g., DeepAgent). +// +// Why TypedChatModelAgentMiddleware instead of AgentMiddleware? +// +// AgentMiddleware is a struct type, which has inherent limitations: +// - Struct types are closed: users cannot add new methods to extend functionality +// - The framework only recognizes AgentMiddleware's fixed fields, so even if users +// embed AgentMiddleware in a custom struct and add methods, the framework cannot +// call those methods (config.Middlewares is []AgentMiddleware, not a user type) +// - Callbacks in AgentMiddleware only return error, cannot return modified context +// +// TypedChatModelAgentMiddleware is an interface type, which is open for extension: +// - Users can implement custom handlers with arbitrary internal state and methods +// - Hook methods return (context.Context, ..., error) for direct context propagation +// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the +// wrapped endpoint chain: wrappers can pass modified context to the next wrapper +// - Configuration is centralized in struct fields rather than scattered in closures +// +// TypedChatModelAgentMiddleware vs AgentMiddleware: +// - Use AgentMiddleware for simple, static additions (extra instruction/tools) +// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping +// - AgentMiddleware is kept for backward compatibility with existing users +// - Both can be used together; see AgentMiddleware documentation for execution order +// +// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op +// implementations for all methods. +type TypedChatModelAgentMiddleware[M MessageType] interface { + // BeforeAgent is called before each agent run, allowing modification of + // the agent's instruction and tools configuration. + BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) + + // AfterAgent is called after the agent run reaches a successful terminal state. + // Successful terminal states are: final answer (model response with no tool calls), + // and return-directly tool result. + // + // AfterAgent is NOT called when the agent terminates with an error (e.g., + // ErrExceedMaxIterations, context cancellation, model errors). + // + // The state parameter contains the final conversation state, including all messages + // from the completed run. + // + // AfterAgent handlers are called in the same order as BeforeAgent handlers + // (first registered = first called). Consistent with all other middleware hooks, + // if any handler returns an error, subsequent handlers are NOT called (fail-fast) + // and the error is sent to the event stream. + AfterAgent(ctx context.Context, state *TypedChatModelAgentState[M]) (context.Context, error) + + // BeforeModelRewriteState is called before each model invocation. + // The returned state is persisted to the agent's internal state and passed to the model. + // The returned context is propagated to the model call and subsequent handlers. + // + // The ChatModelAgentState struct provides access to: + // - Messages: the conversation history + // - ToolInfos: the tool list that will be sent to the model (modifiable) + // - DeferredToolInfos: tools for server-side search (modifiable, nil if unused) + // + // This is the recommended place to modify messages and tools before a model call. + // Changes here are persisted in state and reflected in subsequent iterations. + BeforeModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error) + + // AfterModelRewriteState is called after each model invocation. + // The input state includes the model's response as the last message. + // The returned state is persisted to the agent's internal state. + // + // The ChatModelAgentState struct provides access to: + // - Messages: the conversation history including the model's response + // - ToolInfos: the tool list that was sent to the model + // - DeferredToolInfos: tools for server-side search (nil if unused) + AfterModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error) + + // WrapInvokableToolCall wraps a tool's synchronous execution with custom behavior. + // Return the input endpoint unchanged and nil error if no wrapping is needed. + // + // This method is only called for tools that implement InvokableTool. + // If a tool only implements StreamableTool, this method will not be called for that tool. + // + // This method is called at request time when the tool is about to be executed. + // The tCtx parameter provides metadata about the tool: + // - Name: The name of the tool being wrapped + // - CallID: The unique identifier for this specific tool call + WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error) + + // WrapStreamableToolCall wraps a tool's streaming execution with custom behavior. + // Return the input endpoint unchanged and nil error if no wrapping is needed. + // + // This method is only called for tools that implement StreamableTool. + // If a tool only implements InvokableTool, this method will not be called for that tool. + // + // This method is called at request time when the tool is about to be executed. + // The tCtx parameter provides metadata about the tool: + // - Name: The name of the tool being wrapped + // - CallID: The unique identifier for this specific tool call + WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error) + + // WrapEnhancedInvokableToolCall wraps an enhanced tool's synchronous execution with custom behavior. + // Return the input endpoint unchanged and nil error if no wrapping is needed. + // + // This method is only called for tools that implement EnhancedInvokableTool. + // If a tool only implements EnhancedStreamableTool, this method will not be called for that tool. + // + // This method is called at request time when the tool is about to be executed. + // The tCtx parameter provides metadata about the tool: + // - Name: The name of the tool being wrapped + // - CallID: The unique identifier for this specific tool call + WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error) + + // WrapEnhancedStreamableToolCall wraps an enhanced tool's streaming execution with custom behavior. + // Return the input endpoint unchanged and nil error if no wrapping is needed. + // + // This method is only called for tools that implement EnhancedStreamableTool. + // If a tool only implements EnhancedInvokableTool, this method will not be called for that tool. + // + // This method is called at request time when the tool is about to be executed. + // The tCtx parameter provides metadata about the tool: + // - Name: The name of the tool being wrapped + // - CallID: The unique identifier for this specific tool call + WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error) + + // WrapModel wraps a chat model with custom behavior around the actual model call. + // Return the input model unchanged and nil error if no wrapping is needed. + // + // This method is called at request time when the model is about to be invoked. + // Note: The parameter is model.BaseModel[M] (not ToolCallingChatModel) because wrappers + // only need to intercept Generate/Stream calls. Tool binding (WithTools) is handled + // separately by the framework and does not flow through user wrappers. + // + // Recommended use cases (behavior around the model call itself): + // - Model call retry logic + // - Model failover (switching to a backup model) + // - Sending events (e.g. streaming progress) + // - Processing or transforming the response stream + // - Changing call configurations (temperature, top_p, etc.) + // + // Discouraged use cases (use BeforeModelRewriteState instead): + // - Modifying input messages: changes here are NOT persisted in state, only + // affect a single model call, and break prompt cache across iterations. + // - Modifying the tool list: use state.ToolInfos / state.DeferredToolInfos in + // BeforeModelRewriteState, which is the source of truth for tool configuration. + // + // The mc parameter provides read-only context about the current model call: + // - Tools: The tool infos that will be sent to the model (Deprecated: read state.ToolInfos instead) + WrapModel(ctx context.Context, m model.BaseModel[M], mc *TypedModelContext[M]) (model.BaseModel[M], error) +} + +// ChatModelAgentMiddleware is the default middleware type using *schema.Message. +// See TypedChatModelAgentMiddleware for full documentation. +type ChatModelAgentMiddleware = TypedChatModelAgentMiddleware[*schema.Message] + +type TypedBaseChatModelAgentMiddleware[M MessageType] struct{} + +// BaseChatModelAgentMiddleware provides default no-op implementations for ChatModelAgentMiddleware. +// Embed *BaseChatModelAgentMiddleware in custom handlers to only override the methods you need. +// +// Example: +// +// type MyHandler struct { +// *adk.BaseChatModelAgentMiddleware +// // custom fields +// } +// +// func (h *MyHandler) BeforeModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) { +// // custom logic +// return ctx, state, nil +// } +type BaseChatModelAgentMiddleware = TypedBaseChatModelAgentMiddleware[*schema.Message] + +func (b *TypedBaseChatModelAgentMiddleware[M]) WrapInvokableToolCall(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) (InvokableToolCallEndpoint, error) { + return endpoint, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) WrapStreamableToolCall(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) (StreamableToolCallEndpoint, error) { + return endpoint, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) WrapEnhancedInvokableToolCall(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) (EnhancedInvokableToolCallEndpoint, error) { + return endpoint, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) WrapEnhancedStreamableToolCall(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) (EnhancedStreamableToolCallEndpoint, error) { + return endpoint, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) WrapModel(_ context.Context, m model.BaseModel[M], _ *TypedModelContext[M]) (model.BaseModel[M], error) { + return m, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + return ctx, runCtx, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) AfterAgent(ctx context.Context, state *TypedChatModelAgentState[M]) (context.Context, error) { + return ctx, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error) { + return ctx, state, nil +} + +func (b *TypedBaseChatModelAgentMiddleware[M]) AfterModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error) { + return ctx, state, nil +} + +func processTypedState(ctx context.Context, fn func(extra map[string]any) map[string]any) error { + runCtx := getRunCtx(ctx) + if runCtx != nil && runCtx.AgenticRootInput != nil { + return compose.ProcessState(ctx, func(_ context.Context, st *typedState[*schema.AgenticMessage]) error { + st.Extra = fn(st.Extra) + return nil + }) + } + return compose.ProcessState(ctx, func(_ context.Context, st *typedState[*schema.Message]) error { + st.Extra = fn(st.Extra) + return nil + }) +} + +// SetRunLocalValue sets a key-value pair that persists for the duration of the current agent Run() invocation. +// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances. +// +// Values stored here are compatible with interrupt/resume cycles - they will be serialized and restored +// when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]() +// in an init() function to ensure proper serialization. +// +// This function can only be called from within a ChatModelAgentMiddleware during agent execution. +// Returns an error if called outside of an agent execution context. +func SetRunLocalValue(ctx context.Context, key string, value any) error { + if err := checkGobEncodability(key, value); err != nil { + return err + } + + err := processTypedState(ctx, func(extra map[string]any) map[string]any { + if extra == nil { + extra = make(map[string]any) + } + extra[key] = value + return extra + }) + if err != nil { + return fmt.Errorf("SetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err) + } + + return nil +} + +// GetRunLocalValue retrieves a value that was set during the current agent Run() invocation. +// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances. +// +// Values stored via SetRunLocalValue are compatible with interrupt/resume cycles - they will be serialized +// and restored when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]() +// in an init() function to ensure proper serialization. +// +// This function can only be called from within a ChatModelAgentMiddleware during agent execution. +// Returns the value and true if found, or nil and false if not found or if called outside of an agent execution context. +func GetRunLocalValue(ctx context.Context, key string) (any, bool, error) { + var val any + var found bool + err := processTypedState(ctx, func(extra map[string]any) map[string]any { + if extra != nil { + val, found = extra[key] + } + return extra + }) + if err != nil { + return nil, false, fmt.Errorf("GetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err) + } + return val, found, nil +} + +// DeleteRunLocalValue removes a value that was set during the current agent Run() invocation. +// +// This function can only be called from within a ChatModelAgentMiddleware during agent execution. +// Returns an error if called outside of an agent execution context. +func DeleteRunLocalValue(ctx context.Context, key string) error { + err := processTypedState(ctx, func(extra map[string]any) map[string]any { + if extra != nil { + delete(extra, key) + } + return extra + }) + if err != nil { + return fmt.Errorf("DeleteRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err) + } + return nil +} + +// TypedSendEvent sends a custom TypedAgentEvent to the event stream during agent execution. +// This allows TypedChatModelAgentMiddleware implementations to emit custom events that will be +// received by the caller iterating over the agent's event stream. +// +// Note: TypedSendEvent is a pure transport — it does NOT auto-assign message IDs. +// Framework-created messages (model output, tool results) receive IDs automatically +// via internal wrapper layers. If your middleware constructs its own messages, call +// EnsureMessageID before sending to assign an ID. +// +// This function can only be called from within a TypedChatModelAgentMiddleware during agent execution. +// Returns an error if called outside of an agent execution context. +func TypedSendEvent[M MessageType](ctx context.Context, event *TypedAgentEvent[M]) error { + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + if execCtx == nil || execCtx.generator == nil { + return fmt.Errorf("TypedSendEvent failed: must be called within a ChatModelAgent Run() or Resume() execution context") + } + + execCtx.send(event) + return nil +} + +// SendEvent sends a custom AgentEvent to the event stream during agent execution. +// This allows ChatModelAgentMiddleware implementations to emit custom events that will be +// received by the caller iterating over the agent's event stream. +// +// This function can only be called from within a ChatModelAgentMiddleware during agent execution. +// Returns an error if called outside of an agent execution context. +func SendEvent(ctx context.Context, event *AgentEvent) error { + return TypedSendEvent(ctx, event) +} + +// checkGobEncodability probes whether the value can be gob-encoded as part of +// a map[string]any, which is exactly how State.Extra is serialized during +// checkpoint. This catches unregistered types early at Set time, rather than +// letting them fail at checkpoint/resume time with a confusing error. +func checkGobEncodability(key string, value any) error { + probe := map[string]any{key: value} + if err := gob.NewEncoder(io.Discard).Encode(probe); err != nil { + typeName := reflect.TypeOf(value).String() + return fmt.Errorf("SetRunLocalValue: the value (type %s) for key %q is not gob-serializable, "+ + "which means it will fail when the agent checkpoint is saved or resumed.\n\n"+ + "To fix this, register the type in an init() function in your package:\n\n"+ + " func init() {\n"+ + " schema.RegisterName[%s](\"a_unique_name_for_this_type\")\n"+ + " }\n\n"+ + "This is required because agent state (including values set via SetRunLocalValue) is "+ + "persisted using gob encoding for interrupt/resume support. All concrete types stored "+ + "in interface-typed fields (like map[string]any) must be registered with gob.\n\n"+ + "If this value does not need to survive interrupt/resume, store it on the context instead, "+ + "for example via context.WithValue, so you don't need gob registration.\n\n"+ + "Underlying error: %w", typeName, key, typeName, err) + } + return nil +} diff --git a/adk/handler_test.go b/adk/handler_test.go new file mode 100644 index 0000000..811cd2b --- /dev/null +++ b/adk/handler_test.go @@ -0,0 +1,2571 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type testInstructionHandler struct { + *BaseChatModelAgentMiddleware + text string +} + +func (h *testInstructionHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + if runCtx.Instruction == "" { + runCtx.Instruction = h.text + } else if h.text != "" { + runCtx.Instruction = runCtx.Instruction + "\n" + h.text + } + return ctx, runCtx, nil +} + +type testInstructionFuncHandler struct { + *BaseChatModelAgentMiddleware + fn func(ctx context.Context, instruction string) (context.Context, string, error) +} + +func (h *testInstructionFuncHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + newCtx, newInstruction, err := h.fn(ctx, runCtx.Instruction) + if err != nil { + return ctx, runCtx, err + } + runCtx.Instruction = newInstruction + return newCtx, runCtx, nil +} + +type testToolsHandler struct { + *BaseChatModelAgentMiddleware + tools []tool.BaseTool +} + +func (h *testToolsHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + runCtx.Tools = append(runCtx.Tools, h.tools...) + return ctx, runCtx, nil +} + +type testToolsFuncHandler struct { + *BaseChatModelAgentMiddleware + fn func(ctx context.Context, tools []tool.BaseTool, returnDirectly map[string]bool) (context.Context, []tool.BaseTool, map[string]bool, error) +} + +func (h *testToolsFuncHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + newCtx, newTools, newReturnDirectly, err := h.fn(ctx, runCtx.Tools, runCtx.ReturnDirectly) + if err != nil { + return ctx, runCtx, err + } + runCtx.Tools = newTools + runCtx.ReturnDirectly = newReturnDirectly + return newCtx, runCtx, nil +} + +type testBeforeAgentHandler struct { + *BaseChatModelAgentMiddleware + fn func(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) +} + +func (h *testBeforeAgentHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + return h.fn(ctx, runCtx) +} + +type testBeforeModelRewriteStateHandler struct { + *BaseChatModelAgentMiddleware + fn func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) +} + +func (h *testBeforeModelRewriteStateHandler) BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + return h.fn(ctx, state, mc) +} + +type testAfterModelRewriteStateHandler struct { + *BaseChatModelAgentMiddleware + fn func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) +} + +func (h *testAfterModelRewriteStateHandler) AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + return h.fn(ctx, state, mc) +} + +type testToolWrapperHandler struct { + *BaseChatModelAgentMiddleware + wrapInvokableFn func(context.Context, InvokableToolCallEndpoint, *ToolContext) InvokableToolCallEndpoint + wrapStreamableFn func(context.Context, StreamableToolCallEndpoint, *ToolContext) StreamableToolCallEndpoint +} + +func (h *testToolWrapperHandler) WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error) { + if h.wrapInvokableFn != nil { + return h.wrapInvokableFn(ctx, endpoint, tCtx), nil + } + return endpoint, nil +} + +func (h *testToolWrapperHandler) WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error) { + if h.wrapStreamableFn != nil { + return h.wrapStreamableFn(ctx, endpoint, tCtx), nil + } + return endpoint, nil +} + +type testModelWrapperHandler struct { + *BaseChatModelAgentMiddleware + fn func(context.Context, model.BaseChatModel, *ModelContext) model.BaseChatModel +} + +func (h *testModelWrapperHandler) WrapModel(ctx context.Context, m model.BaseChatModel, mc *ModelContext) (model.BaseChatModel, error) { + return h.fn(ctx, m, mc), nil +} + +func newTestInvokableToolCallWrapper(beforeFn, afterFn func()) func(context.Context, InvokableToolCallEndpoint, *ToolContext) InvokableToolCallEndpoint { + return func(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) InvokableToolCallEndpoint { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + if beforeFn != nil { + beforeFn() + } + result, err := endpoint(ctx, argumentsInJSON, opts...) + if afterFn != nil { + afterFn() + } + return result, err + } + } +} + +func newResultModifyingInvokableToolCallWrapper(modifyFn func(string) string) func(context.Context, InvokableToolCallEndpoint, *ToolContext) InvokableToolCallEndpoint { + return func(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) InvokableToolCallEndpoint { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + result, err := endpoint(ctx, argumentsInJSON, opts...) + if err == nil && modifyFn != nil { + result = modifyFn(result) + } + return result, err + } + } +} + +func newTestStreamableToolCallWrapper(beforeFn, afterFn func()) func(context.Context, StreamableToolCallEndpoint, *ToolContext) StreamableToolCallEndpoint { + return func(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) StreamableToolCallEndpoint { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + if beforeFn != nil { + beforeFn() + } + result, err := endpoint(ctx, argumentsInJSON, opts...) + if afterFn != nil { + afterFn() + } + return result, err + } + } +} + +func TestHandlerExecutionOrder(t *testing.T) { + t.Run("MultipleInstructionHandlersPipeline", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var capturedInstruction string + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + if len(msgs) > 0 && msgs[0].Role == schema.System { + capturedInstruction = msgs[0].Content + } + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "Base instruction.", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testInstructionHandler{text: "Handler 1 addition."}, + &testInstructionHandler{text: "Handler 2 addition."}, + &testInstructionFuncHandler{fn: func(ctx context.Context, instruction string) (context.Context, string, error) { + return ctx, instruction + "\nHandler 3 dynamic.", nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Contains(t, capturedInstruction, "Base instruction.") + assert.Contains(t, capturedInstruction, "Handler 1 addition.") + assert.Contains(t, capturedInstruction, "Handler 2 addition.") + assert.Contains(t, capturedInstruction, "Handler 3 dynamic.") + }) + + t.Run("MiddlewaresBeforeHandlers", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var capturedInstruction string + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + if len(msgs) > 0 && msgs[0].Role == schema.System { + capturedInstruction = msgs[0].Content + } + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "Base.", + Model: cm, + Middlewares: []AgentMiddleware{ + {AdditionalInstruction: "Middleware instruction."}, + }, + Handlers: []ChatModelAgentMiddleware{ + &testInstructionHandler{text: "Handler instruction."}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + middlewareIdx := len(capturedInstruction) - len("Middleware instruction.") - len("\nHandler instruction.") + handlerIdx := len(capturedInstruction) - len("Handler instruction.") + assert.True(t, middlewareIdx < handlerIdx, "Middleware should be applied before Handler") + }) +} + +func TestToolsHandlerCombinations(t *testing.T) { + t.Run("MultipleToolsHandlersAppend", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &fakeToolForTest{tarCount: 1} + tool2 := &fakeToolForTest{tarCount: 2} + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + var capturedToolCount int + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + options := model.GetCommonOptions(&model.Options{}, opts...) + capturedToolCount = len(options.Tools) + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolsHandler{tools: []tool.BaseTool{tool2}}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, 2, capturedToolCount) + }) + + t.Run("ToolsFuncCanRemoveTools", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "tool1"} + tool2 := &namedTool{name: "tool2"} + tool3 := &namedTool{name: "tool3"} + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + var capturedToolNames []string + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + options := model.GetCommonOptions(&model.Options{}, opts...) + for _, t := range options.Tools { + capturedToolNames = append(capturedToolNames, t.Name) + } + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1, tool2, tool3}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolsFuncHandler{fn: func(ctx context.Context, tools []tool.BaseTool, returnDirectly map[string]bool) (context.Context, []tool.BaseTool, map[string]bool, error) { + filtered := make([]tool.BaseTool, 0) + for _, t := range tools { + info, _ := t.Info(ctx) + if info.Name != "tool2" { + filtered = append(filtered, t) + } + } + return ctx, filtered, returnDirectly, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Contains(t, capturedToolNames, "tool1") + assert.NotContains(t, capturedToolNames, "tool2") + assert.Contains(t, capturedToolNames, "tool3") + }) + + t.Run("ReturnDirectlyModification", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "tool1"} + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: "tool1", Arguments: "{}"}}, + }), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolsFuncHandler{fn: func(ctx context.Context, tools []tool.BaseTool, returnDirectly map[string]bool) (context.Context, []tool.BaseTool, map[string]bool, error) { + for _, t := range tools { + info, _ := t.Info(ctx) + if info.Name == "tool1" { + returnDirectly[info.Name] = true + } + } + return ctx, tools, returnDirectly, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + eventCount := 0 + for { + event, ok := iter.Next() + if !ok { + break + } + eventCount++ + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.Message != nil && + event.Output.MessageOutput.Message.Role == schema.Tool { + assert.Equal(t, "tool1 result", event.Output.MessageOutput.Message.Content) + } + } + assert.Equal(t, 2, eventCount) + }) + + t.Run("DynamicToolCanBeCalledByModel", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + dynamicToolCalled := false + dynamicTool := &callableTool{ + name: "dynamic_tool", + invokeFn: func() { + dynamicToolCalled = true + }, + } + info, _ := dynamicTool.Info(ctx) + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using dynamic tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil).Times(1) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testToolsHandler{tools: []tool.BaseTool{dynamicTool}}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, dynamicToolCalled, "Dynamic tool should have been called") + }) +} + +func TestMessageRewriteHandlers(t *testing.T) { + t.Run("BeforeModelRewriteStatePipeline", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var capturedMsgCount int + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + capturedMsgCount = len(msgs) + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "instruction", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + state.Messages = append(state.Messages, schema.UserMessage("injected1")) + return ctx, state, nil + }}, + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + state.Messages = append(state.Messages, schema.UserMessage("injected2")) + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("original")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, 4, capturedMsgCount) + }) + + t.Run("AfterModelRewriteState", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + afterCalled := false + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + afterCalled = true + assert.True(t, len(state.Messages) > 0) + lastMsg := state.Messages[len(state.Messages)-1] + assert.Equal(t, schema.Assistant, lastMsg.Role) + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, afterCalled) + }) +} + +func TestToolCallWrapperHandlers(t *testing.T) { + t.Run("MultipleToolWrappersPipeline", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &namedTool{name: "test_tool"} + info, _ := testTool.Info(ctx) + + var callOrder []string + var mu sync.Mutex + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil).Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolWrapperHandler{wrapInvokableFn: newTestInvokableToolCallWrapper( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper1-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper1-after") + mu.Unlock() + }, + )}, + &testToolWrapperHandler{wrapInvokableFn: newTestInvokableToolCallWrapper( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper2-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper2-after") + mu.Unlock() + }, + )}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, []string{"wrapper1-before", "wrapper2-before", "wrapper2-after", "wrapper1-after"}, callOrder) + }) + + t.Run("StreamingToolWrappersPipeline", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &streamingNamedTool{name: "streaming_tool"} + info, _ := testTool.Info(ctx) + + var callOrder []string + var mu sync.Mutex + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), + }), nil).Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("done", nil), + }), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolWrapperHandler{wrapStreamableFn: newTestStreamableToolCallWrapper( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper1-stream-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper1-stream-after") + mu.Unlock() + }, + )}, + &testToolWrapperHandler{wrapStreamableFn: newTestStreamableToolCallWrapper( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper2-stream-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper2-stream-after") + mu.Unlock() + }, + )}, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true, CheckPointStore: newBridgeStore()}) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var hasStreamingToolResult bool + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.IsStreaming && + event.Output.MessageOutput.Role == schema.Tool { + hasStreamingToolResult = true + for { + _, err := event.Output.MessageOutput.MessageStream.Recv() + if err != nil { + break + } + } + } + } + + assert.True(t, hasStreamingToolResult, "Should have streaming tool result") + assert.Equal(t, []string{"wrapper1-stream-before", "wrapper2-stream-before", "wrapper2-stream-after", "wrapper1-stream-after"}, callOrder, + "Streaming wrappers should be called in correct order") + }) + + t.Run("ToolWrapperCanModifyResult", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &namedTool{name: "test_tool"} + info, _ := testTool.Info(ctx) + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil).Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolWrapperHandler{wrapInvokableFn: newResultModifyingInvokableToolCallWrapper(func(result string) string { + return "modified: " + result + })}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.Message != nil && + event.Output.MessageOutput.Message.Role == schema.Tool { + assert.Equal(t, "modified: test_tool result", event.Output.MessageOutput.Message.Content) + } + } + }) +} + +func TestToolContextFunctions(t *testing.T) { + t.Run("ModelContextToolsField", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &namedTool{name: "base_tool"} + info, _ := testTool.Info(ctx) + + var wrapperSeenTools []*schema.ToolInfo + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, mc *ModelContext) model.BaseChatModel { + return &toolChainingTestModel{ + inner: m, + mc: mc, + wrapFn: func(ctx context.Context, opts []model.Option) []model.Option { + wrapperSeenTools = mc.Tools + return opts + }, + } + }, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Len(t, wrapperSeenTools, 1, "Wrapper should see base tool") + assert.Equal(t, info.Name, wrapperSeenTools[0].Name, "Wrapper should see base_tool") + }) +} + +type toolChainingTestModel struct { + inner model.BaseChatModel + mc *ModelContext + wrapFn func(ctx context.Context, opts []model.Option) []model.Option +} + +func (m *toolChainingTestModel) Generate(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + if m.wrapFn != nil { + opts = m.wrapFn(ctx, opts) + } + return m.inner.Generate(ctx, msgs, opts...) +} + +func (m *toolChainingTestModel) Stream(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + if m.wrapFn != nil { + opts = m.wrapFn(ctx, opts) + } + return m.inner.Stream(ctx, msgs, opts...) +} + +func (m *toolChainingTestModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +func TestContextPropagation(t *testing.T) { + t.Run("ContextPassedThroughBeforeModelHandlers", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + type ctxKey string + const key1 ctxKey = "key1" + const key2 ctxKey = "key2" + + var handler2ReceivedValue1 interface{} + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + return context.WithValue(ctx, key1, "value1"), state, nil + }}, + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + handler2ReceivedValue1 = ctx.Value(key1) + return context.WithValue(ctx, key2, "value2"), state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, "value1", handler2ReceivedValue1, "Handler 2 should receive context value set by Handler 1") + }) + + t.Run("BeforeAgentContextPropagation", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + type ctxKey string + const key1 ctxKey = "key1" + + var handler2ReceivedValue interface{} + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeAgentHandler{fn: func(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + return context.WithValue(ctx, key1, "value1"), runCtx, nil + }}, + &testBeforeAgentHandler{fn: func(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + handler2ReceivedValue = ctx.Value(key1) + return ctx, runCtx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, "value1", handler2ReceivedValue, "Handler 2 should receive context value set by Handler 1 during BeforeAgent") + }) +} + +func TestCustomHandler(t *testing.T) { + t.Run("CustomHandlerWithState", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + customHandler := &countingHandler{} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{customHandler}, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, 1, customHandler.beforeAgentCount) + assert.Equal(t, 1, customHandler.afterAgentCount) + assert.Equal(t, 1, customHandler.beforeModelCount) + assert.Equal(t, 1, customHandler.afterModelCount) + }) +} + +func TestHandlerErrorHandling(t *testing.T) { + t.Run("BeforeAgentErrorStopsRun", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeAgentHandler{fn: func(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + return ctx, runCtx, assert.AnError + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []*schema.Message{schema.UserMessage("test")}, + }) + + var gotErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + gotErr = event.Err + } + } + + assert.Error(t, gotErr) + assert.Contains(t, gotErr.Error(), "BeforeAgent failed") + }) +} + +type namedTool struct { + name string +} + +func (t *namedTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: t.name + " description"}, nil +} + +func (t *namedTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return t.name + " result", nil +} + +type streamingNamedTool struct { + name string +} + +func (t *streamingNamedTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: t.name + " description"}, nil +} + +func (t *streamingNamedTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return t.name + " result", nil +} + +func (t *streamingNamedTool) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) { + return schema.StreamReaderFromArray([]string{t.name + " stream result"}), nil +} + +type callableTool struct { + name string + invokeFn func() +} + +func (t *callableTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: t.name, Desc: t.name + " description"}, nil +} + +func (t *callableTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + if t.invokeFn != nil { + t.invokeFn() + } + return t.name + " result", nil +} + +type countingHandler struct { + *BaseChatModelAgentMiddleware + beforeAgentCount int + afterAgentCount int + beforeModelCount int + afterModelCount int + mu sync.Mutex +} + +func (h *countingHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + h.mu.Lock() + h.beforeAgentCount++ + h.mu.Unlock() + return ctx, runCtx, nil +} + +func (h *countingHandler) AfterAgent(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + h.mu.Lock() + h.afterAgentCount++ + h.mu.Unlock() + return ctx, nil +} + +func (h *countingHandler) BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + h.mu.Lock() + h.beforeModelCount++ + h.mu.Unlock() + return ctx, state, nil +} + +func (h *countingHandler) AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + h.mu.Lock() + h.afterModelCount++ + h.mu.Unlock() + return ctx, state, nil +} + +func newTestModelWrapperFn(beforeFn, afterFn func()) func(context.Context, model.BaseChatModel, *ModelContext) model.BaseChatModel { + return func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return &testWrappedModel{ + inner: m, + beforeFn: beforeFn, + afterFn: afterFn, + } + } +} + +type testWrappedModel struct { + inner model.BaseChatModel + beforeFn func() + afterFn func() +} + +func (m *testWrappedModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + if m.beforeFn != nil { + m.beforeFn() + } + result, err := m.inner.Generate(ctx, input, opts...) + if m.afterFn != nil { + m.afterFn() + } + return result, err +} + +func (m *testWrappedModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + if m.beforeFn != nil { + m.beforeFn() + } + result, err := m.inner.Stream(ctx, input, opts...) + if m.afterFn != nil { + m.afterFn() + } + return result, err +} + +func TestModelWrapperHandlers(t *testing.T) { + t.Run("MultipleModelWrappersPipeline", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callOrder []string + var mu sync.Mutex + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testModelWrapperHandler{fn: newTestModelWrapperFn( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper1-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper1-after") + mu.Unlock() + }, + )}, + &testModelWrapperHandler{fn: newTestModelWrapperFn( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper2-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper2-after") + mu.Unlock() + }, + )}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, []string{"wrapper1-before", "wrapper2-before", "wrapper2-after", "wrapper1-after"}, callOrder) + }) + + t.Run("ModelWrapperBeforeAfterCallOrder", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var callOrder []string + var mu sync.Mutex + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + mu.Lock() + callOrder = append(callOrder, "model-generate") + mu.Unlock() + return schema.AssistantMessage("original response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testModelWrapperHandler{fn: newTestModelWrapperFn( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper-after") + mu.Unlock() + }, + )}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, []string{"wrapper-before", "model-generate", "wrapper-after"}, callOrder) + }) + + t.Run("ModelWrapperWithTools", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &namedTool{name: "test_tool"} + info, _ := testTool.Info(ctx) + + var callOrder []string + var mu sync.Mutex + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + mu.Lock() + callOrder = append(callOrder, "model-call") + mu.Unlock() + return schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil + }).Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + mu.Lock() + callOrder = append(callOrder, "model-call") + mu.Unlock() + return schema.AssistantMessage("done", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testModelWrapperHandler{fn: newTestModelWrapperFn( + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper-before") + mu.Unlock() + }, + func() { + mu.Lock() + callOrder = append(callOrder, "wrapper-after") + mu.Unlock() + }, + )}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, []string{ + "wrapper-before", "model-call", "wrapper-after", + "wrapper-before", "model-call", "wrapper-after", + }, callOrder) + }) +} + +func newInputModifyingWrapperFn(inputPrefix string) func(context.Context, model.BaseChatModel, *ModelContext) model.BaseChatModel { + return func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return &inputOutputModifyingModel{ + inner: m, + inputPrefix: inputPrefix, + } + } +} + +type inputOutputModifyingModel struct { + inner model.BaseChatModel + inputPrefix string +} + +func (m *inputOutputModifyingModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + modifiedMessages := make([]*schema.Message, len(input)) + for i, msg := range input { + if msg.Role == schema.User { + modifiedMessages[i] = schema.UserMessage(m.inputPrefix + msg.Content) + } else { + modifiedMessages[i] = msg + } + } + return m.inner.Generate(ctx, modifiedMessages, opts...) +} + +func (m *inputOutputModifyingModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + modifiedMessages := make([]*schema.Message, len(input)) + for i, msg := range input { + if msg.Role == schema.User { + modifiedMessages[i] = schema.UserMessage(m.inputPrefix + msg.Content) + } else { + modifiedMessages[i] = msg + } + } + return m.inner.Stream(ctx, modifiedMessages, opts...) +} + +func TestModelWrapper_InputModification(t *testing.T) { + t.Run("ModelWrapperModifiesInput_Generate", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var modelReceivedInput []*schema.Message + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + modelReceivedInput = input + return schema.AssistantMessage("original response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testModelWrapperHandler{fn: newInputModifyingWrapperFn("[WRAPPER]")}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test input")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.NotNil(t, modelReceivedInput) + assert.True(t, len(modelReceivedInput) > 0) + found := false + for _, msg := range modelReceivedInput { + if msg.Content == "[WRAPPER]test input" { + found = true + break + } + } + assert.True(t, found, "Model should receive wrapper-modified input") + }) + + t.Run("ModelWrapperModifiesInput_Stream", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var modelReceivedInput []*schema.Message + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + modelReceivedInput = input + return schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("chunk1", nil), + schema.AssistantMessage("chunk2", nil), + }), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testModelWrapperHandler{fn: newInputModifyingWrapperFn("[WRAPPER]")}, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true, CheckPointStore: newBridgeStore()}) + iter := r.Run(ctx, []Message{schema.UserMessage("test input")}) + + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.IsStreaming && + event.Output.MessageOutput.Role == schema.Assistant { + for { + _, err := event.Output.MessageOutput.MessageStream.Recv() + if err != nil { + break + } + } + } + } + + assert.NotNil(t, modelReceivedInput) + assert.True(t, len(modelReceivedInput) > 0) + found := false + for _, msg := range modelReceivedInput { + if msg.Content == "[WRAPPER]test input" { + found = true + break + } + } + assert.True(t, found, "Model should receive wrapper-modified input") + }) +} + +func TestRunLocalValueFunctions(t *testing.T) { + t.Run("SetAndGetRunLocalValue", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var capturedValue any + var capturedFound bool + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + err := SetRunLocalValue(ctx, "test_key", "test_value") + assert.NoError(t, err) + return ctx, state, nil + }}, + &testAfterModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + val, found, err := GetRunLocalValue(ctx, "test_key") + assert.NoError(t, err) + capturedValue = val + capturedFound = found + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, capturedFound, "Value should be found") + assert.Equal(t, "test_value", capturedValue, "Value should match what was set") + }) + + t.Run("DeleteRunLocalValue", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var valueAfterDelete any + var foundAfterDelete bool + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + err := SetRunLocalValue(ctx, "delete_key", "delete_value") + assert.NoError(t, err) + + err = DeleteRunLocalValue(ctx, "delete_key") + assert.NoError(t, err) + return ctx, state, nil + }}, + &testAfterModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + val, found, err := GetRunLocalValue(ctx, "delete_key") + assert.NoError(t, err) + valueAfterDelete = val + foundAfterDelete = found + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.False(t, foundAfterDelete, "Value should not be found after deletion") + assert.Nil(t, valueAfterDelete, "Value should be nil after deletion") + }) + + t.Run("GetNonExistentKey", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var capturedValue any + var capturedFound bool + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + val, found, err := GetRunLocalValue(ctx, "non_existent_key") + assert.NoError(t, err) + capturedValue = val + capturedFound = found + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.False(t, capturedFound, "Non-existent key should not be found") + assert.Nil(t, capturedValue, "Non-existent key should return nil value") + }) + + t.Run("RunLocalValueGobEncodabilityCheck", func(t *testing.T) { + type unregisteredType struct { + Data string + } + + // unregistered custom struct should fail + err := checkGobEncodability("key", unregisteredType{Data: "hello"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not gob-serializable") + assert.Contains(t, err.Error(), "schema.RegisterName") + assert.Contains(t, err.Error(), "context.WithValue") + assert.Contains(t, err.Error(), "unregisteredType") + + // primitives should succeed + assert.NoError(t, checkGobEncodability("key", "hello")) + assert.NoError(t, checkGobEncodability("key", 42)) + assert.NoError(t, checkGobEncodability("key", true)) + assert.NoError(t, checkGobEncodability("key", 3.14)) + }) + + t.Run("RunLocalValueOutsideContext", func(t *testing.T) { + ctx := context.Background() + + err := SetRunLocalValue(ctx, "key", "value") + assert.Error(t, err, "SetRunLocalValue should fail outside agent context") + assert.Contains(t, err.Error(), "SetRunLocalValue failed") + + _, _, err = GetRunLocalValue(ctx, "key") + assert.Error(t, err, "GetRunLocalValue should fail outside agent context") + assert.Contains(t, err.Error(), "GetRunLocalValue failed") + + err = DeleteRunLocalValue(ctx, "key") + assert.Error(t, err, "DeleteRunLocalValue should fail outside agent context") + assert.Contains(t, err.Error(), "DeleteRunLocalValue failed") + }) + + t.Run("RunLocalValuePersistsAcrossModelCalls", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &namedTool{name: "test_tool"} + info, _ := testTool.Info(ctx) + + var firstCallValue any + var secondCallValue any + callCount := 0 + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil).Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + callCount++ + if callCount == 1 { + err := SetRunLocalValue(ctx, "persist_key", "persist_value") + assert.NoError(t, err) + val, _, _ := GetRunLocalValue(ctx, "persist_key") + firstCallValue = val + } else { + val, _, _ := GetRunLocalValue(ctx, "persist_key") + secondCallValue = val + } + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, "persist_value", firstCallValue, "First call should set value") + assert.Equal(t, "persist_value", secondCallValue, "Value should persist to second model call") + }) +} + +func TestHandlerErrorPropagation(t *testing.T) { + t.Run("BeforeModelRewriteStateErrorStopsRun", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + return ctx, state, assert.AnError + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + var gotErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + gotErr = event.Err + } + } + + assert.Error(t, gotErr) + }) + + t.Run("AfterModelRewriteStateErrorStopsRun", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + return ctx, state, assert.AnError + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + + var gotErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + gotErr = event.Err + } + } + + assert.Error(t, gotErr) + }) + + t.Run("MultipleHandlersFirstErrorStops", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + secondHandlerCalled := false + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + return ctx, state, assert.AnError + }}, + &testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error) { + secondHandlerCalled = true + return ctx, state, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.False(t, secondHandlerCalled, "Second handler should not be called after first handler error") + }) +} + +func TestToolContextInWrappers(t *testing.T) { + t.Run("ToolContextHasCorrectNameAndCallID", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + testTool := &namedTool{name: "context_test_tool"} + info, _ := testTool.Info(ctx) + + var capturedToolName string + var capturedCallID string + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "test_call_id_123", Function: schema.FunctionCall{Name: info.Name, Arguments: "{}"}}, + }), nil).Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapInvokableFn: func(_ context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) InvokableToolCallEndpoint { + capturedToolName = tCtx.Name + capturedCallID = tCtx.CallID + return endpoint + }, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, "context_test_tool", capturedToolName, "ToolContext should have correct tool name") + assert.Equal(t, "test_call_id_123", capturedCallID, "ToolContext should have correct call ID") + }) +} + +func TestAfterToolCallsHook(t *testing.T) { + t.Run("CalledAfterToolCalls", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "tool_alpha"} + tool2 := &namedTool{name: "tool_beta"} + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // First call: model returns two tool calls + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling tools", []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "tool_alpha", Arguments: "{}"}}, + {ID: "call_2", Function: schema.FunctionCall{Name: "tool_beta", Arguments: "{}"}}, + }), nil).Times(1) + + // Second call: model returns final response + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + var mu sync.Mutex + callCount := 0 + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1, tool2}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + WithAfterToolCallsHook(func(ctx context.Context) error { + mu.Lock() + callCount++ + mu.Unlock() + return nil + })) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + // Should be called exactly once (one iteration with tool calls) + assert.Equal(t, 1, callCount) + }) + + t.Run("NotCalledWithoutToolCalls", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Model returns a direct response with no tool calls + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("direct response", nil), nil).Times(1) + + callCount := 0 + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + WithAfterToolCallsHook(func(ctx context.Context) error { + callCount++ + return nil + })) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, 0, callCount, "AfterToolCallsHook should not be called when no tool calls happen") + }) + + t.Run("ToolResultsInStateBeforeHookFires", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "mytool"} + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // First call: model returns a tool call + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling", []schema.ToolCall{ + {ID: "c1", Function: schema.FunctionCall{Name: "mytool", Arguments: "{}"}}, + }), nil).Times(1) + + // Second call: final response + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final", nil), nil).Times(1) + + var hookToolResultCount int + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("original")}}, + WithAfterToolCallsHook(func(ctx context.Context) error { + // Verify tool results are already in state when the hook fires + _ = compose.ProcessState(ctx, func(_ context.Context, st *State) error { + for _, msg := range st.Messages { + if msg.Role == schema.Tool { + hookToolResultCount++ + } + } + return nil + }) + return nil + })) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, 1, hookToolResultCount, "Tool results should be in state when hook fires") + }) + + t.Run("HookErrorPropagation", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "mytool"} + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling", []schema.ToolCall{ + {ID: "c1", Function: schema.FunctionCall{Name: "mytool", Arguments: "{}"}}, + }), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + WithAfterToolCallsHook(func(ctx context.Context) error { + return fmt.Errorf("hook failure") + })) + + var sawError bool + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + assert.Contains(t, ev.Err.Error(), "hook failure") + sawError = true + } + } + assert.True(t, sawError, "hook error should propagate as an agent error event") + }) + + t.Run("HookCalledPerIteration", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "mytool"} + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Iteration 1: tool call + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling1", []schema.ToolCall{ + {ID: "c1", Function: schema.FunctionCall{Name: "mytool", Arguments: "{}"}}, + }), nil).Times(1) + + // Iteration 2: tool call again + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling2", []schema.ToolCall{ + {ID: "c2", Function: schema.FunctionCall{Name: "mytool", Arguments: "{}"}}, + }), nil).Times(1) + + // Iteration 3: final answer + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil).Times(1) + + var mu sync.Mutex + hookCount := 0 + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}, + WithAfterToolCallsHook(func(ctx context.Context) error { + mu.Lock() + hookCount++ + mu.Unlock() + return nil + })) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + assert.Equal(t, 2, hookCount, "hook should fire once per tool-call iteration") + }) +} + +func TestToolResultNotDuplicated(t *testing.T) { + t.Run("SecondModelCallHasNoToolResultDuplication", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "mytool"} + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling", []schema.ToolCall{ + {ID: "c1", Function: schema.FunctionCall{Name: "mytool", Arguments: "{}"}}, + }), nil).Times(1) + + var capturedMsgs []*schema.Message + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + capturedMsgs = append([]*schema.Message{}, msgs...) + return schema.AssistantMessage("final", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are helpful.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hello")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.NotNil(t, capturedMsgs) + assert.Equal(t, 4, len(capturedMsgs), + "expected [system, user, assistant, tool_result], got %d messages", len(capturedMsgs)) + assert.Equal(t, schema.System, capturedMsgs[0].Role) + assert.Equal(t, schema.User, capturedMsgs[1].Role) + assert.Equal(t, schema.Assistant, capturedMsgs[2].Role) + assert.Equal(t, schema.Tool, capturedMsgs[3].Role) + + toolResultCount := 0 + for _, msg := range capturedMsgs { + if msg.Role == schema.Tool { + toolResultCount++ + } + } + assert.Equal(t, 1, toolResultCount, + "tool result should appear exactly once, got %d", toolResultCount) + }) + + t.Run("HookInjectedMessagePresentWithoutDuplication", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + tool1 := &namedTool{name: "mytool"} + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("calling", []schema.ToolCall{ + {ID: "c1", Function: schema.FunctionCall{Name: "mytool", Arguments: "{}"}}, + }), nil).Times(1) + + var capturedMsgs []*schema.Message + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + capturedMsgs = append([]*schema.Message{}, msgs...) + return schema.AssistantMessage("final", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are helpful.", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + }, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("hello")}}, + WithAfterToolCallsHook(func(ctx context.Context) error { + return compose.ProcessState(ctx, func(_ context.Context, st *State) error { + st.Messages = append(st.Messages, schema.UserMessage("injected")) + return nil + }) + })) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.NotNil(t, capturedMsgs) + assert.Equal(t, 5, len(capturedMsgs), + "expected [system, user, assistant, tool_result, injected], got %d messages", len(capturedMsgs)) + assert.Equal(t, schema.System, capturedMsgs[0].Role) + assert.Equal(t, schema.User, capturedMsgs[1].Role) + assert.Equal(t, schema.Assistant, capturedMsgs[2].Role) + assert.Equal(t, schema.Tool, capturedMsgs[3].Role) + assert.Equal(t, "injected", capturedMsgs[4].Content) + + toolResultCount := 0 + for _, msg := range capturedMsgs { + if msg.Role == schema.Tool { + toolResultCount++ + } + } + assert.Equal(t, 1, toolResultCount, + "tool result should appear exactly once, got %d", toolResultCount) + }) +} + +type testAfterAgentHandler struct { + *BaseChatModelAgentMiddleware + fn func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) +} + +func (h *testAfterAgentHandler) AfterAgent(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + return h.fn(ctx, state) +} + +type testAgenticAfterAgentHandler struct { + *TypedBaseChatModelAgentMiddleware[*schema.AgenticMessage] + fn func(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage]) (context.Context, error) +} + +func (h *testAgenticAfterAgentHandler) AfterAgent(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage]) (context.Context, error) { + return h.fn(ctx, state) +} + +func TestAfterAgent(t *testing.T) { + t.Run("FinalAnswer", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + var called bool + var capturedState *ChatModelAgentState + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + called = true + capturedState = state + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, called, "AfterAgent should be called on final answer") + assert.NotNil(t, capturedState) + assert.GreaterOrEqual(t, len(capturedState.Messages), 2, "state should contain at least user + assistant messages") + }) + + t.Run("ReturnDirectly", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + myTool := &namedTool{name: "myTool"} + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: "myTool", Arguments: "{}"}}, + }), nil).Times(1) + + var called bool + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{myTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testToolsFuncHandler{fn: func(ctx context.Context, tools []tool.BaseTool, returnDirectly map[string]bool) (context.Context, []tool.BaseTool, map[string]bool, error) { + returnDirectly["myTool"] = true + return ctx, tools, returnDirectly, nil + }}, + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + called = true + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, called, "AfterAgent should be called on return-directly tool result") + }) + + t.Run("NotCalledOnModelError", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("model error")).Times(1) + + var called bool + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + called = true + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.False(t, called, "AfterAgent should NOT be called when model errors") + }) + + t.Run("NotCalledOnMaxIterations", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + myTool := &namedTool{name: "myTool"} + + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Using tool", []schema.ToolCall{ + {ID: "call1", Function: schema.FunctionCall{Name: "myTool", Arguments: "{}"}}, + }), nil).AnyTimes() + + var called bool + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + MaxIterations: 1, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{myTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + called = true + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.False(t, called, "AfterAgent should NOT be called on max iterations exceeded") + }) + + t.Run("ErrorStopsRun", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + return ctx, fmt.Errorf("after agent hook error") + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + var gotErr error + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + gotErr = event.Err + } + } + + assert.Error(t, gotErr) + assert.Contains(t, gotErr.Error(), "AfterAgent") + }) + + t.Run("ContextPropagation", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + type ctxKey string + const key1 ctxKey = "afterAgentKey" + + var handler2ReceivedValue interface{} + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + return context.WithValue(ctx, key1, "afterValue"), nil + }}, + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + handler2ReceivedValue = ctx.Value(key1) + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.Equal(t, "afterValue", handler2ReceivedValue, + "Handler 2 should receive context value set by Handler 1 during AfterAgent") + }) + + t.Run("NoToolsPath", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + var called bool + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + called = true + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, called, "AfterAgent should be called on no-tools path") + }) + + t.Run("FailFast", func(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil).Times(1) + + var handler2Called bool + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: cm, + Handlers: []ChatModelAgentMiddleware{ + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + return ctx, fmt.Errorf("first handler error") + }}, + &testAfterAgentHandler{fn: func(ctx context.Context, state *ChatModelAgentState) (context.Context, error) { + handler2Called = true + return ctx, nil + }}, + }, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.False(t, handler2Called, "Handler 2 should NOT be called when Handler 1 errors (fail-fast)") + }) + + t.Run("AgenticFinalAnswer", func(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "agentic response"}), + }, + } + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticResponse, nil + }, + } + + var called bool + var capturedState *TypedChatModelAgentState[*schema.AgenticMessage] + + handler := &testAgenticAfterAgentHandler{fn: func(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage]) (context.Context, error) { + called = true + capturedState = state + return ctx, nil + }} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticTestAgent", + Description: "test", + Model: m, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&namedTool{name: "dummyTool"}}, + }, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{handler}, + }) + assert.NoError(t, err) + + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("test")}, + }) + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.True(t, called, "AfterAgent should be called on agentic final answer") + assert.NotNil(t, capturedState) + assert.GreaterOrEqual(t, len(capturedState.Messages), 2, "state should contain at least user + assistant messages") + }) +} diff --git a/adk/instruction.go b/adk/instruction.go new file mode 100644 index 0000000..635b65c --- /dev/null +++ b/adk/instruction.go @@ -0,0 +1,64 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "fmt" + "strings" + + "github.com/cloudwego/eino/adk/internal" +) + +const ( + TransferToAgentInstruction = `Available other agents: %s + +Decision rule: +- If you're best suited for the question according to your description: ANSWER +- If another agent is better according its description: CALL '%s' function with their agent name + +When transferring: OUTPUT ONLY THE FUNCTION CALL` + + TransferToAgentInstructionChinese = `可用的其他 agent:%s + +决策规则: +- 如果根据你的职责描述,你最适合回答这个问题:ANSWER +- 如果根据其职责描述,另一个 agent 更适合:调用 %s 函数,并传入该 agent 的名称 + +当进行移交时:只输出函数调用,不要输出其他任何内容` + + agentDescriptionTpl = "\n- Agent name: %s\n Agent description: %s" + agentDescriptionTplChinese = "\n- Agent 名字: %s\n Agent 描述: %s" +) + +func genTransferToAgentInstruction[M MessageType](ctx context.Context, agents []TypedAgent[M]) string { + tpl := internal.SelectPrompt(internal.I18nPrompts{ + English: agentDescriptionTpl, + Chinese: agentDescriptionTplChinese, + }) + instruction := internal.SelectPrompt(internal.I18nPrompts{ + English: TransferToAgentInstruction, + Chinese: TransferToAgentInstructionChinese, + }) + + var sb strings.Builder + for _, agent := range agents { + sb.WriteString(fmt.Sprintf(tpl, agent.Name(ctx), agent.Description(ctx))) + } + + return fmt.Sprintf(instruction, sb.String(), TransferToAgentToolName) +} diff --git a/adk/interface.go b/adk/interface.go new file mode 100644 index 0000000..8905950 --- /dev/null +++ b/adk/interface.go @@ -0,0 +1,519 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "fmt" + "io" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/schema" +) + +// ComponentOfAgent is the component type identifier for ADK agents in callbacks. +// Use this to filter callback events to only agent-related events. +const ComponentOfAgent components.Component = "Agent" + +// ComponentOfAgenticAgent is the component type identifier for ADK agents +// that use *schema.AgenticMessage in callbacks. +const ComponentOfAgenticAgent components.Component = "AgenticAgent" + +// MessageType is the sealed type constraint for message types used in ADK. +// Only *schema.Message and *schema.AgenticMessage satisfy this constraint. +// External packages cannot add new types to this union; all generic functions +// in ADK use exhaustive type switches on these two types. +type MessageType interface { + *schema.Message | *schema.AgenticMessage +} + +type Message = *schema.Message +type MessageStream = *schema.StreamReader[Message] + +type AgenticMessage = *schema.AgenticMessage +type AgenticMessageStream = *schema.StreamReader[AgenticMessage] + +// isNilMessage checks whether a generic message value is nil. +// Direct `msg == nil` does not compile for generic pointer types in Go; +// the canonical workaround is to compare through the `any` interface. +func isNilMessage[M MessageType](msg M) bool { + var zero M + return any(msg) == any(zero) +} + +// TypedMessageVariant represents a message output from an agent event. +// It carries either a complete message or a streaming reader, along with +// metadata describing the event's origin. +// +// Role and ToolName are only meaningful for *schema.Message events. For +// *schema.AgenticMessage events (created via EventFromAgenticMessage), these +// fields are always zero-valued because AgenticMessage carries tool results as +// ContentBlocks within the message itself and does not support agent transfer. +// +// For *schema.Message events, Role and ToolName exist independently of the inner +// Message because in streaming mode (IsStreaming=true, Message=nil), the message +// has not materialized yet and the consumer needs metadata without consuming the stream. +type TypedMessageVariant[M MessageType] struct { + IsStreaming bool + + Message M + MessageStream *schema.StreamReader[M] + + // Role indicates the origin of this event within the agent's ReAct loop. + // Only meaningful for *schema.Message events: + // - schema.Assistant: the event carries model output (generation or stream). + // - schema.Tool: the event carries a tool execution result. + // Always zero-valued for *schema.AgenticMessage events; use AgenticRole instead. + Role schema.RoleType + + // AgenticRole indicates the role of the agentic message (assistant, user, system). + // Only meaningful for *schema.AgenticMessage events. + // In streaming mode, this is available before consuming the stream. + // Always zero-valued for *schema.Message events; use Role instead. + AgenticRole schema.AgenticRoleType + + // ToolName is the name of the tool that produced this event. + // Only meaningful for *schema.Message events: non-empty when Role == schema.Tool. + // In streaming mode, this is the only way to identify the source tool before + // the stream is consumed. + // Always empty for *schema.AgenticMessage events. + ToolName string +} + +func (mv *TypedMessageVariant[M]) GetMessage() (M, error) { + if mv.IsStreaming { + return concatMessageStream(mv.MessageStream) + } + return mv.Message, nil +} + +type MessageVariant = TypedMessageVariant[*schema.Message] + +type messageVariantSerialization struct { + IsStreaming bool + Message Message + MessageStream Message + Role schema.RoleType + ToolName string +} + +type agenticMessageVariantSerialization struct { + IsStreaming bool + Message *schema.AgenticMessage + MessageStream *schema.AgenticMessage + Role schema.RoleType + AgenticRole schema.AgenticRoleType + ToolName string +} + +func (mv *TypedMessageVariant[M]) GobEncode() ([]byte, error) { + if mvMsg, ok := any(mv).(*TypedMessageVariant[*schema.Message]); ok { + return gobEncodeMessageVariant(mvMsg) + } + if mvAgentic, ok := any(mv).(*TypedMessageVariant[*schema.AgenticMessage]); ok { + return gobEncodeAgenticMessageVariant(mvAgentic) + } + return nil, fmt.Errorf("gob encoding not supported for this message type") +} + +func (mv *TypedMessageVariant[M]) GobDecode(b []byte) error { + if mvMsg, ok := any(mv).(*TypedMessageVariant[*schema.Message]); ok { + return gobDecodeMessageVariant(mvMsg, b) + } + if mvAgentic, ok := any(mv).(*TypedMessageVariant[*schema.AgenticMessage]); ok { + return gobDecodeAgenticMessageVariant(mvAgentic, b) + } + return fmt.Errorf("gob decoding not supported for this message type") +} + +func gobEncodeMessageVariant(mv *TypedMessageVariant[*schema.Message]) ([]byte, error) { + s := &messageVariantSerialization{ + IsStreaming: mv.IsStreaming, + Message: mv.Message, + Role: mv.Role, + ToolName: mv.ToolName, + } + if mv.IsStreaming { + var messages []Message + for { + frame, err := mv.MessageStream.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("error receiving message stream: %w", err) + } + messages = append(messages, frame) + } + m, err := schema.ConcatMessages(messages) + if err != nil { + return nil, fmt.Errorf("failed to encode message: cannot concat message stream: %w", err) + } + s.MessageStream = m + } + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(s) + if err != nil { + return nil, fmt.Errorf("failed to gob encode message variant: %w", err) + } + return buf.Bytes(), nil +} + +func gobDecodeMessageVariant(mv *TypedMessageVariant[*schema.Message], b []byte) error { + s := &messageVariantSerialization{} + err := gob.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return fmt.Errorf("failed to decoding message variant: %w", err) + } + mv.IsStreaming = s.IsStreaming + mv.Message = s.Message + mv.Role = s.Role + mv.ToolName = s.ToolName + if s.MessageStream != nil { + mv.MessageStream = schema.StreamReaderFromArray([]*schema.Message{s.MessageStream}) + } + return nil +} + +func gobEncodeAgenticMessageVariant(mv *TypedMessageVariant[*schema.AgenticMessage]) ([]byte, error) { + s := &agenticMessageVariantSerialization{ + IsStreaming: mv.IsStreaming, + Message: mv.Message, + Role: mv.Role, + AgenticRole: mv.AgenticRole, + ToolName: mv.ToolName, + } + if mv.IsStreaming { + var messages []*schema.AgenticMessage + for { + frame, err := mv.MessageStream.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("error receiving agentic message stream: %w", err) + } + messages = append(messages, frame) + } + m, err := schema.ConcatAgenticMessages(messages) + if err != nil { + return nil, fmt.Errorf("failed to encode agentic message: cannot concat message stream: %w", err) + } + s.MessageStream = m + } + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(s) + if err != nil { + return nil, fmt.Errorf("failed to gob encode agentic message variant: %w", err) + } + return buf.Bytes(), nil +} + +func gobDecodeAgenticMessageVariant(mv *TypedMessageVariant[*schema.AgenticMessage], b []byte) error { + s := &agenticMessageVariantSerialization{} + err := gob.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return fmt.Errorf("failed to decode agentic message variant: %w", err) + } + mv.IsStreaming = s.IsStreaming + mv.Message = s.Message + mv.Role = s.Role + mv.AgenticRole = s.AgenticRole + mv.ToolName = s.ToolName + if s.MessageStream != nil { + mv.MessageStream = schema.StreamReaderFromArray([]*schema.AgenticMessage{s.MessageStream}) + } + return nil +} + +// typedEventFromMessage creates a TypedAgentEvent containing the given message and optional stream. +func typedEventFromMessage[M MessageType](msg M, msgStream *schema.StreamReader[M], + role schema.RoleType, toolName string) *TypedAgentEvent[M] { + return &TypedAgentEvent[M]{ + Output: &TypedAgentOutput[M]{ + MessageOutput: &TypedMessageVariant[M]{ + IsStreaming: msgStream != nil, + Message: msg, + MessageStream: msgStream, + Role: role, + ToolName: toolName, + }, + }, + } +} + +// typedModelOutputEvent creates a model-output event for the generic path. +// For *schema.Message, Role is set to schema.Assistant. +// For *schema.AgenticMessage, AgenticRole is set to schema.AgenticRoleTypeAssistant. +func typedModelOutputEvent[M MessageType](msg M, msgStream *schema.StreamReader[M]) *TypedAgentEvent[M] { + var role schema.RoleType + var agenticRole schema.AgenticRoleType + var zero M + if _, ok := any(zero).(*schema.Message); ok { + role = schema.Assistant + } else { + agenticRole = schema.AgenticRoleTypeAssistant + } + event := typedEventFromMessage(msg, msgStream, role, "") + event.Output.MessageOutput.AgenticRole = agenticRole + return event +} + +// EventFromMessage creates an AgentEvent containing the given message and optional stream. +// +// role identifies the origin of this event: +// - schema.Assistant: model output (generation or stream). +// - schema.Tool: tool execution result; toolName must be non-empty. +// +// For *schema.AgenticMessage events, use EventFromAgenticMessage instead. +func EventFromMessage(msg Message, msgStream *schema.StreamReader[Message], + role schema.RoleType, toolName string) *AgentEvent { + return typedEventFromMessage(msg, msgStream, role, toolName) +} + +// EventFromAgenticMessage creates a TypedAgentEvent for the AgenticMessage path. +// Unlike EventFromMessage, it does not require role or toolName parameters because +// AgenticMessage carries tool results as ContentBlocks within the message itself, +// and does not support agent transfer. +// +// agenticRole identifies the role of the message (e.g. schema.AgenticRoleTypeAssistant). +// In streaming mode, the role is available on the event before consuming the stream. +func EventFromAgenticMessage(msg AgenticMessage, msgStream AgenticMessageStream, agenticRole schema.AgenticRoleType) *TypedAgentEvent[AgenticMessage] { + return &TypedAgentEvent[AgenticMessage]{ + Output: &TypedAgentOutput[AgenticMessage]{ + MessageOutput: &TypedMessageVariant[AgenticMessage]{ + IsStreaming: msgStream != nil, + Message: msg, + MessageStream: msgStream, + AgenticRole: agenticRole, + }, + }, + } +} + +// TransferToAgentAction represents a transfer-to-agent action. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +type TransferToAgentAction struct { + DestAgentName string +} + +type TypedAgentOutput[M MessageType] struct { + MessageOutput *TypedMessageVariant[M] + + CustomizedOutput any +} + +type AgentOutput = TypedAgentOutput[*schema.Message] + +// NewTransferToAgentAction creates an action to transfer to the specified agent. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func NewTransferToAgentAction(destAgentName string) *AgentAction { + return &AgentAction{TransferToAgent: &TransferToAgentAction{DestAgentName: destAgentName}} +} + +// NewExitAction creates an action that signals the agent to exit. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func NewExitAction() *AgentAction { + return &AgentAction{Exit: true} +} + +// AgentAction represents actions that an agent can emit during execution. +// +// Action Scoping in Agent Tools: +// When an agent is wrapped as an agent tool (via NewAgentTool), actions emitted by the inner agent +// are scoped to the tool boundary: +// - Interrupted: Propagated via CompositeInterrupt to allow proper interrupt/resume across boundaries +// - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool; these actions only affect +// the inner agent's execution and do not propagate to the parent agent +// +// This scoping ensures that nested agents cannot unexpectedly terminate or transfer control +// of their parent agent's execution flow. +type AgentAction struct { + Exit bool + + Interrupted *InterruptInfo + + TransferToAgent *TransferToAgentAction + + BreakLoop *BreakLoopAction + + CustomizedAction any + + internalInterrupted *core.InterruptSignal +} + +// RunStep represents a step in the agent execution path. +// CheckpointSchema: persisted via serialization.RunCtx (gob). +// +// NOT RECOMMENDED: RunStep is mainly relevant for agent transfer and workflow agents, +// which have not proven to be more effective empirically. Consider using ChatModelAgent +// with AgentTool or DeepAgent instead for most multi-agent scenarios. +type RunStep struct { + agentName string +} + +func init() { + schema.RegisterName[[]RunStep]("eino_run_step_list") +} + +func (r *RunStep) String() string { + return r.agentName +} + +func (r *RunStep) Equals(r1 RunStep) bool { + return r.agentName == r1.agentName +} + +func (r *RunStep) GobEncode() ([]byte, error) { + s := &runStepSerialization{AgentName: r.agentName} + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(s) + if err != nil { + return nil, fmt.Errorf("failed to gob encode RunStep: %w", err) + } + return buf.Bytes(), nil +} + +func (r *RunStep) GobDecode(b []byte) error { + s := &runStepSerialization{} + err := gob.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return fmt.Errorf("failed to gob decode RunStep: %w", err) + } + r.agentName = s.AgentName + return nil +} + +type runStepSerialization struct { + AgentName string +} + +// TypedAgentEvent represents a single event emitted during agent execution. +// CheckpointSchema: persisted via serialization.RunCtx (gob). +type TypedAgentEvent[M MessageType] struct { + AgentName string + + // RunPath represents the execution path from root agent to the current event source. + // This field is managed entirely by the framework and cannot be set by end-users. + // + // NOT RECOMMENDED: RunPath is mainly relevant for agent transfer and workflow agents, + // which have not proven to be more effective empirically. For ChatModelAgent with + // AgentTool or DeepAgent, RunPath is trivial. Consider those patterns instead. + RunPath []RunStep + + Output *TypedAgentOutput[M] + + Action *AgentAction + + Err error +} + +// AgentEvent is the default event type using *schema.Message. +type AgentEvent = TypedAgentEvent[*schema.Message] + +type TypedAgentInput[M MessageType] struct { + Messages []M + EnableStreaming bool +} + +type AgentInput = TypedAgentInput[*schema.Message] + +// TypedAgent is the base agent interface parameterized by message type. +// +// For M = *schema.Message, the full ADK feature set is supported (multi-agent +// orchestration, cancel monitoring, retry, flowAgent). +// For M = *schema.AgenticMessage, single-agent execution works but cancel +// monitoring on the model stream and retry are not yet wired. +type TypedAgent[M MessageType] interface { + Name(ctx context.Context) string + Description(ctx context.Context) string + + // Run runs the agent. + // The returned AgentEvent within the AsyncIterator must be safe to modify. + // If the returned AgentEvent within the AsyncIterator contains MessageStream, + // the MessageStream MUST be exclusive and safe to be received directly. + // NOTE: it's recommended to use SetAutomaticClose() on the MessageStream of AgentEvents emitted by AsyncIterator, + // so that even the events are not processed, the MessageStream can still be closed. + Run(ctx context.Context, input *TypedAgentInput[M], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] +} + +//go:generate mockgen -destination ../internal/mock/adk/Agent_mock.go --package adk github.com/cloudwego/eino/adk Agent,ResumableAgent +type Agent = TypedAgent[*schema.Message] + +// OnSubAgents is the interface for agents that support sub-agent registration and transfer. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +type OnSubAgents interface { + OnSetSubAgents(ctx context.Context, subAgents []Agent) error + OnSetAsSubAgent(ctx context.Context, parent Agent) error + + OnDisallowTransferToParent(ctx context.Context) error +} + +type TypedResumableAgent[M MessageType] interface { + TypedAgent[M] + + Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] +} + +type ResumableAgent = TypedResumableAgent[*schema.Message] + +func concatMessageStream[M MessageType](stream *schema.StreamReader[M]) (M, error) { + var zero M + switch s := any(stream).(type) { + case *schema.StreamReader[*schema.Message]: + result, err := schema.ConcatMessageStream(s) + if err != nil { + return zero, err + } + return any(result).(M), nil + case *schema.StreamReader[*schema.AgenticMessage]: + defer s.Close() + var msgs []*schema.AgenticMessage + for { + frame, err := s.Recv() + if err == io.EOF { + break + } + if err != nil { + return zero, err + } + msgs = append(msgs, frame) + } + result, err := schema.ConcatAgenticMessages(msgs) + if err != nil { + return zero, err + } + return any(result).(M), nil + default: + panic("unreachable: unknown MessageType") + } +} diff --git a/adk/internal/config.go b/adk/internal/config.go new file mode 100644 index 0000000..9232d87 --- /dev/null +++ b/adk/internal/config.go @@ -0,0 +1,76 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package internal provides adk internal utils. +package internal + +import ( + "fmt" + "sync/atomic" +) + +// Language represents the language setting for the ADK built-in prompts. +type Language uint8 + +const ( + // LanguageEnglish represents English language. + LanguageEnglish Language = iota + // LanguageChinese represents Chinese language. + LanguageChinese +) + +var language atomic.Value + +// SetLanguage sets the language for the ADK built-in prompts. +// The default language is English if not explicitly set. +func SetLanguage(lang Language) error { + if lang != LanguageEnglish && + lang != LanguageChinese { + return fmt.Errorf("invalid language: %v", lang) + } + language.Store(lang) + return nil +} + +// GetLanguage returns the current language setting for the ADK built-in prompts. +// Returns LanguageEnglish if no language has been set. +func getLanguage() Language { + if l, ok := language.Load().(Language); ok { + return l + } + return LanguageEnglish +} + +// I18nPrompts holds prompt strings for different languages. +type I18nPrompts struct { + English string + Chinese string +} + +// SelectPrompt returns the appropriate prompt string based on the current language setting. +// Returns an error if the current language is not supported. +func SelectPrompt(prompts I18nPrompts) string { + lang := getLanguage() + switch lang { + case LanguageEnglish: + return prompts.English + case LanguageChinese: + return prompts.Chinese + default: + // unreachable + panic(fmt.Sprintf("invalid language: %v", lang)) + } +} diff --git a/adk/internal/message_id.go b/adk/internal/message_id.go new file mode 100644 index 0000000..0c7ada1 --- /dev/null +++ b/adk/internal/message_id.go @@ -0,0 +1,58 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import "github.com/google/uuid" + +// EinoMsgIDKey is the Extra key used to store the eino-internal message ID. +const EinoMsgIDKey = "_eino_msg_id" + +// GetMessageID returns the message ID from Extra, or "" if not set. +// Works with any map[string]any (Message.Extra or AgenticMessage.Extra). +func GetMessageID(extra map[string]any) string { + if extra == nil { + return "" + } + id, _ := extra[EinoMsgIDKey].(string) + return id +} + +// SetMessageID sets the message ID in Extra and returns the resulting map. +// +// Copy-on-write: the input map is never mutated, because a message's Extra can +// be shared across fan-out stream readers and an in-place write would race with +// concurrent reads (fatal "concurrent map read and map write"). Reassigning the +// returned map to a shared Extra field is still a field-level race that cannot +// be eliminated while Extra is exported; COW only removes the map-level panic. +func SetMessageID(extra map[string]any, id string) map[string]any { + next := make(map[string]any, len(extra)+1) + for k, v := range extra { + next[k] = v + } + next[EinoMsgIDKey] = id + return next +} + +// EnsureMessageID assigns a UUID v4 if no message ID is present. +// Idempotent: if ID already set, no-op. +// Returns the (possibly newly created) Extra map. +func EnsureMessageID(extra map[string]any) map[string]any { + if GetMessageID(extra) != "" { + return extra + } + return SetMessageID(extra, uuid.NewString()) +} diff --git a/adk/internal/message_id_test.go b/adk/internal/message_id_test.go new file mode 100644 index 0000000..f7c536f --- /dev/null +++ b/adk/internal/message_id_test.go @@ -0,0 +1,87 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMessageID(t *testing.T) { + t.Run("nil extra returns empty", func(t *testing.T) { + assert.Equal(t, "", GetMessageID(nil)) + }) + + t.Run("empty extra returns empty", func(t *testing.T) { + assert.Equal(t, "", GetMessageID(map[string]any{})) + }) + + t.Run("wrong type returns empty", func(t *testing.T) { + extra := map[string]any{EinoMsgIDKey: 123} + assert.Equal(t, "", GetMessageID(extra)) + }) + + t.Run("returns set ID", func(t *testing.T) { + extra := map[string]any{EinoMsgIDKey: "test-id-123"} + assert.Equal(t, "test-id-123", GetMessageID(extra)) + }) +} + +func TestSetMessageID(t *testing.T) { + t.Run("nil extra creates map", func(t *testing.T) { + extra := SetMessageID(nil, "id-1") + assert.NotNil(t, extra) + assert.Equal(t, "id-1", extra[EinoMsgIDKey]) + }) + + t.Run("existing extra preserved", func(t *testing.T) { + extra := map[string]any{"other_key": "other_val"} + result := SetMessageID(extra, "id-2") + assert.Equal(t, "id-2", result[EinoMsgIDKey]) + assert.Equal(t, "other_val", result["other_key"]) + }) +} + +func TestEnsureMessageID(t *testing.T) { + t.Run("nil extra gets ID", func(t *testing.T) { + extra := EnsureMessageID(nil) + id := GetMessageID(extra) + assert.NotEmpty(t, id) + assert.Len(t, id, 36) // UUID v4 format: 8-4-4-4-12 = 36 chars + }) + + t.Run("idempotent - does not overwrite existing ID", func(t *testing.T) { + extra := SetMessageID(nil, "existing-id") + result := EnsureMessageID(extra) + assert.Equal(t, "existing-id", GetMessageID(result)) + }) + + t.Run("empty extra gets new ID", func(t *testing.T) { + extra := map[string]any{} + result := EnsureMessageID(extra) + id := GetMessageID(result) + assert.NotEmpty(t, id) + assert.Len(t, id, 36) + }) + + t.Run("generates unique IDs", func(t *testing.T) { + extra1 := EnsureMessageID(nil) + extra2 := EnsureMessageID(nil) + assert.NotEqual(t, GetMessageID(extra1), GetMessageID(extra2)) + }) +} diff --git a/adk/interrupt.go b/adk/interrupt.go new file mode 100644 index 0000000..3d31054 --- /dev/null +++ b/adk/interrupt.go @@ -0,0 +1,412 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + "sync" + + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/schema" +) + +// ResumeInfo holds all the information necessary to resume an interrupted agent execution. +// It is created by the framework and passed to an agent's Resume method. +type ResumeInfo struct { + // EnableStreaming indicates whether the original execution was in streaming mode. + EnableStreaming bool + + // Deprecated: use InterruptContexts from the embedded InterruptInfo for user-facing details, + // and GetInterruptState for internal state retrieval. + *InterruptInfo + + WasInterrupted bool + InterruptState any + IsResumeTarget bool + ResumeData any +} + +// InterruptInfo contains all the information about an interruption event. +// It is created by the framework when an agent returns an interrupt action. +type InterruptInfo struct { + Data any + + // InterruptContexts provides a structured, user-facing view of the interrupt chain. + // Each context represents a step in the agent hierarchy that was interrupted. + InterruptContexts []*InterruptCtx +} + +// TypedInterrupt creates a typed interrupt event that pauses execution to request external input. +// It is the generic counterpart of Interrupt; see Interrupt for full documentation. +func TypedInterrupt[M MessageType](ctx context.Context, info any) *TypedAgentEvent[M] { + var rp []RunStep + rCtx := getRunCtx(ctx) + if rCtx != nil { + rp = rCtx.RunPath + } + + is, err := core.Interrupt(ctx, info, nil, nil, + core.WithLayerPayload(rp)) + if err != nil { + return &TypedAgentEvent[M]{Err: err} + } + + contexts := core.ToInterruptContexts(is, allowedAddressSegmentTypes) + + return &TypedAgentEvent[M]{ + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + InterruptContexts: contexts, + }, + internalInterrupted: is, + }, + } +} + +// Interrupt creates a basic interrupt action. +// This is used when an agent needs to pause its execution to request external input or intervention, +// but does not need to save any internal state to be restored upon resumption. +// The `info` parameter is user-facing data that describes the reason for the interrupt. +func Interrupt(ctx context.Context, info any) *AgentEvent { + return TypedInterrupt[*schema.Message](ctx, info) +} + +// TypedStatefulInterrupt creates a typed interrupt event that also saves the agent's internal state. +// It is the generic counterpart of StatefulInterrupt; see StatefulInterrupt for full documentation. +func TypedStatefulInterrupt[M MessageType](ctx context.Context, info any, state any) *TypedAgentEvent[M] { + var rp []RunStep + rCtx := getRunCtx(ctx) + if rCtx != nil { + rp = rCtx.RunPath + } + + is, err := core.Interrupt(ctx, info, state, nil, + core.WithLayerPayload(rp)) + if err != nil { + return &TypedAgentEvent[M]{Err: err} + } + + contexts := core.ToInterruptContexts(is, allowedAddressSegmentTypes) + + return &TypedAgentEvent[M]{ + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + InterruptContexts: contexts, + }, + internalInterrupted: is, + }, + } +} + +// StatefulInterrupt creates an interrupt action that also saves the agent's internal state. +// This is used when an agent has internal state that must be restored for it to continue correctly. +// The `info` parameter is user-facing data describing the interrupt. +// The `state` parameter is the agent's internal state object, which will be serialized and stored. +func StatefulInterrupt(ctx context.Context, info any, state any) *AgentEvent { + return TypedStatefulInterrupt[*schema.Message](ctx, info, state) +} + +// TypedCompositeInterrupt creates a typed interrupt event that aggregates sub-interrupt signals. +// It is the generic counterpart of CompositeInterrupt; see CompositeInterrupt for full documentation. +func TypedCompositeInterrupt[M MessageType](ctx context.Context, info any, state any, + subInterruptSignals ...*InterruptSignal) *TypedAgentEvent[M] { + var rp []RunStep + rCtx := getRunCtx(ctx) + if rCtx != nil { + rp = rCtx.RunPath + } + + is, err := core.Interrupt(ctx, info, state, subInterruptSignals, + core.WithLayerPayload(rp)) + if err != nil { + return &TypedAgentEvent[M]{Err: err} + } + + contexts := core.ToInterruptContexts(is, allowedAddressSegmentTypes) + + return &TypedAgentEvent[M]{ + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + InterruptContexts: contexts, + }, + internalInterrupted: is, + }, + } +} + +// CompositeInterrupt creates an interrupt event that aggregates sub-interrupt signals. +func CompositeInterrupt(ctx context.Context, info any, state any, + subInterruptSignals ...*InterruptSignal) *AgentEvent { + return TypedCompositeInterrupt[*schema.Message](ctx, info, state, subInterruptSignals...) +} + +// Address represents the unique, hierarchical address of a component within an execution. +// It is a slice of AddressSegments, where each segment represents one level of nesting. +// This is a type alias for core.Address. See the core package for more details. +type Address = core.Address +type AddressSegment = core.AddressSegment +type AddressSegmentType = core.AddressSegmentType + +const ( + AddressSegmentAgent AddressSegmentType = "agent" + AddressSegmentTool AddressSegmentType = "tool" +) + +var allowedAddressSegmentTypes = []AddressSegmentType{AddressSegmentAgent, AddressSegmentTool} + +// AppendAddressSegment adds an address segment for the current execution context. +func AppendAddressSegment(ctx context.Context, segType AddressSegmentType, segID string) context.Context { + return core.AppendAddressSegment(ctx, segType, segID, "") +} + +// InterruptCtx provides a structured, user-facing view of a single point of interruption. +// It contains the ID and Address of the interrupted component, as well as user-defined info. +// This is a type alias for core.InterruptCtx. See the core package for more details. +type InterruptCtx = core.InterruptCtx +type InterruptSignal = core.InterruptSignal + +// FromInterruptContexts converts user-facing interrupt contexts to an interrupt signal. +func FromInterruptContexts(contexts []*InterruptCtx) *InterruptSignal { + return core.FromInterruptContexts(contexts) +} + +// WithCheckPointID sets the checkpoint ID used for interruption persistence. +func WithCheckPointID(id string) AgentRunOption { + return WrapImplSpecificOptFn(func(t *options) { + t.checkPointID = &id + }) +} + +func init() { + schema.RegisterName[*serialization]("_eino_adk_serialization") + schema.RegisterName[*WorkflowInterruptInfo]("_eino_adk_workflow_interrupt_info") + // Register []byte for gob: the cancel refactor routes bridge store checkpoint + // bytes ([]byte) through InterruptState.State (type any) inside the outer + // serialization struct. Gob requires concrete types behind interface fields + // to be registered. + gob.Register([]byte{}) +} + +// serialization CheckpointSchema: root checkpoint payload (gob). +// Any type tagged with `CheckpointSchema:` is persisted and must remain backward compatible. +type serialization struct { + RunCtx *runContext + // deprecated: still keep it here for backward compatibility + Info *InterruptInfo + EnableStreaming bool + InterruptID2Address map[string]Address + InterruptID2State map[string]core.InterruptState +} + +func runnerLoadCheckPointImpl(store CheckPointStore, ctx context.Context, checkpointID string) ( + context.Context, *runContext, *ResumeInfo, error) { + data, existed, err := store.Get(ctx, checkpointID) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get checkpoint from store: %w", err) + } + if !existed { + return nil, nil, nil, fmt.Errorf("checkpoint[%s] not exist", checkpointID) + } + + data = preprocessADKCheckpoint(data) + + s := &serialization{} + err = gob.NewDecoder(bytes.NewReader(data)).Decode(s) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode checkpoint: %w", err) + } + ctx = core.PopulateInterruptState(ctx, s.InterruptID2Address, s.InterruptID2State) + + return ctx, s.RunCtx, &ResumeInfo{ + EnableStreaming: s.EnableStreaming, + InterruptInfo: s.Info, + }, nil +} + +// preprocessADKCheckpoint fixes a gob incompatibility when resuming old ChatModelAgent/DeepAgents checkpoints. +// +// Background +// - ADK checkpoints are gob-encoded. +// - Some values inside checkpoints are stored as `any`, so gob includes a concrete type name +// string in the wire format and uses that name to pick the local Go type to decode into. +// +// Problem (v0.8.0-v0.8.3 checkpoints) +// - In v0.8.0-v0.8.3, *State was registered under the name "_eino_adk_react_state" AND +// implemented GobEncode/GobDecode, so the wire format for that name is "GobEncoder payload" +// (opaque bytes). +// - In v0.7.*, the same name "_eino_adk_react_state" was used but encoded as a normal struct +// (no GobEncode). Gob treats these two wire formats as incompatible. +// - Gob only allows one local Go type per name. Today we register "_eino_adk_react_state" to +// a v0.7-compatible struct decoder (stateV07). If we try to decode a v0.8.0-v0.8.3 +// checkpoint under that same name, gob fails with a "want struct; got non-struct" mismatch. +// +// Solution +// - We keep "_eino_adk_react_state" mapped to the v0.7 decoder. +// - For v0.8.0-v0.8.3 checkpoints only, we rewrite the on-wire name to a same-length alias +// "_eino_adk_state_v080_", which is registered to a GobDecoder-compatible type (stateV080). +// - The alias is the same length as the original, so we can safely replace the length-prefixed +// bytes without re-encoding the whole stream. +func preprocessADKCheckpoint(data []byte) []byte { + const ( + lenPrefixedReactStateName = "\x15" + stateGobNameV07 + lenPrefixedCompatName = "\x15" + stateGobNameV080 + lenPrefixedStateSerializationName = "\x12stateSerialization" + ) + + // the following line checks whether the checkpoint is persisted through v0.8.0-v0.8.3 + if !bytes.Contains(data, []byte(lenPrefixedReactStateName)) || !bytes.Contains(data, []byte(lenPrefixedStateSerializationName)) { + return data + } + return bytes.ReplaceAll(data, + []byte(lenPrefixedReactStateName), + []byte(lenPrefixedCompatName)) +} + +func runnerSaveCheckPointImpl( + enableStreaming bool, + store CheckPointStore, + ctx context.Context, + key string, + info *InterruptInfo, + is *core.InterruptSignal, +) error { + if store == nil { + return nil + } + + runCtx := getRunCtx(ctx) + + id2Addr, id2State := core.SignalToPersistenceMaps(is) + + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(&serialization{ + RunCtx: runCtx, + Info: info, + InterruptID2Address: id2Addr, + InterruptID2State: id2State, + EnableStreaming: enableStreaming, + }) + if err != nil { + return fmt.Errorf("failed to encode checkpoint: %w", err) + } + return store.Set(ctx, key, buf.Bytes()) +} + +const bridgeCheckpointID = "adk_react_mock_key" + +func newBridgeStore() *bridgeStore { + return &bridgeStore{data: make(map[string][]byte)} +} + +func newResumeBridgeStore(checkPointID string, data []byte) *bridgeStore { + return &bridgeStore{ + data: map[string][]byte{checkPointID: data}, + } +} + +type bridgeStore struct { + mu sync.Mutex + data map[string][]byte +} + +func (m *bridgeStore) Get(_ context.Context, key string) ([]byte, bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + if v, ok := m.data[key]; ok { + return v, true, nil + } + return nil, false, nil +} + +func (m *bridgeStore) Set(_ context.Context, key string, checkPoint []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + if m.data == nil { + m.data = make(map[string][]byte) + } + m.data[key] = checkPoint + return nil +} + +func getNextResumeAgent(ctx context.Context, _ *ResumeInfo) (string, error) { + nextAgents, err := core.GetNextResumptionPoints(ctx) + if err != nil { + return "", fmt.Errorf("failed to get next agent leading to interruption: %w", err) + } + + if len(nextAgents) == 0 { + return "", errors.New("no child agents leading to interrupted agent were found") + } + + if len(nextAgents) > 1 { + return "", errors.New("agent has multiple child agents leading to interruption, " + + "but concurrent transfer is not supported") + } + + // get the single next agent to delegate to. + var nextAgentID string + for id := range nextAgents { + nextAgentID = id + break + } + + return nextAgentID, nil +} + +func getNextResumeAgents(ctx context.Context, _ *ResumeInfo) (map[string]bool, error) { + nextAgents, err := core.GetNextResumptionPoints(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get next agents leading to interruption: %w", err) + } + + if len(nextAgents) == 0 { + return nil, errors.New("no child agents leading to interrupted agent were found") + } + + return nextAgents, nil +} + +func buildResumeInfo(ctx context.Context, nextAgentID string, info *ResumeInfo) ( + context.Context, *ResumeInfo) { + ctx = AppendAddressSegment(ctx, AddressSegmentAgent, nextAgentID) + nextResumeInfo := &ResumeInfo{ + EnableStreaming: info.EnableStreaming, + InterruptInfo: info.InterruptInfo, + } + + wasInterrupted, hasState, state := core.GetInterruptState[any](ctx) + nextResumeInfo.WasInterrupted = wasInterrupted + if hasState { + nextResumeInfo.InterruptState = state + } + + if wasInterrupted { + isResumeTarget, hasData, data := core.GetResumeContext[any](ctx) + nextResumeInfo.IsResumeTarget = isResumeTarget + if hasData { + nextResumeInfo.ResumeData = data + } + } + + ctx = updateRunPathOnly(ctx, nextAgentID) + + return ctx, nextResumeInfo +} diff --git a/adk/interrupt_test.go b/adk/interrupt_test.go new file mode 100644 index 0000000..480c0f8 --- /dev/null +++ b/adk/interrupt_test.go @@ -0,0 +1,2149 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type interruptTestToolsHandler struct { + *BaseChatModelAgentMiddleware + tools []tool.BaseTool +} + +func TestPreprocessADKCheckpoint(t *testing.T) { + t.Run("no-op when missing markers", func(t *testing.T) { + in := []byte("random") + out := preprocessADKCheckpoint(append([]byte(nil), in...)) + assert.Equal(t, in, out) + }) + + t.Run("rewrite legacy name for v0.8.0-v0.8.3", func(t *testing.T) { + const ( + lenPrefixedReactStateName = "\x15" + stateGobNameV07 + lenPrefixedCompatName = "\x15" + stateGobNameV080 + lenPrefixedStateSerializationName = "\x12stateSerialization" + ) + + in := []byte(lenPrefixedReactStateName + "xxx" + lenPrefixedStateSerializationName + "yyy") + out := preprocessADKCheckpoint(append([]byte(nil), in...)) + assert.True(t, bytes.Contains(out, []byte(lenPrefixedCompatName))) + assert.False(t, bytes.Contains(out, []byte(lenPrefixedReactStateName))) + }) +} + +func (h *interruptTestToolsHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) { + runCtx.Tools = append(runCtx.Tools, h.tools...) + return ctx, runCtx, nil +} + +func TestSaveAgentEventWrapper(t *testing.T) { + sr, sw := schema.Pipe[Message](1) + sw.Send(schema.UserMessage("test"), nil) + sw.Close() + sr = sr.Copy(2)[1] + + w := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + RunPath: []RunStep{ + { + "a1", + }, + { + "a2", + }, + }, + }, + mu: sync.Mutex{}, + concatenatedMessage: nil, + } + + _, err := getMessageFromWrappedEvent(w) + assert.NoError(t, err) + + buf, err := w.GobEncode() + assert.NoError(t, err) + assert.NoError(t, err) + + w1 := &agentEventWrapper{} + err = w1.GobDecode(buf) + assert.NoError(t, err) +} + +func TestInterruptFunctionsPopulateInterruptContextsImmediately(t *testing.T) { + ctx := context.Background() + ctx, _ = initRunCtx(ctx, "TestAgent", &AgentInput{Messages: []Message{}}) + ctx = AppendAddressSegment(ctx, AddressSegmentAgent, "TestAgent") + + t.Run("Interrupt populates InterruptContexts", func(t *testing.T) { + event := Interrupt(ctx, "test info") + assert.NotNil(t, event.Action) + assert.NotNil(t, event.Action.Interrupted) + assert.NotNil(t, event.Action.Interrupted.InterruptContexts) + assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts)) + assert.Equal(t, "test info", event.Action.Interrupted.InterruptContexts[0].Info) + assert.True(t, event.Action.Interrupted.InterruptContexts[0].IsRootCause) + assert.Equal(t, Address{ + {Type: AddressSegmentAgent, ID: "TestAgent"}, + }, event.Action.Interrupted.InterruptContexts[0].Address) + }) + + t.Run("StatefulInterrupt populates InterruptContexts", func(t *testing.T) { + event := StatefulInterrupt(ctx, "stateful info", "my state") + assert.NotNil(t, event.Action) + assert.NotNil(t, event.Action.Interrupted) + assert.NotNil(t, event.Action.Interrupted.InterruptContexts) + assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts)) + assert.Equal(t, "stateful info", event.Action.Interrupted.InterruptContexts[0].Info) + assert.True(t, event.Action.Interrupted.InterruptContexts[0].IsRootCause) + }) + + t.Run("CompositeInterrupt populates InterruptContexts with filtered parent chain", func(t *testing.T) { + subCtx := AppendAddressSegment(ctx, AddressSegmentAgent, "SubAgent") + subEvent := Interrupt(subCtx, "sub info") + event := CompositeInterrupt(ctx, "composite info", "composite state", subEvent.Action.internalInterrupted) + assert.NotNil(t, event.Action) + assert.NotNil(t, event.Action.Interrupted) + assert.NotNil(t, event.Action.Interrupted.InterruptContexts) + assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts)) + + rootCause := event.Action.Interrupted.InterruptContexts[0] + assert.Equal(t, "sub info", rootCause.Info) + assert.True(t, rootCause.IsRootCause) + assert.Equal(t, Address{ + {Type: AddressSegmentAgent, ID: "TestAgent"}, + {Type: AddressSegmentAgent, ID: "SubAgent"}, + }, rootCause.Address) + + assert.NotNil(t, rootCause.Parent, "Parent should not be nil for composite interrupt") + assert.Equal(t, "composite info", rootCause.Parent.Info) + assert.Equal(t, Address{ + {Type: AddressSegmentAgent, ID: "TestAgent"}, + }, rootCause.Parent.Address) + }) + + t.Run("Address only contains agent/tool segments", func(t *testing.T) { + event := Interrupt(ctx, "test info") + addr := event.Action.Interrupted.InterruptContexts[0].Address + for _, seg := range addr { + assert.True(t, seg.Type == AddressSegmentAgent || seg.Type == AddressSegmentTool, + "Address should only contain agent/tool segments, got: %s", seg.Type) + } + }) +} + +func TestSimpleInterrupt(t *testing.T) { + data := "hello world" + agent := &myAgent{ + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + Message: nil, + MessageStream: schema.StreamReaderFromArray([]Message{ + schema.UserMessage("hello "), + schema.UserMessage("world"), + }), + }, + }, + }) + intEvent := Interrupt(ctx, data) + intEvent.Action.Interrupted.Data = data + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.WasInterrupted) + assert.Nil(t, info.InterruptState) + assert.True(t, info.EnableStreaming) + assert.Equal(t, data, info.Data) + + assert.True(t, info.IsResumeTarget) + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } + store := newMyStore() + ctx := context.Background() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + CheckPointStore: store, + }) + iter := runner.Query(ctx, "hello world", WithCheckPointID("1")) + _, ok := iter.Next() + assert.True(t, ok) + interruptEvent, ok := iter.Next() + assert.True(t, ok) + assert.Equal(t, data, interruptEvent.Action.Interrupted.Data) + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts[0].ID) + assert.True(t, interruptEvent.Action.Interrupted.InterruptContexts[0].IsRootCause) + assert.Equal(t, data, interruptEvent.Action.Interrupted.InterruptContexts[0].Info) + assert.Equal(t, Address{{Type: AddressSegmentAgent, ID: "myAgent"}}, + interruptEvent.Action.Interrupted.InterruptContexts[0].Address) + _, ok = iter.Next() + assert.False(t, ok) + + iter, err := runner.ResumeWithParams(ctx, "1", &ResumeParams{ + Targets: map[string]any{ + interruptEvent.Action.Interrupted.InterruptContexts[0].ID: nil, + }, + }) + assert.NoError(t, err) + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestMultiAgentInterrupt(t *testing.T) { + ctx := context.Background() + sa1 := &myAgent{ + name: "sa1", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "sa1", + Action: &AgentAction{ + TransferToAgent: &TransferToAgentAction{ + DestAgentName: "sa2", + }, + }, + }) + generator.Close() + return iter + }, + } + sa2 := &myAgent{ + name: "sa2", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + intEvent := StatefulInterrupt(ctx, "hello world", "temp state") + intEvent.Action.Interrupted.Data = "hello world" + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.NotNil(t, info) + assert.Equal(t, info.Data, "hello world") + + assert.True(t, info.WasInterrupted) + assert.NotNil(t, info.InterruptState) + assert.Equal(t, "temp state", info.InterruptState) + + assert.True(t, info.IsResumeTarget) + assert.NotNil(t, info.ResumeData) + assert.Equal(t, "resume data", info.ResumeData) + + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "sa2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{Message: schema.UserMessage(info.ResumeData.(string))}, + }, + }) + generator.Close() + return iter + }, + } + a, err := SetSubAgents(ctx, sa1, []Agent{sa2}) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + EnableStreaming: false, + CheckPointStore: newMyStore(), + }) + iter := runner.Query(ctx, "", WithCheckPointID("1")) + event, ok := iter.Next() + assert.True(t, ok) + assert.NotNil(t, event.Action.TransferToAgent) + event, ok = iter.Next() + assert.True(t, ok) + assert.NotNil(t, event.Action.Interrupted) + assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts)) + assert.Equal(t, "hello world", event.Action.Interrupted.InterruptContexts[0].Info) + assert.True(t, event.Action.Interrupted.InterruptContexts[0].IsRootCause) + assert.Equal(t, Address{ + {Type: AddressSegmentAgent, ID: "sa1"}, + {Type: AddressSegmentAgent, ID: "sa2"}, + }, event.Action.Interrupted.InterruptContexts[0].Address) + assert.NotEmpty(t, event.Action.Interrupted.InterruptContexts[0].ID) + + interruptID := event.Action.Interrupted.InterruptContexts[0].ID + _, ok = iter.Next() + assert.False(t, ok) + + iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{ + Targets: map[string]any{ + interruptID: "resume data", + }, + }) + assert.NoError(t, err) + event, ok = iter.Next() + assert.True(t, ok) + assert.Equal(t, event.Output.MessageOutput.Message.Content, "resume data") + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestWorkflowInterrupt(t *testing.T) { + ctx := context.Background() + sa1 := &myAgent{ + name: "sa1", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + + intEvent := Interrupt(ctx, "sa1 interrupt data") + intEvent.Action.Interrupted.Data = "sa1 interrupt data" + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.Equal(t, info.InterruptInfo.Data, "sa1 interrupt data") + assert.True(t, info.WasInterrupted) + assert.Nil(t, info.InterruptState) + assert.True(t, info.IsResumeTarget) + assert.Equal(t, "resume sa1", info.ResumeData) + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } // interrupt once + sa2 := &myAgent{ + name: "sa2", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + + intEvent := StatefulInterrupt(ctx, "sa2 interrupt data", "sa2 interrupt") + intEvent.Action.Interrupted.Data = "sa2 interrupt data" + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.Equal(t, info.InterruptInfo.Data, "sa2 interrupt data") + assert.True(t, info.WasInterrupted) + assert.NotNil(t, info.InterruptState) + assert.Equal(t, "sa2 interrupt", info.InterruptState) + + assert.True(t, info.IsResumeTarget) + assert.NotNil(t, info.ResumeData) + assert.Equal(t, "resume sa2", info.ResumeData) + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } // interrupt once + sa3 := &myAgent{ + name: "sa3", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "sa3", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa3 completed"), + }, + }, + }) + generator.Close() + return iter + }, + } // won't interrupt + sa4 := &myAgent{ + name: "sa4", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "sa4", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa4 completed"), + }, + }, + }) + generator.Close() + return iter + }, + } // won't interrupt + + firstInterruptEvent := &AgentEvent{ + AgentName: "sa1", + RunPath: []RunStep{{"sequential"}, {"sa1"}}, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: &WorkflowInterruptInfo{ + OrigInput: &AgentInput{ + Messages: []Message{schema.UserMessage("hello world")}, + }, + SequentialInterruptIndex: 0, + SequentialInterruptInfo: &InterruptInfo{ + Data: "sa1 interrupt data", + }, + LoopIterations: 0, + }, + InterruptContexts: []*InterruptCtx{ + { + ID: "agent:sequential;agent:sa1", + Info: "sa1 interrupt data", + Address: Address{ + { + ID: "sequential", + Type: AddressSegmentAgent, + }, + { + ID: "sa1", + Type: AddressSegmentAgent, + }, + }, + IsRootCause: true, + Parent: &InterruptCtx{ + ID: "agent:sequential", + Info: "Sequential workflow interrupted", + Address: Address{ + { + ID: "sequential", + Type: AddressSegmentAgent, + }, + }, + }, + }, + }, + }, + }, + } + _ = firstInterruptEvent + secondInterruptEvent := &AgentEvent{ + AgentName: "sa2", + RunPath: []RunStep{{"sequential"}, {"sa1"}, {"sa2"}}, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: &WorkflowInterruptInfo{ + OrigInput: &AgentInput{ + Messages: []Message{schema.UserMessage("hello world")}, + }, + SequentialInterruptIndex: 1, + SequentialInterruptInfo: &InterruptInfo{ + Data: "sa2 interrupt data", + }, + }, + InterruptContexts: []*InterruptCtx{ + { + ID: "agent:sequential;agent:sa1;agent:sa2", + Info: "sa2 interrupt data", + Address: Address{ + { + ID: "sequential", + Type: AddressSegmentAgent, + }, + { + ID: "sa2", + Type: AddressSegmentAgent, + }, + }, + IsRootCause: true, + Parent: &InterruptCtx{ + ID: "agent:sequential", + Info: "Sequential workflow interrupted", + Address: Address{ + { + ID: "sequential", + Type: AddressSegmentAgent, + }, + }, + }, + }, + }, + }, + }, + } + _ = secondInterruptEvent + messageEvents := []*AgentEvent{ + { + AgentName: "sa3", + RunPath: []RunStep{{"sequential"}, {"sa1"}, {"sa2"}, {"sa3"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa3 completed"), + }, + }, + }, + { + AgentName: "sa4", + RunPath: []RunStep{{"sequential"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa4 completed"), + }, + }, + }, + } + _ = messageEvents + + t.Run("test sequential workflow agent", func(t *testing.T) { + + // sequential + a, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "sequential", + Description: "sequential agent", + SubAgents: []Agent{sa1, sa2, sa3, sa4}, + }) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + CheckPointStore: newMyStore(), + }) + var events []*AgentEvent + iter := runner.Query(ctx, "hello world", WithCheckPointID("sequential-1")) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 1, len(events)) + assert.Equal(t, firstInterruptEvent.AgentName, events[0].AgentName) + assert.Equal(t, firstInterruptEvent.RunPath, events[0].RunPath) + assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(firstInterruptEvent.Action.Interrupted.InterruptContexts[0])) + interruptID1 := events[0].Action.Interrupted.InterruptContexts[0].ID + events = []*AgentEvent{} + + // Resume after sa1 interrupt + iter, err = runner.ResumeWithParams(ctx, "sequential-1", &ResumeParams{ + Targets: map[string]any{ + interruptID1: "resume sa1", + }, + }) + assert.NoError(t, err) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 1, len(events)) + assert.Equal(t, secondInterruptEvent.AgentName, events[0].AgentName) + assert.Equal(t, secondInterruptEvent.RunPath, events[0].RunPath) + assert.True(t, events[0].Action.Interrupted.InterruptContexts[0]. + EqualsWithoutID(secondInterruptEvent.Action.Interrupted.InterruptContexts[0])) + interruptID2 := events[0].Action.Interrupted.InterruptContexts[0].ID + events = []*AgentEvent{} + + // Resume after sa2 interrupt + iter, err = runner.ResumeWithParams(ctx, "sequential-1", &ResumeParams{ + Targets: map[string]any{ + interruptID2: "resume sa2", + }, + }) + assert.NoError(t, err) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 2, len(events)) + assert.Equal(t, messageEvents, events) + }) + + t.Run("test loop workflow agent", func(t *testing.T) { + // loop + a, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "loop", + SubAgents: []Agent{sa1, sa2, sa3, sa4}, + MaxIterations: 2, + }) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + CheckPointStore: newMyStore(), + }) + var events []*AgentEvent + iter := runner.Query(ctx, "hello world", WithCheckPointID("loop-1")) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + loopFirstInterruptEvent := &AgentEvent{ + AgentName: "sa1", + RunPath: []RunStep{{"loop"}, {"sa1"}}, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: &WorkflowInterruptInfo{ + OrigInput: &AgentInput{ + Messages: []Message{schema.UserMessage("hello world")}, + }, + SequentialInterruptIndex: 0, + SequentialInterruptInfo: &InterruptInfo{ + Data: "sa1 interrupt data", + }, + LoopIterations: 0, + }, + InterruptContexts: []*InterruptCtx{ + { + ID: "agent:loop;agent:sa1", + Info: "sa1 interrupt data", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + { + ID: "sa1", + Type: AddressSegmentAgent, + }, + }, + IsRootCause: true, + Parent: &InterruptCtx{ + ID: "agent:loop", + Info: "Loop workflow interrupted", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + }, + }, + }, + }, + }, + }, + } + assert.Equal(t, 1, len(events)) + assert.Equal(t, loopFirstInterruptEvent.AgentName, events[0].AgentName) + assert.Equal(t, loopFirstInterruptEvent.RunPath, events[0].RunPath) + assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopFirstInterruptEvent.Action.Interrupted.InterruptContexts[0])) + loopInterruptID1 := events[0].Action.Interrupted.InterruptContexts[0].ID + events = []*AgentEvent{} + + // Resume after sa1 interrupt + iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{ + Targets: map[string]any{ + loopInterruptID1: "resume sa1", + }, + }) + assert.NoError(t, err) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + loopSecondInterruptEvent := &AgentEvent{ + AgentName: "sa2", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}}, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: &WorkflowInterruptInfo{ + OrigInput: &AgentInput{ + Messages: []Message{schema.UserMessage("hello world")}, + }, + SequentialInterruptIndex: 1, + SequentialInterruptInfo: &InterruptInfo{ + Data: "sa2 interrupt data", + }, + LoopIterations: 0, + }, + InterruptContexts: []*InterruptCtx{ + { + ID: "agent:loop;agent:sa1;agent:sa2", + Info: "sa2 interrupt data", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + { + ID: "sa2", + Type: AddressSegmentAgent, + }, + }, + IsRootCause: true, + Parent: &InterruptCtx{ + ID: "agent:loop", + Info: "Loop workflow interrupted", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + }, + }, + }, + }, + }, + }, + } + assert.Equal(t, 1, len(events)) + assert.Equal(t, loopSecondInterruptEvent.AgentName, events[0].AgentName) + assert.Equal(t, loopSecondInterruptEvent.RunPath, events[0].RunPath) + assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopSecondInterruptEvent.Action.Interrupted.InterruptContexts[0])) + loopInterruptID2 := events[0].Action.Interrupted.InterruptContexts[0].ID + events = []*AgentEvent{} + + // Resume after sa2 interrupt + iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{ + Targets: map[string]any{ + loopInterruptID2: "resume sa2", + }, + }) + assert.NoError(t, err) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + loopThirdInterruptEvent := &AgentEvent{ + AgentName: "sa1", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}}, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: &WorkflowInterruptInfo{ + OrigInput: &AgentInput{ + Messages: []Message{schema.UserMessage("hello world")}, + }, + SequentialInterruptIndex: 0, + SequentialInterruptInfo: &InterruptInfo{ + Data: "sa1 interrupt data", + }, + LoopIterations: 1, + }, + InterruptContexts: []*InterruptCtx{ + { + ID: "agent:loop;agent:sa1;agent:sa2;agent:sa3;agent:sa4;agent:sa1", + Info: "sa1 interrupt data", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + { + ID: "sa1", + Type: AddressSegmentAgent, + }, + }, + IsRootCause: true, + Parent: &InterruptCtx{ + ID: "agent:loop", + Info: "Loop workflow interrupted", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + }, + }, + }, + }, + }, + }, + } + + loopFourthInterruptEvent := &AgentEvent{ + AgentName: "sa2", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}, {"sa2"}}, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: &WorkflowInterruptInfo{ + OrigInput: &AgentInput{ + Messages: []Message{schema.UserMessage("hello world")}, + }, + SequentialInterruptIndex: 1, + SequentialInterruptInfo: &InterruptInfo{ + Data: "sa2 interrupt data", + }, + LoopIterations: 1, + }, + InterruptContexts: []*InterruptCtx{ + { + ID: "agent:loop;agent:sa1;agent:sa2;agent:sa3;agent:sa4;agent:sa1;agent:sa2", + Info: "sa2 interrupt data", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + { + ID: "sa2", + Type: AddressSegmentAgent, + }, + }, + IsRootCause: true, + Parent: &InterruptCtx{ + ID: "agent:loop", + Info: "Loop workflow interrupted", + Address: Address{ + { + ID: "loop", + Type: AddressSegmentAgent, + }, + }, + }, + }, + }, + }, + }, + } + + loopMessageEvents := []*AgentEvent{ + { + AgentName: "sa3", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa3 completed"), + }, + }, + }, + { + AgentName: "sa4", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa4 completed"), + }, + }, + }, + loopThirdInterruptEvent, + } + assert.Equal(t, 3, len(events)) + // Check the first two message events + assert.Equal(t, loopMessageEvents[0].AgentName, events[0].AgentName) + assert.Equal(t, loopMessageEvents[0].RunPath, events[0].RunPath) + assert.Equal(t, loopMessageEvents[0].Output.MessageOutput.Message.Content, events[0].Output.MessageOutput.Message.Content) + + assert.Equal(t, loopMessageEvents[1].AgentName, events[1].AgentName) + assert.Equal(t, loopMessageEvents[1].RunPath, events[1].RunPath) + assert.Equal(t, loopMessageEvents[1].Output.MessageOutput.Message.Content, events[1].Output.MessageOutput.Message.Content) + + // Check the third interrupt event using EqualsWithoutID + assert.Equal(t, loopMessageEvents[2].AgentName, events[2].AgentName) + assert.Equal(t, loopMessageEvents[2].RunPath, events[2].RunPath) + assert.True(t, events[2].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopMessageEvents[2].Action.Interrupted.InterruptContexts[0])) + loopInterruptID3 := events[2].Action.Interrupted.InterruptContexts[0].ID + events = []*AgentEvent{} + + // Resume after third interrupt + iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{ + Targets: map[string]any{ + loopInterruptID3: "resume sa1", + }, + }) + assert.NoError(t, err) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + assert.Equal(t, 1, len(events)) + assert.Equal(t, loopFourthInterruptEvent.AgentName, events[0].AgentName) + assert.Equal(t, loopFourthInterruptEvent.RunPath, events[0].RunPath) + assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopFourthInterruptEvent.Action.Interrupted.InterruptContexts[0])) + loopInterruptID4 := events[0].Action.Interrupted.InterruptContexts[0].ID + events = []*AgentEvent{} + + // Resume after fourth interrupt + iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{ + Targets: map[string]any{ + loopInterruptID4: "resume sa2", + }, + }) + assert.NoError(t, err) + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + loopFinalMessageEvents := []*AgentEvent{ + { + AgentName: "sa3", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}, {"sa2"}, {"sa3"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa3 completed"), + }, + }, + }, + { + AgentName: "sa4", + RunPath: []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa4 completed"), + }, + }, + }, + } + assert.Equal(t, 2, len(events)) + assert.Equal(t, loopFinalMessageEvents, events) + }) + + t.Run("test parallel workflow agent", func(t *testing.T) { + // parallel + a, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "parallel agent", + SubAgents: []Agent{sa1, sa2, sa3, sa4}, + }) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + CheckPointStore: newMyStore(), + }) + iter := runner.Query(ctx, "hello world", WithCheckPointID("1")) + var ( + events []*AgentEvent + interruptEvent *AgentEvent + ) + + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + continue + } + events = append(events, event) + } + assert.Equal(t, 2, len(events)) + + // Debug: Print actual events to see what we're getting + for i, event := range events { + t.Logf("Event %d: AgentName=%s, RunPath=%v, Output=%v", i, event.AgentName, event.RunPath, event.Output) + } + + // Define parallel message events separately + parallelMessageEvents := []*AgentEvent{ + { + AgentName: "sa4", + RunPath: []RunStep{{"parallel agent"}, {"sa4"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa4 completed"), + }, + }, + }, + { + AgentName: "sa3", + RunPath: []RunStep{{"parallel agent"}, {"sa3"}}, + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa3 completed"), + }, + }, + }, + } + + assert.Contains(t, events, parallelMessageEvents[0]) + assert.Contains(t, events, parallelMessageEvents[1]) + + assert.NotNil(t, interruptEvent) + assert.Equal(t, "parallel agent", interruptEvent.AgentName) + assert.Equal(t, []RunStep{{"parallel agent"}}, interruptEvent.RunPath) + assert.NotNil(t, interruptEvent.Action.Interrupted) + wii, ok := interruptEvent.Action.Interrupted.Data.(*WorkflowInterruptInfo) + assert.True(t, ok) + assert.Equal(t, 2, len(wii.ParallelInterruptInfo)) + + var sa1Found, sa2Found bool + for _, info := range wii.ParallelInterruptInfo { + switch info.Data { + case "sa1 interrupt data": + sa1Found = true + case "sa2 interrupt data": + sa2Found = true + } + } + assert.True(t, sa1Found) + assert.True(t, sa2Found) + + var sa1InfoFound, sa2InfoFound bool + for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + switch ctx.Info { + case "sa1 interrupt data": + sa1InfoFound = true + case "sa2 interrupt data": + sa2InfoFound = true + } + } + + assert.Equal(t, 2, len(interruptEvent.Action.Interrupted.InterruptContexts)) + assert.True(t, sa1InfoFound) + assert.True(t, sa2InfoFound) + + var parallelInterruptID1, parallelInterruptID2 string + for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + switch ctx.Info { + case "sa1 interrupt data": + parallelInterruptID1 = ctx.ID + case "sa2 interrupt data": + parallelInterruptID2 = ctx.ID + } + } + assert.NotEmpty(t, parallelInterruptID1) + assert.NotEmpty(t, parallelInterruptID2) + + iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{ + Targets: map[string]any{ + parallelInterruptID1: "resume sa1", + parallelInterruptID2: "resume sa2", + }, + }) + assert.NoError(t, err) + _, ok = iter.Next() + assert.False(t, ok) + }) +} + +func TestChatModelInterrupt(t *testing.T) { + ctx := context.Background() + a, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "name", + Description: "description", + Instruction: "instruction", + Model: &myModel{ + validator: func(i int, messages []*schema.Message) bool { + if i > 0 && (len(messages) != 4 || messages[2].Content != "new user message") { + return false + } + return true + }, + messages: []*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "1", + Function: schema.FunctionCall{ + Name: "tool1", + Arguments: "arguments", + }, + }, + }), + schema.AssistantMessage("completed", nil), + }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&myTool1{}}, + }, + }, + }) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + CheckPointStore: newMyStore(), + }) + iter := runner.Query(ctx, "hello world", WithCheckPointID("1")) + _, ok := iter.Next() + assert.True(t, ok) + event, ok := iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.NotNil(t, event.Action.Interrupted) + assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts)) + assert.Equal(t, Address{ + {Type: AddressSegmentAgent, ID: "name"}, + {Type: AddressSegmentTool, ID: "tool1", SubID: "1"}, + }, event.Action.Interrupted.InterruptContexts[0].Address) + + var ( + chatModelAgentID string + toolID string + ) + + intCtx := event.Action.Interrupted.InterruptContexts[0] + for intCtx != nil { + if intCtx.Address[len(intCtx.Address)-1].Type == AddressSegmentTool { + toolID = intCtx.ID + } else if intCtx.Address[len(intCtx.Address)-1].Type == AddressSegmentAgent { + chatModelAgentID = intCtx.ID + } + intCtx = intCtx.Parent + } + + _, ok = iter.Next() + assert.False(t, ok) + + iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{ + Targets: map[string]any{ + chatModelAgentID: &ChatModelAgentResumeData{ + HistoryModifier: func(ctx context.Context, history []Message) []Message { + history[2].Content = "new user message" + return history + }, + }, + toolID: "tool resume result", + }, + }) + assert.NoError(t, err) + event, ok = iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.Equal(t, event.Output.MessageOutput.Message.Content, "tool resume result") + event, ok = iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.Equal(t, event.Output.MessageOutput.Message.Content, "completed") +} + +func TestChatModelAgentToolInterrupt(t *testing.T) { + sa := &myAgent{ + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + intAct := Interrupt(ctx, "hello world") + intAct.Action.Interrupted.Data = "hello world" + generator.Send(intAct) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.NotNil(t, info) + assert.False(t, info.EnableStreaming) + + if !info.IsResumeTarget { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + intAct := Interrupt(ctx, "interrupt again") + intAct.Action.Interrupted.Data = "interrupt again" + generator.Send(intAct) + generator.Close() + return iter + } + + assert.NotNil(t, info.ResumeData) + assert.Equal(t, "resume sa", info.ResumeData) + + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{Output: &AgentOutput{MessageOutput: &MessageVariant{Message: schema.UserMessage(fmt.Sprintf("my agent completed with data %s", info.ResumeData))}}}) + generator.Close() + return iter + }, + } + ctx := context.Background() + a, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "name", + Description: "description", + Instruction: "instruction", + Model: &myModel{ + messages: []*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "1", + Function: schema.FunctionCall{ + Name: "myAgent", + Arguments: "{\"request\":\"123\"}", + }, + }, + }), + schema.AssistantMessage("completed", nil), + }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{NewAgentTool(ctx, sa)}, + }, + }, + }) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + CheckPointStore: newMyStore(), + }) + + iter := runner.Query(ctx, "hello world", WithCheckPointID("1")) + _, ok := iter.Next() + assert.True(t, ok) + event, ok := iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.NotNil(t, event.Action.Interrupted) + _, ok = iter.Next() + assert.False(t, ok) + + iter, err = runner.Resume(ctx, "1") + assert.NoError(t, err) + event, ok = iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.NotNil(t, event.Action.Interrupted) + assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts)) + for _, ctx := range event.Action.Interrupted.InterruptContexts { + if ctx.IsRootCause { + assert.Equal(t, Address{ + {Type: AddressSegmentAgent, ID: "name"}, + {Type: AddressSegmentTool, ID: "myAgent", SubID: "1"}, + {Type: AddressSegmentAgent, ID: "myAgent"}, + }, ctx.Address) + assert.Equal(t, "interrupt again", ctx.Info) + } + } + + var toolInterruptID string + for _, ctx := range event.Action.Interrupted.InterruptContexts { + if ctx.IsRootCause { + toolInterruptID = ctx.ID + break + } + } + assert.NotEmpty(t, toolInterruptID) + + _, ok = iter.Next() + assert.False(t, ok) + + iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{ + Targets: map[string]any{ + toolInterruptID: "resume sa", + }, + }) + assert.NoError(t, err) + event, ok = iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.Equal(t, event.Output.MessageOutput.Message.Content, "my agent completed with data resume sa") + event, ok = iter.Next() + assert.True(t, ok) + assert.NoError(t, event.Err) + assert.Equal(t, event.Output.MessageOutput.Message.Content, "completed") + _, ok = iter.Next() + assert.False(t, ok) +} + +func newMyStore() *myStore { + return &myStore{ + m: map[string][]byte{}, + } +} + +type myStore struct { + m map[string][]byte +} + +func (m *myStore) Set(_ context.Context, key string, value []byte) error { + m.m[key] = value + return nil +} + +func (m *myStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := m.m[key] + return v, ok, nil +} + +type myAgentOptions struct { + value string +} + +func withValue(value string) AgentRunOption { + return WrapImplSpecificOptFn(func(t *myAgentOptions) { + t.value = value + }) +} + +type myAgent struct { + name string + runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] + resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] +} + +func (m *myAgent) Name(_ context.Context) string { + if len(m.name) > 0 { + return m.name + } + return "myAgent" +} + +func (m *myAgent) Description(_ context.Context) string { + return "myAgent description" +} + +func (m *myAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return m.runFn(ctx, input, options...) +} + +func (m *myAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + return m.resumeFn(ctx, info, opts...) +} + +type myModel struct { + times int + messages []*schema.Message + validator func(int, []*schema.Message) bool +} + +func (m *myModel) Generate(_ context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) { + if m.validator != nil && !m.validator(m.times, input) { + return nil, errors.New("invalid input") + } + if m.times >= len(m.messages) { + return nil, errors.New("exceeded max number of messages") + } + t := m.times + m.times++ + return m.messages[t], nil +} + +func (m *myModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + panic("implement me") +} + +func (m *myModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +type myTool1 struct{} + +func (m *myTool1) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "tool1", + Desc: "desc", + }, nil +} + +func (m *myTool1) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + if wasInterrupted, _, _ := tool.GetInterruptState[any](ctx); !wasInterrupted { + return "", tool.Interrupt(ctx, nil) + } + + if isResumeFlow, hasResumeData, data := tool.GetResumeContext[string](ctx); !isResumeFlow { + return "", tool.Interrupt(ctx, nil) + } else if hasResumeData { + return data, nil + } + + return "result", nil +} + +func TestCyclicalAgentInterrupt(t *testing.T) { + ctx := context.Background() + + var agentA, agentB, agentC Agent + + // agentC interrupts + agentC = &myAgent{ + name: "C", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + intAct := Interrupt(ctx, "interrupt from C") + generator.Send(intAct) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.IsResumeTarget) + assert.NotNil(t, info.ResumeData) + assert.Equal(t, "resume C", info.ResumeData) + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "C", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{Message: schema.UserMessage("C completed")}, + }, + }) + generator.Close() + return iter + }, + } + + // agentB transfers back to its parent A + agentB = &myAgent{ + name: "B", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "B", + Action: &AgentAction{ + TransferToAgent: &TransferToAgentAction{ + DestAgentName: "A", // Transfer back to parent + }, + }, + }) + generator.Close() + return iter + }, + } + + // agentA is the parent, orchestrating the A->B->A->C flow + agentA = &myAgent{ + name: "A", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + runCtx := getRunCtx(ctx) + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + + // If the last agent was B, we are in the A->B->A path, so transfer to C. + // Otherwise, it's the first run, transfer to B. + dest := "B" + if len(runCtx.RunPath) > 1 && runCtx.RunPath[len(runCtx.RunPath)-2].agentName == "B" { + dest = "C" + } + + generator.Send(&AgentEvent{ + AgentName: "A", + Action: &AgentAction{ + TransferToAgent: &TransferToAgentAction{ + DestAgentName: dest, + }, + }, + }) + generator.Close() + return iter + }, + } + + // Set up the hierarchy: A is parent of B and C. + agentA, err := SetSubAgents(ctx, agentA, []Agent{agentB, agentC}) + assert.NoError(t, err) + + // Run the test + runner := NewRunner(ctx, RunnerConfig{ + Agent: agentA, + CheckPointStore: newMyStore(), + }) + iter := runner.Query(ctx, "start", WithCheckPointID("cyclical-1")) + + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + // We expect 3 transfer events (A->B, B->A, A->C) and 1 interrupt event from C. + assert.Equal(t, 4, len(events)) + + interruptEvent := events[3] + assert.NotNil(t, interruptEvent.Action.Interrupted) + assert.Equal(t, "C", interruptEvent.AgentName) + + // Check the interrupt context + assert.Equal(t, 1, len(interruptEvent.Action.Interrupted.InterruptContexts)) + interruptCtx := interruptEvent.Action.Interrupted.InterruptContexts[0] + assert.True(t, interruptCtx.IsRootCause) + assert.Equal(t, "interrupt from C", interruptCtx.Info) + + expectedAddr := Address{ + {Type: AddressSegmentAgent, ID: "A"}, + {Type: AddressSegmentAgent, ID: "B"}, + {Type: AddressSegmentAgent, ID: "A"}, + {Type: AddressSegmentAgent, ID: "C"}, + } + assert.Equal(t, expectedAddr, interruptCtx.Address) + assert.NotEmpty(t, interruptCtx.ID) + + // Resume the execution + iter, err = runner.ResumeWithParams(ctx, "cyclical-1", &ResumeParams{ + Targets: map[string]any{ + interruptCtx.ID: "resume C", + }, + }) + assert.NoError(t, err) + + events = []*AgentEvent{} + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + + // We expect one output event from C + assert.Equal(t, 1, len(events)) + assert.Equal(t, "C completed", events[0].Output.MessageOutput.Message.Content) +} + +// myStatefulTool is a tool that can interrupt and has internal state to track invocations. + +type myStatefulTool struct { + name string + t *testing.T +} + +func (m *myStatefulTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: m.name, + Desc: "desc", + }, nil +} + +type myStatefulToolState struct { + InterruptCount int +} + +func init() { + schema.Register[myStatefulToolState]() +} + +func (m *myStatefulTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + wasInterrupted, hasState, state := tool.GetInterruptState[myStatefulToolState](ctx) + if !wasInterrupted { + return "", tool.StatefulInterrupt(ctx, fmt.Sprintf("interrupt from %s", m.name), myStatefulToolState{InterruptCount: 1}) + } + + isResumeFlow, hasResumeData, data := tool.GetResumeContext[string](ctx) + if !isResumeFlow || !hasResumeData { + assert.True(m.t, hasState, "tool %s should have interrupt state on resume", m.name) + return "", tool.StatefulInterrupt(ctx, fmt.Sprintf("interrupt from %s", m.name), myStatefulToolState{InterruptCount: state.InterruptCount + 1}) + } + + return data, nil +} + +func TestChatModelParallelToolInterruptAndResume(t *testing.T) { + ctx := context.Background() + + toolA := &myStatefulTool{name: "toolA", t: t} + toolB := &myStatefulTool{name: "toolB", t: t} + + chatModel, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "ParallelToolAgent", + Description: "An agent that uses parallel tools", + Model: &myModel{ + messages: []*schema.Message{ + // 1. First model response: call toolA and toolB in parallel + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "1", Function: schema.FunctionCall{Name: "toolA", Arguments: "{}"}}, + {ID: "2", Function: schema.FunctionCall{Name: "toolB", Arguments: "{}"}}, + }), + // 2. Second model response (after tools are resumed): call them again to check state + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "3", Function: schema.FunctionCall{Name: "toolA", Arguments: "{}"}}, + {ID: "4", Function: schema.FunctionCall{Name: "toolB", Arguments: "{}"}}, + }), + // 3. Final completion + schema.AssistantMessage("all done", nil), + }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{toolA, toolB}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: chatModel, + CheckPointStore: newMyStore(), + }) + + // 1. Initial query -> parallel interrupt from toolA and toolB + iter := runner.Query(ctx, "start", WithCheckPointID("parallel-tool-test-1")) + normalEvents, interruptEvent := consumeUntilInterrupt(iter) + + assert.Equal(t, 1, len(normalEvents)) + assert.NotNil(t, interruptEvent) + assert.Equal(t, 2, len(interruptEvent.Action.Interrupted.InterruptContexts), + "should have 2 interrupts") + + var toolAInterruptID, toolBInterruptID string + for _, info := range interruptEvent.Action.Interrupted.InterruptContexts { + switch info.Info { + case "interrupt from toolA": + toolAInterruptID = info.ID + assert.True(t, info.IsRootCause) + case "interrupt from toolB": + toolBInterruptID = info.ID + assert.True(t, info.IsRootCause) + } + } + assert.NotEmpty(t, toolAInterruptID) + assert.NotEmpty(t, toolBInterruptID) + + // 2. Resume, targeting only toolA. toolB should re-interrupt. + iter, err = runner.ResumeWithParams(ctx, "parallel-tool-test-1", &ResumeParams{ + Targets: map[string]any{ + toolAInterruptID: "toolA resumed", + }, + }) + assert.NoError(t, err) + _, interruptEvent = consumeUntilInterrupt(iter) + + assert.NotNil(t, interruptEvent, "expected a re-interrupt from toolB") + assert.Equal(t, 1, len(interruptEvent.Action.Interrupted.InterruptContexts), + "should have 1 remaining interrupts") + + var rootCause *InterruptCtx + for _, info := range interruptEvent.Action.Interrupted.InterruptContexts { + if info.IsRootCause { + rootCause = info + break + } + } + + if rootCause == nil { + t.Fatal("expected a root cause interrupt from toolB") + } + assert.Equal(t, "interrupt from toolB", rootCause.Info) + toolBReInterruptID := rootCause.ID + + // 3. Resume the re-interrupted toolB. The agent should then call the tools again. + iter, err = runner.ResumeWithParams(ctx, "parallel-tool-test-1", &ResumeParams{ + Targets: map[string]any{ + toolBReInterruptID: "toolB resumed", + }, + }) + assert.NoError(t, err) + + // 4. Consume all final events. The internal assertions in the tools will check the wasInterrupted flag. + // We expect to see the results of the second tool calls, and then the final agent completion. + finalEvents, interruptEvent := consumeUntilInterrupt(iter) + assert.Equal(t, 2, len(finalEvents)) + assert.NotNil(t, interruptEvent) +} + +// TestNestedChatModelAgentWithAgentTool verifies that the shouldFire method correctly prevents +// duplicate event firing in nested ChatModelAgent scenarios (ChatModelAgent -> AgentTool -> ChatModelAgent). +// This ensures that only the inner agent's cbHandler fires, not the outer agent's. +func TestNestedChatModelAgentWithAgentTool(t *testing.T) { + ctx := context.Background() + + // Create an interruptible tool for the inner agent + innerTool := &myStatefulTool{name: "innerTool", t: t} + + // Create the inner ChatModelAgent that will be wrapped by AgentTool + innerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "InnerAgent", + Description: "Inner agent with interruptible tool", + Model: &myModel{ + messages: []*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "1", Function: schema.FunctionCall{Name: "innerTool", Arguments: "{}"}}, + }), + schema.AssistantMessage("inner agent completed", nil), + }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{innerTool}, + }, + }, + }) + assert.NoError(t, err) + + // Wrap the inner agent in an AgentTool + agentTool := NewAgentTool(ctx, innerAgent) + + // Create the outer ChatModelAgent that uses the AgentTool + outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "OuterAgent", + Description: "Outer agent with AgentTool containing inner agent", + Model: &myModel{ + messages: []*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "1", Function: schema.FunctionCall{Name: "InnerAgent", Arguments: "{}"}}, + }), + schema.AssistantMessage("outer agent completed", nil), + }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{agentTool}, + }, + }, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: outerAgent, + CheckPointStore: newMyStore(), + }) + + // Run the query - this should trigger the nested agent structure + iter := runner.Query(ctx, "start", WithCheckPointID("nested-agent-test-1")) + + // Collect all events to verify no duplicates + var allEvents []*AgentEvent + var interruptEvent *AgentEvent + + for { + event, ok := iter.Next() + if !ok { + break + } + + if event.Action != nil && event.Action.Interrupted != nil { + assert.Nil(t, interruptEvent) + interruptEvent = event + } + + allEvents = append(allEvents, event) + } + + if interruptEvent == nil { + t.Fatal("expected an interrupt event") + } + + // Verify we got exactly one interrupt event (not duplicated) + assert.NotNil(t, interruptEvent, "should have an interrupt event") + assert.Equal(t, 1, len(interruptEvent.Action.Interrupted.InterruptContexts), + "should have exactly one interrupt context") + + // Verify the interrupt comes from the inner tool, not duplicated + interruptCtx := interruptEvent.Action.Interrupted.InterruptContexts[0] + assert.True(t, interruptCtx.IsRootCause, "interrupt should be root cause") + assert.Equal(t, "interrupt from innerTool", interruptCtx.Info) + + // Verify the address path shows the correct nested structure + expectedAddress := Address{ + {Type: AddressSegmentAgent, ID: "OuterAgent"}, + {Type: AddressSegmentTool, ID: "InnerAgent", SubID: "1"}, + {Type: AddressSegmentAgent, ID: "InnerAgent"}, + {Type: AddressSegmentTool, ID: "innerTool", SubID: "1"}, + } + assert.Equal(t, expectedAddress, interruptCtx.Address, + "interrupt address should show correct nested structure") + + // Verify no duplicate events by checking agent names in events + var agentNames []string + for _, event := range allEvents { + if event.AgentName != "" { + agentNames = append(agentNames, event.AgentName) + } + } + + // Should only have events from the outer agent (the inner agent's events should be handled + // by the AgentTool and not duplicated by the outer agent's cbHandler) + for _, name := range agentNames { + assert.Equal(t, "OuterAgent", name, + "all events should come from OuterAgent, not duplicated from InnerAgent") + } + + // Now resume the interrupt + interruptID := interruptCtx.ID + iter, err = runner.ResumeWithParams(ctx, "nested-agent-test-1", &ResumeParams{ + Targets: map[string]any{ + interruptID: "resume inner tool", + }, + }) + assert.NoError(t, err) + + // Collect final events after resume + var finalEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + finalEvents = append(finalEvents, event) + } + + // Verify completion events + assert.Greater(t, len(finalEvents), 0, "should have completion events after resume") + + // Check that we get the expected completion messages + var foundInnerCompletion, foundOuterCompletion bool + for _, event := range finalEvents { + if event.Output != nil && event.Output.MessageOutput != nil { + if event.Output.MessageOutput.Message != nil { + content := event.Output.MessageOutput.Message.Content + switch content { + case "inner agent completed": + foundInnerCompletion = true + case "outer agent completed": + foundOuterCompletion = true + } + } + } + } + + assert.True(t, foundInnerCompletion, "should have inner agent completion") + assert.True(t, foundOuterCompletion, "should have outer agent completion") +} + +// consumeUntilInterrupt consumes events from the iterator until an interrupt is found or it's exhausted. +func consumeUntilInterrupt(iter *AsyncIterator[*AgentEvent]) (normalEvents []*AgentEvent, interruptEvent *AgentEvent) { + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + continue + } + normalEvents = append(normalEvents, event) + } + return +} + +type returnDirectlyTool struct { + name string +} + +func (t *returnDirectlyTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "A tool that returns directly", + }, nil +} + +func (t *returnDirectlyTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return "return directly result", nil +} + +type interruptingTool struct { + name string +} + +func (i *interruptingTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: i.name, + Desc: "A tool that interrupts", + }, nil +} + +func (i *interruptingTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + if wasInterrupted, _, _ := compose.GetInterruptState[any](ctx); !wasInterrupted { + return "", compose.Interrupt(ctx, "interrupt data") + } + + if isResumeFlow, hasResumeData, data := compose.GetResumeContext[string](ctx); isResumeFlow && hasResumeData { + return data, nil + } + + return "resumed without data", nil +} + +type twoToolCallModel struct { + returnDirectlyToolName string + interruptingToolName string + callCount int + receivedTools []*schema.ToolInfo + mu sync.Mutex +} + +func (m *twoToolCallModel) Generate(_ context.Context, _ []*schema.Message, opts ...model.Option) (*schema.Message, error) { + m.mu.Lock() + m.callCount++ + callNum := m.callCount + options := model.GetCommonOptions(&model.Options{}, opts...) + if options.Tools != nil { + m.receivedTools = options.Tools + } + m.mu.Unlock() + + if callNum == 1 { + return &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: "call_return_directly", + Type: "function", + Function: schema.FunctionCall{ + Name: m.returnDirectlyToolName, + Arguments: "{}", + }, + }, + { + ID: "call_interrupting", + Type: "function", + Function: schema.FunctionCall{ + Name: m.interruptingToolName, + Arguments: "{}", + }, + }, + }, + }, nil + } + return schema.AssistantMessage("final response", nil), nil +} + +func (m *twoToolCallModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + panic("not implemented") +} + +func (m *twoToolCallModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +func (m *twoToolCallModel) GetReceivedTools() []*schema.ToolInfo { + m.mu.Lock() + defer m.mu.Unlock() + return m.receivedTools +} + +type dynamicTool struct { + name string +} + +func (t *dynamicTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "A dynamically added tool", + }, nil +} + +func (t *dynamicTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return "dynamic tool result", nil +} + +func TestReturnDirectlyEventSentAfterResume(t *testing.T) { + ctx := context.Background() + + returnDirectlyToolName := "return_directly_tool" + interruptingToolName := "interrupting_tool" + dynamicToolName := "dynamic_tool" + + mdl := &twoToolCallModel{ + returnDirectlyToolName: returnDirectlyToolName, + interruptingToolName: interruptingToolName, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent for return directly + interrupt", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{ + &returnDirectlyTool{name: returnDirectlyToolName}, + &interruptingTool{name: interruptingToolName}, + }, + }, + ReturnDirectly: map[string]bool{ + returnDirectlyToolName: true, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &interruptTestToolsHandler{tools: []tool.BaseTool{&dynamicTool{name: dynamicToolName}}}, + }, + }) + assert.NoError(t, err) + + store := newMyStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + CheckPointStore: store, + }) + + iter := runner.Query(ctx, "test input", WithCheckPointID("test_checkpoint")) + + var interruptEvent *AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + } + + assert.NotNil(t, interruptEvent, "Should have an interrupt event") + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts) + + receivedToolsBeforeResume := mdl.GetReceivedTools() + var hasDynamicToolBeforeResume bool + for _, ti := range receivedToolsBeforeResume { + if ti.Name == dynamicToolName { + hasDynamicToolBeforeResume = true + } + } + assert.True(t, hasDynamicToolBeforeResume, "Dynamic tool should be in tool list before interrupt") + + interruptID := interruptEvent.Action.Interrupted.InterruptContexts[0].ID + resumeIter, err := runner.ResumeWithParams(ctx, "test_checkpoint", &ResumeParams{ + Targets: map[string]any{ + interruptID: "resume data", + }, + }) + assert.NoError(t, err) + + var resumeEvents []*AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + resumeEvents = append(resumeEvents, event) + } + + var hasReturnDirectlyEvent bool + for _, e := range resumeEvents { + if e.Output != nil && e.Output.MessageOutput != nil { + if e.Output.MessageOutput.Role == schema.Tool && e.Output.MessageOutput.ToolName == returnDirectlyToolName { + hasReturnDirectlyEvent = true + } + } + } + assert.True(t, hasReturnDirectlyEvent, "ReturnDirectlyEvent should be sent after resume") + + receivedToolsAfterResume := mdl.GetReceivedTools() + var hasDynamicToolAfterResume bool + for _, ti := range receivedToolsAfterResume { + if ti.Name == dynamicToolName { + hasDynamicToolAfterResume = true + } + } + assert.True(t, hasDynamicToolAfterResume, "Dynamic tool should be in tool list after resume (bc.toolUpdated path)") +} + +// streamErrorThenToolCallModel simulates a model that: +// - On the first Stream call: emits several good chunks then an error (triggering retry) +// - On the second Stream call (retry): returns a tool call message (success) +type streamErrorThenToolCallModel struct { + callCount int32 + toolCallName string +} + +func (m *streamErrorThenToolCallModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("final answer", nil), nil +} + +func (m *streamErrorThenToolCallModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + count := atomic.AddInt32(&m.callCount, 1) + + sr, sw := schema.Pipe[*schema.Message](10) + go func() { + defer sw.Close() + if count == 1 { + // First call: emit good chunks then error + sw.Send(schema.AssistantMessage("chunk1", nil), nil) + sw.Send(schema.AssistantMessage("chunk2", nil), nil) + sw.Send(schema.AssistantMessage("chunk3", nil), nil) + sw.Send(nil, errRetryAble) + return + } + // Second call (retry): return tool call + sw.Send(schema.AssistantMessage("", []schema.ToolCall{{ + ID: "call-1", + Function: schema.FunctionCall{Name: m.toolCallName, Arguments: "{}"}, + }}), nil) + }() + return sr, nil +} + +func (m *streamErrorThenToolCallModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +// TestStreamRetryThenToolInterruptCheckpoint reproduces a bug where: +// 1. ChatModelAgent with ModelRetryConfig.MaxRetries = 2 +// 2. First model Stream call emits good chunks then a retryable error +// 3. Retry succeeds, model returns a tool call +// 4. The tool triggers an interrupt, causing the Runner to save a checkpoint +// 5. The checkpoint save fails because the first (failed) model call's stream event +// is in the session, and when MessageVariant.GobEncode consumes the stream, +// it hits the error chunk and returns an encoding error. +func TestStreamRetryThenToolInterruptCheckpoint(t *testing.T) { + ctx := context.Background() + + interruptToolName := "interrupt_tool" + mdl := &streamErrorThenToolCallModel{toolCallName: interruptToolName} + + interruptTool := &interruptingTool{name: interruptToolName} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "RetryInterruptAgent", + Description: "Agent that retries model then tool interrupts", + Instruction: "You are a test agent.", + Model: mdl, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 2, + IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) }, + }, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{interruptTool}, + }, + }, + }) + assert.NoError(t, err) + + store := newMyStore() + runner := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, []Message{schema.UserMessage("test query")}, WithCheckPointID("retry_interrupt_ckpt")) + + var events []*AgentEvent + var checkpointErr error + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + if event.Err != nil { + checkpointErr = event.Err + t.Logf("event error: %v", event.Err) + } + } + + // The bug: checkpoint save fails because the failed stream's error chunk + // is encountered during gob encoding of the session events. + // If the bug is fixed, checkpointErr should be nil and we should see an interrupt event. + assert.NoError(t, checkpointErr, "checkpoint save should not fail due to failed stream's error in session") + + var hasInterrupt bool + for _, event := range events { + if event.Action != nil && event.Action.Interrupted != nil { + hasInterrupt = true + } + } + assert.True(t, hasInterrupt, "should receive an interrupt event from the tool") + + // Verify the model was called twice (first call errored, second succeeded) + assert.Equal(t, int32(2), atomic.LoadInt32(&mdl.callCount), "model should be called exactly twice (1 failure + 1 retry)") +} diff --git a/adk/message_id_test.go b/adk/message_id_test.go new file mode 100644 index 0000000..ef85335 --- /dev/null +++ b/adk/message_id_test.go @@ -0,0 +1,1094 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +func isValidUUID(s string) bool { + // UUID v4 format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12 = 36 chars) + if len(s) != 36 { + return false + } + for i, c := range s { + if i == 8 || i == 13 || i == 18 || i == 23 { + if c != '-' { + return false + } + } else if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + return false + } + } + return true +} + +// collectEvents drains all events from the iterator (non-streaming). +func collectEvents(t *testing.T, iter *AsyncIterator[*AgentEvent]) []*AgentEvent { + t.Helper() + var events []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + events = append(events, event) + } + return events +} + +// Scenario 1: AgentEvent messages have IDs (Generate mode) +func TestMessageID_EventHasID_Generate(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("hello world", nil), nil). + Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + }) + + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + require.NotNil(t, events[0].Output.MessageOutput) + + msg := events[0].Output.MessageOutput.Message + require.NotNil(t, msg) + msgID := GetMessageID(msg) + assert.NotEmpty(t, msgID, "event message should have an ID") + assert.True(t, isValidUUID(msgID), "message ID should be a valid UUID, got: %s", msgID) +} + +// Scenario 2: Event and state messages share the same ID +func TestMessageID_EventAndStateShareSameID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("response", nil), nil). + Times(1) + + var stateMessagesAfterModel []*schema.Message + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + Middlewares: []AgentMiddleware{ + { + AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + // Capture state messages after model call (including the model output) + stateMessagesAfterModel = make([]*schema.Message, len(state.Messages)) + copy(stateMessagesAfterModel, state.Messages) + return nil + }, + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + }) + + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + + eventMsg := events[0].Output.MessageOutput.Message + eventMsgID := GetMessageID(eventMsg) + assert.NotEmpty(t, eventMsgID) + + // The last message in state should be the model output with the same ID + require.NotEmpty(t, stateMessagesAfterModel) + lastStateMsg := stateMessagesAfterModel[len(stateMessagesAfterModel)-1] + stateMsgID := GetMessageID(lastStateMsg) + + assert.Equal(t, eventMsgID, stateMsgID, + "event msg ID (%s) and state msg ID (%s) must match", eventMsgID, stateMsgID) +} + +// Scenario 3: Stream — first chunk carries ID, concatenated message has correct ID +func TestMessageID_Stream_FirstChunkOnly(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("chunk1", nil), + schema.AssistantMessage("chunk2", nil), + schema.AssistantMessage("chunk3", nil), + }), nil). + Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + EnableStreaming: true, + }) + + event, ok := iter.Next() + require.True(t, ok) + require.Nil(t, event.Err) + require.NotNil(t, event.Output.MessageOutput) + require.True(t, event.Output.MessageOutput.IsStreaming) + + stream := event.Output.MessageOutput.MessageStream + require.NotNil(t, stream) + + var chunks []*schema.Message + for { + msg, err := stream.Recv() + if err != nil { + break + } + chunks = append(chunks, msg) + } + require.GreaterOrEqual(t, len(chunks), 1) + + // First chunk should have the ID + firstChunkID := GetMessageID(chunks[0]) + assert.NotEmpty(t, firstChunkID, "first chunk should carry the message ID") + assert.True(t, isValidUUID(firstChunkID)) + + // Subsequent chunks should NOT have the ID in Extra (first-chunk-only injection) + for i := 1; i < len(chunks); i++ { + chunkID := GetMessageID(chunks[i]) + assert.Empty(t, chunkID, "chunk %d should not have message ID (first-chunk-only)", i) + } + + // No more events + _, ok = iter.Next() + assert.False(t, ok) +} + +// Scenario 4: Tool messages have IDs +func TestMessageID_ToolMessagesHaveID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolForTest{tarCount: 1} + info, err := fakeTool.Info(ctx) + require.NoError(t, err) + + generateCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tool", + []schema.ToolCall{{ + ID: "tc-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "tester"}`, + }, + }}), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Capture tool result messages from state via BeforeChatModel on the 2nd model call. + var toolMsgIDInState string + beforeModelCount := 0 + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + }, + Middlewares: []AgentMiddleware{ + { + BeforeChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + beforeModelCount++ + if beforeModelCount == 2 { + // 2nd model call: state.Messages contains tool result messages + for _, m := range state.Messages { + if m.Role == schema.Tool && m.ToolCallID == "tc-1" { + toolMsgIDInState = GetMessageID(m) + } + } + } + return nil + }, + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("use tool")}, + }) + + events := collectEvents(t, iter) + // Expect 3 events: model(tool_call) + tool(result) + model(final) + require.Len(t, events, 3) + + // Tool event (index 1) + toolEvent := events[1] + require.Nil(t, toolEvent.Err) + require.NotNil(t, toolEvent.Output.MessageOutput) + assert.Equal(t, schema.Tool, toolEvent.Output.MessageOutput.Role) + + toolMsg := toolEvent.Output.MessageOutput.Message + require.NotNil(t, toolMsg) + toolMsgID := GetMessageID(toolMsg) + assert.NotEmpty(t, toolMsgID, "tool message should have an ID") + assert.True(t, isValidUUID(toolMsgID)) + + // All events should have IDs + for i, ev := range events { + require.Nil(t, ev.Err) + require.NotNil(t, ev.Output.MessageOutput) + msg := ev.Output.MessageOutput.Message + require.NotNil(t, msg) + assert.NotEmpty(t, GetMessageID(msg), "event[%d] should have a message ID", i) + } + + // The tool message in state should share the same ID as the event tool message. + assert.NotEmpty(t, toolMsgIDInState, "tool message in state should have an ID") + assert.Equal(t, toolMsgID, toolMsgIDInState, + "tool event msg ID (%s) and state msg ID (%s) must match", toolMsgID, toolMsgIDInState) +} + +// Scenario 5: Retry — the final accepted result carries a message ID +func TestMessageID_Retry_FinalResultHasID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + retryErr := errors.New("retryable error") + + var callCount int32 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + count := atomic.AddInt32(&callCount, 1) + if count < 3 { + return nil, retryErr + } + return schema.AssistantMessage("Success after retry", nil), nil + }).Times(3) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + ShouldRetry: func(ctx context.Context, retryCtx *RetryContext) *RetryDecision { + return &RetryDecision{Retry: errors.Is(retryCtx.Err, retryErr)} + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hello")}, + }) + + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + + msg := events[0].Output.MessageOutput.Message + msgID := GetMessageID(msg) + assert.NotEmpty(t, msgID, "surviving message should have an ID") + assert.True(t, isValidUUID(msgID)) + assert.Equal(t, int32(3), atomic.LoadInt32(&callCount)) +} + +// Scenario 6: WrapModel handler sees model output with ID +func TestMessageID_WrapModelSeesID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("model output", nil), nil). + Times(1) + + var capturedMsgID string + + handler := &wrapModelIDCheckHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + onGenerate: func(result *schema.Message) { + capturedMsgID = GetMessageID(result) + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + Handlers: []ChatModelAgentMiddleware{handler}, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + }) + + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + + assert.NotEmpty(t, capturedMsgID, "WrapModel handler should see message ID on model output") + assert.True(t, isValidUUID(capturedMsgID)) + + // The event should carry the same ID + eventMsgID := GetMessageID(events[0].Output.MessageOutput.Message) + assert.Equal(t, capturedMsgID, eventMsgID, + "WrapModel-captured ID (%s) should match event ID (%s)", capturedMsgID, eventMsgID) +} + +// wrapModelIDCheckHandler wraps the model to inspect the output for message ID. +type wrapModelIDCheckHandler struct { + *BaseChatModelAgentMiddleware + onGenerate func(result *schema.Message) +} + +func (h *wrapModelIDCheckHandler) WrapModel(_ context.Context, m model.BaseChatModel, _ *ModelContext) (model.BaseChatModel, error) { + return &idCheckModelWrapper{inner: m, onGenerate: h.onGenerate}, nil +} + +type idCheckModelWrapper struct { + inner model.BaseChatModel + onGenerate func(result *schema.Message) +} + +func (w *idCheckModelWrapper) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + result, err := w.inner.Generate(ctx, input, opts...) + if err == nil && w.onGenerate != nil { + w.onGenerate(result) + } + return result, err +} + +func (w *idCheckModelWrapper) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return w.inner.Stream(ctx, input, opts...) +} + +// Scenario 7: User input messages do NOT get automatic IDs (they are external, not framework-created). +// Only framework-created messages (model output, tool results, TypedSendEvent) get auto-assigned IDs. +func TestMessageID_UserInputNoAutoID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + var stateMessagesBeforeModel []*schema.Message + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + // Capture input messages + stateMessagesBeforeModel = make([]*schema.Message, len(input)) + copy(stateMessagesBeforeModel, input) + return schema.AssistantMessage("response", nil), nil + }).Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hello")}, + }) + + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + + // User input messages should NOT have auto-assigned IDs. + // Framework only assigns IDs to messages it creates (model output, tool results, SendEvent). + require.NotEmpty(t, stateMessagesBeforeModel) + + for i, msg := range stateMessagesBeforeModel { + msgID := GetMessageID(msg) + assert.Empty(t, msgID, "input message[%d] (role=%s) should NOT have auto-assigned ID", i, msg.Role) + } +} + +// Scenario 8: Middleware must call EnsureMessageID before SendEvent; pointer identity ensures state consistency +// TestMessageID_SendEvent_MiddlewareMustEnsureID verifies that TypedSendEvent is a pure +// transport and does NOT auto-assign message IDs. Middleware authors must call +// EnsureMessageID themselves before sending. +func TestMessageID_SendEvent_MiddlewareMustEnsureID(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("model response", nil), nil). + Times(1) + + // Track the message pointer that the middleware creates and writes to both state and event + var middlewareMsg *schema.Message + var stateMsgIDAfterSendEvent string + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMsgID", + Instruction: "test", + Model: cm, + Middlewares: []AgentMiddleware{ + { + AfterChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + // Middleware creates a new message and writes the SAME pointer to both state and event + middlewareMsg = schema.AssistantMessage("middleware injected", nil) + + // Middleware is responsible for assigning the ID before sending + EnsureMessageID(middlewareMsg) + + // Write to state + state.Messages = append(state.Messages, middlewareMsg) + + // Send as event — TypedSendEvent does NOT auto-assign ID + event := EventFromMessage(middlewareMsg, nil, schema.Assistant, "") + err := SendEvent(ctx, event) + if err != nil { + return err + } + + // Because we called EnsureMessageID on the shared pointer, + // the state copy also has the ID (pointer identity) + stateMsgIDAfterSendEvent = internal.GetMessageID(middlewareMsg.Extra) + + return nil + }, + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + }) + + var allEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + allEvents = append(allEvents, event) + } + + // We expect at least 2 events: model response + middleware injected message + require.GreaterOrEqual(t, len(allEvents), 2) + + // The middleware message pointer should have an ID (assigned by middleware via EnsureMessageID) + require.NotNil(t, middlewareMsg) + middlewareMsgID := GetMessageID(middlewareMsg) + assert.NotEmpty(t, middlewareMsgID, "middleware should have assigned an ID via EnsureMessageID") + assert.True(t, isValidUUID(middlewareMsgID)) + + // The ID captured right after SendEvent (via pointer identity) should be the same + assert.Equal(t, middlewareMsgID, stateMsgIDAfterSendEvent, + "pointer identity: ID read from state pointer (%s) should match message ID (%s)", + stateMsgIDAfterSendEvent, middlewareMsgID) + + // Find the middleware event in the collected events + var middlewareEventMsgID string + for _, ev := range allEvents { + if ev.Err != nil || ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + msg := ev.Output.MessageOutput.Message + if msg != nil && msg.Content == "middleware injected" { + middlewareEventMsgID = GetMessageID(msg) + break + } + } + assert.Equal(t, middlewareMsgID, middlewareEventMsgID, + "event message ID (%s) should match the middleware message ID (%s)", + middlewareEventMsgID, middlewareMsgID) +} + +func TestAttack_ConcatCorruptsIDIfMultipleChunksCarryIt(t *testing.T) { + id := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + msgs := []*schema.Message{ + {Role: schema.Assistant, Content: "chunk1", Extra: map[string]any{internal.EinoMsgIDKey: id}}, + {Role: schema.Assistant, Content: "chunk2", Extra: map[string]any{internal.EinoMsgIDKey: id}}, + {Role: schema.Assistant, Content: "chunk3", Extra: map[string]any{internal.EinoMsgIDKey: id}}, + } + concatenated, err := schema.ConcatMessages(msgs) + require.NoError(t, err) + + resultID := internal.GetMessageID(concatenated.Extra) + // ConcatMessages string-concatenates duplicate Extra keys, corrupting the ID + assert.NotEqual(t, id, resultID, "ConcatMessages should corrupt the ID when multiple chunks carry it") + assert.NotEqual(t, 36, len(resultID), "corrupted ID should not be 36 chars") + assert.Equal(t, "chunk1chunk2chunk3", concatenated.Content) +} + +func TestAttack_ConcatPreservesIDIfOnlyFirstChunkHasIt(t *testing.T) { + id := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + msgs := []*schema.Message{ + {Role: schema.Assistant, Content: "chunk1", Extra: map[string]any{internal.EinoMsgIDKey: id}}, + {Role: schema.Assistant, Content: "chunk2"}, + {Role: schema.Assistant, Content: "chunk3"}, + } + concatenated, err := schema.ConcatMessages(msgs) + require.NoError(t, err) + + resultID := internal.GetMessageID(concatenated.Extra) + assert.Equal(t, id, resultID, "ID should be preserved when only first chunk carries it") + assert.Equal(t, "chunk1chunk2chunk3", concatenated.Content) +} + +func TestAttack_ConcurrentGenerate_NoSharedExtraMutation(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Shared singleton message - same pointer returned every time + sharedMsg := schema.AssistantMessage("shared response", nil) + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(sharedMsg, nil). + AnyTimes() + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAttack", + Instruction: "test", + Model: cm, + }) + require.NoError(t, err) + + const N = 10 + ids := make([]string, N) + var wg sync.WaitGroup + wg.Add(N) + for i := 0; i < N; i++ { + go func(idx int) { + defer wg.Done() + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + }) + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + msg := events[0].Output.MessageOutput.Message + require.NotNil(t, msg) + ids[idx] = GetMessageID(msg) + }(i) + } + wg.Wait() + + // All IDs should be unique and valid + seen := make(map[string]bool) + for i, id := range ids { + assert.NotEmpty(t, id, "goroutine %d should have an ID", i) + assert.True(t, isValidUUID(id), "goroutine %d ID should be valid UUID: %s", i, id) + assert.False(t, seen[id], "goroutine %d has duplicate ID: %s", i, id) + seen[id] = true + } + + // The original shared message should NOT have been mutated (or if it was, it should still be valid) + // The important thing is no panic and unique IDs +} + +func TestAttack_GenerateCopyDoesNotAffectOriginal(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + originalMsg := schema.AssistantMessage("original", nil) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(originalMsg, nil). + Times(1) + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAttack", + Instruction: "test", + Model: cm, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("hi")}, + }) + + events := collectEvents(t, iter) + require.Len(t, events, 1) + require.Nil(t, events[0].Err) + + eventMsg := events[0].Output.MessageOutput.Message + eventMsgID := GetMessageID(eventMsg) + assert.NotEmpty(t, eventMsgID) + + // The ORIGINAL message returned by the model should NOT have an ID + // because wrapGenerateEndpoint copies before mutating + originalID := GetMessageID(originalMsg) + assert.Empty(t, originalID, "original model output should NOT be mutated by ID assignment") +} + +// ============================================================ +// AgenticMessage Integration Tests +// ============================================================ + +// TestMessageID_AgenticGenerate verifies that AgenticMessage-typed agents +// get message IDs assigned on Generate output, covering the *schema.AgenticMessage +// branches in EnsureMessageID, GetMessageID, and copyMessage. +func TestMessageID_AgenticGenerate(t *testing.T) { + ctx := context.Background() + + agenticResponse := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "agentic response"}), + }, + } + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticResponse, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticMsgID", + Instruction: "test", + Model: m, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("hi")}, + }) + + event, ok := iter.Next() + require.True(t, ok) + require.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + require.NotNil(t, msg) + + // Verify via the AgenticMessage-specific public API + msgID := GetMessageID(msg) + assert.NotEmpty(t, msgID, "agentic model output should have message ID") + assert.True(t, isValidUUID(msgID), "agentic message ID should be valid UUID: %s", msgID) + + // Original message should NOT be mutated (copyMessage for AgenticMessage branch) + originalID := GetMessageID(agenticResponse) + assert.Empty(t, originalID, "original agentic model output should NOT be mutated") + + // Drain iterator + for { + _, ok := iter.Next() + if !ok { + break + } + } +} + +// TestMessageID_AgenticStream verifies first-chunk-only ID injection for AgenticMessage streams. +func TestMessageID_AgenticStream(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return nil, errors.New("should not be called") + }, + streamFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + r, w := schema.Pipe[*schema.AgenticMessage](3) + go func() { + defer w.Close() + for i := 0; i < 3; i++ { + w.Send(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "chunk"}), + }, + }, nil) + } + }() + return r, nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "AgenticStreamMsgID", + Instruction: "test", + Model: m, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &TypedAgentInput[*schema.AgenticMessage]{ + Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("hi")}, + EnableStreaming: true, + }) + + event, ok := iter.Next() + require.True(t, ok) + require.Nil(t, event.Err) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + require.True(t, event.Output.MessageOutput.IsStreaming) + + stream := event.Output.MessageOutput.MessageStream + require.NotNil(t, stream) + + var streamMsgID string + for { + chunk, err := stream.Recv() + if err != nil { + break + } + chunkID := GetMessageID(chunk) + if streamMsgID == "" && chunkID != "" { + streamMsgID = chunkID + } else if chunkID != "" { + // Subsequent chunks should not have ID (first-chunk-only) + t.Errorf("expected only first chunk to have ID, got ID on later chunk: %s", chunkID) + } + } + + // Drain remaining events + for { + _, ok := iter.Next() + if !ok { + break + } + } + + assert.NotEmpty(t, streamMsgID, "first stream chunk should have message ID") + assert.True(t, isValidUUID(streamMsgID), "stream message ID should be valid UUID: %s", streamMsgID) +} + +// TestMessageID_AgenticPublicAPIHelpers tests the batch helpers and ensures +// the AgenticMessage public API variants work correctly. +func TestMessageID_AgenticPublicAPIHelpers(t *testing.T) { + t.Run("EnsureMessageID_idempotent", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "test"}), + }, + } + assert.Empty(t, GetMessageID(msg)) + + EnsureMessageID(msg) + id1 := GetMessageID(msg) + assert.NotEmpty(t, id1) + assert.True(t, isValidUUID(id1)) + + // Idempotent: second call should not change the ID + EnsureMessageID(msg) + id2 := GetMessageID(msg) + assert.Equal(t, id1, id2) + }) + + t.Run("EnsureMessageIDs_batch", func(t *testing.T) { + msgs := []*schema.AgenticMessage{ + {Role: schema.AgenticRoleTypeAssistant}, + {Role: schema.AgenticRoleTypeUser}, + {Role: schema.AgenticRoleTypeAssistant}, + } + for _, msg := range msgs { + EnsureMessageID(msg) + } + + seen := make(map[string]bool) + for i, msg := range msgs { + id := GetMessageID(msg) + assert.NotEmpty(t, id, "msg[%d] should have ID", i) + assert.True(t, isValidUUID(id), "msg[%d] ID should be valid UUID: %s", i, id) + assert.False(t, seen[id], "msg[%d] has duplicate ID: %s", i, id) + seen[id] = true + } + }) +} + +// --- Adversarial attack tests for message ID system --- + +// TestAttack_PopToolMsgID_DoublePop tests that calling popToolMsgID twice for the +// same key returns "" on second call. +func TestAttack_PopToolMsgID_DoublePop(t *testing.T) { + st := &typedState[*schema.Message]{} + st.setToolMsgID("myTool", "call-1", "uuid-abc") + + // First pop returns the ID + id1 := st.popToolMsgID("myTool", "call-1") + assert.Equal(t, "uuid-abc", id1) + + // Second pop returns empty + id2 := st.popToolMsgID("myTool", "call-1") + assert.Empty(t, id2, "double-pop should return empty") + + // Inner map should be cleaned up + assert.Nil(t, st.ToolMsgIDs["myTool"], "inner map should be removed when empty") +} + +// namedFakeToolForTest is a variant of fakeToolForTest with a configurable name. +type namedFakeToolForTest struct { + name string +} + +func (t *namedFakeToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: t.name + " tool for testing", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Desc: "user name for testing", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (t *namedFakeToolForTest) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return `{"say": "ok"}`, nil +} + +// TestAttack_ToolMsgIDConsistency_MultipleTools is an integration test: when an agent +// has multiple tools called in one turn, verify that EACH tool's event message ID +// matches its corresponding state message ID. +func TestAttack_ToolMsgIDConsistency_MultipleTools(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tool1 := &namedFakeToolForTest{name: "greet"} + tool2 := &namedFakeToolForTest{name: "farewell"} + + info1, err := tool1.Info(ctx) + require.NoError(t, err) + info2, err := tool2.Info(ctx) + require.NoError(t, err) + + var generateCount int + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + generateCount++ + if generateCount == 1 { + return schema.AssistantMessage("calling tools", []schema.ToolCall{ + {ID: "tc-1", Function: schema.FunctionCall{Name: info1.Name, Arguments: `{"name": "alice"}`}}, + {ID: "tc-2", Function: schema.FunctionCall{Name: info2.Name, Arguments: `{"name": "bob"}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Capture state message IDs + var stateMsgIDs map[string]string // callID -> msgID + beforeModelCount := 0 + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestMultiTool", + Instruction: "test", + Model: cm, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1, tool2}, + }, + }, + Middlewares: []AgentMiddleware{ + { + BeforeChatModel: func(ctx context.Context, state *ChatModelAgentState) error { + beforeModelCount++ + if beforeModelCount == 2 { + stateMsgIDs = make(map[string]string) + for _, m := range state.Messages { + if m.Role == schema.Tool { + stateMsgIDs[m.ToolCallID] = GetMessageID(m) + } + } + } + return nil + }, + }, + }, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &AgentInput{ + Messages: []Message{schema.UserMessage("use tools")}, + }) + + events := collectEvents(t, iter) + // Expect: model(tool_calls) + tool1(result) + tool2(result) + model(final) = 4 events + require.GreaterOrEqual(t, len(events), 4) + + // Collect tool event IDs + eventMsgIDs := make(map[string]string) // callID -> msgID + for _, ev := range events { + if ev.Err != nil { + continue + } + if ev.Output != nil && ev.Output.MessageOutput != nil { + msg := ev.Output.MessageOutput.Message + if msg != nil && msg.Role == schema.Tool { + eventMsgIDs[msg.ToolCallID] = GetMessageID(msg) + } + } + } + + // Each tool call should have an ID in both event and state, and they must match + require.NotEmpty(t, stateMsgIDs, "state should have tool message IDs") + for callID, stateID := range stateMsgIDs { + assert.NotEmpty(t, stateID, "state msg for %s should have ID", callID) + assert.True(t, isValidUUID(stateID), "state msg ID should be UUID: %s", stateID) + eventID, ok := eventMsgIDs[callID] + assert.True(t, ok, "event should have msg for callID %s", callID) + assert.Equal(t, stateID, eventID, + "event and state msg IDs for callID %s must match: event=%s state=%s", callID, eventID, stateID) + } +} + +// TestAttack_ToolResultToBlocks_EdgeCases verifies toolResultToBlocks handles +// nil ToolResult, empty Parts, and Parts with nil media fields. +func TestAttack_ToolResultToBlocks_EdgeCases(t *testing.T) { + t.Run("nil ToolResult", func(t *testing.T) { + blocks := toolResultToBlocks(nil) + assert.Nil(t, blocks, "nil ToolResult should produce nil blocks") + }) + + t.Run("empty Parts", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{}} + blocks := toolResultToBlocks(tr) + assert.Nil(t, blocks, "empty Parts should produce nil blocks") + }) + + t.Run("text part with empty text", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: ""}, + }} + blocks := toolResultToBlocks(tr) + require.Len(t, blocks, 1) + assert.NotNil(t, blocks[0].Text) + assert.Equal(t, "", blocks[0].Text.Text) + }) + + t.Run("image part with nil Image field", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeImage, Image: nil}, + }} + blocks := toolResultToBlocks(tr) + assert.Empty(t, blocks) + }) + + t.Run("audio part with nil Audio field", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeAudio, Audio: nil}, + }} + blocks := toolResultToBlocks(tr) + assert.Empty(t, blocks) + }) + + t.Run("video part with nil Video field", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeVideo, Video: nil}, + }} + blocks := toolResultToBlocks(tr) + assert.Empty(t, blocks) + }) + + t.Run("file part with nil File field", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeFile, File: nil}, + }} + blocks := toolResultToBlocks(tr) + assert.Empty(t, blocks) + }) + + t.Run("mixed: valid text + nil image + valid text", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "hello"}, + {Type: schema.ToolPartTypeImage, Image: nil}, + {Type: schema.ToolPartTypeText, Text: "world"}, + }} + blocks := toolResultToBlocks(tr) + require.Len(t, blocks, 2) + assert.Equal(t, "hello", blocks[0].Text.Text) + assert.Equal(t, "world", blocks[1].Text.Text) + }) + + t.Run("image part with nil URL pointers", func(t *testing.T) { + tr := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeImage, Image: &schema.ToolOutputImage{ + MessagePartCommon: schema.MessagePartCommon{ + URL: nil, + Base64Data: nil, + MIMEType: "image/png", + }, + }}, + }} + blocks := toolResultToBlocks(tr) + require.Len(t, blocks, 1) + assert.NotNil(t, blocks[0].Image) + assert.Equal(t, "", blocks[0].Image.URL, "nil URL pointer should deref to empty string") + assert.Equal(t, "", blocks[0].Image.Base64Data) + assert.Equal(t, "image/png", blocks[0].Image.MIMEType) + }) +} diff --git a/adk/middlewares/agentsmd/agentsmd.go b/adk/middlewares/agentsmd/agentsmd.go new file mode 100644 index 0000000..5339998 --- /dev/null +++ b/adk/middlewares/agentsmd/agentsmd.go @@ -0,0 +1,213 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package agentsmd provides a middleware that automatically injects Agents.md +// file contents into model input messages. The injection is transient — content +// is prepended at model call time and never persisted to conversation state, +// so it is naturally excluded from summarization / compression. +package agentsmd + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// Config defines the configuration for the agentsmd middleware. +type Config struct { + // Backend provides file access for loading Agents.md files. + // Implementations can use local filesystem, remote storage, or any other backend. + // Required. + Backend Backend + + // AgentsMDFiles specifies the ordered list of Agents.md file paths to load. + // Files are loaded and injected in the given order. + // Supports @import syntax inside files for recursive inclusion (max depth 5). + AgentsMDFiles []string + + // AllAgentsMDMaxBytes limits the total byte size of all loaded Agents.md content. + // Files are loaded in order; once the cumulative size exceeds this limit, + // remaining files are skipped. Each individual file is always loaded in full. + // 0 means no limit. + AllAgentsMDMaxBytes int + + // OnLoadWarning is an optional callback invoked when a non-fatal error occurs + // during Agents.md file loading (e.g. file not found, circular @import, depth + // exceeded). If nil, warnings are logged via log.Printf. + // + // Note: Backend.Read errors other than os.ErrNotExist (e.g. permission denied, + // I/O errors) are NOT treated as warnings and will abort the loading process. + OnLoadWarning func(filePath string, err error) +} + +// NewTyped creates a generic agentsmd middleware that injects Agents.md content into every +// model call. The content is loaded from the configured file paths via Backend +// on each model invocation. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +// +// Recommended: place this middleware AFTER the summarization middleware, so that +// Agents.md content is excluded from summarization/compression. +func NewTyped[M adk.MessageType](_ context.Context, cfg *Config) (adk.TypedChatModelAgentMiddleware[M], error) { + if err := cfg.validate(); err != nil { + return nil, err + } + + return &typedMiddleware[M]{ + loader: newLoaderConfig(cfg.Backend, cfg.AgentsMDFiles, cfg.AllAgentsMDMaxBytes, cfg.OnLoadWarning), + }, nil +} + +// New creates an agentsmd middleware that injects Agents.md content into every +// model call. The content is loaded from the configured file paths via Backend +// on each model invocation. +// +// Recommended: place this middleware AFTER the summarization middleware, so that +// Agents.md content is excluded from summarization/compression. +func New(ctx context.Context, cfg *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped[*schema.Message](ctx, cfg) +} + +type typedMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + loader *loaderConfig +} + +const agentsMDCacheKey = "__agentsmd_content_cache__" +const agentsMDExtraKey = "__agentsmd_content__" + +// BeforeModelRewriteState injects Agents.md content as a User message before +// the first User message in the conversation. The injected message is tagged +// with an Extra key so that repeated invocations are idempotent. +func (m *typedMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *adk.TypedChatModelAgentState[M], _ *adk.TypedModelContext[M]) (context.Context, *adk.TypedChatModelAgentState[M], error) { + // Idempotent: if we already injected, return early. + for _, msg := range state.Messages { + if hasAgentsMDExtra(msg) { + return ctx, state, nil + } + } + + content, err := m.loadContent(ctx) + if err != nil { + return ctx, nil, err + } + if content == "" { + return ctx, state, nil + } + + nState := *state + nState.Messages = typedInsertBeforeFirstUser(state.Messages, content) + return ctx, &nState, nil +} + +// hasAgentsMDExtra checks whether a message has the agentsmd extra key set. +func hasAgentsMDExtra[M adk.MessageType](msg M) bool { + switch v := any(msg).(type) { + case *schema.Message: + if v.Extra != nil { + if _, ok := v.Extra[agentsMDExtraKey]; ok { + return true + } + } + case *schema.AgenticMessage: + if v.Extra != nil { + if _, ok := v.Extra[agentsMDExtraKey]; ok { + return true + } + } + } + return false +} + +// typedInsertBeforeFirstUser inserts a user message with agentsmd content before the first User message. +func typedInsertBeforeFirstUser[M adk.MessageType](msgs []M, content string) []M { + newMsg := makeUserMsgWithExtra[M](content) + result := make([]M, 0, len(msgs)+1) + for i, msg := range msgs { + if isUserRole(msg) { + result = append(result, newMsg) + result = append(result, msgs[i:]...) + return result + } + result = append(result, msg) + } + result = append(result, newMsg) + return result +} + +func isUserRole[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.User + case *schema.AgenticMessage: + return m.Role == schema.AgenticRoleTypeUser + } + return false +} + +func makeUserMsgWithExtra[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.UserMessage(content) + msg.Extra = map[string]any{agentsMDExtraKey: true} + return any(msg).(M) + case *schema.AgenticMessage: + msg := schema.UserAgenticMessage(content) + msg.Extra = map[string]any{agentsMDExtraKey: true} + return any(msg).(M) + } + panic("unreachable") +} + +// loadContent retrieves the Agents.md content, using a per-Run cache to avoid +// reloading on every model call within the same Run(). +func (m *typedMiddleware[M]) loadContent(ctx context.Context) (string, error) { + if cached, found, err := adk.GetRunLocalValue(ctx, agentsMDCacheKey); err == nil && found { + if s, ok := cached.(string); ok { + return s, nil + } + } + + content, err := m.loader.load(ctx) + if err != nil { + return "", fmt.Errorf("[agentsmd]: failed to load agent files: %w", err) + } + + if content != "" { + _ = adk.SetRunLocalValue(ctx, agentsMDCacheKey, content) + } + + return content, nil +} + +func (c *Config) validate() error { + if c == nil { + return fmt.Errorf("[agentsmd]: config is required") + } + if c.Backend == nil { + return fmt.Errorf("[agentsmd]: backend is required") + } + if len(c.AgentsMDFiles) == 0 { + return fmt.Errorf("[agentsmd]: at least one agent file path is required") + } + if c.AllAgentsMDMaxBytes < 0 { + return fmt.Errorf("[agentsmd]: AllAgentMDDocsMaxBytes must be non-negative") + } + return nil +} diff --git a/adk/middlewares/agentsmd/agentsmd_generic_test.go b/adk/middlewares/agentsmd/agentsmd_generic_test.go new file mode 100644 index 0000000..28e2569 --- /dev/null +++ b/adk/middlewares/agentsmd/agentsmd_generic_test.go @@ -0,0 +1,351 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agentsmd + +import ( + "context" + "strings" + "testing" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// --- generic table-driven test helpers --- + +func makeUserMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{Role: schema.User, Content: content}).(M) + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(content)).(M) + default: + panic("unreachable") + } +} + +func makeSystemMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{Role: schema.System, Content: content}).(M) + case *schema.AgenticMessage: + return any(schema.SystemAgenticMessage(content)).(M) + default: + panic("unreachable") + } +} + +func makeAssistantMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{Role: schema.Assistant, Content: content}).(M) + case *schema.AgenticMessage: + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.AssistantGenText{Text: content})}, + }).(M) + default: + panic("unreachable") + } +} + +func getMsgRole[M adk.MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return string(v.Role) + case *schema.AgenticMessage: + return string(v.Role) + default: + panic("unreachable") + } +} + +func getMsgContent[M adk.MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return v.Content + case *schema.AgenticMessage: + for _, block := range v.ContentBlocks { + if block == nil { + continue + } + if block.UserInputText != nil { + return block.UserInputText.Text + } + if block.AssistantGenText != nil { + return block.AssistantGenText.Text + } + } + return "" + default: + panic("unreachable") + } +} + +func getMsgExtra[M adk.MessageType](msg M) map[string]any { + switch v := any(msg).(type) { + case *schema.Message: + return v.Extra + case *schema.AgenticMessage: + return v.Extra + default: + panic("unreachable") + } +} + +// --- generic table-driven test --- + +type agentsMDTestCase struct { + name string + run func(t *testing.T) +} + +func testAgentsMDGeneric[M adk.MessageType](t *testing.T) { + tests := []agentsMDTestCase{ + { + name: "BasicInjection", + run: func(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "You are a helpful assistant.") + + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.TypedChatModelAgentState[M]{Messages: []M{makeUserMsg[M]("hello")}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages, got %d", len(state.Messages)) + } + if getMsgRole(state.Messages[0]) != "user" { + t.Fatalf("expected first message role user, got %s", getMsgRole(state.Messages[0])) + } + if !strings.Contains(getMsgContent(state.Messages[0]), "You are a helpful assistant.") { + t.Fatalf("expected agent.md content in first message, got %q", getMsgContent(state.Messages[0])) + } + if !strings.Contains(getMsgContent(state.Messages[0]), "") { + t.Fatalf("expected system-reminder tag, got %q", getMsgContent(state.Messages[0])) + } + if count := strings.Count(getMsgContent(state.Messages[0]), ""); count != 1 { + t.Fatalf("expected exactly one opening system-reminder tag, got %d in %q", count, getMsgContent(state.Messages[0])) + } + if count := strings.Count(getMsgContent(state.Messages[0]), ""); count != 1 { + t.Fatalf("expected exactly one closing system-reminder tag, got %d in %q", count, getMsgContent(state.Messages[0])) + } + if getMsgContent(state.Messages[1]) != "hello" { + t.Fatalf("expected original message preserved, got %q", getMsgContent(state.Messages[1])) + } + }, + }, + { + name: "InsertBeforeFirstUserMessage", + run: func(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + input := []M{ + makeSystemMsg[M]("system prompt"), + makeUserMsg[M]("hello"), + } + state := &adk.TypedChatModelAgentState[M]{Messages: input} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + if len(state.Messages) != 3 { + t.Fatalf("expected 3 messages, got %d", len(state.Messages)) + } + if getMsgRole(state.Messages[0]) != "system" { + t.Fatalf("expected first message role system, got %s", getMsgRole(state.Messages[0])) + } + if getMsgContent(state.Messages[0]) != "system prompt" { + t.Fatalf("expected system prompt preserved, got %q", getMsgContent(state.Messages[0])) + } + if getMsgRole(state.Messages[1]) != "user" || !strings.Contains(getMsgContent(state.Messages[1]), "agent instructions") { + t.Fatalf("expected agentmd message at index 1, got role=%s content=%q", getMsgRole(state.Messages[1]), getMsgContent(state.Messages[1])) + } + if getMsgRole(state.Messages[2]) != "user" || getMsgContent(state.Messages[2]) != "hello" { + t.Fatalf("expected original user message at index 2, got role=%s content=%q", getMsgRole(state.Messages[2]), getMsgContent(state.Messages[2])) + } + }, + }, + { + name: "InsertWithNoUserMessage", + run: func(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + input := []M{ + makeSystemMsg[M]("system prompt"), + makeAssistantMsg[M]("assistant reply"), + } + state := &adk.TypedChatModelAgentState[M]{Messages: input} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + if len(state.Messages) != 3 { + t.Fatalf("expected 3 messages, got %d", len(state.Messages)) + } + if getMsgRole(state.Messages[0]) != "system" { + t.Fatalf("expected System at index 0, got %s", getMsgRole(state.Messages[0])) + } + if getMsgRole(state.Messages[1]) != "assistant" { + t.Fatalf("expected Assistant at index 1, got %s", getMsgRole(state.Messages[1])) + } + if getMsgRole(state.Messages[2]) != "user" || !strings.Contains(getMsgContent(state.Messages[2]), "agent instructions") { + t.Fatalf("expected agentmd appended at end, got role=%s content=%q", getMsgRole(state.Messages[2]), getMsgContent(state.Messages[2])) + } + }, + }, + { + name: "AllFilesEmpty", + run: func(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "") + + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.TypedChatModelAgentState[M]{Messages: []M{makeUserMsg[M]("hello")}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 1 { + t.Fatalf("expected 1 message (no agentmd prepended), got %d", len(state.Messages)) + } + if getMsgContent(state.Messages[0]) != "hello" { + t.Fatalf("expected original message unchanged, got %q", getMsgContent(state.Messages[0])) + } + }, + }, + { + name: "Idempotency", + run: func(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.TypedChatModelAgentState[M]{Messages: []M{makeUserMsg[M]("hello")}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after first call, got %d", len(state.Messages)) + } + + // Verify the marker is set in Extra. + extra := getMsgExtra(state.Messages[0]) + if extra == nil { + t.Fatal("expected Extra to be set on injected message") + } + if _, ok := extra[agentsMDExtraKey]; !ok { + t.Fatalf("expected agentsMDExtraKey in Extra, got %v", extra) + } + + // Call again with the same state (which now contains the marker message). + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after second call (idempotent), got %d", len(state.Messages)) + } + if !strings.Contains(getMsgContent(state.Messages[0]), "agent instructions") { + t.Fatalf("expected agentmd content preserved, got %q", getMsgContent(state.Messages[0])) + } + }, + }, + { + name: "ReinsertAfterRemoval", + run: func(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.TypedChatModelAgentState[M]{Messages: []M{makeUserMsg[M]("hello")}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after first call, got %d", len(state.Messages)) + } + + // Simulate removal of the marker message (e.g., by summarization). + state = &adk.TypedChatModelAgentState[M]{Messages: []M{makeUserMsg[M]("hello")}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after re-insert, got %d", len(state.Messages)) + } + if !strings.Contains(getMsgContent(state.Messages[0]), "agent instructions") { + t.Fatalf("expected agentmd content re-inserted, got %q", getMsgContent(state.Messages[0])) + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, tc.run) + } +} + +func TestAgentsMDGeneric(t *testing.T) { + t.Run("Message", testAgentsMDGeneric[*schema.Message]) + t.Run("AgenticMessage", testAgentsMDGeneric[*schema.AgenticMessage]) +} diff --git a/adk/middlewares/agentsmd/agentsmd_test.go b/adk/middlewares/agentsmd/agentsmd_test.go new file mode 100644 index 0000000..5d016e1 --- /dev/null +++ b/adk/middlewares/agentsmd/agentsmd_test.go @@ -0,0 +1,1367 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agentsmd + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/schema" +) + +// --- test helpers --- + +type memBackend struct { + files map[string]string +} + +func newMemBackend() *memBackend { + return &memBackend{files: make(map[string]string)} +} + +func (b *memBackend) set(path string, content string) { + b.files[path] = content +} + +func (b *memBackend) Read(_ context.Context, req *ReadRequest) (*filesystem.FileContent, error) { + content, ok := b.files[req.FilePath] + if !ok { + return nil, fmt.Errorf("file not found: %s: %w", req.FilePath, os.ErrNotExist) + } + return &filesystem.FileContent{Content: content}, nil +} + +// errBackend always returns a non-ErrNotExist error on Read, simulating I/O failures. +type errBackend struct{} + +func (b *errBackend) Read(_ context.Context, req *ReadRequest) (*filesystem.FileContent, error) { + return nil, fmt.Errorf("permission denied: %s", req.FilePath) +} + +// partialErrBackend returns content for known files and I/O error for others. +type partialErrBackend struct { + files map[string]string +} + +func (b *partialErrBackend) Read(_ context.Context, req *ReadRequest) (*filesystem.FileContent, error) { + content, ok := b.files[req.FilePath] + if !ok { + return nil, fmt.Errorf("I/O error reading %s", req.FilePath) + } + return &filesystem.FileContent{Content: content}, nil +} + +// --- tests --- + +func TestNew_Validation(t *testing.T) { + ctx := context.Background() + b := newMemBackend() + + _, err := New(ctx, nil) + if err == nil { + t.Fatal("expected error for nil config") + } + + _, err = New(ctx, &Config{}) + if err == nil { + t.Fatal("expected error for empty config") + } + + _, err = New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/test.md"}, AllAgentsMDMaxBytes: -1}) + if err == nil { + t.Fatal("expected error for negative max bytes") + } + + _, err = New(ctx, &Config{AgentsMDFiles: []string{"/test.md"}}) + if err == nil { + t.Fatal("expected error for nil backend") + } +} + +func TestMiddleware_BasicInjection(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "You are a helpful assistant.") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + userMsg := &schema.Message{Role: schema.User, Content: "hello"} + state := &adk.ChatModelAgentState{Messages: []*schema.Message{userMsg}} + + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages, got %d", len(state.Messages)) + } + if state.Messages[0].Role != schema.User { + t.Fatalf("expected first message role User, got %s", state.Messages[0].Role) + } + if !strings.Contains(state.Messages[0].Content, "You are a helpful assistant.") { + t.Fatalf("expected agent.md content in first message, got %q", state.Messages[0].Content) + } + if !strings.Contains(state.Messages[0].Content, "") { + t.Fatalf("expected system-reminder tag, got %q", state.Messages[0].Content) + } + if count := strings.Count(state.Messages[0].Content, ""); count != 1 { + t.Fatalf("expected exactly one opening system-reminder tag, got %d in %q", count, state.Messages[0].Content) + } + if count := strings.Count(state.Messages[0].Content, ""); count != 1 { + t.Fatalf("expected exactly one closing system-reminder tag, got %d in %q", count, state.Messages[0].Content) + } + if state.Messages[1].Content != "hello" { + t.Fatalf("expected original message preserved, got %q", state.Messages[1].Content) + } +} + +func TestMiddleware_MultipleFiles(t *testing.T) { + b := newMemBackend() + b.set("/a.md", "instruction A") + b.set("/b.md", "instruction B") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/a.md", "/b.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + content := state.Messages[0].Content + idxA := strings.Index(content, "instruction A") + idxB := strings.Index(content, "instruction B") + if idxA < 0 || idxB < 0 { + t.Fatalf("both files should be included, content: %q", content) + } + if idxA >= idxB { + t.Fatal("file A should appear before file B") + } +} + +func TestMiddleware_ImportResolution(t *testing.T) { + b := newMemBackend() + b.set("/project/agent.md", "main instructions\n@sub/rules.md\nend") + b.set("/project/sub/rules.md", "imported rule") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/project/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + content := state.Messages[0].Content + // Original text should be preserved with @path intact. + if !strings.Contains(content, "main instructions") { + t.Fatalf("should contain original text, got %q", content) + } + if !strings.Contains(content, "@sub/rules.md") { + t.Fatalf("@import reference should be preserved in original text, got %q", content) + } + if !strings.Contains(content, "end") { + t.Fatalf("should contain original trailing text, got %q", content) + } + // Imported file should appear as a separate section. + if !strings.Contains(content, "Contents of /project/sub/rules.md") { + t.Fatalf("imported file should have its own section, got %q", content) + } + if !strings.Contains(content, "imported rule") { + t.Fatalf("imported file content should be present, got %q", content) + } +} + +func TestMiddleware_RecursiveImport(t *testing.T) { + b := newMemBackend() + b.set("/a.md", "top\n@/b.md") + b.set("/b.md", "middle\n@/c.md") + b.set("/c.md", "leaf content") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/a.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + content := state.Messages[0].Content + // All three files should appear as separate sections. + for _, section := range []string{"Contents of /a.md", "Contents of /b.md", "Contents of /c.md"} { + if !strings.Contains(content, section) { + t.Fatalf("expected section %q in content, got %q", section, content) + } + } + for _, text := range []string{"top", "middle", "leaf content"} { + if !strings.Contains(content, text) { + t.Fatalf("expected %q in content, got %q", text, content) + } + } + // Sections should appear in order: a, b, c. + idxA := strings.Index(content, "Contents of /a.md") + idxB := strings.Index(content, "Contents of /b.md") + idxC := strings.Index(content, "Contents of /c.md") + if !(idxA < idxB && idxB < idxC) { + t.Fatalf("sections should appear in order a < b < c, got a=%d b=%d c=%d", idxA, idxB, idxC) + } +} + +func TestMiddleware_MaxImportDepth(t *testing.T) { + b := newMemBackend() + for i := 0; i < 7; i++ { + var content string + if i < 6 { + content = fmt.Sprintf("level %d\n@/level%d.md", i, i+1) + } else { + content = fmt.Sprintf("level %d", i) + } + b.set(fmt.Sprintf("/level%d.md", i), content) + } + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/level0.md"}}) + if err != nil { + t.Fatal(err) + } + + // Import failure at depth > 5 is logged, not returned as error. + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatalf("expected no error (depth exceeded is logged), got %v", err) + } + // Levels 0-5 should be present as sections; level 6 fails silently. + content := state.Messages[0].Content + for i := 0; i <= 5; i++ { + want := fmt.Sprintf("Contents of /level%d.md", i) + if !strings.Contains(content, want) { + t.Fatalf("expected %q in content, got %q", want, content) + } + } + if strings.Contains(content, "Contents of /level6.md") { + t.Fatalf("level6 should not be present (depth exceeded), got %q", content) + } +} + +func TestMiddleware_CircularImport(t *testing.T) { + b := newMemBackend() + b.set("/a.md", "@/b.md") + b.set("/b.md", "@/a.md") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/a.md"}}) + if err != nil { + t.Fatal(err) + } + + // Circular import failure is logged, not returned as error. + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatalf("expected no error (circular import is logged), got %v", err) + } + // /a.md and /b.md should both be present; the circular ref from b->a is skipped. + content := state.Messages[0].Content + if !strings.Contains(content, "Contents of /a.md") { + t.Fatalf("expected /a.md section, got %q", content) + } + if !strings.Contains(content, "Contents of /b.md") { + t.Fatalf("expected /b.md section, got %q", content) + } +} + +func TestMiddleware_MaxBytesLimit(t *testing.T) { + b := newMemBackend() + b.set("/a.md", "AAAA") // 4 bytes + b.set("/b.md", "BBBB") // 4 bytes + + ctx := context.Background() + mw, err := New(ctx, &Config{ + Backend: b, + AgentsMDFiles: []string{"/a.md", "/b.md"}, + AllAgentsMDMaxBytes: 5, // file a (4) fits, file b (4) would exceed + }) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + content := state.Messages[0].Content + if !strings.Contains(content, "AAAA") { + t.Fatal("first file should be included") + } + if strings.Contains(content, "BBBB") { + t.Fatal("second file should be excluded due to max bytes") + } +} + +func TestMiddleware_InjectedInState(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + originalMsgs := []*schema.Message{{Role: schema.User, Content: "hello"}} + state := &adk.ChatModelAgentState{Messages: originalMsgs} + _, newState, err := mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + // The original slice should not be modified (new slice is returned). + if len(originalMsgs) != 1 { + t.Fatalf("original messages slice should not be modified, got %d messages", len(originalMsgs)) + } + if originalMsgs[0].Content != "hello" { + t.Fatalf("original message should be unchanged, got %q", originalMsgs[0].Content) + } + // The returned state should have the injected message. + if len(newState.Messages) != 2 { + t.Fatalf("new state should have 2 messages (injected + original), got %d", len(newState.Messages)) + } + if !strings.Contains(newState.Messages[0].Content, "agent instructions") { + t.Fatalf("expected agentmd content in first message, got %q", newState.Messages[0].Content) + } + if newState.Messages[1].Content != "hello" { + t.Fatalf("expected original user message preserved, got %q", newState.Messages[1].Content) + } +} + +func TestMiddleware_AbsoluteImportPath(t *testing.T) { + b := newMemBackend() + b.set("/project/main.md", "start\n@/shared/imported.md\nend") + b.set("/shared/imported.md", "absolute import content") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/project/main.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + content := state.Messages[0].Content + // @path preserved in original text. + if !strings.Contains(content, "@/shared/imported.md") { + t.Fatalf("@import reference should be preserved, got %q", content) + } + // Imported content in separate section. + if !strings.Contains(content, "Contents of /shared/imported.md") { + t.Fatalf("expected separate section for imported file, got %q", content) + } + if !strings.Contains(content, "absolute import content") { + t.Fatalf("expected absolute import content, got %q", content) + } +} + +func TestMiddleware_ImportAsSeparateSection(t *testing.T) { + b := newMemBackend() + b.set("/project/agent.md", "Please read @sub/rules.md and also @sub/style.md for guidance.") + b.set("/project/sub/rules.md", "RULE_CONTENT") + b.set("/project/sub/style.md", "STYLE_CONTENT") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/project/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + content := state.Messages[0].Content + // Original text preserved with @paths intact. + if !strings.Contains(content, "Please read @sub/rules.md and also @sub/style.md for guidance.") { + t.Fatalf("original text with @paths should be preserved, got %q", content) + } + // Imported files appear as separate sections. + if !strings.Contains(content, "Contents of /project/sub/rules.md") { + t.Fatalf("expected rules.md section, got %q", content) + } + if !strings.Contains(content, "RULE_CONTENT") { + t.Fatalf("expected imported rule content, got %q", content) + } + if !strings.Contains(content, "Contents of /project/sub/style.md") { + t.Fatalf("expected style.md section, got %q", content) + } + if !strings.Contains(content, "STYLE_CONTENT") { + t.Fatalf("expected imported style content, got %q", content) + } + + // Sections should be ordered: agent.md, rules.md, style.md. + idxAgent := strings.Index(content, "Contents of /project/agent.md") + idxRules := strings.Index(content, "Contents of /project/sub/rules.md") + idxStyle := strings.Index(content, "Contents of /project/sub/style.md") + if !(idxAgent < idxRules && idxRules < idxStyle) { + t.Fatalf("sections should appear in order agent < rules < style, got agent=%d rules=%d style=%d", idxAgent, idxRules, idxStyle) + } +} + +// --- loader-specific tests --- + +func TestLoader_NoImportsPassthrough(t *testing.T) { + // Content without any @path should be returned as-is in its section. + b := newMemBackend() + b.set("/agent.md", "plain text without imports\nline two") + + l := newLoaderConfig(b, []string{"/agent.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "plain text without imports") { + t.Fatalf("expected plain content, got %q", content) + } + if !strings.Contains(content, "line two") { + t.Fatalf("expected second line, got %q", content) + } +} + +func TestLoader_ImportAsSeparateSection(t *testing.T) { + // @path in the middle of a sentence should be preserved; imported file is a separate section. + b := newMemBackend() + b.set("/doc.md", "before @/snippet.md after") + b.set("/snippet.md", "INJECTED") + + l := newLoaderConfig(b, []string{"/doc.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + // Original text preserved. + if !strings.Contains(content, "before @/snippet.md after") { + t.Fatalf("original text should be preserved with @path, got %q", content) + } + // Imported file in separate section. + if !strings.Contains(content, "Contents of /snippet.md") { + t.Fatalf("expected separate section for snippet.md, got %q", content) + } + if !strings.Contains(content, "INJECTED") { + t.Fatalf("expected imported content, got %q", content) + } +} + +func TestLoader_MultipleImportsSameLine(t *testing.T) { + // Multiple @path on one line should each get a separate section. + b := newMemBackend() + b.set("/doc.md", "see @/a.txt and @/b.txt here") + b.set("/a.txt", "AAA") + b.set("/b.txt", "BBB") + + l := newLoaderConfig(b, []string{"/doc.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + // Original text preserved. + if !strings.Contains(content, "see @/a.txt and @/b.txt here") { + t.Fatalf("original text should be preserved, got %q", content) + } + // Each imported file has its own section. + if !strings.Contains(content, "Contents of /a.txt") { + t.Fatalf("expected section for a.txt, got %q", content) + } + if !strings.Contains(content, "AAA") { + t.Fatalf("expected a.txt content, got %q", content) + } + if !strings.Contains(content, "Contents of /b.txt") { + t.Fatalf("expected section for b.txt, got %q", content) + } + if !strings.Contains(content, "BBB") { + t.Fatalf("expected b.txt content, got %q", content) + } +} + +func TestLoader_SameFileTwiceOnSameLine(t *testing.T) { + // The same file referenced twice should appear only once as a section (deduped). + b := newMemBackend() + b.set("/doc.md", "@/shared.md and @/shared.md again") + b.set("/shared.md", "SHARED") + + l := newLoaderConfig(b, []string{"/doc.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + // Original text preserved. + if !strings.Contains(content, "@/shared.md and @/shared.md again") { + t.Fatalf("original text should be preserved, got %q", content) + } + // shared.md content should appear only once (deduped). + count := strings.Count(content, "Contents of /shared.md") + if count != 1 { + t.Fatalf("expected shared.md section to appear once (deduped), got %d in %q", count, content) + } +} + +func TestLoader_ImportFileNotFound(t *testing.T) { + b := newMemBackend() + b.set("/doc.md", "load @/missing.md please") + + l := newLoaderConfig(b, []string{"/doc.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error (missing import is logged), got %v", err) + } + // Original text preserved; missing file simply has no section. + if !strings.Contains(content, "load @/missing.md please") { + t.Fatalf("expected original text preserved, got %q", content) + } + if strings.Contains(content, "Contents of /missing.md") { + t.Fatalf("missing file should not have a section, got %q", content) + } +} + +func TestLoader_RelativePathResolution(t *testing.T) { + // Relative path should resolve relative to the host file's directory. + b := newMemBackend() + b.set("/a/b/host.md", "ref @../c/target.md done") + b.set("/a/c/target.md", "TARGET") + + l := newLoaderConfig(b, []string{"/a/b/host.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + // Original text preserved. + if !strings.Contains(content, "ref @../c/target.md done") { + t.Fatalf("original text should be preserved, got %q", content) + } + // Imported file as separate section. + if !strings.Contains(content, "Contents of /a/c/target.md") { + t.Fatalf("expected section for target.md, got %q", content) + } + if !strings.Contains(content, "TARGET") { + t.Fatalf("expected imported content, got %q", content) + } +} + +func TestLoader_RelativeTopLevelPath(t *testing.T) { + // Top-level file uses relative path; imports with ./ resolve correctly. + b := newMemBackend() + b.set("sub/agents.md", "start @./other.md end") + b.set("sub/other.md", "OTHER CONTENT") + + l := newLoaderConfig(b, []string{"sub/agents.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "start @./other.md end") { + t.Fatalf("expected original text preserved, got %q", content) + } + if !strings.Contains(content, "OTHER CONTENT") { + t.Fatalf("expected imported content, got %q", content) + } +} + +func TestLoader_RelativeTopLevelWithDotDotImport(t *testing.T) { + // Top-level file uses relative path; import with ../ resolves correctly. + b := newMemBackend() + b.set("sub/agents.md", "see @../shared/x.md here") + b.set("shared/x.md", "SHARED X") + + l := newLoaderConfig(b, []string{"sub/agents.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "SHARED X") { + t.Fatalf("expected imported content, got %q", content) + } + // filepath.Clean should normalize "sub/../shared/x.md" to "shared/x.md" + if !strings.Contains(content, "Contents of shared/x.md") { + t.Fatalf("expected normalized path in section header, got %q", content) + } +} + +func TestLoader_RelativeTopLevelDedup(t *testing.T) { + // Two top-level relative paths that resolve to the same file via filepath.Clean + // should be deduped (loaded only once). + b := newMemBackend() + b.set("sub/a.md", "CONTENT A") + + l := newLoaderConfig(b, []string{"sub/a.md", "./sub/a.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + count := strings.Count(content, "CONTENT A") + if count != 1 { + t.Fatalf("expected file loaded once (deduped), got %d occurrences in %q", count, content) + } +} + +func TestLoader_AbsoluteTopLevelWithRelativeImport(t *testing.T) { + // Absolute top-level path with relative @import resolves correctly. + b := newMemBackend() + b.set("/project/agents.md", "ref @./lib/helper.md done") + b.set("/project/lib/helper.md", "HELPER") + + l := newLoaderConfig(b, []string{"/project/agents.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "HELPER") { + t.Fatalf("expected imported content, got %q", content) + } + if !strings.Contains(content, "Contents of /project/lib/helper.md") { + t.Fatalf("expected section header, got %q", content) + } +} + +func TestLoader_AbsoluteTopLevelWithDotDotImport(t *testing.T) { + // Absolute top-level path; @import with ../ resolves and normalizes. + b := newMemBackend() + b.set("/project/sub/agents.md", "load @../shared/x.md here") + b.set("/project/shared/x.md", "SHARED") + + l := newLoaderConfig(b, []string{"/project/sub/agents.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "SHARED") { + t.Fatalf("expected imported content, got %q", content) + } + // filepath.Clean normalizes "/project/sub/../shared/x.md" to "/project/shared/x.md" + if !strings.Contains(content, "Contents of /project/shared/x.md") { + t.Fatalf("expected normalized path in section header, got %q", content) + } +} + +func TestLoader_RelativeImportDedup(t *testing.T) { + // Two different relative @import paths that resolve to the same file + // should be deduped via filepath.Clean. + b := newMemBackend() + b.set("/a/main.md", "first @/a/b/shared.md second @../a/b/shared.md end") + b.set("/a/b/shared.md", "SHARED ONCE") + + l := newLoaderConfig(b, []string{"/a/main.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + count := strings.Count(content, "SHARED ONCE") + if count != 1 { + t.Fatalf("expected shared file loaded once (deduped), got %d in %q", count, content) + } +} + +func TestLoader_NestedRelativeImport(t *testing.T) { + // File A imports B via relative path, B imports C via relative path. + // All three should appear as separate sections. + b := newMemBackend() + b.set("/root/main.md", "start @sub/mid.md end") + b.set("/root/sub/mid.md", "mid @deep/leaf.md mid_end") + b.set("/root/sub/deep/leaf.md", "LEAF") + + l := newLoaderConfig(b, []string{"/root/main.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + for _, section := range []string{"Contents of /root/main.md", "Contents of /root/sub/mid.md", "Contents of /root/sub/deep/leaf.md"} { + if !strings.Contains(content, section) { + t.Fatalf("expected section %q, got %q", section, content) + } + } + if !strings.Contains(content, "LEAF") { + t.Fatalf("expected leaf content, got %q", content) + } +} + +func TestLoader_TransitiveImport(t *testing.T) { + // Imported file itself contains @imports; all should appear as separate sections. + b := newMemBackend() + b.set("/main.md", "header @/mid.md footer") + b.set("/mid.md", "mid-start @/leaf.md mid-end") + b.set("/leaf.md", "LEAF_VALUE") + + l := newLoaderConfig(b, []string{"/main.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + for _, section := range []string{"Contents of /main.md", "Contents of /mid.md", "Contents of /leaf.md"} { + if !strings.Contains(content, section) { + t.Fatalf("expected section %q, got %q", section, content) + } + } + if !strings.Contains(content, "LEAF_VALUE") { + t.Fatalf("expected leaf value, got %q", content) + } +} + +func TestLoader_EmptyFile(t *testing.T) { + b := newMemBackend() + b.set("/empty.md", "") + + l := newLoaderConfig(b, []string{"/empty.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + // Empty file is treated as non-existent, so output should be empty. + if content != "" { + t.Fatalf("expected empty output for empty file, got %q", content) + } +} + +func TestLoader_MaxBytesFirstFileFull(t *testing.T) { + // Even if the first file alone exceeds maxBytes, it should still be loaded in full. + b := newMemBackend() + b.set("/big.md", "ABCDEFGHIJ") // 10 bytes + + l := newLoaderConfig(b, []string{"/big.md"}, 3, nil) + content, err := l.load(context.Background()) // maxBytes=3, but first file always loads + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "ABCDEFGHIJ") { + t.Fatalf("first file should always load in full, got %q", content) + } +} + +func TestLoader_CircularImportInline(t *testing.T) { + // Circular reference via @import should be detected, logged, and skipped. + b := newMemBackend() + b.set("/a.md", "text @/b.md more") + b.set("/b.md", "ref @/a.md back") + + l := newLoaderConfig(b, []string{"/a.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error (circular import is logged), got %v", err) + } + // Both a and b should have sections; circular back-reference a from b is skipped. + if !strings.Contains(content, "Contents of /a.md") { + t.Fatalf("expected /a.md section, got %q", content) + } + if !strings.Contains(content, "Contents of /b.md") { + t.Fatalf("expected /b.md section, got %q", content) + } +} + +func TestLoader_MaxDepthInline(t *testing.T) { + // Deep chain via @import should be logged at depth > 5, not returned as error. + b := newMemBackend() + for i := 0; i < 7; i++ { + var content string + if i < 6 { + content = fmt.Sprintf("level%d @/level%d.md tail", i, i+1) + } else { + content = fmt.Sprintf("level%d", i) + } + b.set(fmt.Sprintf("/level%d.md", i), content) + } + + l := newLoaderConfig(b, []string{"/level0.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error (depth exceeded is logged), got %v", err) + } + // Levels 0-5 should have sections. + for i := 0; i <= 5; i++ { + want := fmt.Sprintf("Contents of /level%d.md", i) + if !strings.Contains(content, want) { + t.Fatalf("expected %q in content, got %q", want, content) + } + } + // Level 6 should not be present. + if strings.Contains(content, "Contents of /level6.md") { + t.Fatalf("level6 should not be present (depth exceeded), got %q", content) + } +} + +func TestLoader_DiamondDependency(t *testing.T) { + // A imports B and D; B imports C; D also imports C. + // C should appear only once (deduped across the whole load). + b := newMemBackend() + b.set("/a.md", "start @/b.md middle @/d.md end") + b.set("/b.md", "B(@/c.md)") + b.set("/d.md", "D(@/c.md)") + b.set("/c.md", "SHARED") + + l := newLoaderConfig(b, []string{"/a.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("diamond dependency should not be circular, got error: %v", err) + } + + // C should appear only once as a section (deduped). + count := strings.Count(content, "Contents of /c.md") + if count != 1 { + t.Fatalf("expected /c.md section once (deduped), got %d in %q", count, content) + } + // All files should have sections. + for _, section := range []string{"Contents of /a.md", "Contents of /b.md", "Contents of /c.md", "Contents of /d.md"} { + if !strings.Contains(content, section) { + t.Fatalf("expected section %q, got %q", section, content) + } + } +} + +func TestLoader_AtSignInNormalText(t *testing.T) { + // Bare @word without "/" or file extension should not trigger import. + // Email-like patterns (@example.com) with non-allowed extensions should also be ignored. + b := newMemBackend() + b.set("/agent.md", "contact me @ anytime or @ spaces and @someone mentioned and user@example.com and @company.org") + + l := newLoaderConfig(b, []string{"/agent.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content, "contact me @ anytime") { + t.Fatalf("bare @ should not trigger import, got %q", content) + } + if !strings.Contains(content, "@someone mentioned") { + t.Fatalf("@someone without / or extension should not trigger import, got %q", content) + } + if !strings.Contains(content, "@example.com") { + t.Fatalf("email-like @example.com should not trigger import, got %q", content) + } + if !strings.Contains(content, "@company.org") { + t.Fatalf("email-like @company.org should not trigger import, got %q", content) + } +} + +func TestLoader_MaxBytesWithImports(t *testing.T) { + // Two top-level files that both import the same shared file. + // Budget should account for imported file bytes. + b := newMemBackend() + b.set("/a.md", "A(@/shared.md)") + b.set("/b.md", "B(@/shared.md)") + b.set("/shared.md", strings.Repeat("X", 100)) // 100 bytes + + l := newLoaderConfig(b, []string{"/a.md", "/b.md"}, 120, nil) + // /a.md = 14 bytes + /shared.md = 100 bytes => 114 total after /a.md. + // Budget = 120: /b.md (14 bytes) would push to 128, exceeding budget. + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("load failed: %v", err) + } + + // /a.md and its import should be included. + if !strings.Contains(content, strings.Repeat("X", 100)) { + t.Fatal("expected /a.md with shared content to be included") + } + + // /b.md should be excluded because totalBytes exceeded budget after loading /a.md. + if strings.Contains(content, "B(") { + t.Fatalf("expected /b.md to be excluded due to budget, got %q", content) + } +} + +func TestNew_Validation_EmptyAgentFiles(t *testing.T) { + ctx := context.Background() + b := newMemBackend() + + _, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{}}) + if err == nil { + t.Fatal("expected error for empty agent files") + } + if !strings.Contains(err.Error(), "at least one agent file path is required") { + t.Fatalf("unexpected error message: %v", err) + } +} + +func TestMiddleware_GenerateError(t *testing.T) { + // Non-ErrNotExist errors (e.g. permission denied) should propagate. + b := &errBackend{} + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/file.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hi"}}} + _, _, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err == nil { + t.Fatal("expected error when backend read fails with non-ErrNotExist") + } + if !strings.Contains(err.Error(), "failed to load agent files") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestLoader_DuplicateTopLevelFiles(t *testing.T) { + // Same file listed twice in AgentFiles; second should be deduped via seen map. + b := newMemBackend() + b.set("/agent.md", "unique content") + + l := newLoaderConfig(b, []string{"/agent.md", "/agent.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + + count := strings.Count(content, "Contents of /agent.md") + if count != 1 { + t.Fatalf("expected /agent.md section once (deduped), got %d", count) + } +} + +func TestLoader_LoadFileError(t *testing.T) { + // Missing file (ErrNotExist) is silently skipped. + b := newMemBackend() + l := newLoaderConfig(b, []string{"/missing.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected missing file to be skipped, got error: %v", err) + } + if content != "" { + t.Fatalf("expected empty output, got %q", content) + } +} + +func TestLoader_MaxBytesStopsImports(t *testing.T) { + // When budget is exhausted, further imports in collectImports should be skipped. + b := newMemBackend() + b.set("/main.md", "@/big.md @/small.md") + b.set("/big.md", strings.Repeat("B", 200)) + b.set("/small.md", "SMALL") + + l := newLoaderConfig(b, []string{"/main.md"}, 50, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + + // main.md itself is loaded (always), big.md pushes over budget, + // small.md should be skipped. + if !strings.Contains(content, "Contents of /main.md") { + t.Fatal("main.md should be present") + } + if strings.Contains(content, "SMALL") { + t.Fatal("small.md should be skipped after budget exhausted") + } +} + +func TestFormatContent_Empty(t *testing.T) { + // formatContent with nil/empty slice should return empty string. + if got := formatContent(nil); got != "" { + t.Fatalf("expected empty string for nil, got %q", got) + } + if got := formatContent([]loadedFile{}); got != "" { + t.Fatalf("expected empty string for empty slice, got %q", got) + } +} + +func TestMiddleware_AllFilesEmpty(t *testing.T) { + // When all agent files have empty content, loader returns "" and + // BeforeModelRewriteState returns the original state unchanged. + b := newMemBackend() + b.set("/agent.md", "") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + userMsg := []*schema.Message{{Role: schema.User, Content: "hello"}} + state := &adk.ChatModelAgentState{Messages: userMsg} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + // Empty file produces no agentmd content, so original messages pass through unchanged. + if len(state.Messages) != 1 { + t.Fatalf("expected 1 message (no agentmd prepended), got %d", len(state.Messages)) + } + if state.Messages[0].Content != "hello" { + t.Fatalf("expected original message unchanged, got %q", state.Messages[0].Content) + } +} + +func TestLoader_ExactOutput(t *testing.T) { + // Verify the exact output format matches the expected structure: + // each file (top-level and imported) gets its own "Contents of ..." section, + // @path references are preserved in the original text. + b := newMemBackend() + b.set("/project/CLAUDE.md", "this is project claude.md\n\n- git workflow @git/git-instructions.md") + b.set("/project/git/git-instructions.md", "this is git-instructions.md") + + l := newLoaderConfig(b, []string{"/project/CLAUDE.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatal(err) + } + + expected := ` +As you answer the user's questions, you can use the following context: +Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written. + +Contents of /project/CLAUDE.md (instructions): + +this is project claude.md + +- git workflow @git/git-instructions.md + +Contents of /project/git/git-instructions.md (instructions): + +this is git-instructions.md +IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task. +` + + if content != expected { + t.Fatalf("output mismatch.\n\ngot:\n%s\n\nexpected:\n%s", content, expected) + } +} + +func TestLoader_MissingFileSkipped(t *testing.T) { + b := newMemBackend() + b.set("/good.md", "GOOD CONTENT") + // /missing.md is not set + + l := newLoaderConfig(b, []string{"/missing.md", "/good.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error for missing file, got %v", err) + } + if !strings.Contains(content, "GOOD CONTENT") { + t.Fatal("expected good.md content in output") + } +} + +func TestLoader_AllMissingFilesSkipped(t *testing.T) { + b := newMemBackend() + + l := newLoaderConfig(b, []string{"/missing1.md", "/missing2.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error for missing files, got %v", err) + } + if content != "" { + t.Fatalf("expected empty output when all files missing, got %q", content) + } +} + +func TestLoader_CircularImportSkipped(t *testing.T) { + b := newMemBackend() + b.set("/a.md", "A content @/b.md") + b.set("/b.md", "B content @/a.md") + + // Circular import in collectImports is logged via onWarning and skipped. + l := newLoaderConfig(b, []string{"/a.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !strings.Contains(content, "A content") { + t.Fatal("expected a.md content") + } + if !strings.Contains(content, "B content") { + t.Fatal("expected b.md content") + } +} + +func TestLoader_DepthExceededSkipped(t *testing.T) { + b := newMemBackend() + // Create a chain that exceeds maxImportDepth (5) + b.set("/l0.md", "@/l1.md") + b.set("/l1.md", "@/l2.md") + b.set("/l2.md", "@/l3.md") + b.set("/l3.md", "@/l4.md") + b.set("/l4.md", "@/l5.md") + b.set("/l5.md", "@/l6.md") + b.set("/l6.md", "DEEP") + + l := newLoaderConfig(b, []string{"/l0.md"}, 0, nil) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error for depth exceeded, got %v", err) + } + // Should have content up to the depth limit, deep file skipped. + if !strings.Contains(content, "/l0.md") { + t.Fatal("expected l0.md in output") + } +} + +func TestLoader_OnLoadWarningCallback(t *testing.T) { + b := newMemBackend() + b.set("/good.md", "GOOD CONTENT") + + var warnings []error + onWarning := func(filePath string, err error) { + warnings = append(warnings, fmt.Errorf("%s: %w", filePath, err)) + } + + l := newLoaderConfig(b, []string{"/missing.md", "/good.md"}, 0, onWarning) + content, err := l.load(context.Background()) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !strings.Contains(content, "GOOD CONTENT") { + t.Fatal("expected good.md content in output") + } + if len(warnings) == 0 { + t.Fatal("expected at least one warning for missing file") + } + if !strings.Contains(warnings[0].Error(), "file not found") { + t.Fatalf("expected file not found warning, got %v", warnings[0]) + } +} + +func TestMiddleware_MissingFile(t *testing.T) { + b := newMemBackend() + // /missing.md not set — will fail to read + + ctx := context.Background() + mw, err := New(ctx, &Config{ + Backend: b, + AgentsMDFiles: []string{"/missing.md"}, + }) + if err != nil { + t.Fatal(err) + } + + userMsg := []*schema.Message{{Role: schema.User, Content: "hello"}} + state := &adk.ChatModelAgentState{Messages: userMsg} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatalf("expected no error for missing file, got %v", err) + } + // No agent.md content, so original messages should be passed through unchanged. + if len(state.Messages) != 1 { + t.Fatalf("expected 1 message (no agentmd prepended), got %d", len(state.Messages)) + } +} + +func TestMiddleware_InsertBeforeFirstUserMessage(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + // Input has a System message before the User message. + input := []*schema.Message{ + {Role: schema.System, Content: "system prompt"}, + {Role: schema.User, Content: "hello"}, + } + state := &adk.ChatModelAgentState{Messages: input} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + if len(state.Messages) != 3 { + t.Fatalf("expected 3 messages, got %d", len(state.Messages)) + } + if state.Messages[0].Role != schema.System { + t.Fatalf("expected first message role System, got %s", state.Messages[0].Role) + } + if state.Messages[0].Content != "system prompt" { + t.Fatalf("expected system prompt preserved, got %q", state.Messages[0].Content) + } + if state.Messages[1].Role != schema.User || !strings.Contains(state.Messages[1].Content, "agent instructions") { + t.Fatalf("expected agentmd message before user message, got role=%s content=%q", state.Messages[1].Role, state.Messages[1].Content) + } + if state.Messages[2].Role != schema.User || state.Messages[2].Content != "hello" { + t.Fatalf("expected original user message at index 2, got role=%s content=%q", state.Messages[2].Role, state.Messages[2].Content) + } +} + +func TestMiddleware_InsertWithNoUserMessage(t *testing.T) { + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + // Input has no User message at all. + input := []*schema.Message{ + {Role: schema.System, Content: "system prompt"}, + {Role: schema.Assistant, Content: "assistant reply"}, + } + state := &adk.ChatModelAgentState{Messages: input} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + + if len(state.Messages) != 3 { + t.Fatalf("expected 3 messages, got %d", len(state.Messages)) + } + if state.Messages[0].Role != schema.System { + t.Fatalf("expected System at index 0, got %s", state.Messages[0].Role) + } + if state.Messages[1].Role != schema.Assistant { + t.Fatalf("expected Assistant at index 1, got %s", state.Messages[1].Role) + } + if state.Messages[2].Role != schema.User || !strings.Contains(state.Messages[2].Content, "agent instructions") { + t.Fatalf("expected agentmd appended at end, got role=%s content=%q", state.Messages[2].Role, state.Messages[2].Content) + } +} + +func TestLoader_ImportIOError(t *testing.T) { + // When an imported file returns a non-ErrNotExist error (e.g. I/O error), + // the load should propagate the error (covers collectImports and loadFile error paths). + b := &partialErrBackend{ + files: map[string]string{ + "/main.md": "content @/broken.md", + }, + // /broken.md is NOT in the map, so Read returns I/O error (not ErrNotExist) + } + + l := newLoaderConfig(b, []string{"/main.md"}, 0, nil) + _, err := l.load(context.Background()) + if err == nil { + t.Fatal("expected error from I/O failure on imported file") + } + if !strings.Contains(err.Error(), "I/O error") { + t.Fatalf("expected I/O error, got: %v", err) + } +} + +func TestMiddleware_Idempotency(t *testing.T) { + // Calling BeforeModelRewriteState twice should NOT duplicate the agentsmd message. + // The marker in msg.Extra[agentsMDExtraKey] prevents re-injection. + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hello"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after first call, got %d", len(state.Messages)) + } + + // Call again with the same state (which now contains the marker message). + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after second call (idempotent), got %d", len(state.Messages)) + } + if !strings.Contains(state.Messages[0].Content, "agent instructions") { + t.Fatalf("expected agentmd content preserved, got %q", state.Messages[0].Content) + } +} + +func TestMiddleware_ReinsertAfterRemoval(t *testing.T) { + // If the marker message is removed from state.Messages, calling + // BeforeModelRewriteState should re-insert it. + b := newMemBackend() + b.set("/agent.md", "agent instructions") + + ctx := context.Background() + mw, err := New(ctx, &Config{Backend: b, AgentsMDFiles: []string{"/agent.md"}}) + if err != nil { + t.Fatal(err) + } + + state := &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hello"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after first call, got %d", len(state.Messages)) + } + + // Simulate removal of the marker message (e.g., by summarization). + // Keep only the original user message. + state = &adk.ChatModelAgentState{Messages: []*schema.Message{{Role: schema.User, Content: "hello"}}} + _, state, err = mw.BeforeModelRewriteState(ctx, state, nil) + if err != nil { + t.Fatal(err) + } + if len(state.Messages) != 2 { + t.Fatalf("expected 2 messages after re-insert, got %d", len(state.Messages)) + } + if !strings.Contains(state.Messages[0].Content, "agent instructions") { + t.Fatalf("expected agentmd content re-inserted, got %q", state.Messages[0].Content) + } +} + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + b := newMemBackend() + b.set("/agent.md", "You are a helpful assistant.") + + mw, err := NewTyped[*schema.AgenticMessage](ctx, &Config{ + Backend: b, + AgentsMDFiles: []string{"/agent.md"}, + }) + if err != nil { + t.Fatal(err) + } + if mw == nil { + t.Fatal("expected non-nil middleware") + } + + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} diff --git a/adk/middlewares/agentsmd/loader.go b/adk/middlewares/agentsmd/loader.go new file mode 100644 index 0000000..db73338 --- /dev/null +++ b/adk/middlewares/agentsmd/loader.go @@ -0,0 +1,299 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agentsmd + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/adk/internal" +) + +// importRegex matches @path/to/file anywhere in text. +// The path must start with a letter, digit, dot, underscore, slash, or tilde, followed by +// path characters (letters, digits, dots, slashes, hyphens, underscores). +// A post-match filter further requires the path to contain "/" or end with +// an allowed extension (see allowedImportExts), so bare words like @someone +// and email-like patterns like @example.com are ignored. +var importRegex = regexp.MustCompile(`@([a-zA-Z0-9_.~/][a-zA-Z0-9_.~/\-]*)`) + +// allowedImportExts is the set of file extensions recognised as @import targets. +// Paths without "/" must end with one of these extensions to be treated as imports; +// this avoids false positives on email addresses (@example.com) and mentions (@foo.bar). +var allowedImportExts = map[string]bool{ + ".md": true, + ".txt": true, + ".mdx": true, + ".yaml": true, + ".yml": true, + ".json": true, + ".toml": true, +} + +const maxImportDepth = 5 + +// ReadRequest is an alias for filesystem.ReadRequest. +type ReadRequest = filesystem.ReadRequest +type FileContent = filesystem.FileContent + +// Backend defines the file access interface for loading Agents.md files. +// Implementations can use local filesystem, remote storage, or any other backend. +type Backend interface { + // Read reads the content of a file. + // If the file does not exist, implementations should return an error wrapping + // os.ErrNotExist (so that errors.Is(err, os.ErrNotExist) returns true). This allows the loader + // to silently skip missing files and notify via OnLoadWarning callback. + // Other errors (e.g. permission denied, I/O errors) will abort the loading process. + Read(ctx context.Context, req *ReadRequest) (*FileContent, error) +} + +// loaderConfig holds the immutable configuration for creating loaders. +// It is safe for concurrent use by multiple goroutines. +type loaderConfig struct { + backend Backend + files []string // ordered file paths from config + maxBytes int // cumulative read budget; 0 means unlimited + onWarning func(filePath string, err error) // callback for non-fatal loading warnings +} + +func newLoaderConfig(backend Backend, files []string, maxBytes int, onWarning func(filePath string, err error)) *loaderConfig { + if onWarning == nil { + onWarning = func(filePath string, err error) { + log.Printf("[agentsmd] warning: %s: %v", filePath, err) + } + } + return &loaderConfig{ + backend: backend, + files: files, + maxBytes: maxBytes, + onWarning: onWarning, + } +} + +// loader handles loading and @import resolution for agents.md files. +// A new loader is created for each load() call to avoid sharing mutable state +// (totalBytes) across concurrent invocations. +type loader struct { + *loaderConfig + totalBytes int // accumulated bytes during this load call +} + +func (cfg *loaderConfig) newLoader() *loader { + return &loader{loaderConfig: cfg} +} + +// load reads all agents.md files and returns the formatted content. +// Each top-level file and its @imported files appear as separate sections. +func (cfg *loaderConfig) load(ctx context.Context) (string, error) { + l := cfg.newLoader() + + var parts []loadedFile + seen := make(map[string]bool) // dedup across all files and imports + + for i, filePath := range l.files { + files, err := l.loadFile(ctx, filePath, 0, make(map[string]bool), seen) + if err != nil { + return "", fmt.Errorf("failed to load %q: %w", filePath, err) + } + + // If loading this file caused the budget to be exceeded, skip it + // (but always include the first file). + if i > 0 && l.maxBytes > 0 && l.totalBytes > l.maxBytes { + l.onWarning(filePath, fmt.Errorf("skipped: cumulative size %d exceeds max bytes %d", l.totalBytes, l.maxBytes)) + break + } + + parts = append(parts, files...) + } + + return formatContent(parts), nil +} + +// loadFile reads a file via Backend and collects @imported files as separate entries. +// Returns a slice where the first element is this file itself, followed by all +// transitively imported files (in encounter order, preserving @path in original text). +// visited tracks the current ancestor chain to detect circular imports. +// seen tracks globally loaded files to avoid duplicate reads and byte counting. +func (l *loader) loadFile(ctx context.Context, filePath string, depth int, visited map[string]bool, seen map[string]bool) ([]loadedFile, error) { + filePath = filepath.Clean(filePath) + + if depth > maxImportDepth { + l.onWarning(filePath, fmt.Errorf("@import depth exceeds maximum of %d", maxImportDepth)) + return nil, nil + } + + if visited[filePath] { + l.onWarning(filePath, fmt.Errorf("circular @import detected")) + return nil, nil + } + + if seen[filePath] { + return nil, nil + } + + visited[filePath] = true + defer delete(visited, filePath) + + fileContent, err := l.backend.Read(ctx, &ReadRequest{FilePath: filePath, Offset: 1}) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + l.onWarning(filePath, fmt.Errorf("file not found, skipping")) + return nil, nil + } + return nil, err + } + content := "" + if fileContent != nil { + content = fileContent.Content + } + + l.totalBytes += len(content) + seen[filePath] = true + + if content == "" { + return nil, nil + } + + // Collect imported files as separate sections (content stays untouched). + imports, err := l.collectImports(ctx, filePath, content, depth, visited, seen) + if err != nil { + return nil, err + } + + // This file first, then its imports. + result := make([]loadedFile, 0, 1+len(imports)) + result = append(result, loadedFile{path: filePath, content: content}) + result = append(result, imports...) + return result, nil +} + +// collectImports scans content for @path/to/file references and loads each +// imported file (plus its transitive imports). The original content is NOT modified. +// Returns the list of imported loadedFile entries in encounter order. +// seen is shared across the entire load call to avoid duplicate reads. +// Non-fatal errors (file not found, depth exceeded, circular import) are reported +// via onWarning and skipped. Fatal errors (e.g. I/O) are returned. +func (l *loader) collectImports(ctx context.Context, hostPath, content string, depth int, visited map[string]bool, seen map[string]bool) ([]loadedFile, error) { + dir := filepath.Dir(hostPath) + var imports []loadedFile + + matches := importRegex.FindAllStringSubmatch(content, -1) + for _, match := range matches { + rawPath := match[1] + + // Only treat as import if path contains "/" or ends with an allowed extension. + // This avoids false positives on email addresses and social mentions. + if !strings.Contains(rawPath, "/") && !allowedImportExts[filepath.Ext(rawPath)] { + continue + } + + // If budget is exhausted, skip further imports. + if l.maxBytes > 0 && l.totalBytes > l.maxBytes { + break + } + + importPath := rawPath + if !filepath.IsAbs(importPath) { + importPath = filepath.Join(dir, importPath) + } + + if seen[importPath] { + continue + } + + files, err := l.loadFile(ctx, importPath, depth+1, visited, seen) + if err != nil { + return nil, fmt.Errorf("failed to import %q from %q: %w", rawPath, hostPath, err) + } + + imports = append(imports, files...) + } + + return imports, nil +} + +type loadedFile struct { + path string + content string +} + +const formatHeaderEn = ` +As you answer the user's questions, you can use the following context: +Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written. +` + +const formatHeaderCn = ` +在回答用户问题时,你可以使用以下上下文: +代码库和用户指令如下。请务必遵守这些指令。重要提示:这些指令会覆盖任何默认行为,你必须严格按照要求执行。 +` + +const formatFileHeaderEn = "\nContents of " + +const formatFileHeaderCn = "\n文件内容:" + +const formatFileLabelEn = " (instructions):\n\n" + +const formatFileLabelCn = "(指令):\n\n" + +const formatFooterEn = `IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task. +` + +const formatFooterCn = `重要提示:此上下文可能与你的任务相关,也可能不相关。除非此上下文与你的任务高度相关,否则不要响应此上下文。 +` + +func formatContent(files []loadedFile) string { + if len(files) == 0 { + return "" + } + + header := internal.SelectPrompt(internal.I18nPrompts{ + English: formatHeaderEn, + Chinese: formatHeaderCn, + }) + fileHeader := internal.SelectPrompt(internal.I18nPrompts{ + English: formatFileHeaderEn, + Chinese: formatFileHeaderCn, + }) + fileLabel := internal.SelectPrompt(internal.I18nPrompts{ + English: formatFileLabelEn, + Chinese: formatFileLabelCn, + }) + footer := internal.SelectPrompt(internal.I18nPrompts{ + English: formatFooterEn, + Chinese: formatFooterCn, + }) + + var sb strings.Builder + sb.WriteString(header) + + for _, f := range files { + sb.WriteString(fileHeader) + sb.WriteString(f.path) + sb.WriteString(fileLabel) + sb.WriteString(f.content) + sb.WriteString("\n") + } + sb.WriteString(footer) + return sb.String() +} diff --git a/adk/middlewares/dynamictool/toolsearch/prompt.go b/adk/middlewares/dynamictool/toolsearch/prompt.go new file mode 100644 index 0000000..5aaa56a --- /dev/null +++ b/adk/middlewares/dynamictool/toolsearch/prompt.go @@ -0,0 +1,162 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package toolsearch + +const ( + toolDescription = `Search for or select deferred tools to make them available for use. + +MANDATORY PREREQUISITE - THIS IS A HARD REQUIREMENT + +You MUST use this tool to load deferred tools BEFORE calling them directly. + +This is a BLOCKING REQUIREMENT - deferred tools are NOT available until you load them using this tool. Look for messages in the conversation for the list of tools you can discover. Both query modes (keyword search and direct selection) load the returned tools — once a tool appears in the results, it is immediately available to call. + +Why this is non-negotiable: +- Deferred tools are not loaded until discovered via this tool +- Calling a deferred tool without first loading it will fail +Query modes: + +1. Keyword search - Use keywords when you're unsure which tool to use or need to discover multiple tools at once: + - "list directory" - find tools for listing directories + - "notebook jupyter" - find notebook editing tools + - "slack message" - find slack messaging tools + - Returns up to 5 matching tools ranked by relevance + - All returned tools are immediately available to call — no further selection step needed +2. Direct selection - Use select: when you know the exact tool name: + - "select:mcp__slack__read_channel" + - "select:NotebookEdit" + - "select:Read,Edit,Grep" - load multiple tools at once with comma separation + - Returns the named tool(s) if they exist +IMPORTANT: Both modes load tools equally. Do NOT follow up a keyword search with select: calls for tools already returned — they are already loaded. + +3. Required keyword - Prefix with + to require a match: + - "+linear create issue" - only tools from "linear", ranked by "create"/"issue" + - "+slack send" - only "slack" tools, ranked by "send" + - Useful when you know the service name but not the exact tool +CORRECT Usage Patterns: + + +User: I need to work with slack somehow +Assistant: Let me search for slack tools. +[Calls tool_search with query: "slack"] +Assistant: Found several options including mcp__slack__read_channel. +[Calls mcp__slack__read_channel directly — it was loaded by the keyword search] + + + +User: Edit the Jupyter notebook +Assistant: Let me load the notebook editing tool. +[Calls tool_search with query: "select:NotebookEdit"] +[Calls NotebookEdit] + + + +User: List files in the src directory +Assistant: I can see mcp__filesystem__list_directory in the available tools. Let me select it. +[Calls tool_search with query: "select:mcp__filesystem__list_directory"] +[Calls the tool] + + +INCORRECT Usage Patterns - NEVER DO THESE: + + +User: Read my slack messages +Assistant: [Directly calls mcp__slack__read_channel without loading it first] +WRONG - You must load the tool FIRST using this tool + + + +Assistant: [Calls tool_search with query: "slack", gets back mcp__slack__read_channel] +Assistant: [Calls tool_search with query: "select:mcp__slack__read_channel"] +WRONG - The keyword search already loaded the tool. The select call is redundant. +` + + toolDescriptionChinese = `搜索或选择延迟加载(deferred)的工具,使其可供调用。 + +强制前提条件(MANDATORY PREREQUISITE)— 硬性要求 + +在直接调用任何 延迟加载工具(deferred tools) 之前,你 必须先使用此工具将其加载。 + +这是一个 阻塞性要求(BLOCKING REQUIREMENT) — 延迟加载工具在被加载之前是 不可用的。你需要在对话中查找 消息,以获取可以发现的工具列表。无论使用哪种查询方式(关键字搜索 或 直接选择),只要工具出现在返回结果中,它们就会自动被加载并立即可调用。 + +为什么这是不可协商的规则: +- 延迟加载工具在被发现之前不会被加载 +- 如果你在加载之前直接调用延迟工具,调用将会失败 +查询模式: + +1. 关键字搜索(Keyword search)- 当你不确定具体需要哪个工具,或希望一次发现多个工具时使用关键字搜索: +- "list directory" — 查找用于列出目录的工具 +- "notebook jupyter" — 查找 Jupyter Notebook 编辑工具 +- "slack message" — 查找 Slack 消息相关工具 +- 返回最多 5 个最相关的工具 +- 所有返回的工具都会立即加载并可直接调用 — 不需要额外执行 select 步骤 + +2. 直接选择(Direct selection)— 当你已经知道工具的确切名称时使用 select:: +- "select:mcp__slack__read_channel" +- "select:NotebookEdit" +- "select:Read,Edit,Grep" — 一次加载多个工具 +- 如果工具存在,将被加载并返回 +重要说明:两种模式的加载效果完全相同。不要在关键词搜索之后,对返回的工具再次进行 select: 选择 — 它们已经加载好了。 + +3. 必须匹配关键字(Required keyword)— 在关键字前添加 + 可以 强制匹配特定服务或来源。 +- "+linear create issue" — 仅返回名字中包含 "linear" 的工具,按 "create" / "issue" 排序 +- "+slack send" — 仅返回名字中包含 "slack" 的工具,按 "send" 排序 +- 适用于你知道服务名称但不知道具体工具名称 + +正确使用示例: + + +User: 我需要处理 Slack 相关的事情 +Assistant: 让我搜索 Slack 工具。 +[调用 tool_search,query: "slack"] +Assistant: 找到多个选项,包括 mcp__slack__read_channel。 +[直接调用 mcp__slack__read_channel — 关键字搜索已经加载了该工具] + + + +User: 编辑这个 Jupyter Notebook +Assistant: 让我加载 Notebook 编辑工具。 +[调用 tool_search,query: "select:NotebookEdit"] +[调用 NotebookEdit] + + + +User: 列出 src 目录中的文件 +Assistant: 我看到可用工具中有 mcp__filesystem__list_directory,让我加载它。 +[调用 tool_search,query: "select:mcp__filesystem__list_directory"] +[调用该工具] + + +错误用法(严禁) + + +User: 读取我的 Slack 消息 +Assistant: [不调用 tool_search 工具加载,直接调用 mcp__slack__read_channel] +错误 — 在调用工具之前没有先使用 tool_search 加载该工具。 + + + +Assistant:[调用 tool_search,query: "slack",返回 mcp__slack__read_channel] +Assistant:[再次调用 tool_search,query: "select:mcp__slack__read_channel"] +错误 — 关键字搜索 已经加载了该工具,再次 select 是冗余操作。` + + systemReminderTpl = ` +{{- range .Tools }} +{{ . }} +{{- end }} +` +) diff --git a/adk/middlewares/dynamictool/toolsearch/toolsearch.go b/adk/middlewares/dynamictool/toolsearch/toolsearch.go new file mode 100644 index 0000000..9215b19 --- /dev/null +++ b/adk/middlewares/dynamictool/toolsearch/toolsearch.go @@ -0,0 +1,647 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package toolsearch provides tool search middleware. +package toolsearch + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "text/template" + "unicode" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +// Config is the configuration for the tool search middleware. +type Config struct { + // DynamicTools is a list of tools that can be dynamically searched and loaded by the agent. + DynamicTools []tool.BaseTool + + // UseModelToolSearch indicates whether the ChatModel natively supports tool search. + // + // When true, the middleware delegates tool search to the model's native capability. + // + // When false (default), the middleware manages tool visibility by filtering the tool list + // based on tool_search results before each model call. Note that this approach may + // invalidate the model's KV-cache (as the tool list changes between calls), and effectiveness + // depends on the model's ability to work with a dynamically changing tool set. + UseModelToolSearch bool +} + +// NewTyped constructs and returns the generic tool search middleware. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +func NewTyped[M adk.MessageType](ctx context.Context, config *Config) (adk.TypedChatModelAgentMiddleware[M], error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if len(config.DynamicTools) == 0 { + return nil, fmt.Errorf("tools is required") + } + + tpl, err := template.New("").Parse(systemReminderTpl) + if err != nil { + return nil, err + } + + dynamicToolInfos := make([]*schema.ToolInfo, 0, len(config.DynamicTools)) + mapOfDynamicTools := make(map[string]*schema.ToolInfo, len(config.DynamicTools)) + toolNames := make([]string, 0, len(config.DynamicTools)) + for _, t := range config.DynamicTools { + info, infoErr := t.Info(ctx) + if infoErr != nil { + return nil, fmt.Errorf("failed to get dynamic tool info: %w", infoErr) + } + + if _, ok := mapOfDynamicTools[info.Name]; ok { + return nil, fmt.Errorf("duplicate dynamic tool name: %s", info.Name) + } + + toolNames = append(toolNames, info.Name) + mapOfDynamicTools[info.Name] = info + dynamicToolInfos = append(dynamicToolInfos, info) + } + + buf := &bytes.Buffer{} + err = tpl.Execute(buf, systemReminder{Tools: toolNames}) + if err != nil { + return nil, fmt.Errorf("failed to format system reminder template: %w", err) + } + + return &typedMiddleware[M]{ + dynamicTools: config.DynamicTools, + mapOfDynamicTools: mapOfDynamicTools, + dynamicToolInfos: dynamicToolInfos, + useModelToolSearch: config.UseModelToolSearch, + sr: buf.String(), + }, nil +} + +// New constructs and returns the tool search middleware. +// +// The tool search middleware enables dynamic tool selection for agents with large tool libraries. +// Instead of passing all tools to the model at once (which can overwhelm context limits), +// this middleware: +// +// 1. Adds a "tool_search" meta-tool that accepts keyword queries to search tools +// 2. Initially hides all dynamic tools from the model's tool list +// 3. When the model calls tool_search, matching tools become available for subsequent calls +// +// Example usage: +// +// middleware, _ := toolsearch.New(ctx, &toolsearch.Config{ +// DynamicTools: []tool.BaseTool{weatherTool, stockTool, currencyTool, ...}, +// }) +// agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ +// // ... +// Handlers: []adk.ChatModelAgentMiddleware{middleware}, +// }) +func New(ctx context.Context, config *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped[*schema.Message](ctx, config) +} + +type systemReminder struct { + Tools []string +} + +type typedMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + dynamicTools []tool.BaseTool + mapOfDynamicTools map[string]*schema.ToolInfo + dynamicToolInfos []*schema.ToolInfo + useModelToolSearch bool + sr string +} + +func (m *typedMiddleware[M]) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) (context.Context, *adk.ChatModelAgentContext, error) { + if runCtx == nil { + return ctx, runCtx, nil + } + + nRunCtx := *runCtx + nRunCtx.Tools = make([]tool.BaseTool, len(runCtx.Tools), len(runCtx.Tools)+1+len(m.dynamicTools)) + copy(nRunCtx.Tools, runCtx.Tools) + nRunCtx.Tools = append(nRunCtx.Tools, newToolSearchTool(m.mapOfDynamicTools, m.useModelToolSearch)) + nRunCtx.Tools = append(nRunCtx.Tools, m.dynamicTools...) + if m.useModelToolSearch { + nRunCtx.ToolSearchTool = getToolSearchToolInfo() + } + return ctx, &nRunCtx, nil +} + +const toolSearchInitializedKey = "__toolsearch_initialized__" +const toolSearchReminderExtraKey = "__toolsearch_reminder__" + +func (m *typedMiddleware[M]) isInitialized(ctx context.Context) bool { + val, ok, err := adk.GetRunLocalValue(ctx, toolSearchInitializedKey) + if err != nil || !ok { + return false + } + b, _ := val.(bool) + return b +} + +func (m *typedMiddleware[M]) markInitialized(ctx context.Context) { + _ = adk.SetRunLocalValue(ctx, toolSearchInitializedKey, true) +} + +func (m *typedMiddleware[M]) ensureReminder(msgs []M) []M { + for _, msg := range msgs { + if hasToolSearchReminderExtra(msg) { + return msgs + } + } + + reminder := makeReminderMsg[M](m.sr) + result := make([]M, 0, len(msgs)+1) + inserted := false + for _, msg := range msgs { + if !inserted && !isSystemRoleTS(msg) { + inserted = true + result = append(result, reminder) + } + result = append(result, msg) + } + if !inserted { + result = append(result, reminder) + } + return result +} + +func isSystemRoleTS[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.System + case *schema.AgenticMessage: + return m.Role == schema.AgenticRoleTypeSystem + } + return false +} + +func makeReminderMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.UserMessage(content) + msg.Extra = map[string]any{toolSearchReminderExtraKey: true} + return any(msg).(M) + case *schema.AgenticMessage: + msg := schema.UserAgenticMessage(content) + msg.Extra = map[string]any{toolSearchReminderExtraKey: true} + return any(msg).(M) + } + panic("unreachable") +} + +func hasToolSearchReminderExtra[M adk.MessageType](msg M) bool { + switch v := any(msg).(type) { + case *schema.Message: + if v.Extra != nil { + if b, ok := v.Extra[toolSearchReminderExtraKey]; ok { + if bVal, _ := b.(bool); bVal { + return true + } + } + } + case *schema.AgenticMessage: + if v.Extra != nil { + if b, ok := v.Extra[toolSearchReminderExtraKey]; ok { + if bVal, _ := b.(bool); bVal { + return true + } + } + } + } + return false +} + +func (m *typedMiddleware[M]) extractDynamicTools(tools []*schema.ToolInfo) []*schema.ToolInfo { + var result []*schema.ToolInfo + for _, t := range tools { + if _, ok := m.mapOfDynamicTools[t.Name]; ok { + result = append(result, t) + } + } + return result +} + +func (m *typedMiddleware[M]) stripDynamicTools(tools []*schema.ToolInfo) []*schema.ToolInfo { + var result []*schema.ToolInfo + for _, t := range tools { + if _, ok := m.mapOfDynamicTools[t.Name]; !ok { + result = append(result, t) + } + } + return result +} + +func removeTool(tools []*schema.ToolInfo, name string) []*schema.ToolInfo { + var result []*schema.ToolInfo + for _, t := range tools { + if t.Name != name { + result = append(result, t) + } + } + return result +} + +func toolNameSet(tools []*schema.ToolInfo) map[string]bool { + m := make(map[string]bool, len(tools)) + for _, t := range tools { + m[t.Name] = true + } + return m +} + +func (m *typedMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *adk.TypedChatModelAgentState[M], _ *adk.TypedModelContext[M]) (context.Context, *adk.TypedChatModelAgentState[M], error) { + state.Messages = m.ensureReminder(state.Messages) + + if !m.isInitialized(ctx) { + m.markInitialized(ctx) + + if m.useModelToolSearch { + // Model-native search: move dynamic tools to DeferredToolInfos for server-side retrieval, + // keep only static tools in ToolInfos, and remove the tool_search tool (the model handles search itself). + state.DeferredToolInfos = m.extractDynamicTools(state.ToolInfos) + state.ToolInfos = m.stripDynamicTools(state.ToolInfos) + state.ToolInfos = removeTool(state.ToolInfos, toolSearchToolName) + } else { + // Client-side search: hide dynamic tools initially; they become visible + // only after the model calls tool_search and forward selection adds them back. + state.ToolInfos = m.stripDynamicTools(state.ToolInfos) + } + } + + // Forward selection (client-side search only): scan tool_search results in the + // conversation history and add the selected dynamic tools back to ToolInfos. + if !m.useModelToolSearch { + existing := toolNameSet(state.ToolInfos) + for _, msg := range state.Messages { + content, ok := extractToolSearchResult(msg, toolSearchToolName) + if !ok { + continue + } + var result toolSearchResult + if err := json.Unmarshal([]byte(content), &result); err != nil { + continue + } + for _, name := range result.Matches { + if existing[name] { + continue + } + if info, ok := m.mapOfDynamicTools[name]; ok { + state.ToolInfos = append(state.ToolInfos, info) + existing[name] = true + } + } + } + } + + return ctx, state, nil +} + +// extractToolSearchResult checks if the given message is a tool result from the tool_search tool, +// and if so returns the content string. Returns ("", false) if not a matching tool result. +func extractToolSearchResult[M adk.MessageType](msg M, toolName string) (string, bool) { + switch v := any(msg).(type) { + case *schema.Message: + if v.Role == schema.Tool && v.ToolName == toolName { + return v.Content, true + } + case *schema.AgenticMessage: + for _, block := range v.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolResult && + block.FunctionToolResult != nil && block.FunctionToolResult.Name == toolName { + for _, b := range block.FunctionToolResult.Content { + if b != nil && b.Text != nil { + return b.Text.Text, true + } + } + } + } + } + return "", false +} + +func newToolSearchTool(tools map[string]*schema.ToolInfo, useModelToolSearch bool) tool.BaseTool { + if useModelToolSearch { + return &modelToolSearchTool{tools: tools} + } + return &toolSearchTool{tools: tools} +} + +type toolSearchArgs struct { + Query string `json:"query"` + MaxResults *int `json:"max_results,omitempty"` +} + +type toolSearchResult struct { + Matches []string `json:"matches"` +} + +type toolSearchTool struct { + tools map[string]*schema.ToolInfo +} + +func (t *toolSearchTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return getToolSearchToolInfo(), nil +} + +func (t *toolSearchTool) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + matches, err := search(argumentsInJSON, t.tools) + if err != nil { + return "", err + } + result := &toolSearchResult{} + for _, m := range matches { + result.Matches = append(result.Matches, m.Name) + } + b, err := json.Marshal(result) + if err != nil { + return "", fmt.Errorf("failed to marshal tool search result: %w", err) + } + return string(b), nil +} + +type modelToolSearchTool struct { + tools map[string]*schema.ToolInfo +} + +func (t *modelToolSearchTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return getToolSearchToolInfo(), nil +} + +func (t *modelToolSearchTool) InvokableRun(_ context.Context, argumentsInJSON *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + ret, err := search(argumentsInJSON.Text, t.tools) + if err != nil { + return nil, err + } + + return &schema.ToolResult{Parts: []schema.ToolOutputPart{ + { + Type: schema.ToolPartTypeToolSearchResult, + ToolSearchResult: &schema.ToolSearchResult{ + Tools: ret, + }, + }, + }}, nil +} + +const ( + toolSearchToolName = "tool_search" + defaultMaxResults = 5 +) + +func getToolSearchToolInfo() *schema.ToolInfo { + return &schema.ToolInfo{ + Name: toolSearchToolName, + Desc: internal.SelectPrompt(internal.I18nPrompts{ + English: toolDescription, + Chinese: toolDescriptionChinese, + }), + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": { + Type: schema.String, + Desc: "Query to find deferred tools. Use \"select:\" for direct selection, or keywords to search.", + Required: true, + }, + "max_results": { + Type: schema.Integer, + Desc: "Maximum number of results to return (default: 5)", + Required: false, + }, + }), + } +} + +func search(argumentsInJSON string, tools map[string]*schema.ToolInfo) ([]*schema.ToolInfo, error) { + var args toolSearchArgs + if err := json.Unmarshal([]byte(argumentsInJSON), &args); err != nil { + return nil, fmt.Errorf("failed to unmarshal tool search arguments: %w", err) + } + + query := strings.TrimSpace(args.Query) + if query == "" { + return nil, fmt.Errorf("query is required") + } + + maxResults := defaultMaxResults + if args.MaxResults != nil && *args.MaxResults > 0 { + maxResults = *args.MaxResults + } + + var matches []string + + // Direct selection mode: select:tool1,tool2 + // max_results is intentionally not applied here because the model has + // already specified the exact tools it wants by name. + if strings.HasPrefix(query, "select:") { + names := strings.Split(strings.TrimPrefix(query, "select:"), ",") + toolSet := make(map[string]bool, len(tools)) + for name := range tools { + toolSet[name] = true + } + for _, name := range names { + name = strings.TrimSpace(name) + if name != "" && toolSet[name] { + matches = append(matches, name) + } + } + } else { + matches = keywordSearch(query, maxResults, tools) + } + + ret := make([]*schema.ToolInfo, 0, len(matches)) + for _, name := range matches { + ti, ok := tools[name] + if !ok { + continue + } + ret = append(ret, ti) + } + return ret, nil +} + +func intMax(a, b int) int { + if a > b { + return a + } + return b +} + +func intMin(a, b int) int { + if a < b { + return a + } + return b +} + +// scoredTool pairs a tool name with its search score. +type scoredTool struct { + name string + score int +} + +// keywordSearch scores all tools against the query keywords and returns the top N. +func keywordSearch(query string, maxResults int, tools map[string]*schema.ToolInfo) []string { + keywords := parseKeywords(query) + if len(keywords) == 0 { + return nil + } + + var scored []scoredTool + + for name, tm := range tools { + nameParts := splitToolName(name) + nameLower := strings.ToLower(name) + descLower := strings.ToLower(tm.Desc) + + totalScore := 0 + allRequiredFound := true + + for _, kw := range keywords { + kwLower := strings.ToLower(kw.word) + kwScore := 0 + + // Score against name parts + for _, part := range nameParts { + partLower := strings.ToLower(part) + if partLower == kwLower { + kwScore = intMax(kwScore, 10) + } else if strings.Contains(partLower, kwLower) { + kwScore = intMax(kwScore, 5) + } + } + + // Score against full name + if strings.Contains(nameLower, kwLower) { + kwScore = intMax(kwScore, 3) + } + + // Score against description (substring match) + if descLower != "" && strings.Contains(descLower, kwLower) { + kwScore = intMax(kwScore, 2) + } + + if kw.required && kwScore == 0 { + allRequiredFound = false + break + } + + totalScore += kwScore + } + + if !allRequiredFound { + continue + } + + if totalScore > 0 { + scored = append(scored, scoredTool{name: name, score: totalScore}) + } + } + + // Sort by score descending, then by name for stability + sort.Slice(scored, func(i, j int) bool { + if scored[i].score != scored[j].score { + return scored[i].score > scored[j].score + } + return scored[i].name < scored[j].name + }) + + results := make([]string, 0, intMin(maxResults, len(scored))) + for i := 0; i < len(scored) && i < maxResults; i++ { + results = append(results, scored[i].name) + } + return results +} + +// keyword represents a parsed search keyword. +type keyword struct { + word string + required bool +} + +// parseKeywords splits a query string into keywords, handling the '+' required prefix. +func parseKeywords(query string) (keywords []keyword) { + parts := strings.Fields(query) + for _, p := range parts { + if strings.HasPrefix(p, "+") { + word := strings.TrimPrefix(p, "+") + if word != "" { + keywords = append(keywords, keyword{word: word, required: true}) + } + } else if p != "" { + keywords = append(keywords, keyword{word: p, required: false}) + } + } + return +} + +// splitToolName splits a tool name into parts by underscores, double underscores (MCP separator), +// and camelCase boundaries. +func splitToolName(name string) []string { + // First split by double underscore (MCP server__tool separator) + segments := strings.Split(name, "__") + + var parts []string + for _, seg := range segments { + // Split each segment by single underscore + underscoreParts := strings.Split(seg, "_") + for _, up := range underscoreParts { + if up == "" { + continue + } + // Further split by camelCase + camelParts := splitCamelCase(up) + parts = append(parts, camelParts...) + } + } + return parts +} + +// splitCamelCase splits a camelCase or PascalCase string into its constituent words. +func splitCamelCase(s string) []string { + if s == "" { + return nil + } + + var parts []string + runes := []rune(s) + start := 0 + + for i := 1; i < len(runes); i++ { + if unicode.IsUpper(runes[i]) { + if unicode.IsLower(runes[i-1]) { + parts = append(parts, string(runes[start:i])) + start = i + } else if i+1 < len(runes) && unicode.IsLower(runes[i+1]) { + parts = append(parts, string(runes[start:i])) + start = i + } + } + } + parts = append(parts, string(runes[start:])) + + return parts +} diff --git a/adk/middlewares/dynamictool/toolsearch/toolsearch_generic_test.go b/adk/middlewares/dynamictool/toolsearch/toolsearch_generic_test.go new file mode 100644 index 0000000..a659f07 --- /dev/null +++ b/adk/middlewares/dynamictool/toolsearch/toolsearch_generic_test.go @@ -0,0 +1,404 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package toolsearch + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +// --------------------------------------------------------------------------- +// Generic table-driven tests covering both *schema.Message and *schema.AgenticMessage +// --------------------------------------------------------------------------- + +// --- Generic message construction helpers --- + +func makeUserMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.UserMessage(content)).(M) + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(content)).(M) + default: + panic("unreachable") + } +} + +func makeSystemMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{Role: schema.System, Content: content}).(M) + case *schema.AgenticMessage: + return any(schema.SystemAgenticMessage(content)).(M) + default: + panic("unreachable") + } +} + +type testToolCall struct { + ID string + Name string + Arguments string +} + +func makeAssistantMsgWithToolCalls[M adk.MessageType](toolCalls []testToolCall) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + tcs := make([]schema.ToolCall, len(toolCalls)) + for i, tc := range toolCalls { + tcs[i] = schema.ToolCall{ + ID: tc.ID, + Function: schema.FunctionCall{Name: tc.Name, Arguments: tc.Arguments}, + } + } + return any(schema.AssistantMessage("", tcs)).(M) + case *schema.AgenticMessage: + blocks := make([]*schema.ContentBlock, len(toolCalls)) + for i, tc := range toolCalls { + blocks[i] = schema.NewContentBlock(&schema.FunctionToolCall{ + CallID: tc.ID, + Name: tc.Name, + Arguments: tc.Arguments, + }) + } + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: blocks, + }).(M) + default: + panic("unreachable") + } +} + +func makeToolResultMsg[M adk.MessageType](content string, callID string, toolName string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{ + Role: schema.Tool, + ToolName: toolName, + ToolCallID: callID, + Content: content, + }).(M) + case *schema.AgenticMessage: + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: callID, + Name: toolName, + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: content}}, + }, + }), + }, + }).(M) + default: + panic("unreachable") + } +} + +func getMsgRole[M adk.MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return string(v.Role) + case *schema.AgenticMessage: + return string(v.Role) + default: + panic("unreachable") + } +} + +func getMsgContent[M adk.MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return v.Content + case *schema.AgenticMessage: + for _, block := range v.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeUserInputText && block.UserInputText != nil { + return block.UserInputText.Text + } + } + return "" + default: + panic("unreachable") + } +} + +func getMsgExtra[M adk.MessageType](msg M) map[string]any { + switch v := any(msg).(type) { + case *schema.Message: + return v.Extra + case *schema.AgenticMessage: + return v.Extra + default: + panic("unreachable") + } +} + +func setMsgExtra[M adk.MessageType](msg M, key string, val any) { + switch v := any(msg).(type) { + case *schema.Message: + if v.Extra == nil { + v.Extra = make(map[string]any) + } + v.Extra[key] = val + case *schema.AgenticMessage: + if v.Extra == nil { + v.Extra = make(map[string]any) + } + v.Extra[key] = val + default: + panic("unreachable") + } +} + +func newTestMiddlewareTyped[M adk.MessageType](t *testing.T, tools []tool.BaseTool) *typedMiddleware[M] { + t.Helper() + ctx := context.Background() + mw, err := NewTyped[M](ctx, &Config{ + DynamicTools: tools, + UseModelToolSearch: false, + }) + require.NoError(t, err) + return mw.(*typedMiddleware[M]) +} + +func countRemindersGeneric[M adk.MessageType](msgs []M) int { + count := 0 + for _, msg := range msgs { + extra := getMsgExtra(msg) + if extra != nil { + if v, _ := extra[toolSearchReminderExtraKey].(bool); v { + count++ + } + } + } + return count +} + +// --- Generic test functions --- + +func testEnsureReminderGeneric[M adk.MessageType](t *testing.T) { + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + m := newTestMiddlewareTyped[M](t, []tool.BaseTool{dynamicA}) + + t.Run("normal: system then user", func(t *testing.T) { + input := []M{ + makeSystemMsg[M]("sys"), + makeUserMsg[M]("hi"), + } + got := m.ensureReminder(input) + require.Len(t, got, 3) + assert.Equal(t, "system", getMsgRole(got[0])) + // Reminder inserted after system + extra := getMsgExtra(got[1]) + require.NotNil(t, extra) + assert.Equal(t, true, extra[toolSearchReminderExtraKey]) + assert.Equal(t, "hi", getMsgContent(got[2])) + }) + + t.Run("all system messages", func(t *testing.T) { + input := []M{ + makeSystemMsg[M]("sys1"), + makeSystemMsg[M]("sys2"), + } + got := m.ensureReminder(input) + require.Len(t, got, 3) + assert.Equal(t, "system", getMsgRole(got[0])) + assert.Equal(t, "system", getMsgRole(got[1])) + // Reminder appended at end + extra := getMsgExtra(got[2]) + require.NotNil(t, extra) + assert.Equal(t, true, extra[toolSearchReminderExtraKey]) + }) + + t.Run("empty input", func(t *testing.T) { + got := m.ensureReminder(nil) + require.Len(t, got, 1) + extra := getMsgExtra(got[0]) + require.NotNil(t, extra) + assert.Equal(t, true, extra[toolSearchReminderExtraKey]) + }) + + t.Run("no system messages", func(t *testing.T) { + input := []M{ + makeUserMsg[M]("hi"), + } + got := m.ensureReminder(input) + require.Len(t, got, 2) + // Reminder inserted at position 0 + extra := getMsgExtra(got[0]) + require.NotNil(t, extra) + assert.Equal(t, true, extra[toolSearchReminderExtraKey]) + assert.Equal(t, "hi", getMsgContent(got[1])) + }) + + t.Run("idempotent: does not insert twice", func(t *testing.T) { + reminder := makeUserMsg[M]("") + setMsgExtra(reminder, toolSearchReminderExtraKey, true) + input := []M{ + reminder, + makeUserMsg[M]("hi"), + } + got := m.ensureReminder(input) + require.Len(t, got, 2) + assert.Equal(t, "hi", getMsgContent(got[1])) + }) +} + +func testMode1InitializationGeneric[M adk.MessageType](t *testing.T) { + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + + m := newTestMiddlewareTyped[M](t, []tool.BaseTool{dynamicA, dynamicB}) + + ctx := context.Background() + + state := &adk.TypedChatModelAgentState[M]{ + Messages: []M{ + makeSystemMsg[M]("sys"), + makeUserMsg[M]("hello"), + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + ti("dynamic_tool_a", "Dynamic tool A"), + ti("dynamic_tool_b", "Dynamic tool B"), + }, + } + + // Initialization strips dynamic tools, keeps tool_search and static tools. + _, state, err := m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names := toolNames(state.ToolInfos) + assert.Equal(t, []string{"static_tool", "tool_search"}, names) + assert.Nil(t, state.DeferredToolInfos, "Mode 1 should not populate DeferredToolInfos") + + // Verify reminder was inserted. + assert.Equal(t, 1, countRemindersGeneric(state.Messages), "reminder should be inserted") +} + +func testMode1ForwardSelectionGeneric[M adk.MessageType](t *testing.T) { + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + + m := newTestMiddlewareTyped[M](t, []tool.BaseTool{dynamicA, dynamicB}) + + ctx := context.Background() + + // Simulate state AFTER initialization (dynamic tools already stripped). + // Include a tool_search result message that selected dynamic_tool_a. + toolSearchResultJSON, _ := json.Marshal(toolSearchResult{Matches: []string{"dynamic_tool_a"}}) + + // Build the reminder message with the extra marker + reminderMsg := makeUserMsg[M]("hello") + setMsgExtra(reminderMsg, toolSearchReminderExtraKey, true) + + state := &adk.TypedChatModelAgentState[M]{ + Messages: []M{ + makeSystemMsg[M]("sys"), + reminderMsg, + makeAssistantMsgWithToolCalls[M]([]testToolCall{ + {ID: "tc1", Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}, + }), + makeToolResultMsg[M](string(toolSearchResultJSON), "tc1", toolSearchToolName), + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + }, + } + + // Forward selection should add dynamic_tool_a from the tool_search result. + _, state, err := m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names := toolNames(state.ToolInfos) + assert.Equal(t, []string{"dynamic_tool_a", "static_tool", "tool_search"}, names) + + // Call again: forward selection should be idempotent (dynamic_tool_a already present). + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names = toolNames(state.ToolInfos) + assert.Equal(t, []string{"dynamic_tool_a", "static_tool", "tool_search"}, names) +} + +func testMalformedJSONGeneric[M adk.MessageType](t *testing.T) { + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + + m := newTestMiddlewareTyped[M](t, []tool.BaseTool{dynamicA}) + + ctx := context.Background() + + // Build the reminder message with the extra marker + reminderMsg := makeUserMsg[M]("reminder") + setMsgExtra(reminderMsg, toolSearchReminderExtraKey, true) + + state := &adk.TypedChatModelAgentState[M]{ + Messages: []M{ + makeSystemMsg[M]("sys"), + reminderMsg, + makeAssistantMsgWithToolCalls[M]([]testToolCall{ + {ID: "tc1", Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}, + }), + makeToolResultMsg[M](`{invalid json!!!`, "tc1", toolSearchToolName), + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + }, + } + + _, state, err := m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err, "malformed JSON in tool_search result should not cause an error") + + names := toolNames(state.ToolInfos) + assert.NotContains(t, names, "dynamic_tool_a", "malformed JSON result should be skipped") + assert.Contains(t, names, "static_tool") + assert.Contains(t, names, "tool_search") +} + +// --- Top-level generic test runner --- + +func TestToolSearchGeneric(t *testing.T) { + t.Run("Message", func(t *testing.T) { + t.Run("EnsureReminder", testEnsureReminderGeneric[*schema.Message]) + t.Run("Mode1Init", testMode1InitializationGeneric[*schema.Message]) + t.Run("Mode1ForwardSelection", testMode1ForwardSelectionGeneric[*schema.Message]) + t.Run("MalformedJSON", testMalformedJSONGeneric[*schema.Message]) + }) + t.Run("AgenticMessage", func(t *testing.T) { + t.Run("EnsureReminder", testEnsureReminderGeneric[*schema.AgenticMessage]) + t.Run("Mode1Init", testMode1InitializationGeneric[*schema.AgenticMessage]) + t.Run("Mode1ForwardSelection", testMode1ForwardSelectionGeneric[*schema.AgenticMessage]) + t.Run("MalformedJSON", testMalformedJSONGeneric[*schema.AgenticMessage]) + }) +} diff --git a/adk/middlewares/dynamictool/toolsearch/toolsearch_test.go b/adk/middlewares/dynamictool/toolsearch/toolsearch_test.go new file mode 100644 index 0000000..4bd1410 --- /dev/null +++ b/adk/middlewares/dynamictool/toolsearch/toolsearch_test.go @@ -0,0 +1,1042 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package toolsearch + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +func makeToolMap(tools ...*schema.ToolInfo) map[string]*schema.ToolInfo { + m := make(map[string]*schema.ToolInfo, len(tools)) + for _, t := range tools { + m[t.Name] = t + } + return m +} + +func ti(name, desc string) *schema.ToolInfo { + return &schema.ToolInfo{Name: name, Desc: desc} +} + +func toolNames(infos []*schema.ToolInfo) []string { + names := make([]string, len(infos)) + for i, info := range infos { + names[i] = info.Name + } + sort.Strings(names) + return names +} + +func searchJSON(query string, maxResults *int) string { + args := toolSearchArgs{Query: query, MaxResults: maxResults} + b, _ := json.Marshal(args) + return string(b) +} + +func intPtr(v int) *int { return &v } + +// --------------------------------------------------------------------------- +// TestSearch — unit tests for the search() function +// --------------------------------------------------------------------------- + +func TestSearch(t *testing.T) { + tools := makeToolMap( + ti("get_weather", "Get current weather for a city"), + ti("search_flights", "Search available flights"), + ti("mcp__slack__send_message", "Send a message to Slack channel"), + ti("mcp__slack__read_channel", "Read messages from Slack channel"), + ti("create_calendar_event", "Create a new calendar event"), + ti("NotebookEdit", "Edit Jupyter notebook cells"), + ) + + tests := []struct { + name string + json string + wantNames []string // sorted; nil means expect empty + wantErr bool + }{ + { + name: "keyword exact name part match", + json: searchJSON("weather", nil), + wantNames: []string{"get_weather"}, + }, + { + name: "keyword matches multiple tools", + json: searchJSON("slack", nil), + wantNames: []string{"mcp__slack__read_channel", "mcp__slack__send_message"}, + }, + { + name: "multi-word ranking - send_message ranked first", + json: searchJSON("send message", nil), + wantNames: []string{"mcp__slack__send_message"}, // check first element only + }, + { + name: "required keyword filters to slack only", + json: searchJSON("+slack send", nil), + wantNames: []string{"mcp__slack__read_channel", "mcp__slack__send_message"}, + }, + { + name: "required keyword no match", + json: searchJSON("+github send", nil), + wantNames: nil, + }, + { + name: "direct select single", + json: searchJSON("select:get_weather", nil), + wantNames: []string{"get_weather"}, + }, + { + name: "direct select multiple", + json: searchJSON("select:get_weather,NotebookEdit", nil), + wantNames: []string{"NotebookEdit", "get_weather"}, + }, + { + name: "direct select nonexistent", + json: searchJSON("select:nonexistent", nil), + wantNames: nil, + }, + { + name: "max_results limits output", + json: searchJSON("slack", intPtr(1)), + wantNames: []string{"mcp__slack__read_channel"}, // just check length below + }, + { + name: "camelCase split matches notebook", + json: searchJSON("notebook", nil), + wantNames: []string{"NotebookEdit"}, + }, + { + name: "empty query returns error", + json: searchJSON("", nil), + wantErr: true, + }, + { + name: "description match - jupyter", + json: searchJSON("jupyter", nil), + wantNames: []string{"NotebookEdit"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := search(tt.json, tools) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + // special case: max_results limit + if tt.name == "max_results limits output" { + assert.Len(t, got, 1) + return + } + + // special case: ranking — just check first element + if tt.name == "multi-word ranking - send_message ranked first" { + require.NotEmpty(t, got) + assert.Equal(t, "mcp__slack__send_message", got[0].Name) + return + } + + gotNames := toolNames(got) + if tt.wantNames == nil { + assert.Empty(t, gotNames) + } else { + assert.Equal(t, tt.wantNames, gotNames) + } + }) + } +} + +// --------------------------------------------------------------------------- +// TestMiddlewareFlow — integration test for UseModelToolSearch=false +// --------------------------------------------------------------------------- + +// simpleTool is a minimal InvokableTool for testing. +type simpleTool struct { + name string + desc string + called bool + mu sync.Mutex +} + +func (s *simpleTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: s.name, + Desc: s.desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: schema.String, Desc: "input", Required: true}, + }), + }, nil +} + +func (s *simpleTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + s.mu.Lock() + s.called = true + s.mu.Unlock() + return `{"result":"ok"}`, nil +} + +func (s *simpleTool) wasCalled() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.called +} + +// mockChatModel implements model.ToolCallingChatModel. +// It drives a 3-turn conversation: +// +// Turn 1: call tool_search with select:dynamic_tool_a +// Turn 2: call dynamic_tool_a +// Turn 3: return final text +type mockChatModel struct { + mu sync.Mutex + generateCall int + // toolsPerCall records the tool names passed via model.WithTools for each Generate call. + toolsPerCall [][]string +} + +func (m *mockChatModel) Generate(_ context.Context, _ []*schema.Message, opts ...model.Option) (*schema.Message, error) { + options := model.GetCommonOptions(nil, opts...) + var names []string + for _, t := range options.Tools { + names = append(names, t.Name) + } + sort.Strings(names) + + m.mu.Lock() + m.generateCall++ + call := m.generateCall + m.toolsPerCall = append(m.toolsPerCall, names) + m.mu.Unlock() + + switch call { + case 1: + // Ask tool_search to select dynamic_tool_a + return schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "tc1", + Function: schema.FunctionCall{ + Name: toolSearchToolName, + Arguments: `{"query":"select:dynamic_tool_a","max_results":5}`, + }, + }, + }), nil + case 2: + // Call dynamic_tool_a + return schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "tc2", + Function: schema.FunctionCall{ + Name: "dynamic_tool_a", + Arguments: `{"input":"hello"}`, + }, + }, + }), nil + default: + // Final response + return schema.AssistantMessage("done", nil), nil + } +} + +func (m *mockChatModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, fmt.Errorf("not implemented") +} + +func (m *mockChatModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +func (m *mockChatModel) getToolsPerCall() [][]string { + m.mu.Lock() + defer m.mu.Unlock() + ret := make([][]string, len(m.toolsPerCall)) + copy(ret, m.toolsPerCall) + return ret +} + +func TestMiddlewareFlow(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + staticTool := &simpleTool{name: "static_tool", desc: "Static tool"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA, dynamicB}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + cm := &mockChatModel{} + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "test_agent", + Description: "test", + Instruction: "you are a test agent", + Model: cm, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{staticTool}, + }, + }, + Handlers: []adk.ChatModelAgentMiddleware{mw}, + }) + require.NoError(t, err) + + input := &adk.AgentInput{ + Messages: []adk.Message{schema.UserMessage("test")}, + } + iter := agent.Run(ctx, input) + + var events []*adk.AgentEvent + for { + ev, ok := iter.Next() + if !ok { + break + } + events = append(events, ev) + } + + // Verify no error event. + for _, ev := range events { + if ev.Err != nil { + t.Fatalf("unexpected error event: %v", ev.Err) + } + } + + // Verify final output is "done". + lastEvent := events[len(events)-1] + require.NotNil(t, lastEvent.Output) + require.NotNil(t, lastEvent.Output.MessageOutput) + assert.Equal(t, "done", lastEvent.Output.MessageOutput.Message.Content) + + // Verify dynamic_tool_a was actually called. + assert.True(t, dynamicA.wasCalled(), "dynamic_tool_a should have been called") + assert.False(t, dynamicB.wasCalled(), "dynamic_tool_b should not have been called") + + // Verify tool lists per Generate call. + toolsPerCall := cm.getToolsPerCall() + require.Len(t, toolsPerCall, 3, "expected 3 Generate calls") + + // Call 1: static_tool visible; dynamic tools are hidden. + assert.Contains(t, toolsPerCall[0], "static_tool") + assert.NotContains(t, toolsPerCall[0], "dynamic_tool_a") + assert.NotContains(t, toolsPerCall[0], "dynamic_tool_b") + + // Call 2: after selecting dynamic_tool_a, it becomes visible. + assert.Contains(t, toolsPerCall[1], "static_tool") + assert.Contains(t, toolsPerCall[1], "dynamic_tool_a") + assert.NotContains(t, toolsPerCall[1], "dynamic_tool_b") + + // Call 3: same as call 2. + assert.Contains(t, toolsPerCall[2], "static_tool") + assert.Contains(t, toolsPerCall[2], "dynamic_tool_a") + assert.NotContains(t, toolsPerCall[2], "dynamic_tool_b") + + // Verify reminder is present in messages (checked via tool list — the wrapper inserts it). + // The model received messages, and the reminder contains "". + // We indirectly verify this by checking that the middleware ran without error and the + // 3-turn flow completed successfully, which requires the tool_search tool to work. + + // Additional: verify that the reminder contains the dynamic tool names. + mwImpl := mw.(*typedMiddleware[*schema.Message]) + assert.True(t, strings.Contains(mwImpl.sr, "dynamic_tool_a")) + assert.True(t, strings.Contains(mwImpl.sr, "dynamic_tool_b")) + assert.True(t, strings.Contains(mwImpl.sr, "")) +} + +// --------------------------------------------------------------------------- +// TestNew — error paths for New() +// --------------------------------------------------------------------------- + +func TestNew(t *testing.T) { + ctx := context.Background() + + t.Run("nil config", func(t *testing.T) { + _, err := New(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") + }) + + t.Run("empty DynamicTools", func(t *testing.T) { + _, err := New(ctx, &Config{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "tools is required") + }) + + t.Run("success", func(t *testing.T) { + st := &simpleTool{name: "t1", desc: "tool 1"} + mw, err := New(ctx, &Config{DynamicTools: []tool.BaseTool{st}}) + require.NoError(t, err) + assert.NotNil(t, mw) + }) +} + +// --------------------------------------------------------------------------- +// TestSplitCamelCase +// --------------------------------------------------------------------------- + +func TestSplitCamelCase(t *testing.T) { + tests := []struct { + input string + want []string + }{ + {"", nil}, + {"hello", []string{"hello"}}, + {"NotebookEdit", []string{"Notebook", "Edit"}}, + {"camelCase", []string{"camel", "Case"}}, + {"HTMLParser", []string{"HTML", "Parser"}}, + {"getURL", []string{"get", "URL"}}, + {"A", []string{"A"}}, + {"AB", []string{"AB"}}, + {"HTTP", []string{"HTTP"}}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := splitCamelCase(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +// --------------------------------------------------------------------------- +// TestEnsureReminder +// --------------------------------------------------------------------------- + +func TestEnsureReminder(t *testing.T) { + m := &typedMiddleware[*schema.Message]{sr: ""} + + t.Run("normal: system then user", func(t *testing.T) { + input := []*schema.Message{ + {Role: schema.System, Content: "sys"}, + {Role: schema.User, Content: "hi"}, + } + got := m.ensureReminder(input) + require.Len(t, got, 3) + assert.Equal(t, schema.System, got[0].Role) + assert.Equal(t, schema.User, got[1].Role) + assert.Equal(t, "", got[1].Content) + assert.Equal(t, true, got[1].Extra[toolSearchReminderExtraKey]) + assert.Equal(t, schema.User, got[2].Role) + assert.Equal(t, "hi", got[2].Content) + }) + + t.Run("all system messages", func(t *testing.T) { + input := []*schema.Message{ + {Role: schema.System, Content: "sys1"}, + {Role: schema.System, Content: "sys2"}, + } + got := m.ensureReminder(input) + require.Len(t, got, 3) + assert.Equal(t, schema.System, got[0].Role) + assert.Equal(t, schema.System, got[1].Role) + assert.Equal(t, "", got[2].Content) + }) + + t.Run("empty input", func(t *testing.T) { + got := m.ensureReminder(nil) + require.Len(t, got, 1) + assert.Equal(t, "", got[0].Content) + }) + + t.Run("no system messages", func(t *testing.T) { + input := []*schema.Message{ + {Role: schema.User, Content: "hi"}, + {Role: schema.Assistant, Content: "hello"}, + } + got := m.ensureReminder(input) + require.Len(t, got, 3) + assert.Equal(t, "", got[0].Content) + assert.Equal(t, "hi", got[1].Content) + assert.Equal(t, "hello", got[2].Content) + }) + + t.Run("idempotent: does not insert twice", func(t *testing.T) { + input := []*schema.Message{ + {Role: schema.User, Content: "", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + {Role: schema.User, Content: "hi"}, + } + got := m.ensureReminder(input) + require.Len(t, got, 2) + assert.Equal(t, "", got[0].Content) + assert.Equal(t, "hi", got[1].Content) + }) +} + +// --------------------------------------------------------------------------- +// TestHelperFunctions +// --------------------------------------------------------------------------- + +func TestHelperFunctions(t *testing.T) { + t.Run("extractDynamicTools", func(t *testing.T) { + m := &typedMiddleware[*schema.Message]{ + mapOfDynamicTools: map[string]*schema.ToolInfo{ + "dyn_a": ti("dyn_a", "A"), + "dyn_b": ti("dyn_b", "B"), + }, + } + tools := []*schema.ToolInfo{ti("static", "S"), ti("dyn_a", "A"), ti("dyn_b", "B")} + got := m.extractDynamicTools(tools) + assert.Len(t, got, 2) + names := toolNames(got) + assert.Equal(t, []string{"dyn_a", "dyn_b"}, names) + }) + + t.Run("stripDynamicTools", func(t *testing.T) { + m := &typedMiddleware[*schema.Message]{ + mapOfDynamicTools: map[string]*schema.ToolInfo{ + "dyn_a": ti("dyn_a", "A"), + "dyn_b": ti("dyn_b", "B"), + }, + } + tools := []*schema.ToolInfo{ti("static", "S"), ti("dyn_a", "A"), ti("tool_search", "TS")} + got := m.stripDynamicTools(tools) + names := toolNames(got) + assert.Equal(t, []string{"static", "tool_search"}, names) + }) + + t.Run("removeTool", func(t *testing.T) { + tools := []*schema.ToolInfo{ti("a", "A"), ti("b", "B"), ti("c", "C")} + got := removeTool(tools, "b") + names := toolNames(got) + assert.Equal(t, []string{"a", "c"}, names) + }) + + t.Run("toolNameSet", func(t *testing.T) { + tools := []*schema.ToolInfo{ti("x", "X"), ti("y", "Y")} + got := toolNameSet(tools) + assert.True(t, got["x"]) + assert.True(t, got["y"]) + assert.False(t, got["z"]) + }) +} + +// --------------------------------------------------------------------------- +// TestBeforeModelRewriteState — direct unit tests for BeforeModelRewriteState +// --------------------------------------------------------------------------- + +// Note: these tests call BeforeModelRewriteState without a full compose context, +// so RunLocalValue (used by isInitialized/markInitialized) always returns error. +// This means every call re-runs the initialization block. Tests are designed +// accordingly: they test single-call behavior or provide pre-initialized state. + +func TestBeforeModelRewriteState_Mode1_Initialization(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA, dynamicB}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + // Simulate state: static_tool + tool_search + dynamic tools (as would come from backfill). + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.System, Content: "sys"}, + {Role: schema.User, Content: "hello"}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + ti("dynamic_tool_a", "Dynamic tool A"), + ti("dynamic_tool_b", "Dynamic tool B"), + }, + } + + // Initialization strips dynamic tools, keeps tool_search and static tools. + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names := toolNames(state.ToolInfos) + assert.Equal(t, []string{"static_tool", "tool_search"}, names) + assert.Nil(t, state.DeferredToolInfos, "Mode 1 should not populate DeferredToolInfos") + + // Verify reminder was inserted. + assert.Equal(t, 1, countReminders(state.Messages), "reminder should be inserted") +} + +func TestBeforeModelRewriteState_Mode1_ForwardSelection(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA, dynamicB}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + // Simulate state AFTER initialization (dynamic tools already stripped). + // Include a tool_search result message that selected dynamic_tool_a. + toolSearchResultJSON, _ := json.Marshal(toolSearchResult{Matches: []string{"dynamic_tool_a"}}) + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.System, Content: "sys"}, + {Role: schema.User, Content: "hello", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: string(toolSearchResultJSON)}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + }, + } + + // Forward selection should add dynamic_tool_a from the tool_search result. + // Note: init block runs (no compose ctx) but ToolInfos has no dynamic tools to strip. + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names := toolNames(state.ToolInfos) + assert.Equal(t, []string{"dynamic_tool_a", "static_tool", "tool_search"}, names) + + // Call again: forward selection should be idempotent (dynamic_tool_a already present). + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names = toolNames(state.ToolInfos) + assert.Equal(t, []string{"dynamic_tool_a", "static_tool", "tool_search"}, names) +} + +func TestBeforeModelRewriteState_Mode2_DeferredToolInfos(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA, dynamicB}, + UseModelToolSearch: true, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.User, Content: "hello"}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + ti("dynamic_tool_a", "Dynamic tool A"), + ti("dynamic_tool_b", "Dynamic tool B"), + }, + } + + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + // Mode 2: static tools in ToolInfos (tool_search removed), dynamic in DeferredToolInfos. + names := toolNames(state.ToolInfos) + assert.Equal(t, []string{"static_tool"}, names, "ToolInfos should only have static tools") + + deferredNames := toolNames(state.DeferredToolInfos) + assert.Equal(t, []string{"dynamic_tool_a", "dynamic_tool_b"}, deferredNames, "DeferredToolInfos should have all dynamic tools") +} + +func TestBeforeModelRewriteState_ReminderReinsertAfterRemoval(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.User, Content: "hello"}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + ti("dynamic_tool_a", "Dynamic tool A"), + }, + } + + // First call: reminder inserted. + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + reminderCount := countReminders(state.Messages) + assert.Equal(t, 1, reminderCount) + + // Simulate summarization removing the reminder message. + var msgsWithoutReminder []*schema.Message + for _, msg := range state.Messages { + isReminder := false + if msg.Extra != nil { + if v, ok := msg.Extra[toolSearchReminderExtraKey].(bool); ok && v { + isReminder = true + } + } + if !isReminder { + msgsWithoutReminder = append(msgsWithoutReminder, msg) + } + } + state.Messages = msgsWithoutReminder + assert.Equal(t, 0, countReminders(state.Messages), "reminder should be gone") + + // Next call: reminder should be re-inserted. + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + reminderCount = countReminders(state.Messages) + assert.Equal(t, 1, reminderCount, "reminder should be re-inserted after removal") +} + +func countReminders(msgs []*schema.Message) int { + count := 0 + for _, msg := range msgs { + if msg.Extra != nil { + if v, _ := msg.Extra[toolSearchReminderExtraKey].(bool); v { + count++ + } + } + } + return count +} + +// --------------------------------------------------------------------------- +// Edge-case tests for BeforeModelRewriteState +// --------------------------------------------------------------------------- + +func TestBeforeModelRewriteState_Mode1_MultipleToolSearchResultsAcrossTurns(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + dynamicC := &simpleTool{name: "dynamic_tool_c", desc: "Dynamic tool C"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA, dynamicB, dynamicC}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + // Build two separate tool_search result messages, each selecting a different tool. + resultA, _ := json.Marshal(toolSearchResult{Matches: []string{"dynamic_tool_a"}}) + resultB, _ := json.Marshal(toolSearchResult{Matches: []string{"dynamic_tool_b"}}) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.System, Content: "sys"}, + {Role: schema.User, Content: "reminder", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: string(resultA)}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc2", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_b"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: string(resultB)}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + }, + } + + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names := toolNames(state.ToolInfos) + assert.Contains(t, names, "dynamic_tool_a", "dynamic_tool_a should be added from first tool_search result") + assert.Contains(t, names, "dynamic_tool_b", "dynamic_tool_b should be added from second tool_search result") + assert.NotContains(t, names, "dynamic_tool_c", "dynamic_tool_c was never selected") + assert.Contains(t, names, "static_tool", "static_tool should remain") + assert.Contains(t, names, "tool_search", "tool_search should remain") +} + +func TestBeforeModelRewriteState_Mode1_MalformedJSONInToolSearchResult(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.System, Content: "sys"}, + {Role: schema.User, Content: "reminder", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: `{invalid json!!!`}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + }, + } + + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err, "malformed JSON in tool_search result should not cause an error") + + names := toolNames(state.ToolInfos) + assert.NotContains(t, names, "dynamic_tool_a", "malformed JSON result should be skipped") + assert.Contains(t, names, "static_tool") + assert.Contains(t, names, "tool_search") +} + +func TestBeforeModelRewriteState_Mode1_NonExistentToolInForwardSelection(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + resultJSON, _ := json.Marshal(toolSearchResult{Matches: []string{"nonexistent_tool", "dynamic_tool_a"}}) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.User, Content: "reminder", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:nonexistent_tool,dynamic_tool_a"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: string(resultJSON)}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + }, + } + + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err, "nonexistent tool in forward selection should not cause an error") + + names := toolNames(state.ToolInfos) + assert.Contains(t, names, "dynamic_tool_a", "valid tool should be added") + assert.NotContains(t, names, "nonexistent_tool", "nonexistent tool should be silently ignored") +} + +func TestBeforeModelRewriteState_Mode2_EmptyToolInfos(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA}, + UseModelToolSearch: true, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.User, Content: "hello"}, + }, + ToolInfos: []*schema.ToolInfo{}, // empty, not nil + } + + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err, "empty ToolInfos should not cause an error") + + assert.Empty(t, state.ToolInfos, "ToolInfos should be empty") + assert.Empty(t, state.DeferredToolInfos, "DeferredToolInfos should be empty when no dynamic tools found in ToolInfos") +} + +func TestBeforeModelRewriteState_Mode1_DoubleInitWithoutComposeContext(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + dynamicB := &simpleTool{name: "dynamic_tool_b", desc: "Dynamic tool B"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA, dynamicB}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + resultJSON, _ := json.Marshal(toolSearchResult{Matches: []string{"dynamic_tool_a"}}) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.User, Content: "reminder", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: string(resultJSON)}, + }, + ToolInfos: []*schema.ToolInfo{ + ti("static_tool", "Static tool"), + getToolSearchToolInfo(), + ti("dynamic_tool_a", "Dynamic tool A"), + }, + } + + // First call: init runs (strips dynamic_tool_a), then forward selection re-adds it. + _, state, err = m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names := toolNames(state.ToolInfos) + assert.Contains(t, names, "dynamic_tool_a", + "forward selection should re-add dynamic_tool_a even after init re-strips it") + assert.Contains(t, names, "static_tool") + assert.Contains(t, names, "tool_search") + + // Second call: init runs AGAIN (no compose ctx), verify behavior is stable. + _, state2, err := m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + names2 := toolNames(state2.ToolInfos) + assert.Contains(t, names2, "dynamic_tool_a", + "second call should also have dynamic_tool_a re-added by forward selection") +} + +func TestBeforeModelRewriteState_ToolInfosSliceMutation(t *testing.T) { + ctx := context.Background() + + dynamicA := &simpleTool{name: "dynamic_tool_a", desc: "Dynamic tool A"} + + mw, err := New(ctx, &Config{ + DynamicTools: []tool.BaseTool{dynamicA}, + UseModelToolSearch: false, + }) + require.NoError(t, err) + + m := mw.(*typedMiddleware[*schema.Message]) + + // Create ToolInfos with excess capacity so append could mutate in place. + originalToolInfos := make([]*schema.ToolInfo, 2, 10) + originalToolInfos[0] = ti("static_tool", "Static tool") + originalToolInfos[1] = getToolSearchToolInfo() + + originalLen := len(originalToolInfos) + + resultJSON, _ := json.Marshal(toolSearchResult{Matches: []string{"dynamic_tool_a"}}) + + state := &adk.ChatModelAgentState{ + Messages: []*schema.Message{ + {Role: schema.User, Content: "reminder", Extra: map[string]any{toolSearchReminderExtraKey: true}}, + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "tc1", Function: schema.FunctionCall{Name: toolSearchToolName, Arguments: `{"query":"select:dynamic_tool_a"}`}}, + }), + {Role: schema.Tool, ToolName: toolSearchToolName, Content: string(resultJSON)}, + }, + ToolInfos: originalToolInfos, + } + + _, newState, err := m.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + + newNames := toolNames(newState.ToolInfos) + assert.Contains(t, newNames, "dynamic_tool_a") + assert.Equal(t, originalLen, len(originalToolInfos), + "original ToolInfos slice length should not be mutated by the middleware") +} + +// --------------------------------------------------------------------------- +// modelToolSearchTool (Mode 2) tests +// --------------------------------------------------------------------------- + +func TestModelToolSearchTool(t *testing.T) { + ctx := context.Background() + + tools := makeToolMap( + ti("alpha", "Alpha tool description"), + ti("beta", "Beta tool description"), + ) + mts := &modelToolSearchTool{tools: tools} + + // Info should return the standard tool_search tool info. + info, err := mts.Info(ctx) + require.NoError(t, err) + assert.Equal(t, toolSearchToolName, info.Name) + + // InvokableRun with a valid query selecting "alpha". + arg := &schema.ToolArgument{Text: searchJSON("select:alpha", nil)} + result, err := mts.InvokableRun(ctx, arg) + require.NoError(t, err) + require.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeToolSearchResult, result.Parts[0].Type) + require.NotNil(t, result.Parts[0].ToolSearchResult) + assert.Len(t, result.Parts[0].ToolSearchResult.Tools, 1) + assert.Equal(t, "alpha", result.Parts[0].ToolSearchResult.Tools[0].Name) + + // InvokableRun with an empty query should return error. + argEmpty := &schema.ToolArgument{Text: `{"query":""}`} + _, err = mts.InvokableRun(ctx, argEmpty) + assert.Error(t, err) +} + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + + // Verify that NewTyped compiles with *schema.AgenticMessage. + // DynamicTools is required, so we expect an error with an empty config. + mw, err := NewTyped[*schema.AgenticMessage](ctx, &Config{ + DynamicTools: []tool.BaseTool{&simpleTool{name: "t1", desc: "desc1"}}, + }) + assert.NoError(t, err) + assert.NotNil(t, mw) + + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} diff --git a/adk/middlewares/filesystem/backend.go b/adk/middlewares/filesystem/backend.go new file mode 100644 index 0000000..eec62f1 --- /dev/null +++ b/adk/middlewares/filesystem/backend.go @@ -0,0 +1,33 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package filesystem provides middlewares. +package filesystem + +import ( + "github.com/cloudwego/eino/adk/filesystem" +) + +type FileInfo = filesystem.FileInfo +type GrepMatch = filesystem.GrepMatch +type LsInfoRequest = filesystem.LsInfoRequest +type ReadRequest = filesystem.ReadRequest +type MultiModalReadRequest = filesystem.MultiModalReadRequest +type GrepRequest = filesystem.GrepRequest +type GlobInfoRequest = filesystem.GlobInfoRequest +type WriteRequest = filesystem.WriteRequest +type EditRequest = filesystem.EditRequest +type FileContent = filesystem.FileContent diff --git a/adk/middlewares/filesystem/filesystem.go b/adk/middlewares/filesystem/filesystem.go new file mode 100644 index 0000000..b9d64ab --- /dev/null +++ b/adk/middlewares/filesystem/filesystem.go @@ -0,0 +1,1245 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "path/filepath" + "runtime/debug" + "sort" + "strconv" + "strings" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +const ( + ToolNameLs = "ls" + ToolNameReadFile = "read_file" + ToolNameWriteFile = "write_file" + ToolNameEditFile = "edit_file" + ToolNameGlob = "glob" + ToolNameGrep = "grep" + ToolNameExecute = "execute" + + noFilesFound = "No files found" + noMatchesFound = "No matches found" +) + +// ToolConfig configures a filesystem tool +type ToolConfig struct { + // Name overrides the tool name used in tool registration + // optional, default tool name will be used if not set (empty string) + Name string + + // Desc overrides the tool description used in tool registration + // optional, default tool description will be used if not set (nil pointer) + Desc *string + + // CustomTool provides a custom implementation for this tool. + // If set, this custom tool will be used instead of the default implementation associated with Backend. + // If not set, the default tool implementation associated with Backend will be created automatically. + // optional + CustomTool tool.BaseTool + + // Disable disables this tool + // If true, the tool will not be registered + // optional, false by default + Disable bool +} + +// Config is the configuration for the filesystem middleware +type Config struct { + // Backend provides filesystem operations used by tools and offloading. + // If set, filesystem tools (read_file, write_file, edit_file, glob, grep) will be registered. + // At least one of Backend, Shell, or StreamingShell must be set. + Backend filesystem.Backend + + // Shell provides shell command execution capability. + // If set, an execute tool will be registered to support shell command execution. + // At least one of Backend, Shell, or StreamingShell must be set. + // Mutually exclusive with StreamingShell. + Shell filesystem.Shell + // StreamingShell provides streaming shell command execution capability. + // If set, a streaming execute tool will be registered to support streaming shell command execution. + // At least one of Backend, Shell, or StreamingShell must be set. + // Mutually exclusive with Shell. + StreamingShell filesystem.StreamingShell + + // LsToolConfig configures the ls tool + // optional + LsToolConfig *ToolConfig + // ReadFileToolConfig configures the read_file tool. + // This config applies to both the standard read_file tool (InvokableTool) and + // the multimodal read_file tool (EnhancedInvokableTool) when UseMultiModalRead is true. + // optional + ReadFileToolConfig *ToolConfig + // WriteFileToolConfig configures the write_file tool + // optional + WriteFileToolConfig *ToolConfig + // EditFileToolConfig configures the edit_file tool + // optional + EditFileToolConfig *ToolConfig + // GlobToolConfig configures the glob tool + // optional + GlobToolConfig *ToolConfig + // GrepToolConfig configures the grep tool + // optional + GrepToolConfig *ToolConfig + + // WithoutLargeToolResultOffloading disables automatic offloading of large tool result to Backend + // optional, false(enabled) by default + WithoutLargeToolResultOffloading bool + // LargeToolResultOffloadingTokenLimit sets the token threshold to trigger offloading + // optional, 20000 by default + LargeToolResultOffloadingTokenLimit int + // LargeToolResultOffloadingPathGen generates the write path for offloaded results based on context and ToolInput + // optional, "/large_tool_result/{ToolCallID}" by default + LargeToolResultOffloadingPathGen func(ctx context.Context, input *compose.ToolInput) (string, error) + + // CustomSystemPrompt overrides the default ToolsSystemPrompt appended to agent instruction + // optional, ToolsSystemPrompt by default + CustomSystemPrompt *string + + // CustomLsToolDesc overrides the ls tool description used in tool registration + // optional, ListFilesToolDesc by default + // Deprecated: Use LsToolConfig.Desc instead + CustomLsToolDesc *string + // CustomReadFileToolDesc overrides the read_file tool description + // optional, ReadFileToolDesc by default + // Deprecated: Use ReadFileToolConfig.Desc instead + CustomReadFileToolDesc *string + // CustomGrepToolDesc overrides the grep tool description + // optional, GrepToolDesc by default + // Deprecated: Use GrepToolConfig.Desc instead + CustomGrepToolDesc *string + // CustomGlobToolDesc overrides the glob tool description + // optional, GlobToolDesc by default + // Deprecated: Use GlobToolConfig.Desc instead + CustomGlobToolDesc *string + // CustomWriteFileToolDesc overrides the write_file tool description + // optional, WriteFileToolDesc by default + // Deprecated: Use WriteFileToolConfig.Desc instead + CustomWriteFileToolDesc *string + // CustomEditToolDesc overrides the edit_file tool description + // optional, EditFileToolDesc by default + // Deprecated: Use EditFileToolConfig.Desc instead + CustomEditToolDesc *string +} + +func (c *Config) Validate() error { + if c == nil { + return errors.New("config should not be nil") + } + if c.Backend == nil { + return errors.New("backend should not be nil") + } + if c.StreamingShell != nil && c.Shell != nil { + return errors.New("shell and streaming shell should not be both set") + } + return nil +} + +// NewMiddleware constructs and returns the filesystem middleware. +// +// Deprecated: Use New instead. New returns +// a ChatModelAgentMiddleware which provides better context propagation through wrapper methods +// and is the recommended approach for new code. See ChatModelAgentMiddleware documentation +// for details on the benefits over AgentMiddleware. +func NewMiddleware(ctx context.Context, config *Config) (adk.AgentMiddleware, error) { + err := config.Validate() + if err != nil { + return adk.AgentMiddleware{}, err + } + ts, err := getFilesystemTools(ctx, &MiddlewareConfig{ + Backend: config.Backend, + Shell: config.Shell, + StreamingShell: config.StreamingShell, + LsToolConfig: config.LsToolConfig, + ReadFileToolConfig: config.ReadFileToolConfig, + WriteFileToolConfig: config.WriteFileToolConfig, + EditFileToolConfig: config.EditFileToolConfig, + GlobToolConfig: config.GlobToolConfig, + GrepToolConfig: config.GrepToolConfig, + CustomSystemPrompt: config.CustomSystemPrompt, + CustomLsToolDesc: config.CustomLsToolDesc, + CustomReadFileToolDesc: config.CustomReadFileToolDesc, + CustomGrepToolDesc: config.CustomGrepToolDesc, + CustomGlobToolDesc: config.CustomGlobToolDesc, + CustomWriteFileToolDesc: config.CustomWriteFileToolDesc, + CustomEditToolDesc: config.CustomEditToolDesc, + }) + if err != nil { + return adk.AgentMiddleware{}, err + } + + var systemPrompt string + if config.CustomSystemPrompt != nil { + systemPrompt = *config.CustomSystemPrompt + } + + m := adk.AgentMiddleware{ + AdditionalInstruction: systemPrompt, + AdditionalTools: ts, + } + + if !config.WithoutLargeToolResultOffloading { + m.WrapToolCall = newToolResultOffloading(ctx, &toolResultOffloadingConfig{ + Backend: config.Backend, + TokenLimit: config.LargeToolResultOffloadingTokenLimit, + PathGenerator: config.LargeToolResultOffloadingPathGen, + }) + } + + return m, nil +} + +// MiddlewareConfig is the configuration for the filesystem middleware +type MiddlewareConfig struct { + // Backend provides filesystem operations used by tools and offloading. + // required + Backend filesystem.Backend + + // Shell provides shell command execution capability. + // If set, an execute tool will be registered to support shell command execution. + // optional, mutually exclusive with StreamingShell + Shell filesystem.Shell + // StreamingShell provides streaming shell command execution capability. + // If set, a streaming execute tool will be registered for real-time output. + // optional, mutually exclusive with Shell + StreamingShell filesystem.StreamingShell + + // LsToolConfig configures the ls tool + // optional + LsToolConfig *ToolConfig + // ReadFileToolConfig configures the read_file tool. + // This config applies to both the standard read_file tool (InvokableTool) and + // the multimodal read_file tool (EnhancedInvokableTool) when UseMultiModalRead is true. + // optional + ReadFileToolConfig *ToolConfig + // WriteFileToolConfig configures the write_file tool + // optional + WriteFileToolConfig *ToolConfig + // EditFileToolConfig configures the edit_file tool + // optional + EditFileToolConfig *ToolConfig + // GlobToolConfig configures the glob tool + // optional + GlobToolConfig *ToolConfig + // GrepToolConfig configures the grep tool + // optional + GrepToolConfig *ToolConfig + + // UseMultiModalRead enables multimodal read_file tool (EnhancedInvokableTool). + // When true, read_file returns results via schema.ToolResult.Parts instead of plain text string. + // + // Requires Backend to implement filesystem.MultiModalReader interface. + // The default implementation supports reading image files (PNG, JPG, etc.) + // and PDF files with page range selection. + // + // If you provide a custom MultiModalReader, you may need to override + // ReadFileToolConfig.Desc to accurately describe your implementation's capabilities. + // The default description is composed of ReadFileToolDesc + EnhancedReadFileDescSuffix. + // + // Note: When enabled, the read_file tool becomes an EnhancedInvokableTool. + // If you use ChatModelAgentMiddleware, you must implement ChatModelAgentMiddleware.WrapEnhancedInvokableToolCall + // for the middleware to take effect on the read_file tool. + // + // Default false, preserving backward compatibility. + UseMultiModalRead bool + + // CustomSystemPrompt overrides the default ToolsSystemPrompt appended to agent instruction + // optional, ToolsSystemPrompt by default + CustomSystemPrompt *string + + // CustomLsToolDesc overrides the ls tool description used in tool registration + // optional, ListFilesToolDesc by default + // Deprecated: Use LsToolConfig.Desc instead + CustomLsToolDesc *string + // CustomReadFileToolDesc overrides the read_file tool description + // optional, ReadFileToolDesc by default + // Deprecated: Use ReadFileToolConfig.Desc instead + CustomReadFileToolDesc *string + // CustomGrepToolDesc overrides the grep tool description + // optional, GrepToolDesc by default + // Deprecated: Use GrepToolConfig.Desc instead + CustomGrepToolDesc *string + // CustomGlobToolDesc overrides the glob tool description + // optional, GlobToolDesc by default + // Deprecated: Use GlobToolConfig.Desc instead + CustomGlobToolDesc *string + // CustomWriteFileToolDesc overrides the write_file tool description + // optional, WriteFileToolDesc by default + // Deprecated: Use WriteFileToolConfig.Desc instead + CustomWriteFileToolDesc *string + // CustomEditToolDesc overrides the edit_file tool description + // optional, EditFileToolDesc by default + // Deprecated: Use EditFileToolConfig.Desc instead + CustomEditToolDesc *string +} + +func (c *MiddlewareConfig) Validate() error { + if c == nil { + return errors.New("config should not be nil") + } + if c.Backend == nil { + return errors.New("backend should not be nil") + } + if c.StreamingShell != nil && c.Shell != nil { + return errors.New("shell and streaming shell should not be both set") + } + return nil +} + +// mergeToolConfigWithDesc merges ToolConfig with legacy Desc field +// Priority: ToolConfig.Desc > legacy Desc +// Returns an empty ToolConfig if both are nil (to allow backend default implementation) +func (c *MiddlewareConfig) mergeToolConfigWithDesc( + toolConfig *ToolConfig, + legacyDesc *string, +) *ToolConfig { + if toolConfig == nil && legacyDesc == nil { + return &ToolConfig{} + } + + if toolConfig == nil { + return &ToolConfig{ + Desc: legacyDesc, + } + } + + if toolConfig.Desc == nil && legacyDesc != nil { + merged := *toolConfig + merged.Desc = legacyDesc + return &merged + } + + return toolConfig +} + +// NewTyped constructs and returns the filesystem middleware as a TypedChatModelAgentMiddleware[M]. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +// It returns a TypedChatModelAgentMiddleware[M] which provides: +// - Better context propagation through WrapInvokableToolCall and WrapStreamableToolCall methods +// - BeforeAgent hook for modifying agent instruction and tools at runtime +// - More flexible extension points compared to the struct-based AgentMiddleware +// +// The middleware provides filesystem tools (ls, read_file, write_file, edit_file, glob, grep) +// and optionally an execute tool if the Backend implements ShellBackend or StreamingShellBackend. +func NewTyped[M adk.MessageType](ctx context.Context, config *MiddlewareConfig) (adk.TypedChatModelAgentMiddleware[M], error) { + err := config.Validate() + if err != nil { + return nil, err + } + ts, err := getFilesystemTools(ctx, config) + if err != nil { + return nil, err + } + var systemPrompt string + if config.CustomSystemPrompt != nil { + systemPrompt = *config.CustomSystemPrompt + } + + m := &typedFilesystemMiddleware[M]{ + additionalInstruction: systemPrompt, + additionalTools: ts, + } + + return m, nil +} + +// New constructs and returns the filesystem middleware as a ChatModelAgentMiddleware. +// +// This is the recommended constructor for new code. It returns a ChatModelAgentMiddleware which provides: +// - Better context propagation through WrapInvokableToolCall and WrapStreamableToolCall methods +// - BeforeAgent hook for modifying agent instruction and tools at runtime +// - More flexible extension points compared to the struct-based AgentMiddleware +// +// The middleware provides filesystem tools (ls, read_file, write_file, edit_file, glob, grep) +// and optionally an execute tool if the Backend implements ShellBackend or StreamingShellBackend. +// +// Example usage: +// +// middleware, err := filesystem.New(ctx, &filesystem.Config{ +// Backend: myBackend, +// }) +// agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ +// // ... +// Handlers: []adk.ChatModelAgentMiddleware{middleware}, +// }) +func New(ctx context.Context, config *MiddlewareConfig) (adk.ChatModelAgentMiddleware, error) { + return NewTyped[*schema.Message](ctx, config) +} + +type typedFilesystemMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + additionalInstruction string + additionalTools []tool.BaseTool +} + +func (m *typedFilesystemMiddleware[M]) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) (context.Context, *adk.ChatModelAgentContext, error) { + if runCtx == nil { + return ctx, runCtx, nil + } + + nRunCtx := *runCtx + if m.additionalInstruction != "" { + nRunCtx.Instruction = nRunCtx.Instruction + "\n" + m.additionalInstruction + } + nRunCtx.Tools = append(nRunCtx.Tools, m.additionalTools...) + return ctx, &nRunCtx, nil +} + +// toolSpec defines a specification for creating a filesystem tool. +// It unifies the tool creation process by encapsulating the tool configuration, +// legacy descriptor, and the creation function. +type toolSpec struct { + config *ToolConfig + legacyDesc *string + createFunc func(name, desc string) (tool.BaseTool, error) +} + +func getFilesystemTools(_ context.Context, middlewareConfig *MiddlewareConfig) ([]tool.BaseTool, error) { + var tools []tool.BaseTool + + toolSpecs := []toolSpec{ + { + config: middlewareConfig.LsToolConfig, + legacyDesc: middlewareConfig.CustomLsToolDesc, + createFunc: func(name, desc string) (tool.BaseTool, error) { + if middlewareConfig.Backend != nil { + return newLsTool(middlewareConfig.Backend, name, desc) + } + return nil, nil + }, + }, + { + config: middlewareConfig.ReadFileToolConfig, + legacyDesc: middlewareConfig.CustomReadFileToolDesc, + createFunc: func(name, desc string) (tool.BaseTool, error) { + if middlewareConfig.Backend != nil { + if middlewareConfig.UseMultiModalRead { + return newMultiModalReadFileTool(middlewareConfig.Backend, name, desc) + } + return newReadFileTool(middlewareConfig.Backend, name, desc) + } + return nil, nil + }, + }, + { + config: middlewareConfig.WriteFileToolConfig, + legacyDesc: middlewareConfig.CustomWriteFileToolDesc, + createFunc: func(name, desc string) (tool.BaseTool, error) { + if middlewareConfig.Backend != nil { + return newWriteFileTool(middlewareConfig.Backend, name, desc) + } + return nil, nil + }, + }, + { + config: middlewareConfig.EditFileToolConfig, + legacyDesc: middlewareConfig.CustomEditToolDesc, + createFunc: func(name, desc string) (tool.BaseTool, error) { + if middlewareConfig.Backend != nil { + return newEditFileTool(middlewareConfig.Backend, name, desc) + } + return nil, nil + }, + }, + { + config: middlewareConfig.GlobToolConfig, + legacyDesc: middlewareConfig.CustomGlobToolDesc, + createFunc: func(name, desc string) (tool.BaseTool, error) { + if middlewareConfig.Backend != nil { + return newGlobTool(middlewareConfig.Backend, name, desc) + } + return nil, nil + }, + }, + { + config: middlewareConfig.GrepToolConfig, + legacyDesc: middlewareConfig.CustomGrepToolDesc, + createFunc: func(name, desc string) (tool.BaseTool, error) { + if middlewareConfig.Backend != nil { + return newGrepTool(middlewareConfig.Backend, name, desc) + } + return nil, nil + }, + }, + } + + for _, spec := range toolSpecs { + t, err := createToolFromSpec(middlewareConfig, spec) + if err != nil { + return nil, err + } + if t != nil { + tools = append(tools, t) + } + } + + // Create execute tool if Shell or StreamingShell is available + if middlewareConfig.StreamingShell != nil { + executeDesc, err := selectToolDesc("", ExecuteToolDesc, ExecuteToolDescChinese) + if err != nil { + return nil, err + } + + executeTool, err := newStreamingExecuteTool(middlewareConfig.StreamingShell, ToolNameExecute, executeDesc) + if err != nil { + return nil, err + } + tools = append(tools, executeTool) + } else if middlewareConfig.Shell != nil { + executeDesc, err := selectToolDesc("", ExecuteToolDesc, ExecuteToolDescChinese) + if err != nil { + return nil, err + } + + executeTool, err := newExecuteTool(middlewareConfig.Shell, ToolNameExecute, executeDesc) + if err != nil { + return nil, err + } + tools = append(tools, executeTool) + } + + return tools, nil +} + +// createToolFromSpec creates a tool instance based on the provided toolSpec. +// It handles configuration merging (ToolConfig + legacy Desc), checks if the tool +// is disabled, and prioritizes CustomTool over the default implementation. +func createToolFromSpec(middlewareConfig *MiddlewareConfig, spec toolSpec) (tool.BaseTool, error) { + mergedConfig := middlewareConfig.mergeToolConfigWithDesc(spec.config, spec.legacyDesc) + + if mergedConfig.Disable { + return nil, nil + } + + return getOrCreateTool(mergedConfig.CustomTool, func() (tool.BaseTool, error) { + desc := "" + if mergedConfig.Desc != nil { + desc = *mergedConfig.Desc + } + return spec.createFunc(mergedConfig.Name, desc) + }) +} + +func getOrCreateTool(customTool tool.BaseTool, createFunc func() (tool.BaseTool, error)) (tool.BaseTool, error) { + if customTool != nil { + return customTool, nil + } + return createFunc() +} + +type lsArgs struct { + Path string `json:"path"` +} + +func newLsTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameLs) + d, err := selectToolDesc(desc, ListFilesToolDesc, ListFilesToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input lsArgs) (string, error) { + infos, err := fs.LsInfo(ctx, &filesystem.LsInfoRequest{Path: input.Path}) + if err != nil { + return "", err + } + if len(infos) == 0 { + return noFilesFound, nil + } + paths := make([]string, 0, len(infos)) + for _, fi := range infos { + paths = append(paths, fi.Path) + } + return strings.Join(paths, "\n"), nil + }) +} + +type readFileArgs struct { + // FilePath is the path to the file to read. + FilePath string `json:"file_path" jsonschema:"description=The path to the file to read"` + + // Offset is the line number to start reading from. + Offset int `json:"offset" jsonschema:"description=The line number to start reading from. Only provide if the file is too large to read at once"` + + // Limit is the number of lines to read. + Limit int `json:"limit" jsonschema:"description=The number of lines to read. Only provide if the file is too large to read at once."` +} + +// multiModalReadFileArgs extends readFileArgs with PDF-specific parameters for MultiModalReadFileTool. +type multiModalReadFileArgs struct { + readFileArgs + + // Pages is the page range for PDF files. + Pages string `json:"pages,omitempty" jsonschema:"description=Page range for PDF files (e.g.\\, \"1-5\"\\, \"3\"\\, \"10-20\"). Only applicable to PDF files. Maximum 20 pages per request."` +} + +func newReadFileTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameReadFile) + d, err := selectToolDesc(desc, ReadFileToolDesc, ReadFileToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input readFileArgs) (string, error) { + if input.Offset <= 0 { + input.Offset = 1 + } + if input.Limit <= 0 { + input.Limit = 2000 + } + + fileCt, err := fs.Read(ctx, &filesystem.ReadRequest{ + FilePath: input.FilePath, + Offset: input.Offset, + Limit: input.Limit, + }) + if err != nil { + return "", err + } + if fileCt == nil { + return fmt.Sprintf("No content found at path: %s", input.FilePath), nil + } + + return formatLineNumbers(fileCt.Content, input.Offset), nil + }) +} + +// formatLineNumbers prefixes each line of content with a 1-based line number +// starting at startLine (e.g. " 1\tfoo"). startLine corresponds to the +// line number of the first line in content (usually ReadRequest.Offset). +func formatLineNumbers(content string, startLine int) string { + lines := strings.Split(content, "\n") + var b strings.Builder + for i, line := range lines { + if i < len(lines)-1 { + fmt.Fprintf(&b, "%6d\t%s\n", startLine+i, line) + } else { + fmt.Fprintf(&b, "%6d\t%s", startLine+i, line) + } + } + return b.String() +} + +const maxPagesPerRequest = 20 + +func validatePages(pages string) error { + parts := strings.SplitN(pages, "-", 2) + start, err := strconv.Atoi(parts[0]) + if err != nil || start < 1 { + return fmt.Errorf("invalid pages parameter %q: expected format like \"3\" or \"1-10\"", pages) + } + if len(parts) == 1 { + return nil + } + if parts[1] == "" { + return fmt.Errorf("invalid pages parameter %q: expected format like \"3\" or \"1-10\"", pages) + } + end, err := strconv.Atoi(parts[1]) + if err != nil || end < 1 { + return fmt.Errorf("invalid pages parameter %q: expected format like \"3\" or \"1-10\"", pages) + } + if end < start { + return fmt.Errorf("invalid pages parameter %q: end page must be >= start page", pages) + } + if end-start+1 > maxPagesPerRequest { + return fmt.Errorf("invalid pages parameter %q: range exceeds maximum of %d pages per request", pages, maxPagesPerRequest) + } + return nil +} + +func newMultiModalReadFileTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + er, ok := fs.(filesystem.MultiModalReader) + if !ok { + return nil, fmt.Errorf("UseMultiModalRead is enabled, but backend (type %T) does not implement filesystem.MultiModalReader interface. "+ + "Either implement the MultiModalReader interface on your backend, or set UseMultiModalRead to false", fs) + } + toolName := selectToolName(name, ToolNameReadFile) + d, err := selectToolDesc(desc, ReadFileToolDesc, ReadFileToolDescChinese) + if err != nil { + return nil, err + } + // Only append the multimodal suffix when falling back to the built-in desc. + // A custom desc is expected to describe its own capabilities, so appending + // would produce duplicated or contradictory descriptions. + if desc == "" { + d += internal.SelectPrompt(internal.I18nPrompts{ + English: EnhancedReadFileDescSuffix, + Chinese: EnhancedReadFileDescSuffixChinese, + }) + } + + return utils.InferEnhancedTool(toolName, d, func(ctx context.Context, input multiModalReadFileArgs) (*schema.ToolResult, error) { + if input.Offset <= 0 { + input.Offset = 1 + } + if input.Limit <= 0 { + input.Limit = 2000 + } + + if input.Pages != "" { + if err := validatePages(input.Pages); err != nil { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: err.Error()}}, + }, nil + } + } + + fileCt, err := er.MultiModalRead(ctx, &filesystem.MultiModalReadRequest{ + ReadRequest: filesystem.ReadRequest{ + FilePath: input.FilePath, + Offset: input.Offset, + Limit: input.Limit, + }, + Pages: input.Pages, + }) + if err != nil { + return nil, err + } + + if fileCt == nil { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: fmt.Sprintf("No content found at path: %s", input.FilePath)}}, + }, nil + } + + // Multimodal result: convert FileContentPart to ToolOutputPart + if len(fileCt.Parts) > 0 { + parts := make([]schema.ToolOutputPart, 0, len(fileCt.Parts)) + enc := base64Encoder{} + for _, p := range fileCt.Parts { + if len(p.Data) == 0 { + return nil, fmt.Errorf("FileContentPart.Data is empty for type %s", p.Type) + } + if p.MIMEType == "" { + return nil, fmt.Errorf("FileContentPart.MIMEType is empty for type %s", p.Type) + } + b64 := enc.encode(p.Data) + switch p.Type { + case filesystem.FileContentPartTypeImage: + parts = append(parts, schema.ToolOutputPart{ + Type: schema.ToolPartTypeImage, + Image: &schema.ToolOutputImage{ + MessagePartCommon: schema.MessagePartCommon{ + MIMEType: p.MIMEType, + Base64Data: &b64, + }, + }, + }) + case filesystem.FileContentPartTypePDF: + parts = append(parts, schema.ToolOutputPart{ + Type: schema.ToolPartTypeFile, + File: &schema.ToolOutputFile{ + MessagePartCommon: schema.MessagePartCommon{ + MIMEType: p.MIMEType, + Base64Data: &b64, + }, + }, + }) + default: + // FileContentPartType is defined by Backend implementations. + // Unrecognized types are unlikely but should fail explicitly rather than silently. + return nil, fmt.Errorf("unsupported FileContentPartType: %s", p.Type) + } + } + return &schema.ToolResult{Parts: parts}, nil + } + + if fileCt.FileContent == nil { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: fmt.Sprintf("No content found at path: %s", input.FilePath)}}, + }, nil + } + + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: formatLineNumbers(fileCt.Content, input.Offset)}}, + }, nil + }) +} + +type writeFileArgs struct { + // FilePath is the path to the file to write. + FilePath string `json:"file_path" jsonschema:"description=The path to the file to write"` + + // Content is the content to write to the file. + Content string `json:"content" jsonschema:"description=The content to write to the file"` +} + +func newWriteFileTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameWriteFile) + d, err := selectToolDesc(desc, WriteFileToolDesc, WriteFileToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input writeFileArgs) (string, error) { + err := fs.Write(ctx, &filesystem.WriteRequest{ + FilePath: input.FilePath, + Content: input.Content, + }) + if err != nil { + return "", err + } + return fmt.Sprintf("Updated file %s", input.FilePath), nil + }) +} + +type editFileArgs struct { + // FilePath is the path to the file to modify. + FilePath string `json:"file_path" jsonschema:"description=The path to the file to modify"` + + // OldString is the text to replace. + OldString string `json:"old_string" jsonschema:"description=The text to replace"` + + // NewString is the text to replace it with. + NewString string `json:"new_string" jsonschema:"description=The text to replace it with (must be different from old_string)"` + + // ReplaceAll indicates whether to replace all occurrences of old_string. + ReplaceAll bool `json:"replace_all" jsonschema:"description=Replace all occurrences of old_string (default false),default=false"` +} + +func newEditFileTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameEditFile) + d, err := selectToolDesc(desc, EditFileToolDesc, EditFileToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input editFileArgs) (string, error) { + err := fs.Edit(ctx, &filesystem.EditRequest{ + FilePath: input.FilePath, + OldString: input.OldString, + NewString: input.NewString, + ReplaceAll: input.ReplaceAll, + }) + if err != nil { + return "", err + } + return fmt.Sprintf("Successfully replaced the string in '%s'", input.FilePath), nil + }) +} + +type globArgs struct { + // Pattern is the glob pattern to match files against. + Pattern string `json:"pattern" jsonschema:"description=The glob pattern to match files against"` + + // Path is the directory to search in. + Path string `json:"path" jsonschema:"description=The directory to search in. If not specified\\, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter 'undefined' or 'null' - simply omit it for the default behavior. Must be a valid directory path if provided."` +} + +func newGlobTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameGlob) + d, err := selectToolDesc(desc, GlobToolDesc, GlobToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input globArgs) (string, error) { + infos, err := fs.GlobInfo(ctx, &filesystem.GlobInfoRequest{ + Pattern: input.Pattern, + Path: input.Path, + }) + if err != nil { + return "", err + } + if len(infos) == 0 { + return noFilesFound, nil + } + paths := make([]string, 0, len(infos)) + for _, fi := range infos { + paths = append(paths, fi.Path) + } + return strings.Join(paths, "\n"), nil + }) +} + +type grepArgs struct { + // Pattern is the regular expression pattern to search for in file contents. + Pattern string `json:"pattern" jsonschema:"description=The regular expression pattern to search for in file contents"` + + // Path is the file or directory to search in. Defaults to current working directory. + Path *string `json:"path,omitempty" jsonschema:"description=File or directory to search in (rg PATH). Defaults to current working directory."` + + // Glob is the glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}"). + Glob *string `json:"glob,omitempty" jsonschema:"description=Glob pattern to filter files (e.g. '*.js'\\, '*.{ts\\,tsx}') - maps to rg --glob"` + + // OutputMode specifies the output format. + // "content" shows matching lines (supports context, line numbers, head_limit). + // "files_with_matches" shows file paths (supports head_limit). + // "count" shows match counts (supports head_limit). + // Defaults to "files_with_matches". + OutputMode string `json:"output_mode,omitempty" jsonschema:"description=Output mode: 'content' shows matching lines (supports -A/-B/-C context\\, -n line numbers\\, head_limit)\\, 'files_with_matches' shows file paths (supports head_limit)\\, 'count' shows match counts (supports head_limit). Defaults to 'files_with_matches'.,enum=content,enum=files_with_matches,enum=count"` + + // Context is the number of lines to show before and after each match. + // Only applicable when output_mode is "content". + Context *int `json:"-C,omitempty" jsonschema:"description=Number of lines to show before and after each match (rg -C). Requires output_mode: 'content'\\, ignored otherwise."` + + // BeforeLines is the number of lines to show before each match. + // Only applicable when output_mode is "content". + BeforeLines *int `json:"-B,omitempty" jsonschema:"description=Number of lines to show before each match (rg -B). Requires output_mode: 'content'\\, ignored otherwise."` + + // AfterLines is the number of lines to show after each match. + // Only applicable when output_mode is "content". + AfterLines *int `json:"-A,omitempty" jsonschema:"description=Number of lines to show after each match (rg -A). Requires output_mode: 'content'\\, ignored otherwise."` + + // ShowLineNumbers enables showing line numbers in output. + // Only applicable when output_mode is "content". Defaults to true. + ShowLineNumbers *bool `json:"-n,omitempty" jsonschema:"description=Show line numbers in output (rg -n). Requires output_mode: 'content'\\, ignored otherwise. Defaults to true."` + + // CaseInsensitive enables case insensitive search. + CaseInsensitive *bool `json:"-i,omitempty" jsonschema:"description=Case insensitive search (rg -i)"` + + // FileType is the file type to search (e.g., js, py, rust, go, java). + // More efficient than Glob for standard file types. + FileType *string `json:"type,omitempty" jsonschema:"description=File type to search (rg --type). Common types: js\\, py\\, rust\\, go\\, java\\, etc. More efficient than include for standard file types."` + + // HeadLimit limits output to first N lines/entries. + // Works across all output modes. Defaults to 0 (unlimited). + HeadLimit *int `json:"head_limit,omitempty" jsonschema:"description=Limit output to first N lines/entries\\, equivalent to '| head -N'. Works across all output modes: content (limits output lines)\\, files_with_matches (limits file paths)\\, count (limits count entries). Defaults to 0 (unlimited)."` + + // Offset skips first N lines/entries before applying HeadLimit. + // Works across all output modes. Defaults to 0. + Offset *int `json:"offset,omitempty" jsonschema:"description=Skip first N lines/entries before applying head_limit\\, equivalent to '| tail -n +N | head -N'. Works across all output modes. Defaults to 0."` + + // Multiline enables multiline mode where patterns can span lines. + // - true: Allows patterns to match across lines, "." matches newlines + // - false: Default, matches only within single lines + Multiline *bool `json:"multiline,omitempty" jsonschema:"description=Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false."` +} + +func newGrepTool(fs filesystem.Backend, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameGrep) + d, err := selectToolDesc(desc, GrepToolDesc, GrepToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input grepArgs) (string, error) { + // Extract string parameters + path := valueOrDefault(input.Path, "") + glob := valueOrDefault(input.Glob, "") + fileType := valueOrDefault(input.FileType, "") + var beforeLines, afterLines int + + if input.Context != nil { + beforeLines = valueOrDefault(input.Context, 0) + afterLines = valueOrDefault(input.Context, 0) + } else { + // Extract context parameters + beforeLines = valueOrDefault(input.BeforeLines, 0) + afterLines = valueOrDefault(input.AfterLines, 0) + } + + // Extract boolean flags + caseInsensitive := valueOrDefault(input.CaseInsensitive, false) + enableMultiline := valueOrDefault(input.Multiline, false) + + // Extract pagination parameters + headLimit := valueOrDefault(input.HeadLimit, 0) + offset := valueOrDefault(input.Offset, 0) + + matches, err := fs.GrepRaw(ctx, &filesystem.GrepRequest{ + Pattern: input.Pattern, + Path: path, + Glob: glob, + FileType: fileType, + CaseInsensitive: caseInsensitive, + AfterLines: afterLines, + BeforeLines: beforeLines, + EnableMultiline: enableMultiline, + }) + if err != nil { + return "", err + } + + sort.SliceStable(matches, func(i, j int) bool { + return filepath.Base(matches[i].Path) < filepath.Base(matches[j].Path) + }) + + switch input.OutputMode { + case "content": + matches = applyPagination(matches, offset, headLimit) + return formatContentMatches(matches, valueOrDefault(input.ShowLineNumbers, true)), nil + + case "count": + return formatCountMatches(matches, offset, headLimit), nil + + case "files_with_matches": + return formatFileMatches(matches, offset, headLimit), nil + + default: + return formatFileMatches(matches, offset, headLimit), nil + } + }) +} + +type executeArgs struct { + Command string `json:"command"` +} + +func newExecuteTool(sb filesystem.Shell, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameExecute) + d, err := selectToolDesc(desc, ExecuteToolDesc, ExecuteToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferTool(toolName, d, func(ctx context.Context, input executeArgs) (string, error) { + result, err := sb.Execute(ctx, &filesystem.ExecuteRequest{ + Command: input.Command, + }) + if err != nil { + return "", err + } + + return convExecuteResponse(result), nil + }) +} + +func newStreamingExecuteTool(sb filesystem.StreamingShell, name string, desc string) (tool.BaseTool, error) { + toolName := selectToolName(name, ToolNameExecute) + d, err := selectToolDesc(desc, ExecuteToolDesc, ExecuteToolDescChinese) + if err != nil { + return nil, err + } + return utils.InferStreamTool(toolName, d, func(ctx context.Context, input executeArgs) (*schema.StreamReader[string], error) { + result, err := sb.ExecuteStreaming(ctx, &filesystem.ExecuteRequest{ + Command: input.Command, + }) + if err != nil { + return nil, err + } + sr, sw := schema.Pipe[string](10) + go func() { + defer func() { + e := recover() + if e != nil { + sw.Send("", fmt.Errorf("panic: %v,\n stack: %s", e, string(debug.Stack()))) + } + sw.Close() + }() + + var hasSentContent bool + var exitCode *int + + for { + chunk, recvErr := result.Recv() + if recvErr == io.EOF { + break + } + if recvErr != nil { + sw.Send("", recvErr) + return + } + + if chunk == nil { + continue + } + if chunk.ExitCode != nil { + exitCode = chunk.ExitCode + } + + parts := make([]string, 0, 2) + if chunk.Output != "" { + parts = append(parts, chunk.Output) + } + if chunk.Truncated { + parts = append(parts, "[Output was truncated due to size limits]") + } + if len(parts) > 0 { + sw.Send(strings.Join(parts, "\n"), nil) + hasSentContent = true + } + } + + if exitCode != nil && *exitCode != 0 { + sw.Send(fmt.Sprintf("\n[Command failed with exit code %d]", *exitCode), nil) + } else if !hasSentContent { + sw.Send("[Command executed successfully with no output]", nil) + } + }() + + return sr, nil + }) +} + +func convExecuteResponse(response *filesystem.ExecuteResponse) string { + if response == nil { + return "" + } + parts := []string{response.Output} + if response.ExitCode != nil && *response.ExitCode != 0 { + parts = append(parts, fmt.Sprintf("[Command failed with exit code %d]", *response.ExitCode)) + } + if response.Truncated { + parts = append(parts, "[Output was truncated due to size limits]") + } + + result := strings.Join(parts, "\n") + if result == "" && (response.ExitCode == nil || *response.ExitCode == 0) { + return "[Command executed successfully with no output]" + } + return result +} + +// valueOrDefault returns the value pointed to by ptr, or defaultValue if ptr is nil. +func valueOrDefault[T any](ptr *T, defaultValue T) T { + if ptr != nil { + return *ptr + } + return defaultValue +} + +// base64Encoder reuses a buffer across multiple base64 encoding calls to reduce allocations. +type base64Encoder struct { + buf []byte +} + +func (e *base64Encoder) encode(data []byte) string { + n := base64.StdEncoding.EncodedLen(len(data)) + if cap(e.buf) < n { + e.buf = make([]byte, n) + } else { + e.buf = e.buf[:n] + } + base64.StdEncoding.Encode(e.buf, data) + return string(e.buf) +} + +func applyPagination[T any](items []T, offset, headLimit int) []T { + if offset < 0 { + offset = 0 + } + if offset >= len(items) { + return []T{} + } + items = items[offset:] + + if headLimit > 0 && headLimit < len(items) { + items = items[:headLimit] + } + return items +} + +func formatFileMatches(matches []filesystem.GrepMatch, offset, headLimit int) string { + if len(matches) == 0 { + return noFilesFound + } + seen := make(map[string]bool) + var uniquePaths []string + for _, match := range matches { + if !seen[match.Path] { + seen[match.Path] = true + uniquePaths = append(uniquePaths, match.Path) + } + } + totalFiles := len(uniquePaths) + uniquePaths = applyPagination(uniquePaths, offset, headLimit) + + fileWord := "files" + if totalFiles == 1 { + fileWord = "file" + } + return fmt.Sprintf("Found %d %s\n%s", totalFiles, fileWord, strings.Join(uniquePaths, "\n")) +} + +func formatContentMatches(matches []filesystem.GrepMatch, showLineNum bool) string { + if len(matches) == 0 { + return noMatchesFound + } + var b strings.Builder + for _, match := range matches { + b.WriteString(match.Path) + if showLineNum { + b.WriteString(":") + b.WriteString(strconv.Itoa(match.Line)) + } + b.WriteString(":") + b.WriteString(match.Content) + b.WriteString("\n") + } + return strings.TrimSuffix(b.String(), "\n") +} + +func formatCountMatches(matches []filesystem.GrepMatch, offset, headLimit int) string { + countMap := make(map[string]int) + for _, match := range matches { + countMap[match.Path]++ + } + + var paths []string + for path := range countMap { + paths = append(paths, path) + } + sort.Strings(paths) + + totalOccurrences := len(matches) + totalFiles := len(paths) + + occurrenceWord := "occurrences" + if totalOccurrences == 1 { + occurrenceWord = "occurrence" + } + fileWord := "files" + if totalFiles == 1 { + fileWord = "file" + } + + if totalOccurrences == 0 { + return fmt.Sprintf("%s\n\nFound %d total %s across %d %s.", noMatchesFound, totalOccurrences, occurrenceWord, totalFiles, fileWord) + } + + paths = applyPagination(paths, offset, headLimit) + + var b strings.Builder + for _, path := range paths { + b.WriteString(path) + b.WriteString(":") + b.WriteString(strconv.Itoa(countMap[path])) + b.WriteString("\n") + } + result := strings.TrimSuffix(b.String(), "\n") + return fmt.Sprintf("%s\n\nFound %d total %s across %d %s.", result, totalOccurrences, occurrenceWord, totalFiles, fileWord) +} + +// selectToolDesc returns the custom description if provided, otherwise selects the appropriate +// i18n description based on the current language setting. +func selectToolDesc(customDesc string, defaultEnglish, defaultChinese string) (string, error) { + if customDesc != "" { + return customDesc, nil + } + return internal.SelectPrompt(internal.I18nPrompts{ + English: defaultEnglish, + Chinese: defaultChinese, + }), nil +} + +// selectToolName returns the custom tool name if provided, otherwise returns the default name. +func selectToolName(customName string, defaultName string) string { + if customName != "" { + return customName + } + return defaultName +} diff --git a/adk/middlewares/filesystem/filesystem_test.go b/adk/middlewares/filesystem/filesystem_test.go new file mode 100644 index 0000000..cb59353 --- /dev/null +++ b/adk/middlewares/filesystem/filesystem_test.go @@ -0,0 +1,2647 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +// setupTestBackend creates a test backend with some initial files +func setupTestBackend() *filesystem.InMemoryBackend { + backend := filesystem.NewInMemoryBackend() + ctx := context.Background() + + // Create test files + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/file1.txt", + Content: "line1\nline2\nline3\nline4\nline5", + }) + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/file2.go", + Content: "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}", + }) + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/dir1/file3.txt", + Content: "hello world\nfoo bar\nhello again", + }) + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/dir1/file4.py", + Content: "print('hello')\nprint('world')", + }) + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/dir2/file5.go", + Content: "package test\n\nfunc test() {}", + }) + + return backend +} + +// invokeTool is a helper to invoke a tool with JSON input +func invokeTool(_ *testing.T, bt tool.BaseTool, input string) (string, error) { + ctx := context.Background() + result, err := bt.(tool.InvokableTool).InvokableRun(ctx, input) + if err != nil { + return "", err + } + return result, nil +} + +func TestLsTool(t *testing.T) { + backend := setupTestBackend() + lsTool, err := newLsTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create ls tool: %v", err) + } + + tests := []struct { + name string + input string + expected []string // expected paths in output + }{ + { + name: "list root", + input: `{"path": "/"}`, + expected: []string{"file1.txt", "file2.go", "dir1", "dir2"}, + }, + { + name: "list empty path (defaults to root)", + input: `{"path": ""}`, + expected: []string{"file1.txt", "file2.go", "dir1", "dir2"}, + }, + { + name: "list dir1", + input: `{"path": "/dir1"}`, + expected: []string{"file3.txt", "file4.py"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := invokeTool(t, lsTool, tt.input) + if err != nil { + t.Fatalf("ls tool failed: %v", err) + } + + for _, expectedPath := range tt.expected { + if !strings.Contains(result, expectedPath) { + t.Errorf("Expected output to contain %q, got: %s", expectedPath, result) + } + } + }) + } +} + +func TestReadFileTool(t *testing.T) { + backend := setupTestBackend() + readTool, err := newReadFileTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create read_file tool: %v", err) + } + + tests := []struct { + name string + input string + expected string + shouldError bool + }{ + { + name: "read full file", + input: `{"file_path": "/file1.txt", "offset": 0, "limit": 100}`, + expected: " 1\tline1\n 2\tline2\n 3\tline3\n 4\tline4\n 5\tline5", + }, + { + name: "read with offset", + input: `{"file_path": "/file1.txt", "offset": 2, "limit": 2}`, + expected: " 2\tline2\n 3\tline3", + }, + { + name: "read with default limit", + input: `{"file_path": "/file1.txt", "offset": 0, "limit": 0}`, + expected: " 1\tline1\n 2\tline2\n 3\tline3\n 4\tline4\n 5\tline5", + }, + { + name: "read with negative offset (treated as 0)", + input: `{"file_path": "/file1.txt", "offset": -1, "limit": 2}`, + expected: " 1\tline1\n 2\tline2", + }, + { + name: "read non-existent file", + input: `{"file_path": "/nonexistent.txt", "offset": 0, "limit": 10}`, + shouldError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := invokeTool(t, readTool, tt.input) + if tt.shouldError { + if err == nil { + t.Error("Expected error but got none") + } + return + } + if err != nil { + t.Fatalf("read_file tool failed: %v", err) + } + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestReadFileTool_DefaultLimit(t *testing.T) { + // Build a file with more than 2000 lines to verify the tool layer applies the default limit + backend := filesystem.NewInMemoryBackend() + var b strings.Builder + totalLines := 2500 + for i := 1; i <= totalLines; i++ { + if i > 1 { + b.WriteString("\n") + } + fmt.Fprintf(&b, "line%d", i) + } + backend.Write(context.Background(), &filesystem.WriteRequest{ + FilePath: "/big.txt", + Content: b.String(), + }) + + readTool, err := newReadFileTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create read_file tool: %v", err) + } + + t.Run("limit=0 defaults to 2000 lines in tool layer", func(t *testing.T) { + result, err := invokeTool(t, readTool, `{"file_path": "/big.txt", "offset": 0, "limit": 0}`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lines := strings.Split(result, "\n") + if len(lines) != 2000 { + t.Errorf("expected 2000 lines, got %d", len(lines)) + } + }) + + t.Run("explicit limit is respected", func(t *testing.T) { + result, err := invokeTool(t, readTool, `{"file_path": "/big.txt", "offset": 0, "limit": 10}`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lines := strings.Split(result, "\n") + if len(lines) != 10 { + t.Errorf("expected 10 lines, got %d", len(lines)) + } + }) + + t.Run("backend read with limit=0 returns all lines", func(t *testing.T) { + content, err := backend.Read(context.Background(), &filesystem.ReadRequest{ + FilePath: "/big.txt", + Limit: 0, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lines := strings.Split(content.Content, "\n") + if len(lines) != totalLines { + t.Errorf("expected %d lines from backend, got %d", totalLines, len(lines)) + } + }) +} + +func TestWriteFileTool(t *testing.T) { + backend := setupTestBackend() + writeTool, err := newWriteFileTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create write_file tool: %v", err) + } + + tests := []struct { + name string + input string + expected string + isError bool + }{ + { + name: "write new file", + input: `{"file_path": "/newfile.txt", "content": "new content"}`, + expected: "Updated file /newfile.txt", + }, + { + name: "overwrite existing file", + input: `{"file_path": "/file1.txt", "content": "overwritten"}`, + isError: false, + expected: "Updated file /file1.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := invokeTool(t, writeTool, tt.input) + if tt.isError { + if err == nil { + t.Errorf("Expected an error, but got none") + } + return + } + if err != nil { + t.Fatalf("write_file tool failed: %v", err) + } + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } + + // Verify the file was actually written + ctx := context.Background() + content, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/newfile.txt", + Offset: 0, + Limit: 100, + }) + if err != nil { + t.Fatalf("Failed to read written file: %v", err) + } + if content.Content != "new content" { + t.Errorf("Expected written content to be 'new content', got %q", content.Content) + } +} + +func TestEditFileTool(t *testing.T) { + backend := setupTestBackend() + editTool, err := newEditFileTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create edit_file tool: %v", err) + } + + tests := []struct { + name string + setupFile string + setupContent string + input string + expected string + shouldError bool + }{ + { + name: "replace first occurrence", + setupFile: "/edit1.txt", + setupContent: "hello world\nhello again\nhello world", + input: `{"file_path": "/edit1.txt", "old_string": "hello again", "new_string": "hi", "replace_all": false}`, + expected: "hello world\nhi\nhello world", + }, + { + name: "replace all occurrences", + setupFile: "/edit2.txt", + setupContent: "hello world\nhello again\nhello world", + input: `{"file_path": "/edit2.txt", "old_string": "hello", "new_string": "hi", "replace_all": true}`, + expected: "hi world\nhi again\nhi world", + }, + { + name: "non-existent file", + setupFile: "", + setupContent: "", + input: `{"file_path": "/nonexistent.txt", "old_string": "old", "new_string": "new", "replace_all": false}`, + shouldError: true, + }, + { + name: "empty old_string", + setupFile: "/edit3.txt", + setupContent: "content", + input: `{"file_path": "/edit3.txt", "old_string": "", "new_string": "new", "replace_all": false}`, + shouldError: true, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup file if needed + if tt.setupFile != "" { + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: tt.setupFile, + Content: tt.setupContent, + }) + } + + _, err := invokeTool(t, editTool, tt.input) + if tt.shouldError { + if err == nil { + t.Error("Expected error but got none") + } + return + } + if err != nil { + t.Fatalf("edit_file tool failed: %v", err) + } + result, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: tt.setupFile, + Offset: 0, + Limit: 0, + }) + if err != nil { + t.Fatalf("edit_file tool failed: %v", err) + } + if result.Content != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result.Content) + } + }) + } +} + +func TestGlobTool(t *testing.T) { + backend := setupTestBackend() + globTool, err := newGlobTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create glob tool: %v", err) + } + + tests := []struct { + name string + input string + expected []string + }{ + { + name: "match all .txt files in root", + input: `{"pattern": "*.txt", "path": "/"}`, + expected: []string{"file1.txt"}, + }, + { + name: "match all .go files in root", + input: `{"pattern": "*.go", "path": "/"}`, + expected: []string{"file2.go"}, + }, + { + name: "match all .txt files in dir1", + input: `{"pattern": "*.txt", "path": "/dir1"}`, + expected: []string{"file3.txt"}, + }, + { + name: "match all .py files in dir1", + input: `{"pattern": "*.py", "path": "/dir1"}`, + expected: []string{"file4.py"}, + }, + + { + name: "empty path defaults to root", + input: `{"pattern": "*.go", "path": ""}`, + expected: []string{"file2.go"}, + }, + + { + name: "match all .txt files in dir1 in root dir", + input: `{"pattern": "/dir1/*.txt", "path": "/"}`, + expected: []string{"/dir1/file3.txt"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := invokeTool(t, globTool, tt.input) + if err != nil { + t.Fatalf("glob tool failed: %v", err) + } + + for _, expectedPath := range tt.expected { + if !strings.Contains(result, expectedPath) { + t.Errorf("Expected output to contain %q, got: %s", expectedPath, result) + } + } + }) + } +} + +func TestGrepTool(t *testing.T) { + backend := setupTestBackend() + grepTool, err := newGrepTool(backend, "", "") + if err != nil { + t.Fatalf("Failed to create grep tool: %v", err) + } + + tests := []struct { + name string + input string + expected string + contains []string + }{ + { + name: "grep with count mode", + input: `{"pattern": "hello", "output_mode": "count"}`, + expected: "/dir1/file3.txt:2\n/dir1/file4.py:1\n/file2.go:1\n\nFound 4 total occurrences across 3 files.", // 2 in file3.txt, 1 in file4.py, 1 in file2.go + }, + { + name: "grep with content mode", + input: `{"pattern": "hello", "output_mode": "content"}`, + contains: []string{"/dir1/file3.txt:1:hello world", "/dir1/file3.txt:3:hello again", "/dir1/file4.py:1:print('hello')"}, + }, + { + name: "grep with files_with_matches mode (default)", + input: `{"pattern": "hello", "output_mode": "files_with_matches"}`, + contains: []string{"/dir1/file3.txt", "/dir1/file4.py"}, + }, + { + name: "grep with glob filter", + input: `{"pattern": "hello", "glob": "*.txt", "output_mode": "count"}`, + expected: "/dir1/file3.txt:2\n\nFound 2 total occurrences across 1 file.", // only in file3.txt + }, + { + name: "grep withpath filter", + input: `{"pattern": "package", "path": "/dir2", "output_mode": "count"}`, + expected: "/dir2/file5.go:1\n\nFound 1 total occurrence across 1 file.", // only in dir2/file5.go + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := invokeTool(t, grepTool, tt.input) + if err != nil { + t.Fatalf("grep tool failed: %v", err) + } + + if tt.expected != "" { + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + } + + for _, expectedStr := range tt.contains { + if !strings.Contains(result, expectedStr) { + t.Errorf("Expected output to contain %q, got: %s", expectedStr, result) + } + } + }) + } +} + +func TestExecuteTool(t *testing.T) { + backend := setupTestBackend() + + tests := []struct { + name string + resp *filesystem.ExecuteResponse + input string + expected string + shouldError bool + }{ + { + name: "successful command execution", + resp: &filesystem.ExecuteResponse{ + Output: "hello world", + ExitCode: ptrOf(0), + }, + input: `{"command": "echo hello world"}`, + expected: "hello world", + }, + { + name: "command with non-zero exit code", + resp: &filesystem.ExecuteResponse{ + Output: "error: file not found", + ExitCode: ptrOf(1), + }, + input: `{"command": "cat nonexistent.txt"}`, + expected: "error: file not found\n[Command failed with exit code 1]", + }, + { + name: "command with truncated output", + resp: &filesystem.ExecuteResponse{ + Output: "partial output...", + ExitCode: ptrOf(0), + Truncated: true, + }, + input: `{"command": "cat largefile.txt"}`, + expected: "partial output...\n[Output was truncated due to size limits]", + }, + { + name: "command with both non-zero exit code and truncated output", + resp: &filesystem.ExecuteResponse{ + Output: "error output...", + ExitCode: ptrOf(2), + Truncated: true, + }, + input: `{"command": "failing command"}`, + expected: "error output...\n[Command failed with exit code 2]\n[Output was truncated due to size limits]", + }, + { + name: "successful command with no output", + resp: &filesystem.ExecuteResponse{ + Output: "", + ExitCode: ptrOf(0), + }, + input: `{"command": "mkdir /tmp/test"}`, + expected: "[Command executed successfully with no output]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + executeTool, err := newExecuteTool(&mockShellBackend{ + Backend: backend, + resp: tt.resp, + }, "", "") + assert.NoError(t, err) + + result, err := invokeTool(t, executeTool, tt.input) + if tt.shouldError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func ptrOf[T any](t T) *T { + return &t +} + +type mockShellBackend struct { + filesystem.Backend + resp *filesystem.ExecuteResponse +} + +func (m *mockShellBackend) Execute(ctx context.Context, req *filesystem.ExecuteRequest) (*filesystem.ExecuteResponse, error) { + return m.resp, nil +} + +func TestGetFilesystemTools(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("returns 6 tools for regular Backend", func(t *testing.T) { + tools, err := getFilesystemTools(ctx, &MiddlewareConfig{Backend: backend}) + assert.NoError(t, err) + assert.Len(t, tools, 6) + + // Verify tool names + toolNames := make([]string, 0, len(tools)) + for _, to := range tools { + info, _ := to.Info(ctx) + toolNames = append(toolNames, info.Name) + } + assert.Contains(t, toolNames, "ls") + assert.Contains(t, toolNames, "read_file") + assert.Contains(t, toolNames, "write_file") + assert.Contains(t, toolNames, "edit_file") + assert.Contains(t, toolNames, "glob") + assert.Contains(t, toolNames, "grep") + }) + + t.Run("returns 7 tools for Shell", func(t *testing.T) { + shellBackend := &mockShellBackend{ + Backend: backend, + resp: &filesystem.ExecuteResponse{Output: "ok"}, + } + tools, err := getFilesystemTools(ctx, &MiddlewareConfig{Backend: shellBackend, Shell: shellBackend}) + assert.NoError(t, err) + assert.Len(t, tools, 7) + + // Verify execute tool is included + toolNames := make([]string, 0, len(tools)) + for _, to := range tools { + info, _ := to.Info(ctx) + toolNames = append(toolNames, info.Name) + } + assert.Contains(t, toolNames, "execute") + }) + + t.Run("custom tool descriptions", func(t *testing.T) { + customLsDesc := "Custom ls description" + customReadDesc := "Custom read description" + + tools, err := getFilesystemTools(ctx, &MiddlewareConfig{ + Backend: backend, + CustomLsToolDesc: &customLsDesc, + CustomReadFileToolDesc: &customReadDesc, + }) + assert.NoError(t, err) + assert.Len(t, tools, 6) + + // Verify custom descriptions are applied + for _, to := range tools { + info, _ := to.Info(ctx) + if info.Name == "ls" { + assert.Equal(t, customLsDesc, info.Desc) + } + if info.Name == "read_file" { + assert.Equal(t, customReadDesc, info.Desc) + } + } + }) +} + +func TestNew(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("nil config returns error", func(t *testing.T) { + _, err := New(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config should not be nil") + }) + + t.Run("nil backend returns error", func(t *testing.T) { + _, err := New(ctx, &MiddlewareConfig{Backend: nil}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend should not be nil") + }) + + t.Run("valid config with default settings", func(t *testing.T) { + m, err := New(ctx, &MiddlewareConfig{Backend: backend}) + assert.NoError(t, err) + assert.NotNil(t, m) + + fm, ok := m.(*typedFilesystemMiddleware[*schema.Message]) + assert.True(t, ok) + assert.Len(t, fm.additionalTools, 6) + }) + + t.Run("custom system prompt", func(t *testing.T) { + customPrompt := "Custom system prompt" + m, err := New(ctx, &MiddlewareConfig{ + Backend: backend, + CustomSystemPrompt: &customPrompt, + }) + assert.NoError(t, err) + + fm, ok := m.(*typedFilesystemMiddleware[*schema.Message]) + assert.True(t, ok) + assert.Equal(t, customPrompt, fm.additionalInstruction) + }) + + t.Run("ShellBackend adds execute tool", func(t *testing.T) { + shellBackend := &mockShellBackend{ + Backend: backend, + resp: &filesystem.ExecuteResponse{Output: "ok"}, + } + m, err := New(ctx, &MiddlewareConfig{Backend: shellBackend, Shell: shellBackend}) + assert.NoError(t, err) + + fm, ok := m.(*typedFilesystemMiddleware[*schema.Message]) + assert.True(t, ok) + assert.Len(t, fm.additionalTools, 7) + }) +} + +func TestFilesystemMiddleware_BeforeAgent(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("adds instruction and tools to context", func(t *testing.T) { + m, err := New(ctx, &MiddlewareConfig{Backend: backend}) + assert.NoError(t, err) + + runCtx := &adk.ChatModelAgentContext{ + Instruction: "Original instruction", + Tools: nil, + } + + newCtx, newRunCtx, err := m.BeforeAgent(ctx, runCtx) + assert.NoError(t, err) + assert.NotNil(t, newCtx) + assert.NotNil(t, newRunCtx) + assert.Contains(t, newRunCtx.Instruction, "Original instruction") + assert.Len(t, newRunCtx.Tools, 6) + }) + + t.Run("nil runCtx returns nil", func(t *testing.T) { + m, err := New(ctx, &MiddlewareConfig{Backend: backend}) + assert.NoError(t, err) + + newCtx, newRunCtx, err := m.BeforeAgent(ctx, nil) + assert.NoError(t, err) + assert.NotNil(t, newCtx) + assert.Nil(t, newRunCtx) + }) +} + +func TestFilesystemMiddleware_WrapInvokableToolCall(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("small result passes through unchanged", func(t *testing.T) { + m, err := New(ctx, &MiddlewareConfig{Backend: backend}) + assert.NoError(t, err) + + endpoint := func(ctx context.Context, args string, opts ...tool.Option) (string, error) { + return "small result", nil + } + + tCtx := &adk.ToolContext{Name: "test_tool", CallID: "call-1"} + wrapped, err := m.WrapInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + + result, err := wrapped(ctx, "{}") + assert.NoError(t, err) + assert.Equal(t, "small result", result) + }) + +} + +func TestGrepToolWithSortingAndPagination(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + ctx := context.Background() + + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/zebra.txt", + Content: "match1\nmatch2\nmatch3", + }) + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/apple.txt", + Content: "match4\nmatch5", + }) + backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/banana.txt", + Content: "match6\nmatch7\nmatch8", + }) + + grepTool, err := newGrepTool(backend, "", "") + assert.NoError(t, err) + + t.Run("files sorted by basename", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "files_with_matches"}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 4, len(lines)) // 1 summary + 3 files + assert.Contains(t, lines[0], "Found 3 files") + assert.Contains(t, lines[1], "apple.txt") + assert.Contains(t, lines[2], "banana.txt") + assert.Contains(t, lines[3], "zebra.txt") + }) + + t.Run("files_with_matches with offset", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "files_with_matches", "offset": 1}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 3, len(lines)) // 1 summary + 2 files (pagination applied) + assert.Contains(t, lines[0], "Found 3 files") // total count before pagination + assert.Contains(t, lines[1], "banana.txt") + assert.Contains(t, lines[2], "zebra.txt") + }) + + t.Run("files_with_matches with head_limit", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "files_with_matches", "head_limit": 2}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 3, len(lines)) // 1 summary + 2 files (pagination applied) + assert.Contains(t, lines[0], "Found 3 files") // total count before pagination + assert.Contains(t, lines[1], "apple.txt") + assert.Contains(t, lines[2], "banana.txt") + }) + + t.Run("files_with_matches with offset and head_limit", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "files_with_matches", "offset": 1, "head_limit": 1}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 2, len(lines)) // 1 summary + 1 file (pagination applied) + assert.Contains(t, lines[0], "Found 3 files") // total count before pagination + assert.Contains(t, lines[1], "banana.txt") + }) + + t.Run("content mode sorted and paginated", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "content", "head_limit": 3}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 3, len(lines)) + assert.Contains(t, lines[0], "apple.txt") + }) + + t.Run("content mode with offset", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "content", "offset": 2, "head_limit": 2}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 2, len(lines)) + }) + + t.Run("count mode sorted", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "count"}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 5, len(lines)) // 3 file counts + 1 empty line + 1 summary line + assert.Contains(t, lines[0], "apple.txt:2") + assert.Contains(t, lines[1], "banana.txt:3") + assert.Contains(t, lines[2], "zebra.txt:3") + assert.Contains(t, lines[4], "Found 8 total occurrences across 3 files.") + }) + + t.Run("count mode with pagination", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "count", "offset": 1, "head_limit": 1}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 3, len(lines)) // 1 file count + 1 empty line + 1 summary line + assert.Contains(t, lines[0], "banana.txt:3") + assert.Contains(t, lines[2], "Found 8 total occurrences across 3 files.") // summary shows total before pagination + }) + + t.Run("offset exceeds result count", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "files_with_matches", "offset": 100}`) + assert.NoError(t, err) + assert.Contains(t, result, "Found 3 files") // still shows total count + }) + + t.Run("negative offset treated as zero", func(t *testing.T) { + result, err := invokeTool(t, grepTool, `{"pattern": "match", "output_mode": "files_with_matches", "offset": -5}`) + assert.NoError(t, err) + lines := strings.Split(strings.TrimSpace(result), "\n") + assert.Equal(t, 4, len(lines)) // 1 summary + 3 files + }) +} + +func TestApplyPagination(t *testing.T) { + t.Run("basic pagination", func(t *testing.T) { + items := []string{"a", "b", "c", "d", "e"} + result := applyPagination(items, 0, 3) + assert.Equal(t, []string{"a", "b", "c"}, result) + }) + + t.Run("with offset", func(t *testing.T) { + items := []string{"a", "b", "c", "d", "e"} + result := applyPagination(items, 2, 2) + assert.Equal(t, []string{"c", "d"}, result) + }) + + t.Run("offset exceeds length", func(t *testing.T) { + items := []string{"a", "b", "c"} + result := applyPagination(items, 10, 5) + assert.Equal(t, []string{}, result) + }) + + t.Run("negative offset", func(t *testing.T) { + items := []string{"a", "b", "c"} + result := applyPagination(items, -1, 2) + assert.Equal(t, []string{"a", "b"}, result) + }) + + t.Run("zero head limit means no limit", func(t *testing.T) { + items := []string{"a", "b", "c", "d", "e"} + result := applyPagination(items, 1, 0) + assert.Equal(t, []string{"b", "c", "d", "e"}, result) + }) +} + +func TestCustomToolNames(t *testing.T) { + backend := setupTestBackend() + ctx := context.Background() + + t.Run("custom tool names applied to individual tools", func(t *testing.T) { + customLsName := "list_files" + customReadName := "read" + customWriteName := "write" + customEditName := "edit" + customGlobName := "find_files" + customGrepName := "search" + + lsTool, err := newLsTool(backend, customLsName, "") + assert.NoError(t, err) + info, _ := lsTool.Info(ctx) + assert.Equal(t, "list_files", info.Name) + + readTool, err := newReadFileTool(backend, customReadName, "") + assert.NoError(t, err) + info, _ = readTool.Info(ctx) + assert.Equal(t, "read", info.Name) + + writeTool, err := newWriteFileTool(backend, customWriteName, "") + assert.NoError(t, err) + info, _ = writeTool.Info(ctx) + assert.Equal(t, "write", info.Name) + + editTool, err := newEditFileTool(backend, customEditName, "") + assert.NoError(t, err) + info, _ = editTool.Info(ctx) + assert.Equal(t, "edit", info.Name) + + globTool, err := newGlobTool(backend, customGlobName, "") + assert.NoError(t, err) + info, _ = globTool.Info(ctx) + assert.Equal(t, "find_files", info.Name) + + grepTool, err := newGrepTool(backend, customGrepName, "") + assert.NoError(t, err) + info, _ = grepTool.Info(ctx) + assert.Equal(t, "search", info.Name) + }) + + t.Run("default tool names when custom names not provided", func(t *testing.T) { + lsTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + info, _ := lsTool.Info(ctx) + assert.Equal(t, ToolNameLs, info.Name) + + readTool, err := newReadFileTool(backend, "", "") + assert.NoError(t, err) + info, _ = readTool.Info(ctx) + assert.Equal(t, ToolNameReadFile, info.Name) + + writeTool, err := newWriteFileTool(backend, "", "") + assert.NoError(t, err) + info, _ = writeTool.Info(ctx) + assert.Equal(t, ToolNameWriteFile, info.Name) + + editTool, err := newEditFileTool(backend, "", "") + assert.NoError(t, err) + info, _ = editTool.Info(ctx) + assert.Equal(t, ToolNameEditFile, info.Name) + + globTool, err := newGlobTool(backend, "", "") + assert.NoError(t, err) + info, _ = globTool.Info(ctx) + assert.Equal(t, ToolNameGlob, info.Name) + + grepTool, err := newGrepTool(backend, "", "") + assert.NoError(t, err) + info, _ = grepTool.Info(ctx) + assert.Equal(t, ToolNameGrep, info.Name) + }) + + t.Run("custom execute tool name", func(t *testing.T) { + customExecuteName := "run_command" + shellBackend := &mockShellBackend{ + Backend: backend, + resp: &filesystem.ExecuteResponse{Output: "ok"}, + } + + executeTool, err := newExecuteTool(shellBackend, customExecuteName, "") + assert.NoError(t, err) + info, _ := executeTool.Info(ctx) + assert.Equal(t, "run_command", info.Name) + }) + + t.Run("custom tool names via ToolConfig in getFilesystemTools", func(t *testing.T) { + customLsName := "list_files" + customReadName := "read" + + tools, err := getFilesystemTools(ctx, &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Name: customLsName, + }, + ReadFileToolConfig: &ToolConfig{ + Name: customReadName, + }, + }) + assert.NoError(t, err) + + toolNames := make(map[string]bool) + for _, to := range tools { + info, _ := to.Info(ctx) + toolNames[info.Name] = true + } + + assert.True(t, toolNames["list_files"]) + assert.True(t, toolNames["read"]) + }) + + t.Run("custom tool names via ToolConfig in middleware", func(t *testing.T) { + customLsName := "list_files" + customReadName := "read" + + m, err := New(ctx, &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Name: customLsName, + }, + ReadFileToolConfig: &ToolConfig{ + Name: customReadName, + }, + }) + assert.NoError(t, err) + + fm, ok := m.(*typedFilesystemMiddleware[*schema.Message]) + assert.True(t, ok) + + toolNames := make(map[string]bool) + for _, to := range fm.additionalTools { + info, _ := to.Info(ctx) + toolNames[info.Name] = true + } + + assert.True(t, toolNames["list_files"]) + assert.True(t, toolNames["read"]) + }) +} + +func TestSelectToolName(t *testing.T) { + t.Run("returns custom name when provided", func(t *testing.T) { + customName := "custom_tool" + result := selectToolName(customName, "default_tool") + assert.Equal(t, "custom_tool", result) + }) + + t.Run("returns default name when custom name is nil", func(t *testing.T) { + result := selectToolName("", "default_tool") + assert.Equal(t, "default_tool", result) + }) +} + +func TestGetOrCreateTool(t *testing.T) { + backend := setupTestBackend() + + t.Run("returns custom tool when provided", func(t *testing.T) { + customTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + + result, err := getOrCreateTool(customTool, func() (tool.BaseTool, error) { + t.Fatal("createFunc should not be called when custom tool is provided") + return nil, nil + }) + + assert.NoError(t, err) + assert.Equal(t, customTool, result) + }) + + t.Run("calls createFunc when custom tool is nil", func(t *testing.T) { + expectedTool, err := newReadFileTool(backend, "", "") + assert.NoError(t, err) + + createFuncCalled := false + result, err := getOrCreateTool(nil, func() (tool.BaseTool, error) { + createFuncCalled = true + return expectedTool, nil + }) + + assert.NoError(t, err) + assert.True(t, createFuncCalled, "createFunc should be called when custom tool is nil") + assert.Equal(t, expectedTool, result) + }) + + t.Run("returns nil when custom tool is nil and createFunc returns nil", func(t *testing.T) { + result, err := getOrCreateTool(nil, func() (tool.BaseTool, error) { + return nil, nil + }) + + assert.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("propagates error from createFunc", func(t *testing.T) { + expectedErr := assert.AnError + + result, err := getOrCreateTool(nil, func() (tool.BaseTool, error) { + return nil, expectedErr + }) + + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + assert.Nil(t, result) + }) +} + +func TestCustomTools(t *testing.T) { + backend := setupTestBackend() + ctx := context.Background() + + t.Run("custom ls tool is used via ToolConfig", func(t *testing.T) { + customLsTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + LsToolConfig: &ToolConfig{ + CustomTool: customLsTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + assert.Equal(t, customLsTool, tools[0]) + }) + + t.Run("custom read file tool is used via ToolConfig", func(t *testing.T) { + customReadTool, err := newReadFileTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + ReadFileToolConfig: &ToolConfig{ + CustomTool: customReadTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + assert.Equal(t, customReadTool, tools[0]) + }) + + t.Run("custom write file tool is used via ToolConfig", func(t *testing.T) { + customWriteTool, err := newWriteFileTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + WriteFileToolConfig: &ToolConfig{ + CustomTool: customWriteTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + assert.Equal(t, customWriteTool, tools[0]) + }) + + t.Run("custom edit file tool is used via ToolConfig", func(t *testing.T) { + customEditTool, err := newEditFileTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + EditFileToolConfig: &ToolConfig{ + CustomTool: customEditTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + assert.Equal(t, customEditTool, tools[0]) + }) + + t.Run("custom glob tool is used via ToolConfig", func(t *testing.T) { + customGlobTool, err := newGlobTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + GlobToolConfig: &ToolConfig{ + CustomTool: customGlobTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + assert.Equal(t, customGlobTool, tools[0]) + }) + + t.Run("custom grep tool is used via ToolConfig", func(t *testing.T) { + customGrepTool, err := newGrepTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + GrepToolConfig: &ToolConfig{ + CustomTool: customGrepTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + assert.Equal(t, customGrepTool, tools[0]) + }) + + t.Run("multiple custom tools can be used together", func(t *testing.T) { + customLsTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + customReadTool, err := newReadFileTool(backend, "", "") + assert.NoError(t, err) + customGlobTool, err := newGlobTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + LsToolConfig: &ToolConfig{ + CustomTool: customLsTool, + }, + ReadFileToolConfig: &ToolConfig{ + CustomTool: customReadTool, + }, + GlobToolConfig: &ToolConfig{ + CustomTool: customGlobTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 3) + }) + + t.Run("custom tools take precedence over backend", func(t *testing.T) { + customLsTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + CustomTool: customLsTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + lsToolFound := false + for _, t := range tools { + if t == customLsTool { + lsToolFound = true + break + } + } + assert.True(t, lsToolFound, "custom ls tool should be in the tools list") + }) + + t.Run("backend tools are created when custom tools not provided", func(t *testing.T) { + config := &MiddlewareConfig{ + Backend: backend, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Greater(t, len(tools), 0, "should create backend tools when custom tools not provided") + }) +} + +func TestToolConfig(t *testing.T) { + backend := setupTestBackend() + ctx := context.Background() + + t.Run("use new ToolConfig", func(t *testing.T) { + customName := "my_ls" + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Name: customName, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 6) + + var lsToolFound bool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == "my_ls" { + lsToolFound = true + break + } + } + assert.True(t, lsToolFound) + }) + + t.Run("ToolConfig disabled", func(t *testing.T) { + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Disable: true, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 5) + + for _, tool := range tools { + info, _ := tool.Info(ctx) + assert.NotEqual(t, ToolNameLs, info.Name) + } + }) + + t.Run("ToolConfig with custom tool", func(t *testing.T) { + customLsTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + CustomTool: customLsTool, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var lsToolFound bool + for _, tool := range tools { + if tool == customLsTool { + lsToolFound = true + break + } + } + assert.True(t, lsToolFound) + }) + + t.Run("ToolConfig Desc takes precedence over legacy Desc", func(t *testing.T) { + customDesc := "new description" + legacyDesc := "old description" + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Desc: &customDesc, + }, + CustomLsToolDesc: &legacyDesc, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var found bool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == ToolNameLs && info.Desc == "new description" { + found = true + break + } + } + assert.True(t, found) + }) + + t.Run("legacy Desc field still works", func(t *testing.T) { + legacyDesc := "legacy description" + config := &MiddlewareConfig{ + Backend: backend, + CustomLsToolDesc: &legacyDesc, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var found bool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == ToolNameLs && info.Desc == "legacy description" { + found = true + break + } + } + assert.True(t, found) + }) + + t.Run("multiple ToolConfig", func(t *testing.T) { + lsName := "my_ls" + readName := "my_read" + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Name: lsName, + }, + ReadFileToolConfig: &ToolConfig{ + Name: readName, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + toolNames := make(map[string]bool) + for _, tool := range tools { + info, _ := tool.Info(ctx) + toolNames[info.Name] = true + } + + assert.True(t, toolNames["my_ls"]) + assert.True(t, toolNames["my_read"]) + }) +} + +func TestToolConfigEdgeCases(t *testing.T) { + backend := setupTestBackend() + ctx := context.Background() + + t.Run("nil ToolConfig.Desc with nil legacyDesc", func(t *testing.T) { + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Desc: nil, + }, + CustomLsToolDesc: nil, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var lsTool tool.BaseTool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == ToolNameLs { + lsTool = tool + break + } + } + assert.NotNil(t, lsTool, "ls tool should be created even with nil Desc") + }) + + t.Run("nil ToolConfig.Desc falls back to legacyDesc", func(t *testing.T) { + legacyDesc := "legacy description from pointer" + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Desc: nil, + }, + CustomLsToolDesc: &legacyDesc, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var found bool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == ToolNameLs && info.Desc == "legacy description from pointer" { + found = true + break + } + } + assert.True(t, found, "nil ToolConfig.Desc should fall back to legacyDesc") + }) + + t.Run("CustomTool with Disable flag should not create tool", func(t *testing.T) { + customLsTool, err := newLsTool(backend, "", "") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + CustomTool: customLsTool, + Disable: true, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + for _, tool := range tools { + info, _ := tool.Info(ctx) + assert.NotEqual(t, ToolNameLs, info.Name, "disabled tool should not be created even if CustomTool is set") + } + }) + + t.Run("multiple ToolConfig with conflicting settings", func(t *testing.T) { + legacyDesc := "legacy ls desc" + customDesc := "custom desc" + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Name: "custom_ls", + Desc: &customDesc, + Disable: false, + }, + CustomLsToolDesc: &legacyDesc, + ReadFileToolConfig: &ToolConfig{ + Disable: true, + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + hasLsTool := false + hasReadTool := false + + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == "custom_ls" { + hasLsTool = true + assert.Equal(t, "custom desc", info.Desc, "ToolConfig.Desc should take precedence over legacy") + } + if info.Name == ToolNameReadFile { + hasReadTool = true + } + } + + assert.True(t, hasLsTool, "ls tool should be created") + assert.False(t, hasReadTool, "read_file tool should be disabled") + }) + + t.Run("nil ToolConfig with nil legacyDesc creates default tool", func(t *testing.T) { + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: nil, + CustomLsToolDesc: nil, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var lsTool tool.BaseTool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == ToolNameLs { + lsTool = tool + break + } + } + assert.NotNil(t, lsTool, "tool should be created with backend even when config is nil") + }) + + t.Run("empty Name in ToolConfig uses default name", func(t *testing.T) { + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + Name: "", + }, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var lsTool tool.BaseTool + for _, tool := range tools { + info, _ := tool.Info(ctx) + if info.Name == ToolNameLs { + lsTool = tool + break + } + } + assert.NotNil(t, lsTool, "tool should use default name when Name is empty") + }) +} + +func TestGetFilesystemTools_DisableAllTools(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{Disable: true}, + ReadFileToolConfig: &ToolConfig{Disable: true}, + WriteFileToolConfig: &ToolConfig{Disable: true}, + EditFileToolConfig: &ToolConfig{Disable: true}, + GlobToolConfig: &ToolConfig{Disable: true}, + GrepToolConfig: &ToolConfig{Disable: true}, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 0) +} + +func TestGetFilesystemTools_StreamingShell(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("returns 7 tools with StreamingShell", func(t *testing.T) { + mockSS := &mockStreamingShell{} + tools, err := getFilesystemTools(ctx, &MiddlewareConfig{ + Backend: backend, + StreamingShell: mockSS, + }) + assert.NoError(t, err) + assert.Len(t, tools, 7) + + toolNames := make([]string, 0, len(tools)) + for _, to := range tools { + info, _ := to.Info(ctx) + toolNames = append(toolNames, info.Name) + } + assert.Contains(t, toolNames, ToolNameExecute) + }) + + t.Run("StreamingShell takes precedence over Shell", func(t *testing.T) { + mockSS := &mockStreamingShell{} + shellBackend := &mockShellBackend{ + Backend: backend, + resp: &filesystem.ExecuteResponse{Output: "ok"}, + } + + // When both are set, Validate should fail + config := &MiddlewareConfig{ + Backend: backend, + Shell: shellBackend, + StreamingShell: mockSS, + } + err := config.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "shell and streaming shell should not be both set") + }) +} + +func TestGetFilesystemTools_NilBackend(t *testing.T) { + ctx := context.Background() + + t.Run("nil backend with shell only returns execute tool", func(t *testing.T) { + mockSS := &mockStreamingShell{} + config := &MiddlewareConfig{ + Backend: nil, + StreamingShell: mockSS, + } + // Validate should fail, but getFilesystemTools itself handles nil backend gracefully + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + // Only execute tool should be returned since backend is nil + assert.Len(t, tools, 1) + + info, _ := tools[0].Info(ctx) + assert.Equal(t, ToolNameExecute, info.Name) + }) + + t.Run("nil backend with regular Shell returns execute tool", func(t *testing.T) { + mockShell := &mockShellBackend{ + resp: &filesystem.ExecuteResponse{Output: "ok"}, + } + config := &MiddlewareConfig{ + Backend: nil, + Shell: mockShell, + } + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 1) + + info, _ := tools[0].Info(ctx) + assert.Equal(t, ToolNameExecute, info.Name) + }) + + t.Run("nil backend and nil shell returns empty tools", func(t *testing.T) { + config := &MiddlewareConfig{ + Backend: nil, + } + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 0) + }) +} + +func TestGetFilesystemTools_PartialDisable(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{Disable: true}, + ReadFileToolConfig: &ToolConfig{Disable: true}, + } + + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + assert.Len(t, tools, 4) + + toolNames := make([]string, 0, len(tools)) + for _, to := range tools { + info, _ := to.Info(ctx) + toolNames = append(toolNames, info.Name) + } + assert.NotContains(t, toolNames, ToolNameLs) + assert.NotContains(t, toolNames, ToolNameReadFile) + assert.Contains(t, toolNames, ToolNameWriteFile) + assert.Contains(t, toolNames, ToolNameEditFile) + assert.Contains(t, toolNames, ToolNameGlob) + assert.Contains(t, toolNames, ToolNameGrep) +} + +type mockStreamingShell struct{} + +func (m *mockStreamingShell) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(&filesystem.ExecuteResponse{ + Output: "streaming output", + ExitCode: ptrOf(0), + }, nil) + }() + return sr, nil +} + +type mockStreamingShellWithError struct{} + +func (m *mockStreamingShellWithError) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + return nil, fmt.Errorf("streaming shell error") +} + +type mockStreamingShellWithRecvError struct{} + +func (m *mockStreamingShellWithRecvError) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(nil, fmt.Errorf("recv error during streaming")) + }() + return sr, nil +} + +type mockStreamingShellWithExitCode struct { + exitCode int +} + +func (m *mockStreamingShellWithExitCode) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(&filesystem.ExecuteResponse{ + Output: "some output", + ExitCode: ptrOf(m.exitCode), + }, nil) + }() + return sr, nil +} + +type mockStreamingShellNoOutput struct{} + +func (m *mockStreamingShellNoOutput) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(&filesystem.ExecuteResponse{ + ExitCode: ptrOf(0), + }, nil) + }() + return sr, nil +} + +type mockStreamingShellTruncated struct{} + +func (m *mockStreamingShellTruncated) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(&filesystem.ExecuteResponse{ + Output: "partial", + Truncated: true, + ExitCode: ptrOf(0), + }, nil) + }() + return sr, nil +} + +type mockStreamingShellNilChunk struct{} + +func (m *mockStreamingShellNilChunk) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(nil, nil) + sw.Send(&filesystem.ExecuteResponse{ + Output: "after nil", + ExitCode: ptrOf(0), + }, nil) + }() + return sr, nil +} + +func TestNewStreamingExecuteTool(t *testing.T) { + t.Run("successful streaming execution", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShell{}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "echo hello"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + assert.True(t, len(chunks) > 0) + result := strings.Join(chunks, "") + assert.Contains(t, result, "streaming output") + }) + + t.Run("streaming execution with ExecuteStreaming error", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShellWithError{}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + _, err = st.StreamableRun(context.Background(), `{"command": "fail"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "streaming shell error") + }) + + t.Run("streaming execution with recv error", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShellWithRecvError{}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "echo hello"}`) + assert.NoError(t, err) + defer sr.Close() + + var gotError bool + for { + _, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + if recvErr != nil { + gotError = true + assert.Contains(t, recvErr.Error(), "recv error during streaming") + break + } + } + assert.True(t, gotError) + }) + + t.Run("streaming execution with non-zero exit code", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShellWithExitCode{exitCode: 1}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "false"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + result := strings.Join(chunks, "") + assert.Contains(t, result, "[Command failed with exit code 1]") + }) + + t.Run("streaming execution with zero exit code and no output", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShellNoOutput{}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "true"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + result := strings.Join(chunks, "") + assert.Contains(t, result, "[Command executed successfully with no output]") + }) + + t.Run("streaming execution with truncated output", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShellTruncated{}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "cat largefile"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + result := strings.Join(chunks, "") + assert.Contains(t, result, "partial") + assert.Contains(t, result, "[Output was truncated due to size limits]") + }) + + t.Run("streaming execution with nil chunk skipped", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShellNilChunk{}, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "echo test"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + result := strings.Join(chunks, "") + assert.Contains(t, result, "after nil") + }) + + t.Run("streaming execution with custom name and desc", func(t *testing.T) { + executeTool, err := newStreamingExecuteTool(&mockStreamingShell{}, "custom_execute", "custom desc") + assert.NoError(t, err) + + info, err := executeTool.Info(context.Background()) + assert.NoError(t, err) + assert.Equal(t, "custom_execute", info.Name) + assert.Equal(t, "custom desc", info.Desc) + }) +} + +func TestNew_StreamingShell(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("StreamingShell adds streaming execute tool", func(t *testing.T) { + m, err := New(ctx, &MiddlewareConfig{ + Backend: backend, + StreamingShell: &mockStreamingShell{}, + }) + assert.NoError(t, err) + + fm, ok := m.(*typedFilesystemMiddleware[*schema.Message]) + assert.True(t, ok) + assert.Len(t, fm.additionalTools, 7) + }) + + t.Run("both Shell and StreamingShell returns error", func(t *testing.T) { + _, err := New(ctx, &MiddlewareConfig{ + Backend: backend, + Shell: &mockShellBackend{Backend: backend, resp: &filesystem.ExecuteResponse{Output: "ok"}}, + StreamingShell: &mockStreamingShell{}, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "shell and streaming shell should not be both set") + }) +} + +func TestNewMiddleware_Validation(t *testing.T) { + ctx := context.Background() + + t.Run("nil config returns error", func(t *testing.T) { + _, err := NewMiddleware(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config should not be nil") + }) + + t.Run("nil backend returns error", func(t *testing.T) { + _, err := NewMiddleware(ctx, &Config{Backend: nil}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend should not be nil") + }) + + t.Run("both Shell and StreamingShell returns error", func(t *testing.T) { + backend := setupTestBackend() + _, err := NewMiddleware(ctx, &Config{ + Backend: backend, + Shell: &mockShellBackend{Backend: backend, resp: &filesystem.ExecuteResponse{Output: "ok"}}, + StreamingShell: &mockStreamingShell{}, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "shell and streaming shell should not be both set") + }) +} + +func TestMiddlewareConfig_Validate(t *testing.T) { + t.Run("nil config returns error", func(t *testing.T) { + var c *MiddlewareConfig + err := c.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "config should not be nil") + }) + + t.Run("nil backend returns error", func(t *testing.T) { + c := &MiddlewareConfig{} + err := c.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend should not be nil") + }) + + t.Run("both shells returns error", func(t *testing.T) { + c := &MiddlewareConfig{ + Backend: setupTestBackend(), + Shell: &mockShellBackend{}, + StreamingShell: &mockStreamingShell{}, + } + err := c.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "shell and streaming shell should not be both set") + }) + + t.Run("valid config passes", func(t *testing.T) { + c := &MiddlewareConfig{ + Backend: setupTestBackend(), + } + err := c.Validate() + assert.NoError(t, err) + }) +} + +func TestNewStreamingExecuteTool_MultipleChunks(t *testing.T) { + mockSS := &mockStreamingShellMultiChunk{} + executeTool, err := newStreamingExecuteTool(mockSS, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "long-running"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + // Should have received multiple chunks + assert.True(t, len(chunks) >= 3) + result := strings.Join(chunks, "") + assert.Contains(t, result, "chunk1") + assert.Contains(t, result, "chunk2") + assert.Contains(t, result, "chunk3") +} + +type mockStreamingShellMultiChunk struct{} + +func (m *mockStreamingShellMultiChunk) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(&filesystem.ExecuteResponse{Output: "chunk1\n"}, nil) + sw.Send(&filesystem.ExecuteResponse{Output: "chunk2\n"}, nil) + sw.Send(&filesystem.ExecuteResponse{Output: "chunk3\n", ExitCode: ptrOf(0)}, nil) + }() + return sr, nil +} + +func TestNewStreamingExecuteTool_ExitCodeOnlyInLastChunk(t *testing.T) { + mockSS := &mockStreamingShellExitCodeLast{exitCode: 2} + executeTool, err := newStreamingExecuteTool(mockSS, "", "") + assert.NoError(t, err) + + st := executeTool.(tool.StreamableTool) + sr, err := st.StreamableRun(context.Background(), `{"command": "fail-at-end"}`) + assert.NoError(t, err) + defer sr.Close() + + var chunks []string + for { + chunk, recvErr := sr.Recv() + if recvErr == io.EOF { + break + } + assert.NoError(t, recvErr) + chunks = append(chunks, chunk) + } + result := strings.Join(chunks, "") + assert.Contains(t, result, "output line") + assert.Contains(t, result, "[Command failed with exit code 2]") +} + +type mockStreamingShellExitCodeLast struct { + exitCode int +} + +func (m *mockStreamingShellExitCodeLast) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { + sr, sw := schema.Pipe[*filesystem.ExecuteResponse](10) + go func() { + defer sw.Close() + sw.Send(&filesystem.ExecuteResponse{Output: "output line"}, nil) + sw.Send(&filesystem.ExecuteResponse{ExitCode: ptrOf(m.exitCode)}, nil) + }() + return sr, nil +} + +func TestConvExecuteResponse_NilResponse(t *testing.T) { + result := convExecuteResponse(nil) + assert.Equal(t, "", result) +} + +func TestConvExecuteResponse_NilExitCode(t *testing.T) { + result := convExecuteResponse(&filesystem.ExecuteResponse{ + Output: "some output", + }) + assert.Equal(t, "some output", result) +} + +func TestConfig_Validate(t *testing.T) { + t.Run("nil config returns error", func(t *testing.T) { + var c *Config + err := c.Validate() + assert.Error(t, err) + }) + + t.Run("nil backend returns error", func(t *testing.T) { + c := &Config{} + err := c.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend should not be nil") + }) + + t.Run("both shells returns error", func(t *testing.T) { + c := &Config{ + Backend: setupTestBackend(), + Shell: &mockShellBackend{}, + StreamingShell: &mockStreamingShell{}, + } + err := c.Validate() + assert.Error(t, err) + }) + + t.Run("valid config passes", func(t *testing.T) { + c := &Config{ + Backend: setupTestBackend(), + } + err := c.Validate() + assert.NoError(t, err) + }) +} + +func TestGetFilesystemTools_CustomToolWithShell(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("custom tool replaces default for all disabled except custom", func(t *testing.T) { + customLs, err := newLsTool(backend, "my_ls", "my ls desc") + assert.NoError(t, err) + + config := &MiddlewareConfig{ + Backend: backend, + LsToolConfig: &ToolConfig{ + CustomTool: customLs, + }, + } + tools, err := getFilesystemTools(ctx, config) + assert.NoError(t, err) + + var found bool + for _, to := range tools { + info, _ := to.Info(ctx) + if info.Name == "my_ls" { + found = true + break + } + } + assert.True(t, found) + }) +} + +func TestMergeToolConfigWithDesc(t *testing.T) { + config := &MiddlewareConfig{Backend: setupTestBackend()} + + t.Run("both nil returns empty ToolConfig", func(t *testing.T) { + result := config.mergeToolConfigWithDesc(nil, nil) + assert.NotNil(t, result) + assert.Equal(t, "", result.Name) + assert.Nil(t, result.Desc) + assert.False(t, result.Disable) + }) + + t.Run("nil toolConfig with legacyDesc", func(t *testing.T) { + desc := "legacy" + result := config.mergeToolConfigWithDesc(nil, &desc) + assert.NotNil(t, result) + assert.Equal(t, "legacy", *result.Desc) + }) + + t.Run("toolConfig with Desc overrides legacyDesc", func(t *testing.T) { + tcDesc := "tc desc" + legacyDesc := "legacy" + tc := &ToolConfig{Desc: &tcDesc} + result := config.mergeToolConfigWithDesc(tc, &legacyDesc) + assert.Equal(t, "tc desc", *result.Desc) + }) + + t.Run("toolConfig with nil Desc falls back to legacyDesc", func(t *testing.T) { + legacyDesc := "legacy" + tc := &ToolConfig{Name: "custom"} + result := config.mergeToolConfigWithDesc(tc, &legacyDesc) + assert.Equal(t, "legacy", *result.Desc) + assert.Equal(t, "custom", result.Name) + }) + + t.Run("toolConfig with nil Desc and nil legacyDesc", func(t *testing.T) { + tc := &ToolConfig{Name: "custom"} + result := config.mergeToolConfigWithDesc(tc, nil) + assert.Nil(t, result.Desc) + assert.Equal(t, "custom", result.Name) + }) +} + +func TestNewMiddleware_WithShell(t *testing.T) { + ctx := context.Background() + backend := setupTestBackend() + + t.Run("Shell backend creates execute tool", func(t *testing.T) { + shellBackend := &mockShellBackend{ + Backend: backend, + resp: &filesystem.ExecuteResponse{Output: "ok"}, + } + m, err := NewMiddleware(ctx, &Config{ + Backend: backend, + Shell: shellBackend, + }) + assert.NoError(t, err) + assert.Len(t, m.AdditionalTools, 7) + }) + + t.Run("StreamingShell backend creates streaming execute tool", func(t *testing.T) { + m, err := NewMiddleware(ctx, &Config{ + Backend: backend, + StreamingShell: &mockStreamingShell{}, + }) + assert.NoError(t, err) + assert.Len(t, m.AdditionalTools, 7) + }) +} + +func TestNewExecuteTool_ShellError(t *testing.T) { + mockShell := &mockShellBackendWithError{} + executeTool, err := newExecuteTool(mockShell, "", "") + assert.NoError(t, err) + + result, err := invokeTool(t, executeTool, `{"command": "fail"}`) + assert.Error(t, err) + assert.Equal(t, "", result) + assert.Contains(t, err.Error(), "shell execution error") +} + +type mockShellBackendWithError struct{} + +func (m *mockShellBackendWithError) Execute(ctx context.Context, req *filesystem.ExecuteRequest) (*filesystem.ExecuteResponse, error) { + return nil, errors.New("shell execution error") +} + +// multiModalBackend wraps InMemoryBackend and implements MultiModalReader for testing. +type multiModalBackend struct { + *filesystem.InMemoryBackend + multiModalReadFunc func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) +} + +func (b *multiModalBackend) MultiModalRead(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + return b.multiModalReadFunc(ctx, req) +} + +func TestMultiModalReadFileTool_TextOnly(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + ct, err := base.Read(ctx, &req.ReadRequest) + if err != nil { + return nil, err + } + return &filesystem.MultiFileContent{ + FileContent: ct, + }, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + result, err := mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/file1.txt", "offset": 0, "limit": 100}`}) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeText, result.Parts[0].Type) + assert.Contains(t, result.Parts[0].Text, "line1") + assert.Contains(t, result.Parts[0].Text, "line5") +} + +func TestMultiModalReadFileTool_Multimodal(t *testing.T) { + base := setupTestBackend() + imgData := []byte("rawimagedata") + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + return &filesystem.MultiFileContent{ + Parts: []filesystem.FileContentPart{ + { + Type: filesystem.FileContentPartTypeImage, + MIMEType: "image/png", + Data: imgData, + }, + }, + }, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + result, err := mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/image.png", "offset": 0, "limit": 100}`}) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeImage, result.Parts[0].Type) + + // Verify base64 encoding correctness + assert.NotNil(t, result.Parts[0].Image) + assert.Equal(t, "image/png", result.Parts[0].Image.MIMEType) + assert.Equal(t, base64.StdEncoding.EncodeToString(imgData), *result.Parts[0].Image.Base64Data) +} + +func TestMultiModalReadFileTool_FileType(t *testing.T) { + base := setupTestBackend() + pdfData := []byte("fakepdfcontent") + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + return &filesystem.MultiFileContent{ + Parts: []filesystem.FileContentPart{ + { + Type: filesystem.FileContentPartTypePDF, + MIMEType: "application/pdf", + Data: pdfData, + }, + }, + }, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + result, err := mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/doc.pdf", "offset": 0, "limit": 100}`}) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeFile, result.Parts[0].Type) + assert.NotNil(t, result.Parts[0].File) + assert.Equal(t, "application/pdf", result.Parts[0].File.MIMEType) + assert.Equal(t, base64.StdEncoding.EncodeToString(pdfData), *result.Parts[0].File.Base64Data) +} + +func TestMultiModalReadFileTool_UnsupportedPartType(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + return &filesystem.MultiFileContent{ + Parts: []filesystem.FileContentPart{ + { + Type: filesystem.FileContentPartType("unknown"), + MIMEType: "application/octet-stream", + Data: []byte("data"), + }, + }, + }, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + _, err = mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/file.bin", "offset": 0, "limit": 100}`}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported FileContentPartType") +} + +func TestMultiModalReadFileTool_PagesPassThrough(t *testing.T) { + base := setupTestBackend() + var capturedPages string + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + capturedPages = req.Pages + return &filesystem.MultiFileContent{FileContent: &filesystem.FileContent{Content: "page content"}}, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + _, err = mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/doc.pdf", "pages": "1-5"}`}) + assert.NoError(t, err) + assert.Equal(t, "1-5", capturedPages) +} + +func TestMultiModalReadFileTool_BackendNotMultiModalReader(t *testing.T) { + base := setupTestBackend() + _, err := newMultiModalReadFileTool(base, "", "") + assert.Error(t, err) + assert.Contains(t, err.Error(), "MultiModalReader") +} + +func TestUseMultiModalRead_Routing(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + ct, err := base.Read(ctx, &req.ReadRequest) + if err != nil { + return nil, err + } + return &filesystem.MultiFileContent{FileContent: ct}, nil + }, + } + + // UseMultiModalRead=false should create standard tool + tools, err := getFilesystemTools(context.Background(), &MiddlewareConfig{ + Backend: base, + UseMultiModalRead: false, + }) + assert.NoError(t, err) + for _, tl := range tools { + info, _ := tl.Info(context.Background()) + if info != nil && info.Name == ToolNameReadFile { + _, isEnhanced := tl.(tool.EnhancedInvokableTool) + assert.False(t, isEnhanced, "should be standard InvokableTool when UseMultiModalRead=false") + } + } + + // UseMultiModalRead=true with enhanced backend should create enhanced tool + tools2, err := getFilesystemTools(context.Background(), &MiddlewareConfig{ + Backend: eb, + UseMultiModalRead: true, + }) + assert.NoError(t, err) + for _, tl := range tools2 { + info, _ := tl.Info(context.Background()) + if info != nil && info.Name == ToolNameReadFile { + _, isEnhanced := tl.(tool.EnhancedInvokableTool) + assert.True(t, isEnhanced, "should be EnhancedInvokableTool when UseMultiModalRead=true") + } + } +} + +// TestMultiModalReadFileTool_SchemaContainsAllFields verifies that the JSON schema +// exposed to the LLM includes both the embedded readFileArgs fields (file_path, +// offset, limit) and the enhanced-only "pages" field. Guards against the +// jsonschema library failing to flatten an unexported anonymous embedded struct. +func TestMultiModalReadFileTool_SchemaContainsAllFields(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + ct, err := base.Read(ctx, &req.ReadRequest) + if err != nil { + return nil, err + } + return &filesystem.MultiFileContent{FileContent: ct}, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + info, err := mmTool.Info(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, info) + + js, err := info.ParamsOneOf.ToJSONSchema() + assert.NoError(t, err) + assert.NotNil(t, js) + assert.NotNil(t, js.Properties, "schema should have properties") + + for _, field := range []string{"file_path", "offset", "limit", "pages"} { + _, ok := js.Properties.Get(field) + assert.True(t, ok, "expected JSON schema to contain field %q, schema=%+v", field, js.Properties) + } +} + +// TestMultiModalReadFileTool_CustomDescNoSuffix verifies that when a custom desc is +// provided, the multimodal suffix is NOT appended (user's desc replaces default). +func TestMultiModalReadFileTool_CustomDescNoSuffix(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + ct, err := base.Read(ctx, &req.ReadRequest) + if err != nil { + return nil, err + } + return &filesystem.MultiFileContent{FileContent: ct}, nil + }, + } + + customDesc := "my custom read tool description" + mmTool, err := newMultiModalReadFileTool(eb, "", customDesc) + assert.NoError(t, err) + + info, err := mmTool.Info(context.Background()) + assert.NoError(t, err) + assert.Equal(t, customDesc, info.Desc, "custom desc should not be augmented with multimodal suffix") + + // With empty desc (fallback to default), suffix should be appended. + defaultTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + defaultInfo, err := defaultTool.Info(context.Background()) + assert.NoError(t, err) + assert.Contains(t, defaultInfo.Desc, "multimodal", "default desc should include multimodal suffix") +} + +// TestMultiModalReadFileTool_EmptyPartDataError verifies that a FileContentPart +// with empty Data fails explicitly rather than silently encoding to an empty +// base64 string. +func TestMultiModalReadFileTool_EmptyPartDataError(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + return &filesystem.MultiFileContent{ + Parts: []filesystem.FileContentPart{ + {Type: filesystem.FileContentPartTypeImage, MIMEType: "image/png", Data: nil}, + }, + }, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + _, err = mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/x"}`}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "empty") +} + +// nilReadBackend wraps InMemoryBackend but returns nil, nil from Read. +type nilReadBackend struct { + *filesystem.InMemoryBackend +} + +func (b *nilReadBackend) Read(_ context.Context, _ *filesystem.ReadRequest) (*filesystem.FileContent, error) { + return nil, nil +} + +// TestReadFileTool_NilResult verifies that newReadFileTool does not panic when +// Backend.Read returns nil, and emits a human-readable fallback message instead. +func TestReadFileTool_NilResult(t *testing.T) { + base := setupTestBackend() + backend := &nilReadBackend{InMemoryBackend: base} + + readTool, err := newReadFileTool(backend, "", "") + assert.NoError(t, err) + + out, err := invokeTool(t, readTool, `{"file_path": "/missing.txt"}`) + assert.NoError(t, err) + assert.Contains(t, out, "No content found at path") + assert.Contains(t, out, "/missing.txt") +} + +// TestMultiModalReadFileTool_NilResult verifies that newMultiModalReadFileTool +// does not panic when MultiModalRead returns nil, and returns a text part with +// a human-readable fallback message. +func TestMultiModalReadFileTool_NilResult(t *testing.T) { + base := setupTestBackend() + eb := &multiModalBackend{ + InMemoryBackend: base, + multiModalReadFunc: func(ctx context.Context, req *filesystem.MultiModalReadRequest) (*filesystem.MultiFileContent, error) { + return nil, nil + }, + } + + mmTool, err := newMultiModalReadFileTool(eb, "", "") + assert.NoError(t, err) + + result, err := mmTool.(tool.EnhancedInvokableTool).InvokableRun( + context.Background(), &schema.ToolArgument{Text: `{"file_path": "/missing.txt"}`}) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeText, result.Parts[0].Type) + assert.Contains(t, result.Parts[0].Text, "No content found at path") + assert.Contains(t, result.Parts[0].Text, "/missing.txt") +} + +func TestValidatePages(t *testing.T) { + tests := []struct { + name string + pages string + wantErr string + }{ + {name: "single page", pages: "3"}, + {name: "valid range", pages: "1-10"}, + {name: "same start end", pages: "1-1"}, + {name: "max 20 pages", pages: "1-20"}, + {name: "trailing dash", pages: "1-", wantErr: "expected format"}, + {name: "leading dash", pages: "-5", wantErr: "expected format"}, + {name: "non-numeric", pages: "abc", wantErr: "expected format"}, + {name: "non-numeric end", pages: "1-abc", wantErr: "expected format"}, + {name: "zero start", pages: "0-5", wantErr: "expected format"}, + {name: "zero end", pages: "1-0", wantErr: "expected format"}, + {name: "end less than start", pages: "10-5", wantErr: "end page must be >= start page"}, + {name: "exceeds max pages", pages: "1-21", wantErr: "range exceeds maximum of 20"}, + {name: "large range", pages: "1-30", wantErr: "range exceeds maximum of 20"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePages(tt.pages) + if tt.wantErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.wantErr) + } + }) + } +} diff --git a/adk/middlewares/filesystem/large_tool_result.go b/adk/middlewares/filesystem/large_tool_result.go new file mode 100644 index 0000000..215b69d --- /dev/null +++ b/adk/middlewares/filesystem/large_tool_result.go @@ -0,0 +1,177 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "strings" + "unicode/utf8" + + "github.com/slongfield/pyfmt" + + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type toolResultOffloadingConfig struct { + Backend filesystem.Backend + TokenLimit int + PathGenerator func(ctx context.Context, input *compose.ToolInput) (string, error) +} + +func newToolResultOffloading(ctx context.Context, config *toolResultOffloadingConfig) compose.ToolMiddleware { + offloading := &toolResultOffloading{ + backend: config.Backend, + tokenLimit: config.TokenLimit, + pathGenerator: config.PathGenerator, + } + + if offloading.tokenLimit == 0 { + offloading.tokenLimit = 20000 + } + + if offloading.pathGenerator == nil { + offloading.pathGenerator = func(ctx context.Context, input *compose.ToolInput) (string, error) { + return fmt.Sprintf("/large_tool_result/%s", input.CallID), nil + } + } + + return compose.ToolMiddleware{ + Invokable: offloading.invoke, + Streamable: offloading.stream, + } +} + +type toolResultOffloading struct { + backend filesystem.Backend + tokenLimit int + pathGenerator func(ctx context.Context, input *compose.ToolInput) (string, error) +} + +func (t *toolResultOffloading) invoke(endpoint compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + output, err := endpoint(ctx, input) + if err != nil { + return nil, err + } + result, err := t.handleResult(ctx, output.Result, input) + if err != nil { + return nil, err + } + return &compose.ToolOutput{Result: result}, nil + } +} + +func (t *toolResultOffloading) stream(endpoint compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + output, err := endpoint(ctx, input) + if err != nil { + return nil, err + } + result, err := concatString(output.Result) + if err != nil { + return nil, err + } + result, err = t.handleResult(ctx, result, input) + if err != nil { + return nil, err + } + return &compose.StreamToolOutput{Result: schema.StreamReaderFromArray([]string{result})}, nil + } +} + +func (t *toolResultOffloading) handleResult(ctx context.Context, result string, input *compose.ToolInput) (string, error) { + if len(result) > t.tokenLimit*4 { + path, err := t.pathGenerator(ctx, input) + if err != nil { + return "", err + } + + nResult := formatToolMessage(result) + msgTemplate := internal.SelectPrompt(internal.I18nPrompts{ + English: tooLargeToolMessage, + Chinese: tooLargeToolMessageChinese, + }) + nResult, err = pyfmt.Fmt(msgTemplate, map[string]any{ + "tool_call_id": input.CallID, + "file_path": path, + "content_sample": nResult, + }) + if err != nil { + return "", err + } + + err = t.backend.Write(ctx, &WriteRequest{ + FilePath: path, + Content: result, + }) + if err != nil { + return "", err + } + + return nResult, nil + } + + return result, nil +} + +func concatString(sr *schema.StreamReader[string]) (string, error) { + if sr == nil { + return "", errors.New("stream is nil") + } + sb := strings.Builder{} + for { + str, err := sr.Recv() + if errors.Is(err, io.EOF) { + return sb.String(), nil + } + if err != nil { + return "", err + } + sb.WriteString(str) + } +} + +func formatToolMessage(s string) string { + reader := bufio.NewScanner(strings.NewReader(s)) + var b strings.Builder + + lineNum := 1 + for reader.Scan() { + if lineNum > 10 { + break + } + line := reader.Text() + + if utf8.RuneCountInString(line) > 1000 { + runes := []rune(line) + line = string(runes[:1000]) + } + + b.WriteString(fmt.Sprintf("%d: %s\n", lineNum, line)) + + lineNum++ + } + + return b.String() +} diff --git a/adk/middlewares/filesystem/large_tool_result_test.go b/adk/middlewares/filesystem/large_tool_result_test.go new file mode 100644 index 0000000..42cf27b --- /dev/null +++ b/adk/middlewares/filesystem/large_tool_result_test.go @@ -0,0 +1,611 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + "testing" + + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// mockBackend is a simple in-memory backend for testing +type mockBackend struct { + files map[string]string +} + +func newMockBackend() *mockBackend { + return &mockBackend{ + files: make(map[string]string), + } +} + +func (m *mockBackend) Write(ctx context.Context, req *WriteRequest) error { + m.files[req.FilePath] = req.Content + return nil +} + +func (m *mockBackend) Read(ctx context.Context, req *ReadRequest) (*FileContent, error) { + content, ok := m.files[req.FilePath] + if !ok { + return nil, errors.New("file not found") + } + return &FileContent{Content: content}, nil +} + +func (m *mockBackend) LsInfo(ctx context.Context, _ *LsInfoRequest) ([]FileInfo, error) { + return nil, nil +} + +func (m *mockBackend) GrepRaw(ctx context.Context, _ *GrepRequest) ([]GrepMatch, error) { + return nil, nil +} + +func (m *mockBackend) GlobInfo(ctx context.Context, _ *GlobInfoRequest) ([]FileInfo, error) { + return nil, nil +} + +func (m *mockBackend) Edit(ctx context.Context, _ *EditRequest) error { + return nil +} + +func TestToolResultOffloading_SmallResult(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 100, // Small limit for testing + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a mock endpoint that returns a small result + smallResult := "This is a small result" + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: smallResult}, nil + } + + // Wrap the endpoint with the middleware + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + // Execute + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_123", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Small result should pass through unchanged + if output.Result != smallResult { + t.Errorf("expected result %q, got %q", smallResult, output.Result) + } + + // No file should be written + if len(backend.files) != 0 { + t.Errorf("expected no files to be written, got %d files", len(backend.files)) + } +} + +func TestToolResultOffloading_LargeResult(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, // Very small limit to trigger offloading + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a large result (more than 10 * 4 = 40 bytes) + largeResult := strings.Repeat("This is a long line of text that will exceed the token limit.\n", 10) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_456", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Result should be replaced with a message + if !strings.Contains(output.Result, "Tool result too large") { + t.Errorf("expected result to contain 'Tool result too large', got %q", output.Result) + } + + if !strings.Contains(output.Result, "call_456") { + t.Errorf("expected result to contain call ID 'call_456', got %q", output.Result) + } + + if !strings.Contains(output.Result, "/large_tool_result/call_456") { + t.Errorf("expected result to contain file path, got %q", output.Result) + } + + // File should be written + if len(backend.files) != 1 { + t.Fatalf("expected 1 file to be written, got %d files", len(backend.files)) + } + + savedContent, ok := backend.files["/large_tool_result/call_456"] + if !ok { + t.Fatalf("expected file at /large_tool_result/call_456, got files: %v", backend.files) + } + + if savedContent != largeResult { + t.Errorf("saved content doesn't match original result") + } +} + +func TestToolResultOffloading_CustomPathGenerator(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + customPath := "/custom/path/result.txt" + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + PathGenerator: func(ctx context.Context, input *compose.ToolInput) (string, error) { + return customPath, nil + }, + } + + middleware := newToolResultOffloading(ctx, config) + + largeResult := strings.Repeat("Large content ", 100) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_789", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check custom path is used + if !strings.Contains(output.Result, customPath) { + t.Errorf("expected result to contain custom path %q, got %q", customPath, output.Result) + } + + // File should be written to custom path + savedContent, ok := backend.files[customPath] + if !ok { + t.Fatalf("expected file at %q, got files: %v", customPath, backend.files) + } + + if savedContent != largeResult { + t.Errorf("saved content doesn't match original result") + } +} + +func TestToolResultOffloading_PathGeneratorError(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + expectedErr := errors.New("path generation failed") + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + PathGenerator: func(ctx context.Context, input *compose.ToolInput) (string, error) { + return "", expectedErr + }, + } + + middleware := newToolResultOffloading(ctx, config) + + largeResult := strings.Repeat("Large content ", 100) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestToolResultOffloading_EndpointError(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 100, + } + + middleware := newToolResultOffloading(ctx, config) + + expectedErr := errors.New("endpoint execution failed") + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return nil, expectedErr + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_endpoint_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestToolResultOffloading_DefaultTokenLimit(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 0, // Should default to 20000 + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a result smaller than 20000 * 4 = 80000 bytes + smallResult := strings.Repeat("x", 1000) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: smallResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_default", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should pass through unchanged + if output.Result != smallResult { + t.Errorf("expected result to pass through unchanged") + } + + // No file should be written + if len(backend.files) != 0 { + t.Errorf("expected no files to be written, got %d files", len(backend.files)) + } +} + +func TestToolResultOffloading_Stream(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a streaming endpoint that returns large content + largeResult := strings.Repeat("Large streaming content ", 100) + mockStreamEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + // Split the result into chunks + chunks := []string{largeResult[:len(largeResult)/2], largeResult[len(largeResult)/2:]} + return &compose.StreamToolOutput{ + Result: schema.StreamReaderFromArray(chunks), + }, nil + } + + wrappedEndpoint := middleware.Streamable(mockStreamEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_stream", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Read the stream + var result strings.Builder + for { + chunk, err := output.Result.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error reading stream: %v", err) + } + result.WriteString(chunk) + } + + resultStr := result.String() + + // Result should be replaced with a message + if !strings.Contains(resultStr, "Tool result too large") { + t.Errorf("expected result to contain 'Tool result too large', got %q", resultStr) + } + + if !strings.Contains(resultStr, "call_stream") { + t.Errorf("expected result to contain call ID 'call_stream', got %q", resultStr) + } + + // File should be written + if len(backend.files) != 1 { + t.Fatalf("expected 1 file to be written, got %d files", len(backend.files)) + } + + savedContent, ok := backend.files["/large_tool_result/call_stream"] + if !ok { + t.Fatalf("expected file at /large_tool_result/call_stream, got files: %v", backend.files) + } + + if savedContent != largeResult { + t.Errorf("saved content doesn't match original result") + } +} + +func TestToolResultOffloading_StreamError(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + } + + middleware := newToolResultOffloading(ctx, config) + + expectedErr := errors.New("stream endpoint failed") + mockStreamEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return nil, expectedErr + } + + wrappedEndpoint := middleware.Streamable(mockStreamEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_stream_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestFormatToolMessage(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "single line", + input: "single line", + expected: "1: single line\n", + }, + { + name: "multiple lines", + input: "line1\nline2\nline3", + expected: "1: line1\n2: line2\n3: line3\n", + }, + { + name: "more than 10 lines", + input: "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12", + expected: "1: 1\n2: 2\n3: 3\n4: 4\n5: 5\n6: 6\n7: 7\n8: 8\n9: 9\n10: 10\n", + }, + { + name: "long line truncation", + input: strings.Repeat("a", 1500), + expected: fmt.Sprintf("1: %s\n", strings.Repeat("a", 1000)), + }, + { + name: "unicode characters", + input: "你好世界\n测试", + expected: "1: 你好世界\n2: 测试\n", + }, + { + name: "long unicode line", + input: strings.Repeat("你", 1500), + expected: fmt.Sprintf("1: %s\n", strings.Repeat("你", 1000)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatToolMessage(tt.input) + if result != tt.expected { + t.Errorf("formatToolMessage() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestConcatString(t *testing.T) { + tests := []struct { + name string + chunks []string + expected string + expectError bool + }{ + { + name: "single chunk", + chunks: []string{"hello"}, + expected: "hello", + }, + { + name: "multiple chunks", + chunks: []string{"hello", " ", "world"}, + expected: "hello world", + }, + { + name: "empty chunks", + chunks: []string{"", "", ""}, + expected: "", + }, + { + name: "mixed chunks", + chunks: []string{"a", "", "b", "c"}, + expected: "abc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sr := schema.StreamReaderFromArray(tt.chunks) + result, err := concatString(sr) + + if tt.expectError { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != tt.expected { + t.Errorf("concatString() = %q, want %q", result, tt.expected) + } + }) + } + + // Test nil stream + t.Run("nil stream", func(t *testing.T) { + _, err := concatString(nil) + if err == nil { + t.Error("expected error for nil stream, got nil") + } + if !strings.Contains(err.Error(), "stream is nil") { + t.Errorf("expected 'stream is nil' error, got %v", err) + } + }) +} + +func TestToolResultOffloading_BackendWriteError(t *testing.T) { + ctx := context.Background() + + // Create a backend that fails on write + backend := &failingBackend{ + writeErr: errors.New("write failed"), + } + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + } + + middleware := newToolResultOffloading(ctx, config) + + largeResult := strings.Repeat("Large content ", 100) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_write_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "write failed") { + t.Errorf("expected 'write failed' error, got %v", err) + } +} + +// failingBackend is a mock backend that can be configured to fail +type failingBackend struct { + writeErr error +} + +func (f *failingBackend) Write(ctx context.Context, req *WriteRequest) error { + if f.writeErr != nil { + return f.writeErr + } + return nil +} + +func (f *failingBackend) Read(ctx context.Context, req *ReadRequest) (*FileContent, error) { + return &FileContent{}, nil +} + +func (f *failingBackend) LsInfo(ctx context.Context, _ *LsInfoRequest) ([]FileInfo, error) { + return nil, nil +} + +func (f *failingBackend) GrepRaw(ctx context.Context, _ *GrepRequest) ([]GrepMatch, error) { + return nil, nil +} + +func (f *failingBackend) GlobInfo(ctx context.Context, _ *GlobInfoRequest) ([]FileInfo, error) { + return nil, nil +} + +func (f *failingBackend) Edit(ctx context.Context, _ *EditRequest) error { + return nil +} diff --git a/adk/middlewares/filesystem/prompt.go b/adk/middlewares/filesystem/prompt.go new file mode 100644 index 0000000..a20d6d7 --- /dev/null +++ b/adk/middlewares/filesystem/prompt.go @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2025 Harrison Chase + * Copyright (c) 2025 CloudWeGo Authors + * SPDX-License-Identifier: MIT + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filesystem + +// This file contains prompt templates and tool descriptions adapted from the DeepAgents project. +// Original source: https://github.com/langchain-ai/deepagents +// +// These prompts are used under the terms of the original project's open source license. +// When using this code in your own open source project, ensure compliance with the original license requirements. + +const ( + tooLargeToolMessage = `Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path} +You can read the result from the filesystem by using the read_file tool, but make sure to only read part of the result at a time. +You can do this by specifying an offset and limit in the read_file tool call. +For example, to read the first 100 lines, you can use the read_file tool with offset=0 and limit=100. + +Here are the first 10 lines of the result: +{content_sample}` + + tooLargeToolMessageChinese = `工具结果过大,此工具调用 {tool_call_id} 的结果已保存到文件系统的以下路径:{file_path} +你可以使用 read_file 工具从文件系统读取结果,但请确保每次只读取部分结果。 +你可以通过在 read_file 工具调用中指定 offset 和 limit 来实现。 +例如,要读取前 100 行,你可以使用 read_file 工具,设置 offset=0 和 limit=100。 + +以下是结果的前 10 行: +{content_sample}` + + ListFilesToolDesc = `Lists all files in the filesystem, filtering by directory. + +Usage: +- The path parameter must be an absolute path, not a relative path +- The ls tool will return a list of all files in the specified directory. +- This is very useful for exploring the file system and finding the right file to read or edit. +- You should almost ALWAYS use this tool before using the read_file or edit_file tools.` + + ListFilesToolDescChinese = `列出文件系统中的所有文件,按目录过滤。 + +使用方法: +- path 参数必须是绝对路径,不能是相对路径 +- ls 工具将返回指定目录中所有文件的列表 +- 这对于探索文件系统和找到要读取或编辑的正确文件非常有用 +- 在使用 read_file 或 edit_file 工具之前,你几乎总是应该先使用此工具` + + ReadFileToolDesc = `Reads a file from the filesystem. You can access any file directly by using this tool. +Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned. + +Usage: +- The file_path parameter must be an absolute path, not a relative path +- By default, it reads up to 2000 lines starting from the beginning of the file +- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow + - First scan: read_file(path, limit=100) to see file structure + - Read more sections: read_file(path, offset=100, limit=200) for next 200 lines + - Only omit limit (read full file) when necessary for editing +- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines +- Results are returned using cat -n format, with line numbers starting at 1 +- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. +- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents. +- You should ALWAYS make sure a file has been read before editing it.` + + ReadFileToolDescChinese = `从文件系统读取文件。你可以使用此工具直接访问任何文件。 +假设此工具能够读取机器上的所有文件。如果用户提供了文件路径,假设该路径是有效的。读取不存在的文件是可以的;将返回错误。 + +使用方法: +- file_path 参数必须是绝对路径,不能是相对路径 +- 默认情况下,从文件开头读取最多 2000 行 +- **大文件和代码库探索的重要提示**:使用 offset 和 limit 参数进行分页,以避免上下文溢出 + - 首次扫描:read_file(path, limit=100) 查看文件结构 + - 读取更多部分:read_file(path, offset=100, limit=200) 读取接下来的 200 行 + - 仅在编辑必要时才省略 limit(读取完整文件) +- 指定 offset 和 limit:read_file(path, offset=0, limit=100) 读取前 100 行 +- 结果以 cat -n 格式返回,行号从 1 开始 +- 你可以在单个响应中调用多个工具。最好同时推测性地批量读取多个可能有用的文件 +- 如果你读取的文件存在但内容为空,你将收到系统提醒警告而不是文件内容 +- 在编辑文件之前,你应该始终确保已读取该文件` + + // EnhancedReadFileDescSuffix is appended to ReadFileToolDesc when using MultiModalReadFileTool. + EnhancedReadFileDescSuffix = ` +- This tool supports reading image files (e.g., PNG, JPG, etc.). When reading an image file, the contents are presented visually, as the underlying model is a multimodal LLM. +- This tool can read PDF files (.pdf). For large PDFs (more than 10 pages), you MUST provide the pages parameter to read specific page ranges (e.g., pages: "1-5"). Reading a large PDF without the pages parameter will fail. Maximum 20 pages per request.` + + EnhancedReadFileDescSuffixChinese = ` +- 此工具支持读取图片文件(如 PNG、JPG 等)。读取图片文件时,内容将以视觉方式呈现,因为底层模型是多模态 LLM。 +- 此工具可以读取 PDF 文件(.pdf)。对于大型 PDF(超过 10 页),你必须提供 pages 参数来指定页面范围(例如 pages: "1-5")。不提供 pages 参数读取大型 PDF 将会失败。每次请求最多 20 页。` + + EditFileToolDesc = `Performs exact string replacements in files. + +Usage: +- You must use your 'read_file' tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. +- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string. +- ALWAYS prefer editing existing files. NEVER write new files unless explicitly required. +- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. +- The edit will FAIL if 'old_string' is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use 'replace_all' to change every instance of 'old_string'. +- Use 'replace_all' for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.` + + EditFileToolDescChinese = `在文件中执行精确的字符串替换。 + +使用方法: +- 在编辑之前,你必须在对话中至少使用一次 'read_file' 工具。如果你在未读取文件的情况下尝试编辑,此工具将报错 +- 当从 Read 工具输出编辑文本时,请确保保留行号前缀之后的确切缩进(制表符/空格)。行号前缀格式为:空格 + 行号 + 制表符。制表符之后的所有内容都是要匹配的实际文件内容。永远不要在 old_string 或 new_string 中包含行号前缀的任何部分 +- 始终优先编辑现有文件。除非明确要求,否则不要创建新文件 +- 仅在用户明确要求时使用表情符号。除非被要求,否则避免在文件中添加表情符号 +- 如果 'old_string' 在文件中不唯一,编辑将失败。要么提供包含更多上下文的更长字符串使其唯一,要么使用 'replace_all' 更改 'old_string' 的每个实例 +- 使用 'replace_all' 在整个文件中替换和重命名字符串。例如,如果你想重命名变量,此参数很有用` + + WriteFileToolDesc = `Writes a file to the local filesystem. + +Usage: +- This tool will overwrite the existing file if there is one at the provided path. +- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first. +- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. +- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. +- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.` + + WriteFileToolDescChinese = `将文件写入本地文件系统。 + +使用方法: +- 如果提供的路径已存在文件,此工具将覆盖现有文件 +- 如果这是一个现有文件,你必须先使用 Read 工具读取文件内容。如果你没有先读取文件,此工具将失败 +- 始终优先编辑代码库中的现有文件。除非明确要求,否则不要创建新文件 +- 不要主动创建文档文件(*.md)或 README 文件。仅在用户明确要求时才创建文档文件 +- 仅在用户明确要求时使用表情符号。除非被要求,否则避免在文件中写入表情符号` + + GlobToolDesc = `Fast file pattern matching tool that works with any codebase size +- Supports glob patterns like "**/*.js" or "src/**/*.ts" +- Returns matching file paths sorted by modification time +- Use this tool when you need to find files by name patterns +- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful. + +Examples: +- '**/*.py' - Find all Python files +- '*.txt' - Find all text files in root +- '/subdir/**/*.md' - Find all markdown files under /subdir` + + GlobToolDescChinese = `适用于任何代码库大小的快速文件模式匹配工具 +- 支持 glob 模式,如 "**/*.js" 或 "src/**/*.ts" +- 返回按修改时间排序的匹配文件路径 +- 当你需要按名称模式查找文件时使用此工具 +- 你可以在单个响应中调用多个工具。最好同时并行执行多个可能有用的搜索 + +示例: +- '**/*.py' - 查找所有 Python 文件 +- '*.txt' - 查找根目录中的所有文本文件 +- '/subdir/**/*.md' - 查找 /subdir 下的所有 markdown 文件` + + GrepToolDesc = ` +A powerful search tool built on ripgrep + + Usage: + - ALWAYS use Grep for search tasks. NEVER invoke 'grep' or 'rg' as a Bash command. The Grep tool has been optimized for correct permissions and access. + - Supports full regex syntax (e.g., "log.*Error", "function\s+\w+") + - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust") + - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts + - Use Task tool for open-ended searches requiring multiple rounds + - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use 'interface\{\}' to find 'interface{}' in Go code) + - Multiline matching: By default patterns match within single lines only. For cross-line patterns like 'struct \{[\s\S]*?field', use 'multiline: true'` + + GrepToolDescChinese = ` +基于 ripgrep 的强大搜索工具 + + 使用方法: + - 始终使用 Grep 进行搜索任务。不要将 'grep' 或 'rg' 作为 Bash 命令调用。Grep 工具已针对正确的权限和访问进行了优化 + - 支持完整的正则表达式语法(例如,"log.*Error","function\s+\w+") + - 使用 glob 参数(例如,"*.js","**/*.tsx")或 type 参数(例如,"js","py","rust")过滤文件 + - 输出模式:"content" 显示匹配行,"files_with_matches" 仅显示文件路径(默认),"count" 显示匹配计数 + - 对于需要多轮的开放式搜索,使用 Task 工具 + - 模式语法:使用 ripgrep(不是 grep)- 字面大括号需要转义(使用 'interface\{\}' 在 Go 代码中查找 'interface{}') + - 多行匹配:默认情况下,模式仅在单行内匹配。对于跨行模式如 'struct \{[\s\S]*?field',使用 'multiline: true'` + + ExecuteToolDesc = ` +Executes a given command in the sandbox environment with proper handling and security measures. + +Before executing the command, please follow these steps: + +1. Directory Verification: +- If the command will create new directories or files, first use the ls tool to verify the parent directory exists and is the correct location +- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory + +2. Command Execution: +- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt") +- Examples of proper quoting: +- cd "/Users/name/My Documents" (correct) +- cd /Users/name/My Documents (incorrect - will fail) +- python "/path/with spaces/script.py" (correct) +- python /path/with spaces/script.py (incorrect - will fail) +- After ensuring proper quoting, execute the command +- Capture the output of the command + +Usage notes: +- The command parameter is required +- Commands run in an isolated sandbox environment +- Returns combined stdout/stderr output with exit code +- If the output is very large, it may be truncated +- VERY IMPORTANT: You MUST avoid using search commands like find and grep. Instead use the grep, glob tools to search. You MUST avoid read tools like cat, head, tail, and use read_file to read files. +- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings) +- Use '&&' when commands depend on each other (e.g., "mkdir dir && cd dir") +- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail +- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of cd + +Examples: +Good examples: +- execute(command="pytest /foo/bar/tests") +- execute(command="python /path/to/script.py") +- execute(command="npm install && npm test") + +Bad examples (avoid these): +- execute(command="cd /foo/bar && pytest tests") # Use absolute path instead +- execute(command="cat file.txt") # Use read_file tool instead +- execute(command="find . -name '*.py'") # Use glob tool instead +- execute(command="grep -r 'pattern' .") # Use grep tool instead +` + + ExecuteToolDescChinese = ` +在沙箱环境中执行给定命令,具有适当的处理和安全措施。 + +执行命令前,请按照以下步骤操作: + +1. 目录验证: +- 如果命令将创建新目录或文件,首先使用 ls 工具验证父目录是否存在且是正确的位置 +- 例如,在运行 "mkdir foo/bar" 之前,首先使用 ls 检查 "foo" 是否存在且是预期的父目录 + +2. 命令执行: +- 始终用双引号引用包含空格的文件路径(例如,cd "path with spaces/file.txt") +- 正确引用的示例: +- cd "/Users/name/My Documents"(正确) +- cd /Users/name/My Documents(错误 - 将失败) +- python "/path/with spaces/script.py"(正确) +- python /path/with spaces/script.py(错误 - 将失败) +- 确保正确引用后,执行命令 +- 捕获命令的输出 + +使用说明: +- command 参数是必需的 +- 命令在隔离的沙箱环境中运行 +- 返回合并的 stdout/stderr 输出和退出代码 +- 如果输出非常大,可能会被截断 +- 非常重要:你必须避免使用 find 和 grep 等搜索命令。请改用 grep、glob 工具进行搜索。你必须避免使用 cat、head、tail 等读取工具,请使用 read_file 读取文件 +- 发出多个命令时,使用 ';' 或 '&&' 运算符分隔它们。不要使用换行符(引号字符串中的换行符是可以的) +- 当命令相互依赖时使用 '&&'(例如,"mkdir dir && cd dir") +- 仅当你需要按顺序运行命令但不关心早期命令是否失败时使用 ';' +- 尝试通过使用绝对路径并避免使用 cd 来在整个会话中保持当前工作目录 + +示例: +好的示例: +- execute(command="pytest /foo/bar/tests") +- execute(command="python /path/to/script.py") +- execute(command="npm install && npm test") + +不好的示例(避免这些): +- execute(command="cd /foo/bar && pytest tests") # 改用绝对路径 +- execute(command="cat file.txt") # 改用 read_file 工具 +- execute(command="find . -name '*.py'") # 改用 glob 工具 +- execute(command="grep -r 'pattern' .") # 改用 grep 工具 +` +) diff --git a/adk/middlewares/patchtoolcalls/patchtoolcalls.go b/adk/middlewares/patchtoolcalls/patchtoolcalls.go new file mode 100644 index 0000000..484c881 --- /dev/null +++ b/adk/middlewares/patchtoolcalls/patchtoolcalls.go @@ -0,0 +1,263 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package patchtoolcalls provides a middleware that patches dangling tool calls in the message history. +package patchtoolcalls + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/schema" +) + +// Config defines the configuration options for the patch tool calls middleware. +type Config struct { + // PatchedContentGenerator is an optional custom function to generate the content + // of patched tool messages. If not provided, a default message will be used. + // + // Parameters: + // - ctx: the context for the operation + // - toolName: the name of the tool that was called + // - toolCallID: the id of the tool call + // + // Returns: + // - string: the content to use for the patched tool message + // - error: any error that occurred during generation + PatchedContentGenerator func(ctx context.Context, toolName, toolCallID string) (string, error) +} + +// NewTyped creates a new generic patch tool calls middleware. +// +// The middleware scans the message history before each model invocation and inserts +// placeholder tool messages for any tool calls that don't have corresponding responses. +func NewTyped[M adk.MessageType](_ context.Context, cfg *Config) (adk.TypedChatModelAgentMiddleware[M], error) { + if cfg == nil { + cfg = &Config{} + } + return &typedMiddleware[M]{ + gen: cfg.PatchedContentGenerator, + }, nil +} + +// New creates a new patch tool calls middleware with the given configuration. +// +// The middleware scans the message history before each model invocation and inserts +// placeholder tool messages for any tool calls that don't have corresponding responses. +func New(ctx context.Context, cfg *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped[*schema.Message](ctx, cfg) +} + +type typedMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + gen func(ctx context.Context, toolName, toolCallID string) (string, error) +} + +func (m *typedMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *adk.TypedChatModelAgentState[M], + mc *adk.TypedModelContext[M], +) (context.Context, *adk.TypedChatModelAgentState[M], error) { + if len(state.Messages) == 0 { + return ctx, state, nil + } + + var zero M + switch any(zero).(type) { + case *schema.Message: + return patchToolCallsForMessage(ctx, m.gen, any(state).(*adk.TypedChatModelAgentState[*schema.Message]), mc) + case *schema.AgenticMessage: + return patchToolCallsForAgenticMessage(ctx, m.gen, any(state).(*adk.TypedChatModelAgentState[*schema.AgenticMessage]), mc) + default: + panic("unreachable: unknown MessageType") + } +} + +func patchToolCallsForMessage[M adk.MessageType](ctx context.Context, + gen func(ctx context.Context, toolName, toolCallID string) (string, error), + state *adk.TypedChatModelAgentState[*schema.Message], + _ *adk.TypedModelContext[M], +) (context.Context, *adk.TypedChatModelAgentState[M], error) { + patched := make([]*schema.Message, 0, len(state.Messages)) + + for i, msg := range state.Messages { + patched = append(patched, msg) + + if msg.Role != schema.Assistant || len(msg.ToolCalls) == 0 { + continue + } + + for _, tc := range msg.ToolCalls { + if hasCorrespondingToolMessage(state.Messages[i+1:], tc.ID) { + continue + } + + toolMsg, err := createPatchedToolMessage(ctx, gen, tc) + if err != nil { + return ctx, nil, err + } + patched = append(patched, toolMsg) + } + } + + nState := *state + nState.Messages = patched + return ctx, any(&nState).(*adk.TypedChatModelAgentState[M]), nil +} + +func patchToolCallsForAgenticMessage[M adk.MessageType](ctx context.Context, + gen func(ctx context.Context, toolName, toolCallID string) (string, error), + state *adk.TypedChatModelAgentState[*schema.AgenticMessage], + _ *adk.TypedModelContext[M], +) (context.Context, *adk.TypedChatModelAgentState[M], error) { + patched := make([]*schema.AgenticMessage, 0, len(state.Messages)) + + for i, msg := range state.Messages { + patched = append(patched, msg) + + if msg.Role != schema.AgenticRoleTypeAssistant { + continue + } + + // Collect tool call IDs from this assistant message. + var toolCalls []struct { + callID string + name string + } + for _, block := range msg.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolCall && block.FunctionToolCall != nil { + toolCalls = append(toolCalls, struct { + callID string + name string + }{callID: block.FunctionToolCall.CallID, name: block.FunctionToolCall.Name}) + } + } + if len(toolCalls) == 0 { + continue + } + + for _, tc := range toolCalls { + if hasCorrespondingAgenticToolResult(state.Messages[i+1:], tc.callID) { + continue + } + + toolMsg, err := createPatchedAgenticToolMessage(ctx, gen, tc.name, tc.callID) + if err != nil { + return ctx, nil, err + } + patched = append(patched, toolMsg) + } + } + + nState := *state + nState.Messages = patched + return ctx, any(&nState).(*adk.TypedChatModelAgentState[M]), nil +} + +func hasCorrespondingToolMessage(messages []*schema.Message, toolCallID string) bool { + for _, msg := range messages { + // Only consider successive tool messages after the tool call message + if msg.Role != schema.Tool { + return false + } + if msg.ToolCallID == toolCallID { + return true + } + } + return false +} + +func hasCorrespondingAgenticToolResult(messages []*schema.AgenticMessage, toolCallID string) bool { + for _, msg := range messages { + // Only consider successive tool messages after the tool call message + if msg.Role != schema.AgenticRoleTypeUser { + return false + } + hasToolResult := false + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + if block.Type == schema.ContentBlockTypeFunctionToolResult { + hasToolResult = true + if block.FunctionToolResult != nil && block.FunctionToolResult.CallID == toolCallID { + return true + } + } + if block.Type == schema.ContentBlockTypeToolSearchResult { + hasToolResult = true + if block.ToolSearchFunctionToolResult != nil && block.ToolSearchFunctionToolResult.CallID == toolCallID { + return true + } + } + } + if !hasToolResult { + return false + } + } + return false +} + +func createPatchedToolMessage(ctx context.Context, gen func(ctx context.Context, toolName, toolCallID string) (string, error), tc schema.ToolCall) (*schema.Message, error) { + if gen != nil { + content, err := gen(ctx, tc.Function.Name, tc.ID) + if err != nil { + return nil, err + } + return schema.ToolMessage(content, tc.ID, schema.WithToolName(tc.Function.Name)), nil + } + tpl := internal.SelectPrompt(internal.I18nPrompts{ + English: defaultPatchedToolMessageTemplate, + Chinese: defaultPatchedToolMessageTemplateChinese, + }) + + return schema.ToolMessage(fmt.Sprintf(tpl, tc.Function.Name, tc.ID), tc.ID, schema.WithToolName(tc.Function.Name)), nil +} + +func createPatchedAgenticToolMessage(ctx context.Context, gen func(ctx context.Context, toolName, toolCallID string) (string, error), toolName, callID string) (*schema.AgenticMessage, error) { + var content string + if gen != nil { + var err error + content, err = gen(ctx, toolName, callID) + if err != nil { + return nil, err + } + } else { + tpl := internal.SelectPrompt(internal.I18nPrompts{ + English: defaultPatchedToolMessageTemplate, + Chinese: defaultPatchedToolMessageTemplateChinese, + }) + content = fmt.Sprintf(tpl, toolName, callID) + } + + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: callID, + Name: toolName, + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: content}}, + }, + }), + }, + }, nil +} + +const ( + defaultPatchedToolMessageTemplate = "Tool call %s with id %s was canceled - another message came in before it could be completed." + defaultPatchedToolMessageTemplateChinese = "工具调用 %s(ID 为 %s)已被取消——在其完成之前收到了另一条消息。" +) diff --git a/adk/middlewares/patchtoolcalls/patchtoolcalls_test.go b/adk/middlewares/patchtoolcalls/patchtoolcalls_test.go new file mode 100644 index 0000000..2fdb3c1 --- /dev/null +++ b/adk/middlewares/patchtoolcalls/patchtoolcalls_test.go @@ -0,0 +1,394 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package patchtoolcalls + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped[*schema.AgenticMessage](ctx, nil) + assert.NoError(t, err) + assert.NotNil(t, mw) + + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} + +type testToolCall struct { + ID string + Name string + Arguments string +} + +func makeUserMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.UserMessage(content)).(M) + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(content)).(M) + } + panic("unreachable") +} + +func makeAssistantMsgWithToolCalls[M adk.MessageType](content string, toolCalls []testToolCall) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + tcs := make([]schema.ToolCall, len(toolCalls)) + for i, tc := range toolCalls { + tcs[i] = schema.ToolCall{ID: tc.ID, Function: schema.FunctionCall{Name: tc.Name, Arguments: tc.Arguments}} + } + return any(schema.AssistantMessage(content, tcs)).(M) + case *schema.AgenticMessage: + blocks := make([]*schema.ContentBlock, 0, len(toolCalls)+1) + if content != "" { + blocks = append(blocks, schema.NewContentBlock(&schema.AssistantGenText{Text: content})) + } + for _, tc := range toolCalls { + blocks = append(blocks, schema.NewContentBlock(&schema.FunctionToolCall{CallID: tc.ID, Name: tc.Name, Arguments: tc.Arguments})) + } + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: blocks, + }).(M) + } + panic("unreachable") +} + +func makeToolResultMsg[M adk.MessageType](content string, callID string, toolName string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.ToolMessage(content, callID, schema.WithToolName(toolName))).(M) + case *schema.AgenticMessage: + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: callID, + Name: toolName, + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: content}}, + }, + }), + }, + }).(M) + } + panic("unreachable") +} + +func assertMsgContent[M adk.MessageType](t *testing.T, msg M, expectedContent string) { + t.Helper() + switch m := any(msg).(type) { + case *schema.Message: + assert.Equal(t, expectedContent, m.Content) + case *schema.AgenticMessage: + for _, block := range m.ContentBlocks { + if block.Type == schema.ContentBlockTypeFunctionToolResult && block.FunctionToolResult != nil { + for _, b := range block.FunctionToolResult.Content { + if b.Text != nil { + assert.Equal(t, expectedContent, b.Text.Text) + return + } + } + } + } + t.Errorf("no text content found in agentic message, expected %q", expectedContent) + } +} + +func assertToolResultID[M adk.MessageType](t *testing.T, msg M, expectedID string) { + t.Helper() + switch m := any(msg).(type) { + case *schema.Message: + assert.Equal(t, expectedID, m.ToolCallID) + case *schema.AgenticMessage: + for _, block := range m.ContentBlocks { + if block.Type == schema.ContentBlockTypeFunctionToolResult && block.FunctionToolResult != nil { + assert.Equal(t, expectedID, block.FunctionToolResult.CallID) + return + } + } + t.Errorf("no tool result found in agentic message, expected call ID %q", expectedID) + } +} + +func assertToolResultName[M adk.MessageType](t *testing.T, msg M, expectedName string) { + t.Helper() + switch m := any(msg).(type) { + case *schema.Message: + assert.Equal(t, expectedName, m.ToolName) + case *schema.AgenticMessage: + for _, block := range m.ContentBlocks { + if block.Type == schema.ContentBlockTypeFunctionToolResult && block.FunctionToolResult != nil { + assert.Equal(t, expectedName, block.FunctionToolResult.Name) + return + } + } + t.Errorf("no tool result found in agentic message, expected tool name %q", expectedName) + } +} + +func testPatchToolCallsGeneric[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + config *Config + messages []M + wantLen int + checkPatchedAt int // index of the patched message to check (-1 if no check needed) + wantCallID string + wantToolName string + wantContent string + }{ + { + name: "empty messages", + config: nil, + messages: nil, + wantLen: 0, + checkPatchedAt: -1, + }, + { + name: "no tool calls to patch", + config: nil, + messages: []M{ + makeUserMsg[M]("hello"), + makeAssistantMsgWithToolCalls[M]("hi there", nil), + }, + wantLen: 2, + checkPatchedAt: -1, + }, + { + name: "missing tool result", + config: nil, + messages: []M{ + makeUserMsg[M]("hello"), + makeAssistantMsgWithToolCalls[M]("", []testToolCall{ + {ID: "call_1", Name: "tool_a", Arguments: "{}"}, + {ID: "call_2", Name: "tool_b", Arguments: "{}"}, + }), + makeToolResultMsg[M]("result_a", "call_1", "tool_a"), + }, + wantLen: 4, + checkPatchedAt: 2, + wantCallID: "call_2", + wantToolName: "tool_b", + wantContent: fmt.Sprintf(defaultPatchedToolMessageTemplate, "tool_b", "call_2"), + }, + { + name: "custom content generator", + config: &Config{ + PatchedContentGenerator: func(ctx context.Context, toolName, toolCallID string) (string, error) { + return fmt.Sprintf("123 %s %s", toolName, toolCallID), nil + }, + }, + messages: []M{ + makeUserMsg[M]("hello"), + makeAssistantMsgWithToolCalls[M]("", []testToolCall{ + {ID: "call_1", Name: "tool_a", Arguments: "{}"}, + {ID: "call_2", Name: "tool_b", Arguments: "{}"}, + }), + makeToolResultMsg[M]("result_a", "call_1", "tool_a"), + }, + wantLen: 4, + checkPatchedAt: 2, + wantCallID: "call_2", + wantToolName: "tool_b", + wantContent: "123 tool_b call_2", + }, + { + name: "two consecutive assistant messages with tool calls", + config: nil, + messages: []M{ + makeUserMsg[M]("hello"), + makeAssistantMsgWithToolCalls[M]("", []testToolCall{ + {ID: "call_1", Name: "tool_a", Arguments: "{}"}, + }), + makeAssistantMsgWithToolCalls[M]("continued...", nil), + }, + wantLen: 4, + checkPatchedAt: 2, + wantCallID: "call_1", + wantToolName: "tool_a", + wantContent: fmt.Sprintf(defaultPatchedToolMessageTemplate, "tool_a", "call_1"), + }, + { + name: "assistant message followed by user message without tool result", + config: nil, + messages: []M{ + makeUserMsg[M]("hello"), + makeAssistantMsgWithToolCalls[M]("", []testToolCall{ + {ID: "call_1", Name: "tool_a", Arguments: "{}"}, + }), + makeUserMsg[M]("continued..."), + }, + wantLen: 4, + checkPatchedAt: 2, + wantCallID: "call_1", + wantToolName: "tool_a", + wantContent: fmt.Sprintf(defaultPatchedToolMessageTemplate, "tool_a", "call_1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mw, err := NewTyped[M](ctx, tt.config) + assert.NoError(t, err) + + state := &adk.TypedChatModelAgentState[M]{ + Messages: tt.messages, + } + _, newState, err := mw.BeforeModelRewriteState(ctx, state, nil) + assert.NoError(t, err) + assert.Len(t, newState.Messages, tt.wantLen) + + if tt.checkPatchedAt >= 0 && tt.checkPatchedAt < len(newState.Messages) { + patched := newState.Messages[tt.checkPatchedAt] + assertToolResultID(t, patched, tt.wantCallID) + assertToolResultName(t, patched, tt.wantToolName) + assertMsgContent(t, patched, tt.wantContent) + } + }) + } +} + +func TestPatchToolCallsGeneric(t *testing.T) { + t.Run("Message", testPatchToolCallsGeneric[*schema.Message]) + t.Run("AgenticMessage", testPatchToolCallsGeneric[*schema.AgenticMessage]) +} + +func TestPatchToolCallsAgenticToolSearchResult(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped[*schema.AgenticMessage](ctx, nil) + require.NoError(t, err) + + messages := []*schema.AgenticMessage{ + makeAssistantMsgWithToolCalls[*schema.AgenticMessage]("", []testToolCall{ + {ID: "call_1", Name: "tool_search", Arguments: `{"query":"dynamic"}`}, + }), + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.ToolSearchFunctionToolResult{ + CallID: "call_1", + Name: "tool_search", + Result: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{ + {Name: "dynamic_tool", Desc: "dynamic tool"}, + }}, + }), + }, + }, + } + + state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{Messages: messages} + _, newState, err := mw.BeforeModelRewriteState(ctx, state, nil) + require.NoError(t, err) + assert.Len(t, newState.Messages, 2) + assert.Equal(t, schema.ContentBlockTypeToolSearchResult, newState.Messages[1].ContentBlocks[0].Type) +} + +// TestPatchToolCalls_NilFunctionToolCallInBlock verifies the middleware handles +// a ContentBlock with Type=FunctionToolCall but FunctionToolCall=nil without panicking. +func TestPatchToolCalls_NilFunctionToolCallInBlock(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped[*schema.AgenticMessage](ctx, nil) + require.NoError(t, err) + + msgs := []*schema.AgenticMessage{ + schema.UserAgenticMessage("hello"), + { + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolCall, + FunctionToolCall: nil, // nil despite type indicating tool call + }, + schema.NewContentBlock(&schema.FunctionToolCall{ + CallID: "call_1", + Name: "real_tool", + }), + }, + }, + } + + state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{Messages: msgs} + _, newState, err := mw.BeforeModelRewriteState(ctx, state, nil) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 3, "should patch call_1 but skip nil FunctionToolCall block") + + patchMsg := newState.Messages[2] + assert.Equal(t, schema.AgenticRoleTypeUser, patchMsg.Role) + foundResult := false + for _, block := range patchMsg.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolResult && + block.FunctionToolResult != nil && block.FunctionToolResult.CallID == "call_1" { + foundResult = true + } + } + assert.True(t, foundResult, "patched message should contain tool result for call_1") +} + +// TestPatchToolCalls_AgenticMessage_NilBlockInUserMessage verifies the middleware handles +// a User Agentic Message with nil ContentBlock without panicking. +func TestPatchToolCalls_AgenticMessage_NilBlockInUserMessage(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped[*schema.AgenticMessage](ctx, nil) + require.NoError(t, err) + + msgs := []*schema.AgenticMessage{ + schema.UserAgenticMessage("hello"), + makeAssistantMsgWithToolCalls[*schema.AgenticMessage]("", []testToolCall{ + {ID: "call_1", Name: "tool_a", Arguments: "{}"}, + }), + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + nil, // nil block to test robustness + }, + }, + } + + state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{Messages: msgs} + _, newState, err := mw.BeforeModelRewriteState(ctx, state, nil) + assert.NoError(t, err, "should not panic when encountering nil block in user message") + assert.Len(t, newState.Messages, 4, "should patch call_1 and insert tool response") + + // Verify the patched message is inserted at index 2 + patchMsg := newState.Messages[2] + assert.Equal(t, schema.AgenticRoleTypeUser, patchMsg.Role) + + foundResult := false + for _, block := range patchMsg.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolResult && + block.FunctionToolResult != nil && block.FunctionToolResult.CallID == "call_1" { + foundResult = true + break + } + } + assert.True(t, foundResult, "patched message should contain tool result for call_1") +} diff --git a/adk/middlewares/plantask/backend_test.go b/adk/middlewares/plantask/backend_test.go new file mode 100644 index 0000000..d381ff7 --- /dev/null +++ b/adk/middlewares/plantask/backend_test.go @@ -0,0 +1,80 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "errors" + "path/filepath" + "strings" + "sync" + + fspkg "github.com/cloudwego/eino/adk/filesystem" +) + +type inMemoryBackend struct { + files map[string]string + mu sync.RWMutex +} + +func newInMemoryBackend() *inMemoryBackend { + return &inMemoryBackend{ + files: make(map[string]string), + } +} + +func (b *inMemoryBackend) LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + reqPath := strings.TrimSuffix(req.Path, "/") + var result []FileInfo + for path := range b.files { + dir := filepath.Dir(path) + if dir == reqPath { + result = append(result, FileInfo{Path: path}) + } + } + return result, nil +} + +func (b *inMemoryBackend) Read(ctx context.Context, req *ReadRequest) (*fspkg.FileContent, error) { + b.mu.RLock() + defer b.mu.RUnlock() + + content, ok := b.files[req.FilePath] + if !ok { + return nil, errors.New("file not found") + } + return &fspkg.FileContent{Content: content}, nil +} + +func (b *inMemoryBackend) Write(ctx context.Context, req *WriteRequest) error { + b.mu.Lock() + defer b.mu.Unlock() + + b.files[req.FilePath] = req.Content + return nil +} + +func (b *inMemoryBackend) Delete(ctx context.Context, req *DeleteRequest) error { + b.mu.Lock() + defer b.mu.Unlock() + + delete(b.files, req.FilePath) + return nil +} diff --git a/adk/middlewares/plantask/plantask.go b/adk/middlewares/plantask/plantask.go new file mode 100644 index 0000000..fb201bd --- /dev/null +++ b/adk/middlewares/plantask/plantask.go @@ -0,0 +1,81 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "fmt" + "sync" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// Config is the configuration for the tool search middleware. +type Config struct { + Backend Backend + BaseDir string +} + +// NewTyped creates a new plantask middleware that provides task management tools for agents. +// It adds TaskCreate, TaskGet, TaskUpdate, and TaskList tools to the agent's tool set, +// allowing agents to create and manage structured task lists during coding sessions. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +func NewTyped[M adk.MessageType](_ context.Context, config *Config) (adk.TypedChatModelAgentMiddleware[M], error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if config.Backend == nil { + return nil, fmt.Errorf("backend is required") + } + if config.BaseDir == "" { + return nil, fmt.Errorf("baseDir is required") + } + + return &typedMiddleware[M]{backend: config.Backend, baseDir: config.BaseDir}, nil +} + +// New creates a new plantask middleware that provides task management tools for agents. +// It adds TaskCreate, TaskGet, TaskUpdate, and TaskList tools to the agent's tool set, +// allowing agents to create and manage structured task lists during coding sessions. +func New(ctx context.Context, config *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped[*schema.Message](ctx, config) +} + +type typedMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + backend Backend + baseDir string +} + +func (m *typedMiddleware[M]) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) (context.Context, *adk.ChatModelAgentContext, error) { + if runCtx == nil { + return ctx, runCtx, nil + } + + nRunCtx := *runCtx + lock := sync.Mutex{} + nRunCtx.Tools = append(nRunCtx.Tools, + newTaskCreateTool(m.backend, m.baseDir, &lock), + newTaskGetTool(m.backend, m.baseDir, &lock), + newTaskUpdateTool(m.backend, m.baseDir, &lock), + newTaskListTool(m.backend, m.baseDir, &lock), + ) + + return ctx, &nRunCtx, nil +} diff --git a/adk/middlewares/plantask/plantask_test.go b/adk/middlewares/plantask/plantask_test.go new file mode 100644 index 0000000..2354e79 --- /dev/null +++ b/adk/middlewares/plantask/plantask_test.go @@ -0,0 +1,137 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func TestNew(t *testing.T) { + ctx := context.Background() + + _, err := New(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") + + _, err = New(ctx, &Config{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend is required") + + _, err = New(ctx, &Config{Backend: newInMemoryBackend()}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "baseDir is required") + + m, err := New(ctx, &Config{Backend: newInMemoryBackend(), BaseDir: "/tmp/tasks"}) + assert.NoError(t, err) + assert.NotNil(t, m) +} + +func TestMiddlewareBeforeAgent(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + + m, err := New(ctx, &Config{Backend: backend, BaseDir: baseDir}) + assert.NoError(t, err) + + mw := m.(*typedMiddleware[*schema.Message]) + + ctx, runCtx, err := mw.BeforeAgent(ctx, nil) + assert.NoError(t, err) + assert.Nil(t, runCtx) + + runCtx = &adk.ChatModelAgentContext{ + Tools: []tool.BaseTool{}, + } + ctx, newRunCtx, err := mw.BeforeAgent(ctx, runCtx) + assert.NoError(t, err) + assert.NotNil(t, newRunCtx) + assert.Len(t, newRunCtx.Tools, 4) + + toolNames := make([]string, 0, 4) + for _, t := range newRunCtx.Tools { + info, _ := t.Info(ctx) + toolNames = append(toolNames, info.Name) + } + assert.Contains(t, toolNames, "TaskCreate") + assert.Contains(t, toolNames, "TaskGet") + assert.Contains(t, toolNames, "TaskUpdate") + assert.Contains(t, toolNames, "TaskList") +} + +func TestIntegration(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + createTool := newTaskCreateTool(backend, baseDir, lock) + getTool := newTaskGetTool(backend, baseDir, lock) + updateTool := newTaskUpdateTool(backend, baseDir, lock) + listTool := newTaskListTool(backend, baseDir, lock) + + result, err := createTool.InvokableRun(ctx, `{"subject": "Task 1", "description": "First task"}`) + assert.NoError(t, err) + assert.Contains(t, result, "Task #1") + + result, err = createTool.InvokableRun(ctx, `{"subject": "Task 2", "description": "Second task"}`) + assert.NoError(t, err) + assert.Contains(t, result, "Task #2") + + _, err = updateTool.InvokableRun(ctx, `{"taskId": "2", "addBlockedBy": ["1"]}`) + assert.NoError(t, err) + + result, err = listTool.InvokableRun(ctx, `{}`) + assert.NoError(t, err) + assert.Contains(t, result, "#1 [pending] Task 1") + assert.Contains(t, result, "#2 [pending] Task 2") + assert.Contains(t, result, "[blocked by #1]") + + _, err = updateTool.InvokableRun(ctx, `{"taskId": "1", "status": "in_progress"}`) + assert.NoError(t, err) + + result, err = getTool.InvokableRun(ctx, `{"taskId": "1"}`) + assert.NoError(t, err) + assert.Contains(t, result, "Status: in_progress") + + _, err = updateTool.InvokableRun(ctx, `{"taskId": "1", "status": "completed"}`) + assert.NoError(t, err) + + result, err = listTool.InvokableRun(ctx, `{}`) + assert.NoError(t, err) + assert.Contains(t, result, "#1 [completed] Task 1") +} + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped[*schema.AgenticMessage](ctx, &Config{ + Backend: newInMemoryBackend(), + BaseDir: "/tmp/tasks", + }) + assert.NoError(t, err) + assert.NotNil(t, mw) + + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} diff --git a/adk/middlewares/plantask/task.go b/adk/middlewares/plantask/task.go new file mode 100644 index 0000000..ff1ed28 --- /dev/null +++ b/adk/middlewares/plantask/task.go @@ -0,0 +1,123 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "regexp" + + "github.com/cloudwego/eino/adk/middlewares/filesystem" +) + +var validTaskIDRegex = regexp.MustCompile(`^\d+$`) + +const highWatermarkFileName = ".highwatermark" + +type task struct { + ID string `json:"id"` + Subject string `json:"subject"` + Description string `json:"description"` + Status string `json:"status"` + Blocks []string `json:"blocks"` + BlockedBy []string `json:"blockedBy"` + ActiveForm string `json:"activeForm,omitempty"` + Owner string `json:"owner,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +type taskOut struct { + Result string `json:"result"` +} + +const ( + taskStatusPending = "pending" + taskStatusInProgress = "in_progress" + taskStatusCompleted = "completed" + taskStatusDeleted = "deleted" +) + +type FileInfo = filesystem.FileInfo +type LsInfoRequest = filesystem.LsInfoRequest +type ReadRequest = filesystem.ReadRequest +type WriteRequest = filesystem.WriteRequest + +type DeleteRequest struct { + FilePath string +} + +// Backend defines the storage interface for task persistence. +// Implementations can use local filesystem, remote storage, or any other storage backend. +type Backend interface { + // LsInfo lists file information in the specified directory. + LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error) + // Read reads the content of a file. + Read(ctx context.Context, req *ReadRequest) (*filesystem.FileContent, error) + // Write writes content to a file, creating it if it doesn't exist. + Write(ctx context.Context, req *WriteRequest) error + // Delete removes a file from storage. + Delete(ctx context.Context, req *DeleteRequest) error +} + +func isValidTaskID(taskID string) bool { + return validTaskIDRegex.MatchString(taskID) +} + +func appendUnique(slice []string, items ...string) []string { + seen := make(map[string]struct{}, len(slice)) + for _, s := range slice { + seen[s] = struct{}{} + } + for _, item := range items { + if _, exists := seen[item]; !exists { + slice = append(slice, item) + seen[item] = struct{}{} + } + } + return slice +} + +func hasCyclicDependency(taskMap map[string]*task, blockerID, blockedID string) bool { + if blockerID == blockedID { + return true + } + + visited := make(map[string]bool) + return canReach(taskMap, blockedID, blockerID, visited) +} + +func canReach(taskMap map[string]*task, fromID, toID string, visited map[string]bool) bool { + if fromID == toID { + return true + } + if visited[fromID] { + return false + } + visited[fromID] = true + + fromTask, exists := taskMap[fromID] + if !exists { + return false + } + + for _, blockedID := range fromTask.Blocks { + if canReach(taskMap, blockedID, toID, visited) { + return true + } + } + + return false +} diff --git a/adk/middlewares/plantask/task_create.go b/adk/middlewares/plantask/task_create.go new file mode 100644 index 0000000..478b14d --- /dev/null +++ b/adk/middlewares/plantask/task_create.go @@ -0,0 +1,265 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "fmt" + "path/filepath" + "sync" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func newTaskCreateTool(backend Backend, baseDir string, lock *sync.Mutex) *taskCreateTool { + return &taskCreateTool{Backend: backend, BaseDir: baseDir, lock: lock} +} + +type taskCreateTool struct { + Backend Backend + BaseDir string + lock *sync.Mutex +} + +type taskCreateArgs struct { + Subject string `json:"subject"` + Description string `json:"description"` + ActiveForm string `json:"activeForm,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +func (t *taskCreateTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + desc := internal.SelectPrompt(internal.I18nPrompts{ + English: taskCreateToolDesc, + Chinese: taskCreateToolDescChinese, + }) + + return &schema.ToolInfo{ + Name: TaskCreateToolName, + Desc: desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "subject": { + Type: schema.String, + Desc: "A brief title for the task", + Required: true, + }, + "description": { + Type: schema.String, + Desc: "A detailed description of what needs to be done", + Required: true, + }, + "activeForm": { + Type: schema.String, + Desc: "Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")", + Required: false, + }, + "metadata": { + Type: schema.Object, + Desc: "Arbitrary metadata to attach to the task", + SubParams: map[string]*schema.ParameterInfo{ + "propertyNames": { + Type: schema.String, + }, + }, + Required: false, + }, + }), + }, nil +} + +func (t *taskCreateTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + t.lock.Lock() + defer t.lock.Unlock() + + params := &taskCreateArgs{} + err := sonic.UnmarshalString(argumentsInJSON, params) + if err != nil { + return "", err + } + + files, err := t.Backend.LsInfo(ctx, &LsInfoRequest{ + Path: t.BaseDir, + }) + if err != nil { + return "", fmt.Errorf("%s list files in %s failed, err: %w", TaskCreateToolName, t.BaseDir, err) + } + + highwatermark := int64(0) + for _, file := range files { + fileName := filepath.Base(file.Path) + if fileName == highWatermarkFileName { + content, readErr := t.Backend.Read(ctx, &ReadRequest{ + FilePath: file.Path, + }) + if readErr != nil { + return "", fmt.Errorf("%s read highwatermark file %s failed, err: %w", TaskCreateToolName, file.Path, readErr) + } + if content.Content != "" { + var val int64 + if _, scanErr := fmt.Sscanf(content.Content, "%d", &val); scanErr == nil { + highwatermark = val + } + } + break + } + } + + taskID := highwatermark + 1 + taskFileName := fmt.Sprintf("%d.json", taskID) + + for _, file := range files { + fileName := filepath.Base(file.Path) + if fileName == taskFileName { + return "", fmt.Errorf("task #%d already exists", taskID) + } + } + + newTask := &task{ + ID: fmt.Sprintf("%d", taskID), + Subject: params.Subject, + Description: params.Description, + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + ActiveForm: params.ActiveForm, + Metadata: params.Metadata, + } + + taskData, err := sonic.MarshalString(newTask) + if err != nil { + return "", fmt.Errorf("%s marshal task #%d failed, err: %w", TaskCreateToolName, taskID, err) + } + + // Write highwatermark file first + highwatermarkPath := filepath.Join(t.BaseDir, highWatermarkFileName) + err = t.Backend.Write(ctx, &WriteRequest{ + FilePath: highwatermarkPath, + Content: fmt.Sprintf("%d", taskID), + }) + if err != nil { + return "", fmt.Errorf("%s update highwatermark file %s failed, err: %w", TaskCreateToolName, highwatermarkPath, err) + } + + taskFilePath := filepath.Join(t.BaseDir, taskFileName) + err = t.Backend.Write(ctx, &WriteRequest{ + FilePath: taskFilePath, + Content: taskData, + }) + if err != nil { + return "", fmt.Errorf("%s create Task #%d failed, err: %w", TaskCreateToolName, taskID, err) + } + + resp := &taskOut{ + Result: fmt.Sprintf("Task #%d created successfully: %s", taskID, params.Subject), + } + + jsonResp, err := sonic.MarshalString(resp) + if err != nil { + return "", fmt.Errorf("%s marshal taskOut failed, err: %w", TaskCreateToolName, err) + } + + return jsonResp, nil +} + +const TaskCreateToolName = "TaskCreate" +const taskCreateToolDesc = `Use this tool to create a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user. +It also helps the user understand the progress of the task and overall progress of their requests. + +## When to Use This Tool + +Use this tool proactively in these scenarios: + +- Complex multi-step tasks - When a task requires 3 or more distinct steps or actions +- Non-trivial and complex tasks - Tasks that require careful planning or multiple operations +- Plan mode - When using plan mode, create a task list to track the work +- User explicitly requests todo list - When the user directly asks you to use the todo list +- User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) +- After receiving new instructions - Immediately capture user requirements as tasks +- When you start working on a task - Mark it as in_progress BEFORE beginning work +- After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation + +## When NOT to Use This Tool + +Skip using this tool when: +- There is only a single, straightforward task +- The task is trivial and tracking it provides no organizational benefit +- The task can be completed in less than 3 trivial steps +- The task is purely conversational or informational + +NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly. + +## Task Fields + +- **subject**: A brief, actionable title in imperative form (e.g., "Fix authentication bug in login flow") +- **description**: Detailed description of what needs to be done, including context and acceptance criteria +- **activeForm**: Present continuous form shown in spinner when task is in_progress (e.g., "Fixing authentication bug"). This is displayed to the user while you work on the task. + +**IMPORTANT**: Always provide activeForm when creating tasks. The subject should be imperative ("Run tests") while activeForm should be present continuous ("Running tests"). All tasks are created with status "pending". + +## Tips + +- Create tasks with clear, specific subjects that describe the outcome +- Include enough detail in the description for another agent to understand and complete the task +- After creating tasks, use TaskUpdate to set up dependencies (blocks/blockedBy) if needed +- Check TaskList first to avoid creating duplicate tasks +` + +const taskCreateToolDescChinese = `使用此工具为当前编码会话创建结构化的任务列表。这有助于跟踪进度、组织复杂任务,并向用户展示工作的完整性。 +它还帮助用户了解任务的进度和请求的整体进展。 + +## 何时使用此工具 + +在以下场景中主动使用此工具: + +- 复杂的多步骤任务 - 当任务需要 3 个或更多不同的步骤或操作时 +- 非简单的复杂任务 - 需要仔细规划或多个操作的任务 +- 计划模式 - 使用计划模式时,创建任务列表来跟踪工作 +- 用户明确要求待办列表 - 当用户直接要求使用待办列表时 +- 用户提供多个任务 - 当用户提供待办事项列表时(编号或逗号分隔) +- 收到新指令后 - 立即将用户需求记录为任务 +- 开始处理任务时 - 在开始工作之前将其标记为 in_progress +- 完成任务后 - 将其标记为已完成,并添加实施过程中发现的任何后续任务 + +## 何时不使用此工具 + +在以下情况下跳过使用此工具: +- 只有一个简单直接的任务 +- 任务很简单,跟踪它没有组织上的好处 +- 任务可以在少于 3 个简单步骤内完成 +- 任务纯粹是对话性或信息性的 + +注意:如果只有一个简单任务要做,不应该使用此工具。在这种情况下,直接完成任务更好。 + +## 任务字段 + +- **subject**:简短的、可操作的标题,使用祈使句形式(例如,"修复登录流程中的认证错误") +- **description**:需要完成的工作的详细描述,包括上下文和验收标准 +- **activeForm**:任务处于 in_progress 状态时在加载动画中显示的现在进行时形式(例如,"正在修复认证错误")。这会在你处理任务时显示给用户。 + +**重要**:创建任务时始终提供 activeForm。subject 应该是祈使句("运行测试"),而 activeForm 应该是现在进行时("正在运行测试")。所有任务创建时状态为 "pending"。 + +## 提示 + +- 创建具有清晰、具体主题的任务,描述预期结果 +- 在描述中包含足够的细节,以便其他代理能够理解并完成任务 +- 创建任务后,如果需要,使用 TaskUpdate 设置依赖关系(blocks/blockedBy) +- 先检查 TaskList 以避免创建重复任务 +` diff --git a/adk/middlewares/plantask/task_create_test.go b/adk/middlewares/plantask/task_create_test.go new file mode 100644 index 0000000..e451ffb --- /dev/null +++ b/adk/middlewares/plantask/task_create_test.go @@ -0,0 +1,91 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "path/filepath" + "sync" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" +) + +func TestTaskCreateTool(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + tool := newTaskCreateTool(backend, baseDir, lock) + + info, err := tool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, TaskCreateToolName, info.Name) + assert.Equal(t, taskCreateToolDesc, info.Desc) + + result, err := tool.InvokableRun(ctx, `{"subject": "Test Task", "description": "Test description", "activeForm": "Testing"}`) + assert.NoError(t, err) + assert.Equal(t, `{"result":"Task #1 created successfully: Test Task"}`, result) + + content, err := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.NoError(t, err) + + var taskData task + err = sonic.UnmarshalString(content.Content, &taskData) + assert.NoError(t, err) + assert.Equal(t, "1", taskData.ID) + assert.Equal(t, "Test Task", taskData.Subject) + assert.Equal(t, "Test description", taskData.Description) + assert.Equal(t, taskStatusPending, taskData.Status) + assert.Equal(t, "Testing", taskData.ActiveForm) + + hwContent, err := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, highWatermarkFileName)}) + assert.NoError(t, err) + assert.Equal(t, "1", hwContent.Content) + + result, err = tool.InvokableRun(ctx, `{"subject": "Second Task", "description": "Second description"}`) + assert.NoError(t, err) + assert.Equal(t, `{"result":"Task #2 created successfully: Second Task"}`, result) + + hwContent, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, highWatermarkFileName)}) + assert.NoError(t, err) + assert.Equal(t, "2", hwContent.Content) +} + +func TestTaskCreateToolWithMetadata(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + tool := newTaskCreateTool(backend, baseDir, lock) + + result, err := tool.InvokableRun(ctx, `{"subject": "Task with metadata", "description": "Has metadata", "metadata": {"key1": "value1", "key2": "value2"}}`) + assert.NoError(t, err) + assert.Contains(t, result, "Task #1 created successfully") + + content, err := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.NoError(t, err) + + var taskData task + err = sonic.UnmarshalString(content.Content, &taskData) + assert.NoError(t, err) + assert.Equal(t, "value1", taskData.Metadata["key1"]) + assert.Equal(t, "value2", taskData.Metadata["key2"]) +} diff --git a/adk/middlewares/plantask/task_get.go b/adk/middlewares/plantask/task_get.go new file mode 100644 index 0000000..55760c3 --- /dev/null +++ b/adk/middlewares/plantask/task_get.go @@ -0,0 +1,176 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "sync" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func newTaskGetTool(backend Backend, baseDir string, lock *sync.Mutex) *taskGetTool { + return &taskGetTool{Backend: backend, BaseDir: baseDir, lock: lock} +} + +type taskGetTool struct { + Backend Backend + BaseDir string + lock *sync.Mutex +} + +func (t *taskGetTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + desc := internal.SelectPrompt(internal.I18nPrompts{ + English: taskGetToolDesc, + Chinese: taskGetToolDescChinese, + }) + + return &schema.ToolInfo{ + Name: TaskGetToolName, + Desc: desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "taskId": { + Type: schema.String, + Desc: "The ID of the task to retrieve", + Required: true, + }, + }), + }, nil +} + +type taskGetArgs struct { + TaskID string `json:"taskId"` +} + +func (t *taskGetTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + t.lock.Lock() + defer t.lock.Unlock() + + params := &taskGetArgs{} + err := sonic.UnmarshalString(argumentsInJSON, params) + if err != nil { + return "", err + } + + if !isValidTaskID(params.TaskID) { + return "", fmt.Errorf("%s validate task ID failed, err: invalid format: %s", TaskGetToolName, params.TaskID) + } + + taskFileName := fmt.Sprintf("%s.json", params.TaskID) + taskFilePath := filepath.Join(t.BaseDir, taskFileName) + + content, err := t.Backend.Read(ctx, &ReadRequest{ + FilePath: taskFilePath, + }) + if err != nil { + return "", fmt.Errorf("%s get Task #%s failed, err: %w", TaskGetToolName, params.TaskID, err) + } + + taskData := &task{} + err = sonic.UnmarshalString(content.Content, taskData) + if err != nil { + return "", fmt.Errorf("%s get Task #%s failed, err: %w", TaskGetToolName, params.TaskID, err) + } + + var result strings.Builder + result.WriteString(fmt.Sprintf("Task #%s: %s\n", taskData.ID, taskData.Subject)) + result.WriteString(fmt.Sprintf("Status: %s\n", taskData.Status)) + result.WriteString(fmt.Sprintf("Description: %s\n", taskData.Description)) + + if len(taskData.BlockedBy) > 0 { + blockedByIDs := make([]string, len(taskData.BlockedBy)) + for i, id := range taskData.BlockedBy { + blockedByIDs[i] = "#" + id + } + result.WriteString(fmt.Sprintf("Blocked by: %s\n", strings.Join(blockedByIDs, ", "))) + } + if len(taskData.Blocks) > 0 { + blocksIDs := make([]string, len(taskData.Blocks)) + for i, id := range taskData.Blocks { + blocksIDs[i] = "#" + id + } + result.WriteString(fmt.Sprintf("Blocks: %s\n", strings.Join(blocksIDs, ", "))) + } + if taskData.Owner != "" { + result.WriteString(fmt.Sprintf("Owner: %s\n", taskData.Owner)) + } + + resp := &taskOut{ + Result: result.String(), + } + + jsonResp, err := sonic.MarshalString(resp) + if err != nil { + return "", fmt.Errorf("%s marshal taskOut failed, err: %w", TaskGetToolName, err) + } + + return jsonResp, nil +} + +const TaskGetToolName = "TaskGet" +const taskGetToolDesc = `Use this tool to retrieve a task by its ID from the task list. + +## When to Use This Tool + +- When you need the full description and context before starting work on a task +- To understand task dependencies (what it blocks, what blocks it) +- After being assigned a task, to get complete requirements + +## Output + +Returns full task details: +- **subject**: Task title +- **description**: Detailed requirements and context +- **status**: 'pending', 'in_progress', or 'completed' +- **blocks**: Tasks waiting on this one to complete +- **blockedBy**: Tasks that must complete before this one can start + +## Tips + +- After fetching a task, verify its blockedBy list is empty before beginning work. +- Use TaskList to see all tasks in summary form. +` + +const taskGetToolDescChinese = `使用此工具通过任务 ID 从任务列表中获取任务。 + +## 何时使用此工具 + +- 当你需要在开始处理任务之前获取完整的描述和上下文时 +- 了解任务依赖关系(它阻塞什么,什么阻塞它) +- 被分配任务后,获取完整的需求 + +## 输出 + +返回完整的任务详情: +- **subject**:任务标题 +- **description**:详细的需求和上下文 +- **status**:'pending'、'in_progress' 或 'completed' +- **blocks**:等待此任务完成的任务 +- **blockedBy**:必须在此任务开始之前完成的任务 + +## 提示 + +- 获取任务后,在开始工作之前验证其 blockedBy 列表是否为空。 +- 使用 TaskList 查看所有任务的摘要形式。 +` diff --git a/adk/middlewares/plantask/task_get_test.go b/adk/middlewares/plantask/task_get_test.go new file mode 100644 index 0000000..f1f9863 --- /dev/null +++ b/adk/middlewares/plantask/task_get_test.go @@ -0,0 +1,80 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "path/filepath" + "sync" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" +) + +func TestTaskGetTool(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + taskData := &task{ + ID: "1", + Subject: "Test Task", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{"2", "3"}, + BlockedBy: []string{"4"}, + } + taskJSON, _ := sonic.MarshalString(taskData) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: taskJSON}) + + tool := newTaskGetTool(backend, baseDir, lock) + + info, err := tool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, TaskGetToolName, info.Name) + assert.Equal(t, taskGetToolDesc, info.Desc) + + result, err := tool.InvokableRun(ctx, `{"taskId": "1"}`) + assert.NoError(t, err) + assert.Contains(t, result, "Task #1: Test Task") + assert.Contains(t, result, "Status: "+taskStatusPending) + assert.Contains(t, result, "Description: Test description") + assert.Contains(t, result, "Blocked by: #4") + assert.Contains(t, result, "Blocks: #2, #3") + + _, err = tool.InvokableRun(ctx, `{"taskId": "999"}`) + assert.Error(t, err) +} + +func TestTaskGetToolInvalidTaskID(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + tool := newTaskGetTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "../../../etc/passwd"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate task ID failed") + + _, err = tool.InvokableRun(ctx, `{"taskId": "abc"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate task ID failed") +} diff --git a/adk/middlewares/plantask/task_list.go b/adk/middlewares/plantask/task_list.go new file mode 100644 index 0000000..60a7d04 --- /dev/null +++ b/adk/middlewares/plantask/task_list.go @@ -0,0 +1,194 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "fmt" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func newTaskListTool(backend Backend, baseDir string, lock *sync.Mutex) *taskListTool { + return &taskListTool{Backend: backend, BaseDir: baseDir, lock: lock} +} + +type taskListTool struct { + Backend Backend + BaseDir string + lock *sync.Mutex +} + +func (t *taskListTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + desc := internal.SelectPrompt(internal.I18nPrompts{ + English: taskListToolDesc, + Chinese: taskListToolDescChinese, + }) + + return &schema.ToolInfo{ + Name: TaskListToolName, + Desc: desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{}), + }, nil +} + +func listTasks(ctx context.Context, backend Backend, baseDir string) ([]*task, error) { + files, err := backend.LsInfo(ctx, &LsInfoRequest{ + Path: baseDir, + }) + if err != nil { + return nil, fmt.Errorf("%s list files in %s failed, err: %w", TaskListToolName, baseDir, err) + } + + var tasks []*task + for _, file := range files { + fileName := filepath.Base(file.Path) + if !strings.HasSuffix(fileName, ".json") { + continue + } + + taskID := strings.TrimSuffix(fileName, ".json") + if !isValidTaskID(taskID) { + continue + } + + content, err := backend.Read(ctx, &ReadRequest{ + FilePath: file.Path, + }) + if err != nil { + return nil, fmt.Errorf("%s read task file %s failed, err: %w", TaskListToolName, file.Path, err) + } + + taskData := &task{} + err = sonic.UnmarshalString(content.Content, taskData) + if err != nil { + return nil, fmt.Errorf("%s parse task file %s failed, err: %w", TaskListToolName, file.Path, err) + } + + tasks = append(tasks, taskData) + } + + // sort tasks by ID + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].ID < tasks[j].ID + }) + + return tasks, nil +} + +func (t *taskListTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + t.lock.Lock() + defer t.lock.Unlock() + + tasks, err := listTasks(ctx, t.Backend, t.BaseDir) + if err != nil { + return "", err + } + + if len(tasks) == 0 { + resp := &taskOut{ + Result: "No tasks found.", + } + jsonResp, marshalErr := sonic.MarshalString(resp) + if marshalErr != nil { + return "", fmt.Errorf("%s marshal taskOut failed, err: %w", TaskListToolName, marshalErr) + } + return jsonResp, nil + } + + var result strings.Builder + for i, taskData := range tasks { + if i > 0 { + result.WriteString("\n") + } + result.WriteString(fmt.Sprintf("#%s [%s] %s", taskData.ID, taskData.Status, taskData.Subject)) + if taskData.Owner != "" { + result.WriteString(fmt.Sprintf(" [owner: %s]", taskData.Owner)) + } + if len(taskData.BlockedBy) > 0 { + blockedByIDs := make([]string, len(taskData.BlockedBy)) + for j, id := range taskData.BlockedBy { + blockedByIDs[j] = "#" + id + } + result.WriteString(fmt.Sprintf(" [blocked by %s]", strings.Join(blockedByIDs, ", "))) + } + } + + resp := &taskOut{ + Result: result.String(), + } + + jsonResp, err := sonic.MarshalString(resp) + if err != nil { + return "", fmt.Errorf("%s marshal taskOut failed, err: %w", TaskListToolName, err) + } + + return jsonResp, nil +} + +const TaskListToolName = "TaskList" +const taskListToolDesc = `Use this tool to list all tasks in the task list. + +## When to Use This Tool + +- To see what tasks are available to work on (status: 'pending', no owner, not blocked) +- To check overall progress on the project +- To find tasks that are blocked and need dependencies resolved +- After completing a task, to check for newly unblocked work or claim the next available task +- **Prefer working on tasks in ID order** (lowest ID first) when multiple tasks are available, as earlier tasks often set up context for later ones + +## Output + +Returns a summary of each task: +- **id**: Task identifier (use with TaskGet, TaskUpdate) +- **subject**: Brief description of the task +- **status**: 'pending', 'in_progress', or 'completed' +- **owner**: Agent ID if assigned, empty if available +- **blockedBy**: List of open task IDs that must be resolved first (tasks with blockedBy cannot be claimed until dependencies resolve) + +Use TaskGet with a specific task ID to view full details including description and comments. +` + +const taskListToolDescChinese = `使用此工具列出任务列表中的所有任务。 + +## 何时使用此工具 + +- 查看可以处理的任务(状态:'pending',无所有者,未被阻塞) +- 检查项目的整体进度 +- 查找被阻塞且需要解决依赖关系的任务 +- 完成任务后,检查新解除阻塞的工作或认领下一个可用任务 +- **优先按 ID 顺序处理任务**(最小 ID 优先),当有多个任务可用时,因为较早的任务通常为后续任务建立上下文 + +## 输出 + +返回每个任务的摘要: +- **id**:任务标识符(与 TaskGet、TaskUpdate 一起使用) +- **subject**:任务的简要描述 +- **status**:'pending'、'in_progress' 或 'completed' +- **owner**:如果已分配则为代理 ID,如果可用则为空 +- **blockedBy**:必须首先解决的开放任务 ID 列表(具有 blockedBy 的任务在依赖关系解决之前无法被认领) + +使用 TaskGet 配合特定任务 ID 查看完整详情,包括描述和评论。 +` diff --git a/adk/middlewares/plantask/task_list_test.go b/adk/middlewares/plantask/task_list_test.go new file mode 100644 index 0000000..706f8c6 --- /dev/null +++ b/adk/middlewares/plantask/task_list_test.go @@ -0,0 +1,60 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "path/filepath" + "sync" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" +) + +func TestTaskListTool(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + tool := newTaskListTool(backend, baseDir, lock) + + info, err := tool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, TaskListToolName, info.Name) + assert.Equal(t, taskListToolDesc, info.Desc) + + result, err := tool.InvokableRun(ctx, `{}`) + assert.NoError(t, err) + assert.Equal(t, `{"result":"No tasks found."}`, result) + + task1 := &task{ID: "1", Subject: "Task 1", Status: taskStatusPending, BlockedBy: []string{"2"}} + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ID: "2", Subject: "Task 2", Status: taskStatusInProgress, Owner: "agent1"} + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + result, err = tool.InvokableRun(ctx, `{}`) + assert.NoError(t, err) + assert.Contains(t, result, "#1 ["+taskStatusPending+"] Task 1") + assert.Contains(t, result, "[blocked by #2]") + assert.Contains(t, result, "#2 ["+taskStatusInProgress+"] Task 2") + assert.Contains(t, result, "[owner: agent1]") +} diff --git a/adk/middlewares/plantask/task_update.go b/adk/middlewares/plantask/task_update.go new file mode 100644 index 0000000..7e9eb2d --- /dev/null +++ b/adk/middlewares/plantask/task_update.go @@ -0,0 +1,565 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "sync" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func newTaskUpdateTool(backend Backend, baseDir string, lock *sync.Mutex) *taskUpdateTool { + return &taskUpdateTool{Backend: backend, BaseDir: baseDir, lock: lock} +} + +type taskUpdateTool struct { + Backend Backend + BaseDir string + lock *sync.Mutex +} + +type taskUpdateArgs struct { + TaskID string `json:"taskId"` + Subject string `json:"subject,omitempty"` + Description string `json:"description,omitempty"` + ActiveForm string `json:"activeForm,omitempty"` + Status string `json:"status,omitempty"` + AddBlocks []string `json:"addBlocks,omitempty"` + AddBlockedBy []string `json:"addBlockedBy,omitempty"` + Owner string `json:"owner,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +func (t *taskUpdateTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + desc := internal.SelectPrompt(internal.I18nPrompts{ + English: taskUpdateToolDesc, + Chinese: taskUpdateToolDescChinese, + }) + + return &schema.ToolInfo{ + Name: TaskUpdateToolName, + Desc: desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "taskId": { + Type: schema.String, + Desc: "The ID of the task to update", + Required: true, + }, + "subject": { + Type: schema.String, + Desc: "New subject for the task", + Required: false, + }, + "description": { + Type: schema.String, + Desc: "New description for the task", + Required: false, + }, + "activeForm": { + Type: schema.String, + Desc: "Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")", + Required: false, + }, + "status": { + Type: schema.String, + Desc: "New status for the task: 'pending', 'in_progress', 'completed', or 'deleted'", + Enum: []string{"pending", "in_progress", "completed", "deleted"}, + Required: false, + }, + "addBlocks": { + Type: schema.Array, + Desc: "Task IDs that this task blocks", + ElemInfo: &schema.ParameterInfo{Type: schema.String}, + Required: false, + }, + "addBlockedBy": { + Type: schema.Array, + Desc: "Task IDs that block this task", + ElemInfo: &schema.ParameterInfo{Type: schema.String}, + Required: false, + }, + "owner": { + Type: schema.String, + Desc: "New owner for the task", + Required: false, + }, + "metadata": { + Type: schema.Object, + Desc: "Metadata keys to merge into the task. Set a key to null to delete it.", + SubParams: map[string]*schema.ParameterInfo{ + "propertyNames": { + Type: schema.String, + }, + }, + Required: false, + }, + }), + }, nil +} + +func (t *taskUpdateTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + t.lock.Lock() + defer t.lock.Unlock() + + params := &taskUpdateArgs{} + err := sonic.UnmarshalString(argumentsInJSON, params) + if err != nil { + return "", err + } + + if !isValidTaskID(params.TaskID) { + return "", fmt.Errorf("%s validate task ID failed, err: invalid format: %s", TaskUpdateToolName, params.TaskID) + } + + taskFileName := fmt.Sprintf("%s.json", params.TaskID) + taskFilePath := filepath.Join(t.BaseDir, taskFileName) + + if params.Status == taskStatusDeleted { + if removeErr := t.removeTaskFromDependencies(ctx, params.TaskID); removeErr != nil { + return "", fmt.Errorf("%s remove Task #%s from dependencies failed, err: %w", TaskUpdateToolName, params.TaskID, removeErr) + } + + err = t.Backend.Delete(ctx, &DeleteRequest{ + FilePath: taskFilePath, + }) + if err != nil { + return "", fmt.Errorf("%s delete Task #%s failed, err: %w", TaskUpdateToolName, params.TaskID, err) + } + + resp := &taskOut{ + Result: fmt.Sprintf("Updated task #%s deleted", params.TaskID), + } + jsonResp, marshalErr := sonic.MarshalString(resp) + if marshalErr != nil { + return "", fmt.Errorf("%s marshal taskOut failed, err: %w", TaskUpdateToolName, marshalErr) + } + return jsonResp, nil + } + + content, err := t.Backend.Read(ctx, &ReadRequest{ + FilePath: taskFilePath, + }) + if err != nil { + return "", fmt.Errorf("%s read Task #%s failed, err: %w", TaskUpdateToolName, params.TaskID, err) + } + + taskData := &task{} + err = sonic.UnmarshalString(content.Content, taskData) + if err != nil { + return "", fmt.Errorf("%s parse Task #%s failed, err: %w", TaskUpdateToolName, params.TaskID, err) + } + + var updatedFields []string + + if params.Subject != "" { + taskData.Subject = params.Subject + updatedFields = append(updatedFields, "subject") + } + if params.Description != "" { + taskData.Description = params.Description + updatedFields = append(updatedFields, "description") + } + if params.ActiveForm != "" { + taskData.ActiveForm = params.ActiveForm + updatedFields = append(updatedFields, "activeForm") + } + if params.Status != "" { + taskData.Status = params.Status + updatedFields = append(updatedFields, "status") + } + if len(params.AddBlocks) > 0 || len(params.AddBlockedBy) > 0 { + tasks, listErr := listTasks(ctx, t.Backend, t.BaseDir) + if listErr != nil { + return "", fmt.Errorf("%s list tasks failed, err: %w", TaskUpdateToolName, listErr) + } + taskMap := make(map[string]*task, len(tasks)) + for _, tk := range tasks { + taskMap[tk.ID] = tk + } + + if len(params.AddBlocks) > 0 { + for _, blockedTaskID := range params.AddBlocks { + if !isValidTaskID(blockedTaskID) { + return "", fmt.Errorf("%s validate blocked task ID failed, err: invalid format: %s", TaskUpdateToolName, blockedTaskID) + } + if hasCyclicDependency(taskMap, params.TaskID, blockedTaskID) { + return "", fmt.Errorf("%s adding Task #%s to blocks of Task #%s would create a cyclic dependency", TaskUpdateToolName, blockedTaskID, params.TaskID) + } + } + for _, blockedTaskID := range params.AddBlocks { + if addErr := t.addBlockedByToTask(ctx, blockedTaskID, params.TaskID); addErr != nil { + return "", fmt.Errorf("%s update Task #%s blocks failed, err: %w", TaskUpdateToolName, params.TaskID, addErr) + } + } + taskData.Blocks = appendUnique(taskData.Blocks, params.AddBlocks...) + updatedFields = append(updatedFields, "blocks") + } + if len(params.AddBlockedBy) > 0 { + for _, blockingTaskID := range params.AddBlockedBy { + if !isValidTaskID(blockingTaskID) { + return "", fmt.Errorf("%s validate blocking task ID failed, err: invalid format: %s", TaskUpdateToolName, blockingTaskID) + } + if hasCyclicDependency(taskMap, blockingTaskID, params.TaskID) { + return "", fmt.Errorf("%s adding Task #%s to blockedBy of Task #%s would create a cyclic dependency", TaskUpdateToolName, blockingTaskID, params.TaskID) + } + } + for _, blockingTaskID := range params.AddBlockedBy { + if addErr := t.addBlocksToTask(ctx, blockingTaskID, params.TaskID); addErr != nil { + return "", fmt.Errorf("%s update Task #%s blockedBy failed, err: %w", TaskUpdateToolName, params.TaskID, addErr) + } + } + taskData.BlockedBy = appendUnique(taskData.BlockedBy, params.AddBlockedBy...) + updatedFields = append(updatedFields, "blockedBy") + } + } + if params.Owner != "" { + taskData.Owner = params.Owner + updatedFields = append(updatedFields, "owner") + } + if params.Metadata != nil { + if taskData.Metadata == nil { + taskData.Metadata = make(map[string]any) + } + for k, v := range params.Metadata { + if v == nil { + delete(taskData.Metadata, k) + } else { + taskData.Metadata[k] = v + } + } + updatedFields = append(updatedFields, "metadata") + } + + updatedContent, err := sonic.MarshalString(taskData) + if err != nil { + return "", fmt.Errorf("%s marshal Task #%s failed, err: %w", TaskUpdateToolName, params.TaskID, err) + } + + err = t.Backend.Write(ctx, &WriteRequest{ + FilePath: taskFilePath, + Content: updatedContent, + }) + if err != nil { + return "", fmt.Errorf("%s write Task #%s failed, err: %w", TaskUpdateToolName, params.TaskID, err) + } + + if params.Status == taskStatusCompleted { + if checkErr := t.checkIfNeedDeleteAllTasks(ctx); checkErr != nil { + return "", fmt.Errorf("%s check and delete all tasks failed, err: %w", TaskUpdateToolName, checkErr) + } + } + + resp := &taskOut{ + Result: fmt.Sprintf("Updated task #%s %s", params.TaskID, strings.Join(updatedFields, ", ")), + } + + jsonResp, err := sonic.MarshalString(resp) + if err != nil { + return "", fmt.Errorf("%s marshal taskOut failed, err: %w", TaskUpdateToolName, err) + } + + return jsonResp, nil +} + +func (t *taskUpdateTool) removeTaskFromDependencies(ctx context.Context, deletedTaskID string) error { + tasks, err := listTasks(ctx, t.Backend, t.BaseDir) + if err != nil { + return err + } + + for _, taskData := range tasks { + if taskData.ID == deletedTaskID { + continue + } + + modified := false + newBlocks := make([]string, 0, len(taskData.Blocks)) + for _, id := range taskData.Blocks { + if id != deletedTaskID { + newBlocks = append(newBlocks, id) + } else { + modified = true + } + } + + newBlockedBy := make([]string, 0, len(taskData.BlockedBy)) + for _, id := range taskData.BlockedBy { + if id != deletedTaskID { + newBlockedBy = append(newBlockedBy, id) + } else { + modified = true + } + } + + if modified { + taskData.Blocks = newBlocks + taskData.BlockedBy = newBlockedBy + + updatedContent, err := sonic.MarshalString(taskData) + if err != nil { + return fmt.Errorf("failed to marshal task #%s: %w", taskData.ID, err) + } + + taskFilePath := filepath.Join(t.BaseDir, fmt.Sprintf("%s.json", taskData.ID)) + if err := t.Backend.Write(ctx, &WriteRequest{FilePath: taskFilePath, Content: updatedContent}); err != nil { + return fmt.Errorf("failed to write task #%s: %w", taskData.ID, err) + } + } + } + + return nil +} + +func (t *taskUpdateTool) addBlockedByToTask(ctx context.Context, targetTaskID, blockerTaskID string) error { + taskFilePath := filepath.Join(t.BaseDir, fmt.Sprintf("%s.json", targetTaskID)) + + content, err := t.Backend.Read(ctx, &ReadRequest{FilePath: taskFilePath}) + if err != nil { + return fmt.Errorf("failed to read task #%s for updating blockedBy: %w", targetTaskID, err) + } + + targetTask := &task{} + if unmarshalErr := sonic.UnmarshalString(content.Content, targetTask); unmarshalErr != nil { + return fmt.Errorf("failed to parse task #%s: %w", targetTaskID, unmarshalErr) + } + + targetTask.BlockedBy = appendUnique(targetTask.BlockedBy, blockerTaskID) + + updatedContent, err := sonic.MarshalString(targetTask) + if err != nil { + return fmt.Errorf("failed to marshal task #%s: %w", targetTaskID, err) + } + + if err := t.Backend.Write(ctx, &WriteRequest{FilePath: taskFilePath, Content: updatedContent}); err != nil { + return fmt.Errorf("failed to write task #%s: %w", targetTaskID, err) + } + + return nil +} + +func (t *taskUpdateTool) addBlocksToTask(ctx context.Context, targetTaskID, blockedTaskID string) error { + taskFilePath := filepath.Join(t.BaseDir, fmt.Sprintf("%s.json", targetTaskID)) + + content, err := t.Backend.Read(ctx, &ReadRequest{FilePath: taskFilePath}) + if err != nil { + return fmt.Errorf("failed to read task #%s for updating blocks: %w", targetTaskID, err) + } + + targetTask := &task{} + if unmarshalErr := sonic.UnmarshalString(content.Content, targetTask); unmarshalErr != nil { + return fmt.Errorf("failed to parse task #%s: %w", targetTaskID, unmarshalErr) + } + + targetTask.Blocks = appendUnique(targetTask.Blocks, blockedTaskID) + + updatedContent, err := sonic.MarshalString(targetTask) + if err != nil { + return fmt.Errorf("failed to marshal task #%s: %w", targetTaskID, err) + } + + if err := t.Backend.Write(ctx, &WriteRequest{FilePath: taskFilePath, Content: updatedContent}); err != nil { + return fmt.Errorf("failed to write task #%s: %w", targetTaskID, err) + } + + return nil +} + +// checkIfNeedDeleteAllTasks checks if all tasks are completed, if so, it deletes all tasks +func (t *taskUpdateTool) checkIfNeedDeleteAllTasks(ctx context.Context) error { + tasks, err := listTasks(ctx, t.Backend, t.BaseDir) + if err != nil { + return err + } + + for _, task := range tasks { + if task.Status != taskStatusCompleted { + return nil + } + } + + for _, task := range tasks { + err := t.Backend.Delete(ctx, &DeleteRequest{ + FilePath: filepath.Join(t.BaseDir, task.ID+".json"), + }) + if err != nil { + return err + } + } + + return nil +} + +const TaskUpdateToolName = "TaskUpdate" +const taskUpdateToolDesc = `Use this tool to update a task in the task list. + +## When to Use This Tool + +**Mark tasks as resolved:** +- When you have completed the work described in a task +- When a task is no longer needed or has been superseded +- IMPORTANT: Always mark your assigned tasks as resolved when you finish them +- After resolving, call TaskList to find your next task + +- ONLY mark a task as completed when you have FULLY accomplished it +- If you encounter errors, blockers, or cannot finish, keep the task as in_progress +- When blocked, create a new task describing what needs to be resolved +- Never mark a task as completed if: + - Tests are failing + - Implementation is partial + - You encountered unresolved errors + - You couldn't find necessary files or dependencies + +**Delete tasks:** +- When a task is no longer relevant or was created in error +- Setting status to ` + "`deleted`" + ` permanently removes the task + +**Update task details:** +- When requirements change or become clearer +- When establishing dependencies between tasks + +## Fields You Can Update + +- **status**: The task status (see Status Workflow below) +- **subject**: Change the task title (imperative form, e.g., "Run tests") +- **description**: Change the task description +- **activeForm**: Present continuous form shown in spinner when in_progress (e.g., "Running tests") +- **owner**: Change the task owner (agent name) +- **metadata**: Merge metadata keys into the task (set a key to null to delete it) +- **addBlocks**: Mark tasks that cannot start until this one completes +- **addBlockedBy**: Mark tasks that must complete before this one can start + +## Status Workflow + +Status progresses: ` + "`pending`" + ` → ` + "`in_progress`" + ` → ` + "`completed`" + ` + +Use ` + "`deleted`" + ` to permanently remove a task. + +## Staleness + +Make sure to read a task's latest state using ` + "`TaskGet`" + ` before updating it. + +## Examples + +Mark task as in progress when starting work: +` + "```json" + ` +{"taskId": "1", "status": "in_progress"} +` + "```" + ` + +Mark task as completed after finishing work: +` + "```json" + ` +{"taskId": "1", "status": "completed"} +` + "```" + ` + +Delete a task: +` + "```json" + ` +{"taskId": "1", "status": "deleted"} +` + "```" + ` + +Claim a task by setting owner: +` + "```json" + ` +{"taskId": "1", "owner": "my-name"} +` + "```" + ` + +Set up task dependencies: +` + "```json" + ` +{"taskId": "2", "addBlockedBy": ["1"]} +` + "```" + ` +` + +const taskUpdateToolDescChinese = `使用此工具更新任务列表中的任务。 + +## 何时使用此工具 + +**将任务标记为已完成:** +- 当你完成了任务中描述的工作时 +- 当任务不再需要或已被取代时 +- 重要:完成分配给你的任务后,务必将其标记为已完成 +- 完成后,调用 TaskList 查找下一个任务 + +- 只有在完全完成任务时才将其标记为已完成 +- 如果遇到错误、阻塞或无法完成,请保持任务为 in_progress 状态 +- 当被阻塞时,创建一个新任务描述需要解决的问题 +- 在以下情况下不要将任务标记为已完成: + - 测试失败 + - 实现不完整 + - 遇到未解决的错误 + - 找不到必要的文件或依赖项 + +**删除任务:** +- 当任务不再相关或创建错误时 +- 将状态设置为 ` + "`deleted`" + ` 会永久删除任务 + +**更新任务详情:** +- 当需求变更或变得更清晰时 +- 当建立任务之间的依赖关系时 + +## 可更新的字段 + +- **status**:任务状态(参见下方状态流程) +- **subject**:更改任务标题(使用祈使句形式,例如"运行测试") +- **description**:更改任务描述 +- **activeForm**:in_progress 状态时在加载动画中显示的现在进行时形式(例如"正在运行测试") +- **owner**:更改任务所有者(代理名称) +- **metadata**:将元数据键合并到任务中(将键设置为 null 可删除它) +- **addBlocks**:标记在此任务完成之前无法开始的任务 +- **addBlockedBy**:标记必须在此任务开始之前完成的任务 + +## 状态流程 + +状态进展:` + "`pending`" + ` → ` + "`in_progress`" + ` → ` + "`completed`" + ` + +使用 ` + "`deleted`" + ` 永久删除任务。 + +## 过期性 + +更新任务前,请确保使用 ` + "`TaskGet`" + ` 读取任务的最新状态。 + +## 示例 + +开始工作时将任务标记为进行中: +` + "```json" + ` +{"taskId": "1", "status": "in_progress"} +` + "```" + ` + +完成工作后将任务标记为已完成: +` + "```json" + ` +{"taskId": "1", "status": "completed"} +` + "```" + ` + +删除任务: +` + "```json" + ` +{"taskId": "1", "status": "deleted"} +` + "```" + ` + +通过设置 owner 认领任务: +` + "```json" + ` +{"taskId": "1", "owner": "my-name"} +` + "```" + ` + +设置任务依赖关系: +` + "```json" + ` +{"taskId": "2", "addBlockedBy": ["1"]} +` + "```" + ` +` diff --git a/adk/middlewares/plantask/task_update_test.go b/adk/middlewares/plantask/task_update_test.go new file mode 100644 index 0000000..2869dd6 --- /dev/null +++ b/adk/middlewares/plantask/task_update_test.go @@ -0,0 +1,739 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package plantask + +import ( + "context" + "path/filepath" + "sync" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" +) + +func TestTaskUpdateTool(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + taskData := &task{ + ID: "1", + Subject: "Original Subject", + Description: "Original description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + taskJSON, _ := sonic.MarshalString(taskData) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: taskJSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + info, err := tool.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, TaskUpdateToolName, info.Name) + assert.Equal(t, taskUpdateToolDesc, info.Desc) + + result, err := tool.InvokableRun(ctx, `{"taskId": "1", "status": "in_progress"}`) + assert.NoError(t, err) + assert.Contains(t, result, "Updated task #1") + assert.Contains(t, result, "status") + + content, err := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.NoError(t, err) + var updated task + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, taskStatusInProgress, updated.Status) + + result, err = tool.InvokableRun(ctx, `{"taskId": "1", "subject": "New Subject", "description": "New description"}`) + assert.NoError(t, err) + assert.Contains(t, result, "subject") + assert.Contains(t, result, "description") + + content, _ = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, "New Subject", updated.Subject) + assert.Equal(t, "New description", updated.Description) +} + +func TestTaskUpdateToolOwnerAndMetadata(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + taskData := &task{ + ID: "1", + Subject: "Test Task", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + taskJSON, _ := sonic.MarshalString(taskData) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: taskJSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + result, err := tool.InvokableRun(ctx, `{"taskId": "1", "owner": "agent1"}`) + assert.NoError(t, err) + assert.Contains(t, result, "owner") + + content, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updated task + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, "agent1", updated.Owner) + + result, err = tool.InvokableRun(ctx, `{"taskId": "1", "metadata": {"key1": "value1", "key2": "value2"}}`) + assert.NoError(t, err) + assert.Contains(t, result, "metadata") + + content, _ = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, "value1", updated.Metadata["key1"]) + assert.Equal(t, "value2", updated.Metadata["key2"]) + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "metadata": {"key1": null, "key3": "value3"}}`) + assert.NoError(t, err) + + content, _ = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updated2 task + _ = sonic.UnmarshalString(content.Content, &updated2) + _, key1Exists := updated2.Metadata["key1"] + assert.False(t, key1Exists) + assert.Equal(t, "value2", updated2.Metadata["key2"]) + assert.Equal(t, "value3", updated2.Metadata["key3"]) +} + +func TestTaskUpdateToolBlocks(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Test Task", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + task4 := &task{ + ID: "4", + Subject: "Task 4", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task4JSON, _ := sonic.MarshalString(task4) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "4.json"), Content: task4JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + result, err := tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["2", "3"]}`) + assert.NoError(t, err) + assert.Contains(t, result, "blocks") + + content, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updated task + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, []string{"2", "3"}, updated.Blocks) + + result, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["4"]}`) + assert.NoError(t, err) + assert.Contains(t, result, "blockedBy") + + content, _ = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, []string{"4"}, updated.BlockedBy) +} + +func TestTaskUpdateToolDelete(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + taskData := &task{ + ID: "1", + Subject: "Test Task", + Description: "Test description", + Status: taskStatusPending, + } + taskJSON, _ := sonic.MarshalString(taskData) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: taskJSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + result, err := tool.InvokableRun(ctx, `{"taskId": "1", "status": "deleted"}`) + assert.NoError(t, err) + assert.Contains(t, result, "deleted") + + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.Error(t, err) +} + +func TestTaskUpdateToolInvalidTaskID(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "../../../etc/passwd", "status": "in_progress"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate task ID failed") + + _, err = tool.InvokableRun(ctx, `{"taskId": "abc", "status": "in_progress"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate task ID failed") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1.5", "status": "in_progress"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate task ID failed") + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["invalid"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate blocked task ID failed") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["invalid"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validate blocking task ID failed") +} + +func TestTaskUpdateToolBlocksDeduplication(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{"1"}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{"1"}, + BlockedBy: []string{}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + task4 := &task{ + ID: "4", + Subject: "Task 4", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task4JSON, _ := sonic.MarshalString(task4) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "4.json"), Content: task4JSON}) + + task5 := &task{ + ID: "5", + Subject: "Task 5", + Description: "Test description", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task5JSON, _ := sonic.MarshalString(task5) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "5.json"), Content: task5JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["2", "4", "4"]}`) + assert.NoError(t, err) + + content, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updated task + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, []string{"2", "4"}, updated.Blocks) + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["3", "5", "5"]}`) + assert.NoError(t, err) + + content, _ = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + _ = sonic.UnmarshalString(content.Content, &updated) + assert.Equal(t, []string{"3", "5"}, updated.BlockedBy) +} + +func TestTaskUpdateToolBidirectionalBlocks(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Second task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Third task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["2", "3"]}`) + assert.NoError(t, err) + + content1, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updatedTask1 task + _ = sonic.UnmarshalString(content1.Content, &updatedTask1) + assert.Equal(t, []string{"2", "3"}, updatedTask1.Blocks) + assert.Empty(t, updatedTask1.BlockedBy) + + content2, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "2.json")}) + var updatedTask2 task + _ = sonic.UnmarshalString(content2.Content, &updatedTask2) + assert.Empty(t, updatedTask2.Blocks) + assert.Equal(t, []string{"1"}, updatedTask2.BlockedBy) + + content3, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "3.json")}) + var updatedTask3 task + _ = sonic.UnmarshalString(content3.Content, &updatedTask3) + assert.Empty(t, updatedTask3.Blocks) + assert.Equal(t, []string{"1"}, updatedTask3.BlockedBy) +} + +func TestTaskUpdateToolBidirectionalBlockedBy(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Second task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Third task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "3", "addBlockedBy": ["1", "2"]}`) + assert.NoError(t, err) + + content3, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "3.json")}) + var updatedTask3 task + _ = sonic.UnmarshalString(content3.Content, &updatedTask3) + assert.Empty(t, updatedTask3.Blocks) + assert.Equal(t, []string{"1", "2"}, updatedTask3.BlockedBy) + + content1, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updatedTask1 task + _ = sonic.UnmarshalString(content1.Content, &updatedTask1) + assert.Equal(t, []string{"3"}, updatedTask1.Blocks) + assert.Empty(t, updatedTask1.BlockedBy) + + content2, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "2.json")}) + var updatedTask2 task + _ = sonic.UnmarshalString(content2.Content, &updatedTask2) + assert.Equal(t, []string{"3"}, updatedTask2.Blocks) + assert.Empty(t, updatedTask2.BlockedBy) +} + +func TestTaskUpdateToolBidirectionalWithNonExistentTask(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["999"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "update Task #1 blocks failed") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["999"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "update Task #1 blockedBy failed") +} + +func TestTaskUpdateToolCyclicDependencyDetection(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Second task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Third task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["1"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cyclic dependency") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["1"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cyclic dependency") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlocks": ["2"]}`) + assert.NoError(t, err) + + _, err = tool.InvokableRun(ctx, `{"taskId": "2", "addBlocks": ["1"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cyclic dependency") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["2"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cyclic dependency") + + _, err = tool.InvokableRun(ctx, `{"taskId": "2", "addBlocks": ["3"]}`) + assert.NoError(t, err) + + _, err = tool.InvokableRun(ctx, `{"taskId": "3", "addBlocks": ["1"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cyclic dependency") + + _, err = tool.InvokableRun(ctx, `{"taskId": "1", "addBlockedBy": ["3"]}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cyclic dependency") + + content1, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updatedTask1 task + _ = sonic.UnmarshalString(content1.Content, &updatedTask1) + assert.Equal(t, []string{"2"}, updatedTask1.Blocks) + assert.Empty(t, updatedTask1.BlockedBy) + + content2, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "2.json")}) + var updatedTask2 task + _ = sonic.UnmarshalString(content2.Content, &updatedTask2) + assert.Equal(t, []string{"3"}, updatedTask2.Blocks) + assert.Equal(t, []string{"1"}, updatedTask2.BlockedBy) + + content3, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "3.json")}) + var updatedTask3 task + _ = sonic.UnmarshalString(content3.Content, &updatedTask3) + assert.Empty(t, updatedTask3.Blocks) + assert.Equal(t, []string{"2"}, updatedTask3.BlockedBy) +} + +func TestTaskUpdateToolDeleteCleansDependencies(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusPending, + Blocks: []string{"2", "3"}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Second task", + Status: taskStatusPending, + Blocks: []string{"3"}, + BlockedBy: []string{"1"}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Third task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{"1", "2"}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + result, err := tool.InvokableRun(ctx, `{"taskId": "1", "status": "deleted"}`) + assert.NoError(t, err) + assert.Contains(t, result, "deleted") + + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.Error(t, err) + + content2, err := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "2.json")}) + assert.NoError(t, err) + var updatedTask2 task + _ = sonic.UnmarshalString(content2.Content, &updatedTask2) + assert.Equal(t, []string{"3"}, updatedTask2.Blocks) + assert.Empty(t, updatedTask2.BlockedBy) + + content3, err := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "3.json")}) + assert.NoError(t, err) + var updatedTask3 task + _ = sonic.UnmarshalString(content3.Content, &updatedTask3) + assert.Empty(t, updatedTask3.Blocks) + assert.Equal(t, []string{"2"}, updatedTask3.BlockedBy) +} + +func TestTaskUpdateToolAutoDeleteAllTasksWhenAllCompleted(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusCompleted, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Second task", + Status: taskStatusCompleted, + Blocks: []string{}, + BlockedBy: []string{}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + task3 := &task{ + ID: "3", + Subject: "Task 3", + Description: "Third task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task3JSON, _ := sonic.MarshalString(task3) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "3.json"), Content: task3JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "3", "status": "completed"}`) + assert.NoError(t, err) + + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.Error(t, err) + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "2.json")}) + assert.Error(t, err) + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "3.json")}) + assert.Error(t, err) +} + +func TestTaskUpdateToolNoDeleteWhenNotAllCompleted(t *testing.T) { + ctx := context.Background() + backend := newInMemoryBackend() + baseDir := "/tmp/tasks" + lock := &sync.Mutex{} + + task1 := &task{ + ID: "1", + Subject: "Task 1", + Description: "First task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task1JSON, _ := sonic.MarshalString(task1) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "1.json"), Content: task1JSON}) + + task2 := &task{ + ID: "2", + Subject: "Task 2", + Description: "Second task", + Status: taskStatusPending, + Blocks: []string{}, + BlockedBy: []string{}, + } + task2JSON, _ := sonic.MarshalString(task2) + _ = backend.Write(ctx, &WriteRequest{FilePath: filepath.Join(baseDir, "2.json"), Content: task2JSON}) + + tool := newTaskUpdateTool(backend, baseDir, lock) + + _, err := tool.InvokableRun(ctx, `{"taskId": "1", "status": "completed"}`) + assert.NoError(t, err) + + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + assert.NoError(t, err) + _, err = backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "2.json")}) + assert.NoError(t, err) + + content1, _ := backend.Read(ctx, &ReadRequest{FilePath: filepath.Join(baseDir, "1.json")}) + var updatedTask1 task + _ = sonic.UnmarshalString(content1.Content, &updatedTask1) + assert.Equal(t, taskStatusCompleted, updatedTask1.Status) +} diff --git a/adk/middlewares/reduction/consts.go b/adk/middlewares/reduction/consts.go new file mode 100644 index 0000000..6f76588 --- /dev/null +++ b/adk/middlewares/reduction/consts.go @@ -0,0 +1,122 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package reduction provides middlewares to trim context and clear tool results. +package reduction + +import ( + "fmt" + + "github.com/cloudwego/eino/adk/internal" +) + +const ( + truncFmt = ` +Output too large ({original_size}). Full output saved to: {file_path} +Preview (first {preview_size}): +{preview_first} + +Preview (last {preview_size}): +{preview_last} + +Use {read_tool_name} to view +` + truncFmtZh = ` +输出结果过大 ({original_size}). 完整输出保存到: {file_path} +预览 (前 {preview_size}): +{preview_first} + +预览 (后 {preview_size}): +{preview_last} + +使用 {read_tool_name} 进行查看 +` +) + +const ( + streamTruncFmt = ` +Output truncated after {preview_size} bytes were streamed. {offload_notify} {error_msg_notify} +` + streamTruncFmtZh = ` +输出结果在流式传输 {preview_size} 字节后被截断。{offload_notify} {error_msg_notify} +` +) + +const ( + clearWithOffloadingFmt = `Tool result saved to: {file_path} +Use {read_tool_name} to view` + clearWithOffloadingFmtZh = `工具结果已保存至: {file_path} +使用 {read_tool_name} 进行查看` + + clearWithoutOffloadingFmt = `[Old tool result content cleared]` + clearWithoutOffloadingFmtZh = `[工具输出结果已清理]` +) + +const ( + msgClearedFlag = "_reduction_mw_processed" +) + +func getTruncFmt() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: truncFmt, + Chinese: truncFmtZh, + }) +} + +func getStreamTruncFmt() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: streamTruncFmt, + Chinese: streamTruncFmtZh, + }) +} + +func formatStreamOffloadSavedNotify(filePath, readFileToolName string) string { + return fmt.Sprintf(internal.SelectPrompt(internal.I18nPrompts{ + English: "Full output saved to: %s. Use %s to view.", + Chinese: "完整输出保存到: %s。使用 %s 进行查看。", + }), filePath, readFileToolName) +} + +func formatStreamOffloadFailedNotify(err error) string { + if err == nil { + return "" + } + return fmt.Sprintf(internal.SelectPrompt(internal.I18nPrompts{ + English: "Failed to save full output: %v.", + Chinese: "完整输出保存失败: %v。", + }), err) +} + +func getClearWithOffloadingFmt() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: clearWithOffloadingFmt, + Chinese: clearWithOffloadingFmtZh, + }) +} + +func getClearWithoutOffloadingFmt() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: clearWithoutOffloadingFmt, + Chinese: clearWithoutOffloadingFmtZh, + }) +} + +type scene int + +const ( + sceneTruncation scene = 1 + sceneClear scene = 2 +) diff --git a/adk/middlewares/reduction/internal/clear_tool_result.go b/adk/middlewares/reduction/internal/clear_tool_result.go new file mode 100644 index 0000000..224e1fb --- /dev/null +++ b/adk/middlewares/reduction/internal/clear_tool_result.go @@ -0,0 +1,165 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package internal provides middlewares to trim context and clear tool results. +package internal + +import ( + "context" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// ClearToolResultConfig configures the tool result clearing middleware. +// This middleware clears old tool results when their total token count exceeds a threshold, +// while protecting recent messages within a token budget. +type ClearToolResultConfig struct { + // ToolResultTokenThreshold is the threshold for total tool result tokens. + // When the sum of all tool result tokens exceeds this threshold, old tool results + // (outside the KeepRecentTokens range) will be replaced with a placeholder. + // Token estimation uses a simple heuristic: character count / 4. + // If 0, defaults to 20000. + ToolResultTokenThreshold int + + // KeepRecentTokens is the token budget for recent messages to keep intact. + // Messages within this token budget from the end will not have their tool results cleared, + // even if the total tool result tokens exceed the threshold. + // If 0, defaults to 40000. + KeepRecentTokens int + + // ClearToolResultPlaceholder is the text to replace old tool results with. + // If empty, defaults to "[Old tool result content cleared]". + ClearToolResultPlaceholder string + + // TokenCounter is a custom function to estimate token count for a message. + // If nil, uses the default counter (character count / 4). + TokenCounter func(msg *schema.Message) int + + // ExcludeTools is a list of tool names whose results should never be cleared. + ExcludeTools []string +} + +// NewClearToolResult creates a new middleware that clears old tool results +// based on token thresholds while protecting recent messages. +func NewClearToolResult(ctx context.Context, config *ClearToolResultConfig) (adk.AgentMiddleware, error) { + return adk.AgentMiddleware{ + BeforeChatModel: newClearToolResult(ctx, config), + }, nil +} + +func newClearToolResult(ctx context.Context, config *ClearToolResultConfig) func(ctx context.Context, state *adk.ChatModelAgentState) error { + if config == nil { + config = &ClearToolResultConfig{} + } + + // Set defaults + toolResultTokenThreshold := config.ToolResultTokenThreshold + if toolResultTokenThreshold == 0 { + toolResultTokenThreshold = 20000 + } + + keepRecentTokens := config.KeepRecentTokens + if keepRecentTokens == 0 { + keepRecentTokens = 40000 + } + + placeholder := config.ClearToolResultPlaceholder + if placeholder == "" { + placeholder = "[Old tool result content cleared]" + } + + // Set token estimator + counter := config.TokenCounter + if counter == nil { + counter = defaultTokenCounter + } + return func(ctx context.Context, state *adk.ChatModelAgentState) error { + return reduceByTokens(state, toolResultTokenThreshold, keepRecentTokens, placeholder, counter, config.ExcludeTools) + } +} + +// defaultTokenCounter estimates token count using character count / 4 +// This is a simple heuristic that works reasonably well for most languages +func defaultTokenCounter(msg *schema.Message) int { + count := len(msg.Content) + + // Also count tool call arguments if present + for _, tc := range msg.ToolCalls { + count += len(tc.Function.Arguments) + } + + // Estimate: roughly 4 characters per token + return (count + 3) / 4 +} + +// reduceByTokens reduces context based on tool result token threshold and recent message protection. +// It clears old tool results when: +// 1. The total tokens of all tool results exceed toolResultTokenThreshold +// 2. Only tool results outside the keepRecentTokens range (from the end) are cleared +func reduceByTokens(state *adk.ChatModelAgentState, toolResultTokenThreshold, keepRecentTokens int, placeholder string, counter func(*schema.Message) int, excludedTools []string) error { + if len(state.Messages) == 0 { + return nil + } + + // Step 1: Calculate total tool result tokens + totalToolResultTokens := 0 + for _, msg := range state.Messages { + if msg.Role == schema.Tool && msg.Content != placeholder { + totalToolResultTokens += counter(msg) + } + } + + // If total tool result tokens are under the threshold, no reduction needed + if totalToolResultTokens <= toolResultTokenThreshold { + return nil + } + + // Step 2: Calculate the index from which to protect recent messages + // We need to find the starting index where cumulative tokens from the end <= keepRecentTokens + recentStartIdx := len(state.Messages) + cumulativeTokens := 0 + + for i := len(state.Messages) - 1; i >= 0; i-- { + msgTokens := counter(state.Messages[i]) + if cumulativeTokens+msgTokens > keepRecentTokens { + // Adding this message would exceed the budget, so stop here + recentStartIdx = i + break + } + cumulativeTokens += msgTokens + recentStartIdx = i + } + + // Step 3: Clear tool results outside the protected range (before recentStartIdx) + for i := 0; i < recentStartIdx; i++ { + msg := state.Messages[i] + if msg.Role == schema.Tool && msg.Content != placeholder && !excluded(msg.ToolName, excludedTools) { + msg.Content = placeholder + } + } + + return nil +} + +func excluded(name string, exclude []string) bool { + for _, ex := range exclude { + if name == ex { + return true + } + } + return false +} diff --git a/adk/middlewares/reduction/internal/clear_tool_result_test.go b/adk/middlewares/reduction/internal/clear_tool_result_test.go new file mode 100644 index 0000000..ff9575f --- /dev/null +++ b/adk/middlewares/reduction/internal/clear_tool_result_test.go @@ -0,0 +1,302 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +func Test_reduceByTokens(t *testing.T) { + type args struct { + state *adk.ChatModelAgentState + toolResultTokenThreshold int + keepRecentTokens int + placeholder string + estimator func(*schema.Message) int + } + tests := []struct { + name string + args args + wantErr assert.ErrorAssertionFunc + validateState func(*testing.T, *adk.ChatModelAgentState) + }{ + { + name: "no reduction when tool result tokens under threshold", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("hello"), + schema.AssistantMessage("hi", nil), + schema.ToolMessage("short tool result", "call-1", schema.WithToolName("tool1")), + }, + }, + toolResultTokenThreshold: 100, + keepRecentTokens: 500, + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + assert.Equal(t, "short tool result", state.Messages[2].Content) + }, + }, + { + name: "clear old tool results when total exceeds threshold", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("msg1"), + schema.ToolMessage(strings.Repeat("a", 40), "call-1", schema.WithToolName("tool1")), // ~10 tokens (old) + schema.UserMessage("msg2"), + schema.ToolMessage(strings.Repeat("b", 40), "call-2", schema.WithToolName("tool2")), // ~10 tokens (old) + schema.UserMessage("msg3"), + schema.ToolMessage(strings.Repeat("c", 40), "call-3", schema.WithToolName("tool3")), // ~10 tokens (recent, protected) + }, + }, + toolResultTokenThreshold: 20, + keepRecentTokens: 10, + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + assert.Equal(t, "[Old tool result content cleared]", state.Messages[1].Content) + assert.Equal(t, "[Old tool result content cleared]", state.Messages[3].Content) + assert.Equal(t, strings.Repeat("c", 40), state.Messages[5].Content) + }, + }, + { + name: "protect recent messages even when tool results exceed threshold", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("old msg"), + schema.ToolMessage(strings.Repeat("x", 100), "call-1", schema.WithToolName("tool1")), // ~25 tokens (old) + schema.UserMessage("recent msg"), + schema.ToolMessage(strings.Repeat("x", 100), "call-2", schema.WithToolName("tool2")), // ~25 tokens (recent, protected) + }, + }, + toolResultTokenThreshold: 10, + keepRecentTokens: 20, + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + // Total tool result tokens = 50, exceeds threshold of 10 + // But last 200 tokens are protected (includes last 2 messages) + // So only the first tool result should be cleared + assert.Equal(t, "[Old tool result content cleared]", state.Messages[1].Content) + assert.Equal(t, strings.Repeat("x", 100), state.Messages[3].Content) + }, + }, + { + name: "custom placeholder text", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("msg"), + schema.ToolMessage(strings.Repeat("x", 100), "call-1", schema.WithToolName("tool1")), + schema.UserMessage(strings.Repeat("x", 100)), + }, + }, + toolResultTokenThreshold: 10, + keepRecentTokens: 20, + placeholder: "[历史工具结果已清除]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + assert.Equal(t, "[历史工具结果已清除]", state.Messages[1].Content) + }, + }, + { + name: "no tool messages", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("msg 1"), + schema.AssistantMessage("response 1", nil), + schema.UserMessage("msg 2"), + schema.AssistantMessage("response 2", nil), + }, + }, + toolResultTokenThreshold: 10, + keepRecentTokens: 10, + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + // All messages should remain unchanged + assert.Equal(t, "msg 1", state.Messages[0].Content) + assert.Equal(t, "response 1", state.Messages[1].Content) + assert.Equal(t, "msg 2", state.Messages[2].Content) + assert.Equal(t, "response 2", state.Messages[3].Content) + }, + }, + { + name: "empty messages", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{}, + }, + toolResultTokenThreshold: 100, + keepRecentTokens: 500, + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + assert.Empty(t, state.Messages) + }, + }, + { + name: "custom token estimator - word count", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("hello world"), + schema.ToolMessage("this is a long tool result", "call-1", schema.WithToolName("tool1")), // 6 words (old) + schema.UserMessage("another message"), + schema.ToolMessage("recent tool result here", "call-2", schema.WithToolName("tool2")), // 4 words (recent) + }, + }, + toolResultTokenThreshold: 9, // 10 words total threshold + keepRecentTokens: 5, // 15 words protection budget + placeholder: "[Old tool result content cleared]", + estimator: func(msg *schema.Message) int { + if msg.Content == "" { + return 0 + } + words := 1 + for _, ch := range msg.Content { + if ch == ' ' { + words++ + } + } + return words + }, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + assert.Equal(t, "[Old tool result content cleared]", state.Messages[1].Content) + assert.Equal(t, "recent tool result here", state.Messages[3].Content) + }, + }, + { + name: "already cleared results are not counted", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("msg1"), + schema.ToolMessage("[Old tool result content cleared]", "call-1", schema.WithToolName("tool1")), // Already cleared + schema.UserMessage("msg2"), + schema.ToolMessage(strings.Repeat("a", 100), "call-2", schema.WithToolName("tool2")), // New long result + }, + }, + toolResultTokenThreshold: 10, + keepRecentTokens: 20, + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + // Only the new long result counts toward the threshold + // Both should have placeholder + assert.Equal(t, "[Old tool result content cleared]", state.Messages[1].Content) + assert.Equal(t, strings.Repeat("a", 100), state.Messages[3].Content) + }, + }, + { + name: "all tool results within protected range", + args: args{ + state: &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("msg1"), + schema.ToolMessage(strings.Repeat("a", 40), "call-1", schema.WithToolName("tool1")), // ~10 tokens + schema.UserMessage("msg2"), + schema.ToolMessage(strings.Repeat("b", 40), "call-2", schema.WithToolName("tool2")), // ~10 tokens + }, + }, + toolResultTokenThreshold: 10, // Low threshold (will exceed) + keepRecentTokens: 1000, // Very high protection (protects all) + placeholder: "[Old tool result content cleared]", + estimator: defaultTokenCounter, + }, + wantErr: assert.NoError, + validateState: func(t *testing.T, state *adk.ChatModelAgentState) { + // All messages are within protected range, nothing should be cleared + assert.Equal(t, strings.Repeat("a", 40), state.Messages[1].Content) + assert.Equal(t, strings.Repeat("b", 40), state.Messages[3].Content) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := reduceByTokens(tt.args.state, tt.args.toolResultTokenThreshold, tt.args.keepRecentTokens, tt.args.placeholder, tt.args.estimator, []string{}) + tt.wantErr(t, err, fmt.Sprintf("reduceByTokens(%v, %v, %v, %v)", tt.args.state, tt.args.toolResultTokenThreshold, tt.args.keepRecentTokens, tt.args.placeholder)) + if tt.validateState != nil { + tt.validateState(t, tt.args.state) + } + }) + } +} + +func Test_newClearToolResult(t *testing.T) { + ctx := context.Background() + + t.Run("nil config uses defaults", func(t *testing.T) { + fn := newClearToolResult(ctx, nil) + assert.NotNil(t, fn) + + // Test that function works with nil config (uses defaults) + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("hello"), + schema.ToolMessage("short result", "call-1", schema.WithToolName("tool1")), + }, + } + err := fn(ctx, state) + assert.NoError(t, err) + // Default threshold is 20000, so short result should not be cleared + assert.Equal(t, "short result", state.Messages[1].Content) + }) + + t.Run("empty config uses defaults", func(t *testing.T) { + fn := newClearToolResult(ctx, &ClearToolResultConfig{}) + assert.NotNil(t, fn) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("hello"), + schema.ToolMessage("short result", "call-1", schema.WithToolName("tool1")), + }, + } + err := fn(ctx, state) + assert.NoError(t, err) + assert.Equal(t, "short result", state.Messages[1].Content) + }) +} diff --git a/adk/middlewares/reduction/internal/large_tool_result.go b/adk/middlewares/reduction/internal/large_tool_result.go new file mode 100644 index 0000000..c1e41b1 --- /dev/null +++ b/adk/middlewares/reduction/internal/large_tool_result.go @@ -0,0 +1,210 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "strings" + "unicode/utf8" + + "github.com/slongfield/pyfmt" + + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +const ( + tooLargeToolMessage = `Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path} +You can read the result from the filesystem by using the '{read_file_tool_name}' tool, but make sure to only read part of the result at a time. +You can do this by specifying an offset and limit in the '{read_file_tool_name}' tool call. +For example, to read the first 100 lines, you can use the '{read_file_tool_name}' tool with offset=0 and limit=100. + +Here are the first 10 lines of the result: +{content_sample}` + + tooLargeToolMessageChinese = `工具结果过大,此工具调用 {tool_call_id} 的结果已保存到文件系统的以下路径:{file_path} +你可以使用 '{read_file_tool_name}' 工具从文件系统读取结果,但请确保每次只读取部分结果。 +你可以通过在 '{read_file_tool_name}' 工具调用中指定 offset 和 limit 来实现。 +例如,要读取前 100 行,你可以使用 '{read_file_tool_name}' 工具,设置 offset=0 和 limit=100。 + +以下是结果的前 10 行: +{content_sample}` +) + +type toolResultOffloadingConfig struct { + Backend Backend + ReadFileToolName string + TokenLimit int + PathGenerator func(ctx context.Context, input *compose.ToolInput) (string, error) + TokenCounter func(msg *schema.Message) int +} + +func newToolResultOffloading(_ context.Context, config *toolResultOffloadingConfig) compose.ToolMiddleware { + offloading := &toolResultOffloading{ + backend: config.Backend, + tokenLimit: config.TokenLimit, + pathGenerator: config.PathGenerator, + toolName: config.ReadFileToolName, + counter: config.TokenCounter, + } + + if offloading.tokenLimit == 0 { + offloading.tokenLimit = 20000 + } + + if offloading.pathGenerator == nil { + offloading.pathGenerator = func(ctx context.Context, input *compose.ToolInput) (string, error) { + return fmt.Sprintf("/large_tool_result/%s", input.CallID), nil + } + } + + if len(offloading.toolName) == 0 { + offloading.toolName = "read_file" + } + + if offloading.counter == nil { + offloading.counter = defaultTokenCounter + } + + return compose.ToolMiddleware{ + Invokable: offloading.invoke, + Streamable: offloading.stream, + } +} + +type toolResultOffloading struct { + backend Backend + tokenLimit int + pathGenerator func(ctx context.Context, input *compose.ToolInput) (string, error) + toolName string + counter func(msg *schema.Message) int +} + +func (t *toolResultOffloading) invoke(endpoint compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + output, err := endpoint(ctx, input) + if err != nil { + return nil, err + } + result, err := t.handleResult(ctx, output.Result, input) + if err != nil { + return nil, err + } + return &compose.ToolOutput{Result: result}, nil + } +} + +func (t *toolResultOffloading) stream(endpoint compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + output, err := endpoint(ctx, input) + if err != nil { + return nil, err + } + result, err := concatString(output.Result) + if err != nil { + return nil, err + } + result, err = t.handleResult(ctx, result, input) + if err != nil { + return nil, err + } + return &compose.StreamToolOutput{Result: schema.StreamReaderFromArray([]string{result})}, nil + } +} + +func (t *toolResultOffloading) handleResult(ctx context.Context, result string, input *compose.ToolInput) (string, error) { + if t.counter(schema.ToolMessage(result, input.CallID, schema.WithToolName(input.Name))) > t.tokenLimit*4 { + path, err := t.pathGenerator(ctx, input) + if err != nil { + return "", err + } + + nResult := formatToolMessage(result) + tpl := internal.SelectPrompt(internal.I18nPrompts{ + English: tooLargeToolMessage, + Chinese: tooLargeToolMessageChinese, + }) + nResult, err = pyfmt.Fmt(tpl, map[string]any{ + "tool_call_id": input.CallID, + "file_path": path, + "content_sample": nResult, + "read_file_tool_name": t.toolName, + }) + if err != nil { + return "", err + } + + err = t.backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: path, + Content: result, + }) + if err != nil { + return "", err + } + + return nResult, nil + } + + return result, nil +} + +func concatString(sr *schema.StreamReader[string]) (string, error) { + if sr == nil { + return "", errors.New("stream is nil") + } + sb := strings.Builder{} + for { + str, err := sr.Recv() + if errors.Is(err, io.EOF) { + return sb.String(), nil + } + if err != nil { + return "", err + } + sb.WriteString(str) + } +} + +func formatToolMessage(s string) string { + reader := bufio.NewScanner(strings.NewReader(s)) + var b strings.Builder + + lineNum := 1 + for reader.Scan() { + if lineNum > 10 { + break + } + line := reader.Text() + + if utf8.RuneCountInString(line) > 1000 { + runes := []rune(line) + line = string(runes[:1000]) + } + + b.WriteString(fmt.Sprintf("%d: %s\n", lineNum, line)) + + lineNum++ + } + + return b.String() +} diff --git a/adk/middlewares/reduction/internal/large_tool_result_test.go b/adk/middlewares/reduction/internal/large_tool_result_test.go new file mode 100644 index 0000000..fafd004 --- /dev/null +++ b/adk/middlewares/reduction/internal/large_tool_result_test.go @@ -0,0 +1,568 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + "testing" + + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// mockBackend is a simple in-memory backend for testing +type mockBackend struct { + files map[string]string +} + +func newMockBackend() *mockBackend { + return &mockBackend{ + files: make(map[string]string), + } +} + +func (m *mockBackend) Write(_ context.Context, wr *filesystem.WriteRequest) error { + m.files[wr.FilePath] = wr.Content + return nil +} + +func TestToolResultOffloading_SmallResult(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 100, // Small limit for testing + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a mock endpoint that returns a small result + smallResult := "This is a small result" + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: smallResult}, nil + } + + // Wrap the endpoint with the middleware + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + // Execute + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_123", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Small result should pass through unchanged + if output.Result != smallResult { + t.Errorf("expected result %q, got %q", smallResult, output.Result) + } + + // No file should be written + if len(backend.files) != 0 { + t.Errorf("expected no files to be written, got %d files", len(backend.files)) + } +} + +func TestToolResultOffloading_LargeResult(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, // Very small limit to trigger offloading + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a large result (more than 10 * 4 = 40 bytes) + largeResult := strings.Repeat("This is a long line of text that will exceed the token limit.\n", 10) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_456", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Result should be replaced with a message + if !strings.Contains(output.Result, "Tool result too large") { + t.Errorf("expected result to contain 'Tool result too large', got %q", output.Result) + } + + if !strings.Contains(output.Result, "call_456") { + t.Errorf("expected result to contain call ID 'call_456', got %q", output.Result) + } + + if !strings.Contains(output.Result, "/large_tool_result/call_456") { + t.Errorf("expected result to contain file path, got %q", output.Result) + } + + // File should be written + if len(backend.files) != 1 { + t.Fatalf("expected 1 file to be written, got %d files", len(backend.files)) + } + + savedContent, ok := backend.files["/large_tool_result/call_456"] + if !ok { + t.Fatalf("expected file at /large_tool_result/call_456, got files: %v", backend.files) + } + + if savedContent != largeResult { + t.Errorf("saved content doesn't match original result") + } +} + +func TestToolResultOffloading_CustomPathGenerator(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + customPath := "/custom/path/result.txt" + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + PathGenerator: func(ctx context.Context, input *compose.ToolInput) (string, error) { + return customPath, nil + }, + } + + middleware := newToolResultOffloading(ctx, config) + + largeResult := strings.Repeat("Large content ", 100) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_789", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check custom path is used + if !strings.Contains(output.Result, customPath) { + t.Errorf("expected result to contain custom path %q, got %q", customPath, output.Result) + } + + // File should be written to custom path + savedContent, ok := backend.files[customPath] + if !ok { + t.Fatalf("expected file at %q, got files: %v", customPath, backend.files) + } + + if savedContent != largeResult { + t.Errorf("saved content doesn't match original result") + } +} + +func TestToolResultOffloading_PathGeneratorError(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + expectedErr := errors.New("path generation failed") + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + PathGenerator: func(ctx context.Context, input *compose.ToolInput) (string, error) { + return "", expectedErr + }, + } + + middleware := newToolResultOffloading(ctx, config) + + largeResult := strings.Repeat("Large content ", 100) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestToolResultOffloading_EndpointError(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 100, + } + + middleware := newToolResultOffloading(ctx, config) + + expectedErr := errors.New("endpoint execution failed") + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return nil, expectedErr + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_endpoint_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestToolResultOffloading_DefaultTokenLimit(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 0, // Should default to 20000 + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a result smaller than 20000 * 4 = 80000 bytes + smallResult := strings.Repeat("x", 1000) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: smallResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_default", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should pass through unchanged + if output.Result != smallResult { + t.Errorf("expected result to pass through unchanged") + } + + // No file should be written + if len(backend.files) != 0 { + t.Errorf("expected no files to be written, got %d files", len(backend.files)) + } +} + +func TestToolResultOffloading_Stream(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + } + + middleware := newToolResultOffloading(ctx, config) + + // Create a streaming endpoint that returns large content + largeResult := strings.Repeat("Large streaming content ", 100) + mockStreamEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + // Split the result into chunks + chunks := []string{largeResult[:len(largeResult)/2], largeResult[len(largeResult)/2:]} + return &compose.StreamToolOutput{ + Result: schema.StreamReaderFromArray(chunks), + }, nil + } + + wrappedEndpoint := middleware.Streamable(mockStreamEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_stream", + } + output, err := wrappedEndpoint(ctx, input) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Read the stream + var result strings.Builder + for { + chunk, err := output.Result.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error reading stream: %v", err) + } + result.WriteString(chunk) + } + + resultStr := result.String() + + // Result should be replaced with a message + if !strings.Contains(resultStr, "Tool result too large") { + t.Errorf("expected result to contain 'Tool result too large', got %q", resultStr) + } + + if !strings.Contains(resultStr, "call_stream") { + t.Errorf("expected result to contain call ID 'call_stream', got %q", resultStr) + } + + // File should be written + if len(backend.files) != 1 { + t.Fatalf("expected 1 file to be written, got %d files", len(backend.files)) + } + + savedContent, ok := backend.files["/large_tool_result/call_stream"] + if !ok { + t.Fatalf("expected file at /large_tool_result/call_stream, got files: %v", backend.files) + } + + if savedContent != largeResult { + t.Errorf("saved content doesn't match original result") + } +} + +func TestToolResultOffloading_StreamError(t *testing.T) { + ctx := context.Background() + backend := newMockBackend() + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + } + + middleware := newToolResultOffloading(ctx, config) + + expectedErr := errors.New("stream endpoint failed") + mockStreamEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return nil, expectedErr + } + + wrappedEndpoint := middleware.Streamable(mockStreamEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_stream_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestFormatToolMessage(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "single line", + input: "single line", + expected: "1: single line\n", + }, + { + name: "multiple lines", + input: "line1\nline2\nline3", + expected: "1: line1\n2: line2\n3: line3\n", + }, + { + name: "more than 10 lines", + input: "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12", + expected: "1: 1\n2: 2\n3: 3\n4: 4\n5: 5\n6: 6\n7: 7\n8: 8\n9: 9\n10: 10\n", + }, + { + name: "long line truncation", + input: strings.Repeat("a", 1500), + expected: fmt.Sprintf("1: %s\n", strings.Repeat("a", 1000)), + }, + { + name: "unicode characters", + input: "你好世界\n测试", + expected: "1: 你好世界\n2: 测试\n", + }, + { + name: "long unicode line", + input: strings.Repeat("你", 1500), + expected: fmt.Sprintf("1: %s\n", strings.Repeat("你", 1000)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatToolMessage(tt.input) + if result != tt.expected { + t.Errorf("formatToolMessage() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestConcatString(t *testing.T) { + tests := []struct { + name string + chunks []string + expected string + expectError bool + }{ + { + name: "single chunk", + chunks: []string{"hello"}, + expected: "hello", + }, + { + name: "multiple chunks", + chunks: []string{"hello", " ", "world"}, + expected: "hello world", + }, + { + name: "empty chunks", + chunks: []string{"", "", ""}, + expected: "", + }, + { + name: "mixed chunks", + chunks: []string{"a", "", "b", "c"}, + expected: "abc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sr := schema.StreamReaderFromArray(tt.chunks) + result, err := concatString(sr) + + if tt.expectError { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != tt.expected { + t.Errorf("concatString() = %q, want %q", result, tt.expected) + } + }) + } + + // Test nil stream + t.Run("nil stream", func(t *testing.T) { + _, err := concatString(nil) + if err == nil { + t.Error("expected error for nil stream, got nil") + } + if !strings.Contains(err.Error(), "stream is nil") { + t.Errorf("expected 'stream is nil' error, got %v", err) + } + }) +} + +func TestToolResultOffloading_BackendWriteError(t *testing.T) { + ctx := context.Background() + + // Create a backend that fails on write + backend := &failingBackend{ + writeErr: errors.New("write failed"), + } + + config := &toolResultOffloadingConfig{ + Backend: backend, + TokenLimit: 10, + } + + middleware := newToolResultOffloading(ctx, config) + + largeResult := strings.Repeat("Large content ", 100) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: largeResult}, nil + } + + wrappedEndpoint := middleware.Invokable(mockEndpoint) + + input := &compose.ToolInput{ + Name: "test_tool", + CallID: "call_write_error", + } + _, err := wrappedEndpoint(ctx, input) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "write failed") { + t.Errorf("expected 'write failed' error, got %v", err) + } +} + +// failingBackend is a mock backend that can be configured to fail +type failingBackend struct { + writeErr error +} + +func (f *failingBackend) Write(context.Context, *filesystem.WriteRequest) error { + if f.writeErr != nil { + return f.writeErr + } + return nil +} diff --git a/adk/middlewares/reduction/internal/tool_result.go b/adk/middlewares/reduction/internal/tool_result.go new file mode 100644 index 0000000..ec14ba9 --- /dev/null +++ b/adk/middlewares/reduction/internal/tool_result.go @@ -0,0 +1,121 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "context" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// Backend defines the interface provided by the user to implement file storage. +// It is used to save the content of large tool results to a persistent storage. +type Backend interface { + Write(context.Context, *filesystem.WriteRequest) error +} + +// ToolResultConfig configures the tool result reduction middleware. +type ToolResultConfig struct { + // ClearingTokenThreshold is the threshold for the total token count of all tool results. + // When the sum of all tool result tokens exceeds this threshold, old tool results + // (outside the KeepRecentTokens range) will be replaced with a placeholder. + // Token estimation uses a simple heuristic: character count / 4. + // optional, 20000 by default + ClearingTokenThreshold int + + // KeepRecentTokens is the token budget for recent messages to keep intact. + // Messages within this token budget from the end will not have their tool results cleared, + // even if the total tool result tokens exceed the threshold. + // optional, 40000 by default + KeepRecentTokens int + + // ClearToolResultPlaceholder is the text to replace old tool results with. + // optional, "[Old tool result content cleared]" by default + ClearToolResultPlaceholder string + + // TokenCounter is a custom function to estimate token count for a message. + // optional, uses the default counter (character count / 4) if nil + TokenCounter func(msg *schema.Message) int + + // ExcludeTools is a list of tool names whose results should never be cleared. + // optional + ExcludeTools []string + + // Backend is the storage backend for offloaded tool results. + // required + Backend Backend + + // OffloadingTokenLimit is the token threshold for a single tool result to trigger offloading. + // When a single tool result exceeds OffloadingTokenLimit * 4 characters, it will be + // offloaded to the filesystem. + // optional, 20000 by default + OffloadingTokenLimit int + + // ReadFileToolName is the name of the tool that LLM should use to read offloaded content. + // This name will be included in the summary message sent to the LLM. + // optional, "read_file" by default + // + // NOTE: If you are using the filesystem middleware, the read_file tool name + // is exactly "read_file", which matches the default value. + ReadFileToolName string + + // PathGenerator generates the write path for offloaded results. + // optional, "/large_tool_result/{ToolCallID}" by default + PathGenerator func(ctx context.Context, input *compose.ToolInput) (string, error) +} + +// NewToolResultMiddleware creates a tool result reduction middleware. +// This middleware combines two strategies to manage tool result tokens: +// +// 1. Clearing: Replaces old tool results with a placeholder when the total +// tool result tokens exceed the threshold, while protecting recent messages. +// +// 2. Offloading: Writes large individual tool results to the filesystem and +// returns a summary message guiding the LLM to read the full content. +// +// NOTE: If you are using the filesystem middleware (github.com/cloudwego/eino/adk/middlewares/filesystem), +// this functionality is already included by default. Set Config.WithoutLargeToolResultOffloading = true +// in the filesystem middleware if you want to use this middleware separately instead. +// +// NOTE: This middleware only handles offloading results to the filesystem. +// You MUST also provide a read_file tool to your agent, otherwise the agent +// will not be able to read the offloaded content. You can either: +// - Use the filesystem middleware (github.com/cloudwego/eino/adk/middlewares/filesystem) +// which provides the read_file tool automatically, OR +// - Implement your own read_file tool that reads from the same Backend +func NewToolResultMiddleware(ctx context.Context, cfg *ToolResultConfig) (adk.AgentMiddleware, error) { + bc := newClearToolResult(ctx, &ClearToolResultConfig{ + ToolResultTokenThreshold: cfg.ClearingTokenThreshold, + KeepRecentTokens: cfg.KeepRecentTokens, + ClearToolResultPlaceholder: cfg.ClearToolResultPlaceholder, + TokenCounter: cfg.TokenCounter, + ExcludeTools: cfg.ExcludeTools, + }) + tm := newToolResultOffloading(ctx, &toolResultOffloadingConfig{ + Backend: cfg.Backend, + ReadFileToolName: cfg.ReadFileToolName, + TokenLimit: cfg.OffloadingTokenLimit, + PathGenerator: cfg.PathGenerator, + }) + return adk.AgentMiddleware{ + BeforeChatModel: bc, + WrapToolCall: tm, + }, nil +} diff --git a/adk/middlewares/reduction/legacy.go b/adk/middlewares/reduction/legacy.go new file mode 100644 index 0000000..36c6d4f --- /dev/null +++ b/adk/middlewares/reduction/legacy.go @@ -0,0 +1,69 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reduction + +import "github.com/cloudwego/eino/adk/middlewares/reduction/internal" + +// Package reduction provides historical compatibility exports for reduction middleware APIs. +// +// DEPRECATED: All top-level exports in this file are maintained exclusively for backward compatibility. +// New reduction middleware implementations are now developed and maintained in this package. +// It is STRONGLY RECOMMENDED that new code directly use the New instead. +// +// Existing code relying on these exports will continue to work indefinitely, +// but no new features or bug fixes will be backported to this compatibility layer. + +type ( + ClearToolResultConfig = internal.ClearToolResultConfig + ToolResultConfig = internal.ToolResultConfig + Backend = internal.Backend +) + +var ( + // NewClearToolResult creates a new middleware that clears old tool results + // based on token thresholds while protecting recent messages. + // + // Deprecated: Use New instead, which provides a more comprehensive tool result reduction + // middleware with both truncation and clearing strategies. New returns a ChatModelAgentMiddleware + // for better context propagation through wrapper methods. + NewClearToolResult = internal.NewClearToolResult + + // NewToolResultMiddleware creates a tool result reduction middleware. + // This middleware combines two strategies to manage tool result tokens: + // + // 1. Clearing: Replaces old tool results with a placeholder when the total + // tool result tokens exceed the threshold, while protecting recent messages. + // + // 2. Offloading: Writes large individual tool results to the filesystem and + // returns a summary message guiding the LLM to read the full content. + // + // NOTE: If you are using the filesystem middleware (github.com/cloudwego/eino/adk/middlewares/filesystem), + // this functionality is already included by default. Set Config.WithoutLargeToolResultOffloading = true + // in the filesystem middleware if you want to use this middleware separately instead. + // + // NOTE: This middleware only handles offloading results to the filesystem. + // You MUST also provide a read_file tool to your agent, otherwise the agent + // will not be able to read the offloaded content. You can either: + // - Use the filesystem middleware (github.com/cloudwego/eino/adk/middlewares/filesystem) + // which provides the read_file tool automatically, OR + // - Implement your own read_file tool that reads from the same Backend + // + // Deprecated: Use New instead, which provides a more comprehensive tool result reduction + // middleware with both truncation and clearing strategies, per-tool configuration support, + // and returns a ChatModelAgentMiddleware for better context propagation through wrapper methods. + NewToolResultMiddleware = internal.NewToolResultMiddleware +) diff --git a/adk/middlewares/reduction/reduction.go b/adk/middlewares/reduction/reduction.go new file mode 100644 index 0000000..6a4203d --- /dev/null +++ b/adk/middlewares/reduction/reduction.go @@ -0,0 +1,2084 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reduction + +import ( + "context" + "encoding/json" + "fmt" + "io" + "path/filepath" + "strings" + "unicode/utf8" + + "github.com/bytedance/sonic" + "github.com/google/uuid" + "github.com/slongfield/pyfmt" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +// TypedConfig is the configuration for tool reduction middleware. +// This middleware manages tool outputs in two phases to optimize context usage: +// +// 1. Truncation Phase: +// Triggered immediately after a tool execution completes. +// If the tool output length exceeds MaxLengthForTrunc, the full content is saved +// to the configured Backend, and the tool output is replaced with a truncated notice. +// This prevents immediate context overflow from a single large tool output. +// +// 2. Clear Phase: +// Triggered before sending messages to the model (in BeforeModelRewriteState). +// If the total token count exceeds MaxTokensForClear, the middleware iterates through +// historical messages. Based on GenOffloadFilePath (or RootDir when GenOffloadFilePath is nil) and +// ClearRetentionSuffixLimit, it offloads tool call arguments and results +// to the Backend to reduce token usage, keeping the conversation within limits while retaining access to the +// important information. After all, ClearPostProcess will be called, which you could save or notify current state. +type TypedConfig[M adk.MessageType] struct { + // Backend is the storage backend where offloaded content will be saved. + // Required when truncation is enabled (SkipTruncation is false). + // Optional for clear-only usage. If Backend is nil, clear will still replace tool outputs with placeholders + // but will not offload content. + Backend Backend + + // SkipTruncation skip truncating. + SkipTruncation bool + + // SkipClear skip clearing. + SkipClear bool + + // ReadFileToolName is tool name used to retrieve from file. + // After offloading content to file, you should give agent the same tool to retrieve content. + // Required. Default is "read_file". + ReadFileToolName string + + // RootDir root dir to save truncated/cleared content. + // Optional. + // Default is /tmp, truncated content saves to ${root_dir}/trunc/{tool_call_id}, cleared content saves to ${root_dir}/clear/{tool_call_id} + RootDir string + + // GenTruncOffloadFilePath is used to generate offload file path for truncated content. + // When GenTruncOffloadFilePath is configured, RootDir will be ignored. + // This is useful when tool_call_id is not unique, which may cause incorrect offload file overwrite. + // Optional. Default is nil. + GenTruncOffloadFilePath func(ctx context.Context, toolDetail *ToolDetail) (filePath string, err error) + + // GenClearOffloadFilePath is used to generate offload file path for truncated content. + // When GenClearOffloadFilePath is configured, RootDir will be ignored. + // This is useful when tool_call_id is not unique, which may cause incorrect offload file overwrite. + // Optional. Default is nil. + GenClearOffloadFilePath func(ctx context.Context, toolDetail *ToolDetail) (filePath string, err error) + + // MaxLengthForTrunc is the maximum allowed length of the tool output. + // If the output exceeds this length, it will be truncated. + // Required. Default is 50000. + MaxLengthForTrunc int + + // TruncExcludeTools is list of tool names whose tool results should never be truncated. + // Optional. Default is nil. + TruncExcludeTools []string + + // TokenCounter is used to count the number of tokens in the conversation messages. + // It is used to determine when to trigger clearing based on token usage, and token usage after clearing. + // Optional. If not provided, a default token counter will be used, which estimates tokens by counting 1 token per 4 characters. + TokenCounter func(ctx context.Context, msg []M, tools []*schema.ToolInfo) (int64, error) + + // MaxTokensForClear is the maximum number of tokens allowed in the conversation before clearing is attempted. + // Required. Default is 160000. + MaxTokensForClear int64 + + // ClearRetentionSuffixLimit is the number of most recent tool-call rounds to retain without clearing. + // A round consists of one assistant message (which may contain multiple tool calls) and its corresponding tool-result messages. + // This ensures the model has immediate context to process pending tool results. + // + // Example with ClearRetentionSuffixLimit = 2, the retained suffix looks like: + // Round 2: assistant [call_A, call_B] → result_A, result_B + // Round 1: assistant [call_C] → result_C + // (Round 1 is the most recent, closest to the end of the message list.) + // + // Optional. Default is 1. + ClearRetentionSuffixLimit int + + // ClearAtLeastTokens ensures a minimum number of tokens is cleared each time the strategy activates. + // If the strategy couldn't clear at least the specified amount, clear phase will not be applied. + // This helps determine if context clearing is worth breaking your prompt cache. + // Optional. Default is 0. + ClearAtLeastTokens int64 + + // ClearExcludeTools is list of tool names whose tool uses and results should never be cleared. + // Optional. Default is nil. + ClearExcludeTools []string + + // ClearMessageRewriter is a pre-process handler before clearing specific tool call and tool response pairs. + // You can rewrite tool call and tool messages extracted as parameters and return a rearranged message slice. + // This can be useful when you want to remove some tool calls (e.g., write_file / edit_file) and rewrite them + // as a user message (e.g. ). + // Returned messages will replace the original tool call and tool messages and will count towards ClearAtLeastTokens. + // If returned messagesAfterRewrite is nil, tool call and tool messages will be removed. + // Optional. Default is nil, which means no rewrite. + ClearMessageRewriter func(ctx context.Context, toolCallMsg M, toolResponseMsgs []M) (messagesAfterRewrite []M, err error) + + // ClearPostProcess is clear post process handler. + // Optional. + ClearPostProcess func(ctx context.Context, state *adk.TypedChatModelAgentState[M]) context.Context + + // ToolConfig is the specific configuration that applies to tools by name. + // This configuration takes precedence over GeneralConfig for the specified tools. + // Optional. + ToolConfig map[string]*ToolReductionConfig +} + +// Config is the backward-compatible alias for TypedConfig with *schema.Message. +type Config = TypedConfig[*schema.Message] + +type ToolReductionConfig struct { + // Backend is the storage backend where offloaded content will be saved. + // Required when truncation is enabled for this tool (SkipTruncation is false). + // Optional for clear-only usage. If Backend is nil, clear will still replace tool outputs with placeholders + // but will not offload content. + Backend Backend + + // SkipTruncation skip truncation for this tool. + SkipTruncation bool + + // TruncHandler is used to process tool call results during truncation. + // Optional. Default using defaultTruncHandler when SkipTruncation is false but TruncHandler is nil. + TruncHandler func(ctx context.Context, detail *ToolDetail) (*TruncResult, error) + + // SkipClear skip clear for this tool. + SkipClear bool + + // ClearHandler is used to process tool call arguments and results during clearing. + // Optional. Default using defaultClearHandler when SkipClear is false but ClearHandler is nil. + ClearHandler func(ctx context.Context, detail *ToolDetail) (*ClearResult, error) +} + +type ToolDetail struct { + // ToolContext provides metadata about the tool call (e.g., tool name, call ID). + ToolContext *adk.ToolContext + + // ToolArgument contains the arguments passed to the tool. + ToolArgument *schema.ToolArgument + + // ToolResult contains the output returned by the invokable tool. + ToolResult *schema.ToolResult + + // StreamToolResult contains the output returned by the streamable tool. + StreamToolResult *schema.StreamReader[*schema.ToolResult] +} + +type TruncResult struct { + // NeedTrunc indicates whether the tool result should be truncated. + NeedTrunc bool + + // ToolResult contains the result returned by the invokable tool after trunc. + // Required when NeedTrunc is true and ToolDetail.ToolResult is not nil. + ToolResult *schema.ToolResult + + // StreamToolResult contains the output returned by the streamable tool after trunc. + // Required when NeedTrunc is true and ToolDetail.StreamToolResult is not nil. + StreamToolResult *schema.StreamReader[*schema.ToolResult] + + // NeedOffload indicates whether the tool result should be offloaded. + NeedOffload bool + + // OffloadFilePath is the path where the offloaded content should be stored. + // This path is typically relative to the backend's root. + // Required when NeedOffload is true. + OffloadFilePath string + + // OffloadContent is the actual content to be written to the storage backend. + // Required when NeedOffload is true. + OffloadContent string +} + +// ClearResult contains the result of the Handler's decision. +type ClearResult struct { + // NeedClear indicates whether the tool argument and result should be cleared. + NeedClear bool + + // ToolArgument contains the arguments passed to the tool after clear. + // Required when NeedClear is true. + ToolArgument *schema.ToolArgument + + // ToolResult contains the output returned by the tool after clear. + // Required when NeedClear is true + ToolResult *schema.ToolResult + + // NeedOffload indicates whether the tool argument and result should be offloaded. + NeedOffload bool + + // OffloadFilePath is the path where the offloaded content should be stored. + // This path is typically relative to the backend's root. + // Required when NeedOffload is true. + OffloadFilePath string + + // OffloadContent is the actual content to be written to the storage backend. + // Required when NeedOffload is true. + OffloadContent string +} + +func (t *TypedConfig[M]) copyAndFillDefaults() (*TypedConfig[M], error) { + cfg := &TypedConfig[M]{ + Backend: t.Backend, + SkipTruncation: t.SkipTruncation, + SkipClear: t.SkipClear, + ReadFileToolName: t.ReadFileToolName, + RootDir: t.RootDir, + GenTruncOffloadFilePath: t.GenTruncOffloadFilePath, + GenClearOffloadFilePath: t.GenClearOffloadFilePath, + MaxLengthForTrunc: t.MaxLengthForTrunc, + TruncExcludeTools: t.TruncExcludeTools, + TokenCounter: t.TokenCounter, + MaxTokensForClear: t.MaxTokensForClear, + ClearRetentionSuffixLimit: t.ClearRetentionSuffixLimit, + ClearAtLeastTokens: t.ClearAtLeastTokens, + ClearExcludeTools: t.ClearExcludeTools, + ClearMessageRewriter: t.ClearMessageRewriter, + ClearPostProcess: t.ClearPostProcess, + } + if cfg.TokenCounter == nil { + cfg.TokenCounter = getDefaultTokenCounter[M]() + } + if cfg.ClearRetentionSuffixLimit == 0 { + cfg.ClearRetentionSuffixLimit = 1 + } + if cfg.ReadFileToolName == "" { + cfg.ReadFileToolName = "read_file" + } + if cfg.RootDir == "" { + cfg.RootDir = "/tmp" + } + if cfg.GenTruncOffloadFilePath == nil { + cfg.GenTruncOffloadFilePath = func(ctx context.Context, toolDetail *ToolDetail) (filePath string, err error) { + tcID := toolDetail.ToolContext.CallID + if tcID == "" { + tcID = uuid.NewString() + } + return filepath.Join(cfg.RootDir, "trunc", tcID), nil + } + } + if cfg.GenClearOffloadFilePath == nil { + cfg.GenClearOffloadFilePath = func(ctx context.Context, toolDetail *ToolDetail) (filePath string, err error) { + tcID := toolDetail.ToolContext.CallID + if tcID == "" { + tcID = uuid.NewString() + } + return filepath.Join(cfg.RootDir, "clear", tcID), nil + } + } + if cfg.MaxLengthForTrunc == 0 { + cfg.MaxLengthForTrunc = 50000 + } + if cfg.MaxTokensForClear == 0 { + cfg.MaxTokensForClear = 160000 + } + if t.ToolConfig != nil { + cfg.ToolConfig = make(map[string]*ToolReductionConfig, len(t.ToolConfig)) + for toolName, trc := range t.ToolConfig { + cpConfig := &ToolReductionConfig{ + Backend: trc.Backend, + SkipTruncation: trc.SkipTruncation, + SkipClear: trc.SkipClear, + TruncHandler: trc.TruncHandler, + ClearHandler: trc.ClearHandler, + } + cfg.ToolConfig[toolName] = cpConfig + } + } + + return cfg, nil +} + +// NewTyped creates a generic tool reduction middleware from config. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +// Both message types support the full truncation and clear phases. +func NewTyped[M adk.MessageType](_ context.Context, config *TypedConfig[M]) (adk.TypedChatModelAgentMiddleware[M], error) { + var err error + if config == nil { + return nil, fmt.Errorf("config must not be nil") + } + if config.Backend == nil && !config.SkipTruncation { + return nil, fmt.Errorf("backend must be set when not skipping truncation") + } + + config, err = config.copyAndFillDefaults() + if err != nil { + return nil, err + } + defaultReductionConfig := &ToolReductionConfig{ + Backend: config.Backend, + SkipTruncation: config.SkipTruncation, + SkipClear: config.SkipClear, + } + if !defaultReductionConfig.SkipTruncation { + defaultReductionConfig.TruncHandler = defaultTruncHandler(config.GenTruncOffloadFilePath, config.MaxLengthForTrunc, config.ReadFileToolName) + } + if !defaultReductionConfig.SkipClear { + defaultReductionConfig.ClearHandler = defaultClearHandler(config.GenClearOffloadFilePath, config.Backend != nil, config.ReadFileToolName) + } + excludeTruncTools := make(map[string]struct{}, len(config.TruncExcludeTools)) + for _, toolName := range config.TruncExcludeTools { + excludeTruncTools[toolName] = struct{}{} + } + excludeClearTools := make(map[string]struct{}, len(config.ClearExcludeTools)) + for _, toolName := range config.ClearExcludeTools { + excludeClearTools[toolName] = struct{}{} + } + + return &typedToolReductionMiddleware[M]{ + config: config, + defaultConfig: defaultReductionConfig, + excludeTruncTools: excludeTruncTools, + excludeClearTools: excludeClearTools, + }, nil +} + +// New creates tool reduction middleware from config +func New(ctx context.Context, config *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped(ctx, config) +} + +type typedToolReductionMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + + config *TypedConfig[M] + defaultConfig *ToolReductionConfig + + excludeTruncTools map[string]struct{} + excludeClearTools map[string]struct{} +} + +// getDefaultTokenCounter returns a default token counter function that operates on []M. +// For *schema.Message it delegates to defaultTokenCounter. +// For *schema.AgenticMessage it uses a simple character-based estimation. +func getDefaultTokenCounter[M adk.MessageType]() func(ctx context.Context, msgs []M, tools []*schema.ToolInfo) (int64, error) { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(func(ctx context.Context, msgs []*schema.Message, tools []*schema.ToolInfo) (int64, error) { + return defaultTokenCounter(ctx, msgs, tools) + }).(func(context.Context, []M, []*schema.ToolInfo) (int64, error)) + case *schema.AgenticMessage: + return any(func(ctx context.Context, msgs []*schema.AgenticMessage, tools []*schema.ToolInfo) (int64, error) { + return defaultAgenticTokenCounter(ctx, msgs, tools) + }).(func(context.Context, []M, []*schema.ToolInfo) (int64, error)) + } + panic("unreachable") +} + +func defaultAgenticTokenCounter(_ context.Context, msgs []*schema.AgenticMessage, tools []*schema.ToolInfo) (int64, error) { + var tokens int64 + for _, msg := range msgs { + if msg == nil { + continue + } + tokens += int64(len(msg.Role)) / 4 + for _, block := range msg.ContentBlocks { + if block != nil { + tokens += int64(len(block.String())) / 4 + } + } + } + for _, tl := range tools { + tl_ := *tl + tl_.Extra = nil + text, err := sonic.MarshalString(tl_) + if err != nil { + return 0, fmt.Errorf("failed to marshal tool info: %w", err) + } + tokens += int64(len(text) / 4) + } + return tokens, nil +} + +func (t *typedToolReductionMiddleware[M]) getToolConfig(toolName string, sc scene) *ToolReductionConfig { + if t.config.ToolConfig != nil { + if cfg, ok := t.config.ToolConfig[toolName]; ok { + if (sc == sceneTruncation && !cfg.SkipTruncation && cfg.TruncHandler == nil) || + (sc == sceneClear && !cfg.SkipClear && cfg.ClearHandler == nil) { + return t.defaultConfig + } + return cfg + } + } + return t.defaultConfig +} + +func (t *typedToolReductionMiddleware[M]) WrapInvokableToolCall(_ context.Context, endpoint adk.InvokableToolCallEndpoint, tCtx *adk.ToolContext) (adk.InvokableToolCallEndpoint, error) { + cfg := t.getToolConfig(tCtx.Name, sceneTruncation) + if cfg == nil || cfg.TruncHandler == nil { + return endpoint, nil + } + if _, excluded := t.excludeTruncTools[tCtx.Name]; excluded { + return endpoint, nil + } + + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + output, err := endpoint(ctx, argumentsInJSON, opts...) + if err != nil { + return "", err + } + detail := &ToolDetail{ + ToolContext: tCtx, + ToolArgument: &schema.ToolArgument{ + Text: argumentsInJSON, + }, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: output}, + }, + }, + } + truncResult, err := cfg.TruncHandler(ctx, detail) + if err != nil { + return "", err + } + if !truncResult.NeedTrunc { + return output, nil + } + if truncResult.NeedOffload { + if cfg.Backend == nil { + return "", fmt.Errorf("truncation: no backend for offload") + } + if err = cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: truncResult.OffloadFilePath, + Content: truncResult.OffloadContent, + }); err != nil { + return "", err + } + } + return truncResult.ToolResult.Parts[0].Text, nil + }, nil +} + +func (t *typedToolReductionMiddleware[M]) WrapStreamableToolCall(_ context.Context, endpoint adk.StreamableToolCallEndpoint, tCtx *adk.ToolContext) (adk.StreamableToolCallEndpoint, error) { + cfg := t.getToolConfig(tCtx.Name, sceneTruncation) + if cfg == nil || cfg.TruncHandler == nil { + return endpoint, nil + } + if _, excluded := t.excludeTruncTools[tCtx.Name]; excluded { + return endpoint, nil + } + + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + output, err := endpoint(ctx, argumentsInJSON, opts...) + if err != nil { + return nil, err + } + if cfg == t.defaultConfig { + return t.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, argumentsInJSON, output), nil + } + + readers := output.Copy(2) + output = readers[0] + origResp := readers[1] + + detail := &ToolDetail{ + ToolContext: tCtx, + ToolArgument: &schema.ToolArgument{ + Text: argumentsInJSON, + }, + StreamToolResult: schema.StreamReaderWithConvert(output, func(t string) (*schema.ToolResult, error) { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: t}}}, nil + }), + } + truncResult, err := cfg.TruncHandler(ctx, detail) + if err != nil { + origResp.Close() + return nil, err + } + if !truncResult.NeedTrunc { + return origResp, nil + } + origResp.Close() // close err resp when not using it + + if truncResult.NeedOffload { + if cfg.Backend == nil { + return nil, fmt.Errorf("truncation: no backend for offload") + } + if err = cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: truncResult.OffloadFilePath, + Content: truncResult.OffloadContent, + }); err != nil { + return nil, err + } + } + + sr := schema.StreamReaderWithConvert(truncResult.StreamToolResult, func(t *schema.ToolResult) (string, error) { + if t == nil || len(t.Parts) == 0 { + return "", nil + } + return t.Parts[0].Text, nil + }) + return sr, nil + }, nil +} + +func (t *typedToolReductionMiddleware[M]) wrapDefaultStreamableTruncation( + ctx context.Context, + cfg *ToolReductionConfig, + tCtx *adk.ToolContext, + argumentsInJSON string, + output *schema.StreamReader[string], +) *schema.StreamReader[string] { + var ( + fullOutput strings.Builder + curSize int + reachedLimit bool + truncTextPrefix string + truncTriggeredFullText string + sourceErr error + ) + + // Chunk mapping: + // + // Normal EOF: + // input : C1 -------- C2(crosses limit) -------- C3 -------- EOF + // output: C1 -------- [swallow C2,C3] ---------- prefix(C2)+notice + // + // Source error after truncation: + // input : C1 -------- C2(crosses limit) -------- C3 -------- ERR + // output: C1 -------- [swallow C2,C3] ---------- replay(C2+C3 tail) -- ERR + // + // Before truncation, chunks are forwarded unchanged. Once the limit is reached, + // the wrapper keeps reading and accumulating full output but suppresses chunks. + // On clean EOF, it offloads full output and emits one final truncation notice. + // On source error, it replays the original content swallowed since the truncation + // point, then surfaces the original source error via the outer convert layer. + convertHandler := func(chunk string) (string, error) { + if chunk == "" { + return "", schema.ErrNoValue + } + fullOutput.WriteString(chunk) + if reachedLimit { + return "", schema.ErrNoValue + } + + chunkSize := len(chunk) + if curSize+chunkSize < t.config.MaxLengthForTrunc { + curSize += chunkSize + return chunk, nil + } + + reachedLimit = true + truncTriggeredFullText = fullOutput.String() + + leftSize := t.config.MaxLengthForTrunc - curSize + if leftSize > 0 { + truncTextPrefix = clampPrefixToUTF8Boundary(chunk, leftSize) + } + return "", schema.ErrNoValue + } + + errHandler := func(err error) error { + if reachedLimit { + sourceErr = err + return nil + } + return err + } + + eofHandler := func() (any, error) { + if sourceErr != nil { + if fullOutput.Len() > curSize { + return fullOutput.String()[curSize:], nil + } + return nil, io.EOF + } + if !reachedLimit { + return nil, io.EOF + } + detail := &ToolDetail{ + ToolContext: tCtx, + ToolArgument: &schema.ToolArgument{ + Text: argumentsInJSON, + }, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: truncTriggeredFullText}, + }, + }, + } + + var ( + offloadNotify string + errorNotify string + ) + filePath, err := t.config.GenTruncOffloadFilePath(ctx, detail) + if err != nil { + errorNotify = formatStreamOffloadFailedNotify(err) + } else if cfg.Backend == nil { + errorNotify = formatStreamOffloadFailedNotify(fmt.Errorf("truncation: no backend for offload")) + } else if err = cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: filePath, + Content: fullOutput.String(), + }); err != nil { + errorNotify = formatStreamOffloadFailedNotify(err) + } else { + offloadNotify = formatStreamOffloadSavedNotify(filePath, t.config.ReadFileToolName) + } + + notice, err := formatStreamTruncNotice(t.config.MaxLengthForTrunc, offloadNotify, errorNotify) + if err != nil { + return nil, err + } + return truncTextPrefix + notice, nil + } + + truncatedStream := schema.StreamReaderWithConvert( + output, + convertHandler, + schema.WithErrWrapper(errHandler), + schema.WithOnEOF(eofHandler), + ) + + // StreamReaderWithConvert's WithOnEOF can inject only one value or one error. + // The source-error fallback needs two Recv results: + // 1. replay swallowed original output, + // 2. surface the original source error. + // The inner reader emits the replay value, and this outer reader emits the + // original error when the inner reader reaches EOF after replay. + return schema.StreamReaderWithConvert( + truncatedStream, + func(chunk string) (string, error) { + return chunk, nil + }, + schema.WithOnEOF(func() (any, error) { + if sourceErr != nil { + return nil, sourceErr + } + return nil, io.EOF + }), + ) +} + +func (t *typedToolReductionMiddleware[M]) WrapEnhancedInvokableToolCall(_ context.Context, endpoint adk.EnhancedInvokableToolCallEndpoint, tCtx *adk.ToolContext) (adk.EnhancedInvokableToolCallEndpoint, error) { + cfg := t.getToolConfig(tCtx.Name, sceneTruncation) + if cfg == nil || cfg.TruncHandler == nil { + return endpoint, nil + } + if _, excluded := t.excludeTruncTools[tCtx.Name]; excluded { + return endpoint, nil + } + + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + output, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + detail := &ToolDetail{ + ToolContext: tCtx, + ToolArgument: toolArgument, + ToolResult: output, + } + truncResult, err := cfg.TruncHandler(ctx, detail) + if err != nil { + return nil, err + } + if !truncResult.NeedTrunc { + return output, nil + } + if truncResult.NeedOffload { + if cfg.Backend == nil { + return nil, fmt.Errorf("truncation: no backend for offload") + } + if err = cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: truncResult.OffloadFilePath, + Content: truncResult.OffloadContent, + }); err != nil { + return nil, err + } + } + return truncResult.ToolResult, nil + }, nil +} + +func (t *typedToolReductionMiddleware[M]) WrapEnhancedStreamableToolCall(_ context.Context, endpoint adk.EnhancedStreamableToolCallEndpoint, tCtx *adk.ToolContext) (adk.EnhancedStreamableToolCallEndpoint, error) { + cfg := t.getToolConfig(tCtx.Name, sceneTruncation) + if cfg == nil || cfg.TruncHandler == nil { + return endpoint, nil + } + if _, excluded := t.excludeTruncTools[tCtx.Name]; excluded { + return endpoint, nil + } + + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + output, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + if cfg == t.defaultConfig { + return t.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArgument, output), nil + } + + readers := output.Copy(2) + output = readers[0] + origResp := readers[1] + + detail := &ToolDetail{ + ToolContext: tCtx, + ToolArgument: toolArgument, + StreamToolResult: output, + } + truncResult, err := cfg.TruncHandler(ctx, detail) + if err != nil { + origResp.Close() + return nil, err + } + if !truncResult.NeedTrunc { + return origResp, nil + } + origResp.Close() // close err resp when not using it + + if truncResult.NeedOffload { + if cfg.Backend == nil { + return nil, fmt.Errorf("truncation: no backend for offload") + } + if err = cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: truncResult.OffloadFilePath, + Content: truncResult.OffloadContent, + }); err != nil { + return nil, err + } + } + + return truncResult.StreamToolResult, nil + }, nil +} + +func (t *typedToolReductionMiddleware[M]) wrapDefaultEnhancedStreamableTruncation( + ctx context.Context, + cfg *ToolReductionConfig, + tCtx *adk.ToolContext, + toolArgument *schema.ToolArgument, + output *schema.StreamReader[*schema.ToolResult], +) *schema.StreamReader[*schema.ToolResult] { + var ( + chunks []*schema.ToolResult + replayChunks []*schema.ToolResult + fullLength int + reachedLimit bool + truncTextPrefix *schema.ToolResult + truncTriggeredToolResult *schema.ToolResult + sourceErr error + ) + + // Chunk mapping: + // + // Normal EOF: + // input : C1 -------- C2(crosses limit) -------- C3 -------- EOF + // output: C1 -------- [swallow C2,C3] ---------- prefix(C2)+notice + // + // Source error after truncation: + // input : C1 -------- C2(crosses limit) -------- C3 -------- ERR + // output: C1 -------- [swallow C2,C3] ---------- replay(C2+C3) -- ERR + // + // Replay uses one merged ToolResult because WithOnEOF can inject only one + // value. As in the string stream wrapper, the outer convert layer surfaces + // the original source error on the following Recv. + convertHandler := func(chunk *schema.ToolResult) (*schema.ToolResult, error) { + chunks = append(chunks, chunk) + chunkLength := toolResultTextLength(chunk) + if reachedLimit { + fullLength += chunkLength + replayChunks = append(replayChunks, chunk) + return nil, schema.ErrNoValue + } + if fullLength+chunkLength < t.config.MaxLengthForTrunc { + fullLength += chunkLength + return chunk, nil + } + + reachedLimit = true + replayChunks = append(replayChunks, chunk) + truncTextPrefix = toolResultTextPrefix(chunk, t.config.MaxLengthForTrunc-fullLength) + fullLength += chunkLength + truncTriggeredToolResult = &schema.ToolResult{Parts: flattenToolResultParts(chunks)} + return nil, schema.ErrNoValue + } + + errHandler := func(err error) error { + if reachedLimit { + sourceErr = err + return nil + } + return err + } + + eofHandler := func() (any, error) { + if sourceErr != nil { + replay := &schema.ToolResult{Parts: flattenToolResultParts(replayChunks)} + if len(replay.Parts) > 0 { + return replay, nil + } + return nil, io.EOF + } + if !reachedLimit { + return nil, io.EOF + } + + fullToolResult := &schema.ToolResult{Parts: flattenToolResultParts(chunks)} + detail := &ToolDetail{ + ToolContext: tCtx, + ToolArgument: toolArgument, + ToolResult: truncTriggeredToolResult, + } + + var ( + offloadNotify string + errorNotify string + ) + filePath, err := t.config.GenTruncOffloadFilePath(ctx, detail) + if err != nil { + errorNotify = formatStreamOffloadFailedNotify(err) + } else if cfg.Backend == nil { + errorNotify = formatStreamOffloadFailedNotify(fmt.Errorf("truncation: no backend for offload")) + } else if err = cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: filePath, + Content: stringifyToolOutputParts(fullToolResult.Parts), + }); err != nil { + errorNotify = formatStreamOffloadFailedNotify(err) + } else { + offloadNotify = formatStreamOffloadSavedNotify(filePath, t.config.ReadFileToolName) + } + + notice, err := formatStreamTruncNotice(t.config.MaxLengthForTrunc, offloadNotify, errorNotify) + if err != nil { + return nil, err + } + + parts := make([]schema.ToolOutputPart, 0, 1) + if truncTextPrefix != nil { + parts = append(parts, truncTextPrefix.Parts...) + } + parts = append(parts, schema.ToolOutputPart{Type: schema.ToolPartTypeText, Text: notice}) + return &schema.ToolResult{Parts: parts}, nil + } + + truncatedStream := schema.StreamReaderWithConvert( + output, + convertHandler, + schema.WithErrWrapper(errHandler), + schema.WithOnEOF(eofHandler), + ) + + return schema.StreamReaderWithConvert( + truncatedStream, + func(chunk *schema.ToolResult) (*schema.ToolResult, error) { + return chunk, nil + }, + schema.WithOnEOF(func() (any, error) { + if sourceErr != nil { + return nil, sourceErr + } + return nil, io.EOF + }), + ) +} + +func formatStreamTruncNotice(previewSize int, offloadNotify, errorMsgNotify string) (string, error) { + return pyfmt.Fmt(getStreamTruncFmt(), map[string]any{ + "preview_size": previewSize, + "offload_notify": offloadNotify, + "error_msg_notify": errorMsgNotify, + }) +} + +func toolResultTextPrefix(result *schema.ToolResult, maxLength int) *schema.ToolResult { + if result == nil || maxLength <= 0 { + return nil + } + var ( + length int + parts []schema.ToolOutputPart + ) + for _, part := range result.Parts { + if part.Type != schema.ToolPartTypeText { + parts = append(parts, part) + continue + } + remaining := maxLength - length + if remaining <= 0 { + break + } + if len(part.Text) <= remaining { + parts = append(parts, part) + length += len(part.Text) + continue + } + part.Text = clampPrefixToUTF8Boundary(part.Text, remaining) + if part.Text != "" { + parts = append(parts, part) + } + break + } + if len(parts) == 0 { + return nil + } + return &schema.ToolResult{Parts: parts} +} + +func toolResultTextLength(result *schema.ToolResult) int { + if result == nil { + return 0 + } + var length int + for _, part := range result.Parts { + if part.Type == schema.ToolPartTypeText { + length += len(part.Text) + } + } + return length +} + +func flattenToolResultParts(chunks []*schema.ToolResult) []schema.ToolOutputPart { + var parts []schema.ToolOutputPart + for _, chunk := range chunks { + if chunk == nil || len(chunk.Parts) == 0 { + continue + } + parts = append(parts, chunk.Parts...) + } + return parts +} + +func writeTruncOffload(ctx context.Context, cfg *ToolReductionConfig, truncResult *TruncResult) error { + if !truncResult.NeedOffload { + return nil + } + if cfg.Backend == nil { + return fmt.Errorf("truncation: no backend for offload") + } + return cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: truncResult.OffloadFilePath, + Content: truncResult.OffloadContent, + }) +} + +func (t *typedToolReductionMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *adk.TypedChatModelAgentState[M], mc *adk.TypedModelContext[M]) ( + context.Context, *adk.TypedChatModelAgentState[M], error) { + + return t.beforeModelRewriteStateGeneric(ctx, state, mc) +} + +func (t *typedToolReductionMiddleware[M]) beforeModelRewriteStateGeneric(ctx context.Context, state *adk.TypedChatModelAgentState[M], _ *adk.TypedModelContext[M]) ( + context.Context, *adk.TypedChatModelAgentState[M], error) { + + var ( + err error + estimatedTokens int64 + ) + + // init msg tokens + estimatedTokens, err = t.config.TokenCounter(ctx, state.Messages, state.ToolInfos) + if err != nil { + return ctx, state, err + } + + if estimatedTokens < t.config.MaxTokensForClear { + return ctx, state, nil + } + + // calc range + var ( + start = 0 + end = len(state.Messages) + ) + for ; start < len(state.Messages); start++ { + msg := state.Messages[start] + if isAssistantMsg(msg) && !getMsgClearedFlagGeneric(msg) { + break + } + } + retention := t.config.ClearRetentionSuffixLimit + for ; retention > 0 && end > 0; end-- { + msg := state.Messages[end-1] + if isAssistantMsg(msg) && hasToolCalls(msg) { + retention-- + if retention == 0 { + end-- + break + } + } + } + if start >= end { + return ctx, state, nil + } + var ( + editTarget []M + clearAtLeastTokens = t.config.ClearAtLeastTokens + offloadStash []*offloadStashItem + ) + + editTarget, end, err = t.applyClearRewriteGeneric(ctx, state, start, end, clearAtLeastTokens) + if err != nil { + return ctx, state, err + } + + // recursively handle + toolCallMsgIndex := start + + for toolCallMsgIndex < end { + toolCallMsg := editTarget[toolCallMsgIndex] + toolCalls := getToolCallsGeneric(toolCallMsg) + if isAssistantMsg(toolCallMsg) && len(toolCalls) > 0 { + for _, tc := range toolCalls { + // Find the corresponding tool-result message by callID + resultMsgIndex, found := findToolResultByCallID(editTarget, toolCallMsgIndex+1, toolCallMsgIndex+1+len(toolCalls), tc.CallID) + if !found { + continue // No corresponding tool result found, skip + } + resultMsg := editTarget[resultMsgIndex] + + if _, found := t.excludeClearTools[tc.Name]; found { + continue + } + cfg := t.getToolConfig(tc.Name, sceneClear) + if cfg == nil || cfg.ClearHandler == nil { + continue + } + + toolResult, fromContent, toolResultErr := toolResultFromMsgGeneric(resultMsg) + if toolResultErr != nil { + return ctx, state, toolResultErr + } + + td := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: tc.Name, + CallID: tc.CallID, + }, + ToolArgument: &schema.ToolArgument{ + Text: tc.Arguments, + }, + ToolResult: toolResult, + } + + offloadInfo, offloadErr := cfg.ClearHandler(ctx, td) + if offloadErr != nil { + return ctx, state, offloadErr + } + if !offloadInfo.NeedClear { + continue + } + if offloadInfo.NeedOffload { + if cfg.Backend == nil { + return ctx, state, fmt.Errorf("clear: no backend for offload") + } + if clearAtLeastTokens > 0 { // delay clear offloading + offloadStash = append(offloadStash, &offloadStashItem{ + config: cfg, + offloadInfo: offloadInfo, + }) + } else { // instant clear offloading + writeErr := cfg.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: offloadInfo.OffloadFilePath, + Content: offloadInfo.OffloadContent, + }) + if writeErr != nil { + return ctx, state, writeErr + } + } + } + + setToolCallArguments(toolCallMsg, tc.BlockIndex, offloadInfo.ToolArgument.Text) + setToolResultContent(resultMsg, offloadInfo.ToolResult, fromContent) + } + + // set dedup flag + setMsgClearedFlagGeneric(toolCallMsg) + } + toolCallMsgIndex++ + } + + if clearAtLeastTokens > 0 { + estimatedTokensAfterClear, err := t.config.TokenCounter(ctx, editTarget, state.ToolInfos) + if err != nil { + return ctx, state, err + } + tokensCleared := estimatedTokens - estimatedTokensAfterClear + if tokensCleared < clearAtLeastTokens { + // clear not applied, post process won't apply as well. + return ctx, state, nil + } + for _, item := range offloadStash { + writeErr := item.config.Backend.Write(ctx, &filesystem.WriteRequest{ + FilePath: item.offloadInfo.OffloadFilePath, + Content: item.offloadInfo.OffloadContent, + }) + if writeErr != nil { + return ctx, state, writeErr + } + } + } + + state.Messages = editTarget // replace original state messages + + if t.config.ClearPostProcess != nil { + ctx = t.config.ClearPostProcess(ctx, state) + } + + return ctx, state, nil +} + +func (t *typedToolReductionMiddleware[M]) applyClearRewriteGeneric(ctx context.Context, state *adk.TypedChatModelAgentState[M], start, end int, clearAtLeastTokens int64) ( + []M, int, error) { + var ( + editTarget []M + needProcessPart []M + ) + + editTarget = append(editTarget, state.Messages[:start]...) + + if clearAtLeastTokens > 0 { + needProcessPart = copyMessagesGeneric(state.Messages[start:end]) + } else { + needProcessPart = state.Messages[start:end] + } + + if t.config.ClearMessageRewriter != nil { + var ( + rewritten []M + origLength = len(needProcessPart) + ) + for i := 0; i < len(needProcessPart); { + msg := needProcessPart[i] + if isSystemMsg(msg) || isUserMsg(msg) { + rewritten = append(rewritten, msg) + i++ + } else if isToolResultMsg(msg) { + // tool result message (schema.Tool role or agentic user msg carrying FunctionToolResult) + i++ + } else if isAssistantMsg(msg) { + toolCalls := getToolCallsGeneric(msg) + if len(toolCalls) == 0 { + rewritten = append(rewritten, msg) + i++ + continue + } + var ( + toolResponseMessages []M + trStart = i + 1 + trEnd = i + len(toolCalls) + 1 + ) + if trStart >= trEnd || trStart >= len(needProcessPart) || trEnd > len(needProcessPart) { + toolResponseMessages = nil + } else { + toolResponseMessages = needProcessPart[trStart:trEnd] + } + + rewrittenMessages, rewriteErr := t.config.ClearMessageRewriter(ctx, msg, toolResponseMessages) + if rewriteErr != nil { + return nil, 0, rewriteErr + } + rewritten = append(rewritten, rewrittenMessages...) + i = trEnd + } else { // unexpected + return nil, 0, fmt.Errorf("[applyClearRewrite] unexpected message: %v", any(msg)) + } + } + editTarget = append(editTarget, rewritten...) + editTarget = append(editTarget, state.Messages[end:]...) + end = end - origLength + len(rewritten) + } else { + editTarget = append(editTarget, needProcessPart...) + editTarget = append(editTarget, state.Messages[end:]...) + } + + return editTarget, end, nil +} + +func agenticResultCallID(block *schema.ContentBlock) (string, bool) { + if block == nil { + return "", false + } + if block.Type == schema.ContentBlockTypeFunctionToolResult && block.FunctionToolResult != nil { + return block.FunctionToolResult.CallID, true + } + if block.Type == schema.ContentBlockTypeToolSearchResult && block.ToolSearchFunctionToolResult != nil { + return block.ToolSearchFunctionToolResult.CallID, true + } + return "", false +} + +func findToolResultByCallID[M adk.MessageType](messages []M, startIndex, endIndex int, callID string) (int, bool) { + for i := startIndex; i < endIndex; i++ { + msg := messages[i] + if isToolResultMsg(msg) { + if getToolResultCallID(msg) == callID { + return i, true + } + } else { + return -1, false + } + } + return -1, false +} + +func getToolResultCallID[M adk.MessageType](msg M) string { + switch m := any(msg).(type) { + case *schema.Message: + if m.Role == schema.Tool { + return m.ToolCallID + } + case *schema.AgenticMessage: + if m.Role == schema.AgenticRoleTypeUser { + for _, block := range m.ContentBlocks { + if callID, ok := agenticResultCallID(block); ok { + return callID + } + } + } + } + return "" +} + +type offloadStashItem struct { + config *ToolReductionConfig + offloadInfo *ClearResult +} + +// toolCallInfo represents a tool call extracted from a message for generic processing. +type toolCallInfo struct { + // BlockIndex is the index used to locate the tool call within the message. + // For *schema.Message: index into msg.ToolCalls slice. + // For *schema.AgenticMessage: index into msg.ContentBlocks slice. + BlockIndex int + CallID string + Name string + Arguments string +} + +// isAssistantMsg checks if a message has assistant role. +func isAssistantMsg[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.Assistant + case *schema.AgenticMessage: + return m.Role == schema.AgenticRoleTypeAssistant + } + return false +} + +// isSystemMsg checks if a message has system role. +func isSystemMsg[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.System + case *schema.AgenticMessage: + return m.Role == schema.AgenticRoleTypeSystem + } + return false +} + +// isUserMsg checks if a message has user role (and is not a tool-result message). +func isUserMsg[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.User + case *schema.AgenticMessage: + if m.Role != schema.AgenticRoleTypeUser { + return false + } + // A user-role agentic message that contains any FunctionToolResult block + // is a tool result message, not a normal user message — even if it also + // carries UserInput blocks. This ensures the clear flow's tool-call grouping + // remains correctly aligned. + for _, block := range m.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolResult { + return false + } + } + return len(m.ContentBlocks) > 0 + } + return false +} + +// hasToolCalls checks if an assistant message contains tool calls. +func hasToolCalls[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return len(m.ToolCalls) > 0 + case *schema.AgenticMessage: + for _, block := range m.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolCall { + return true + } + } + } + return false +} + +// isToolResultMsg checks if a message is a tool result message. +// For *schema.Message: role == Tool. +// For *schema.AgenticMessage: user-role message with at least one FunctionToolResult block. +func isToolResultMsg[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.Tool + case *schema.AgenticMessage: + if m.Role != schema.AgenticRoleTypeUser { + return false + } + for _, block := range m.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolResult { + return true + } + } + } + return false +} + +// isToolResultOnlyMsg checks if a message is exclusively a tool result message +// (no other content besides tool results). +// For *schema.Message: role == Tool. +// For *schema.AgenticMessage: user-role message where ALL content blocks are FunctionToolResult. +func isToolResultOnlyMsg[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.Tool + case *schema.AgenticMessage: + if m.Role != schema.AgenticRoleTypeUser || len(m.ContentBlocks) == 0 { + return false + } + for _, block := range m.ContentBlocks { + if block == nil || block.Type != schema.ContentBlockTypeFunctionToolResult { + return false + } + } + return true + } + return false +} + +// getMsgClearedFlagGeneric checks if a message has the cleared flag set. +func getMsgClearedFlagGeneric[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return getMsgClearedFlag(m) + case *schema.AgenticMessage: + if m.Extra == nil { + return false + } + v, ok := m.Extra[msgClearedFlag].(bool) + return ok && v + } + return false +} + +// setMsgClearedFlagGeneric sets the cleared flag on a message. +func setMsgClearedFlagGeneric[M adk.MessageType](msg M) { + switch m := any(msg).(type) { + case *schema.Message: + setMsgClearedFlag(m) + case *schema.AgenticMessage: + if m.Extra == nil { + m.Extra = make(map[string]any) + } + m.Extra[msgClearedFlag] = true + } +} + +// getToolCallsGeneric extracts tool call info from an assistant message. +func getToolCallsGeneric[M adk.MessageType](msg M) []toolCallInfo { + switch m := any(msg).(type) { + case *schema.Message: + if len(m.ToolCalls) == 0 { + return nil + } + result := make([]toolCallInfo, 0, len(m.ToolCalls)) + for i, tc := range m.ToolCalls { + result = append(result, toolCallInfo{ + BlockIndex: i, + CallID: tc.ID, + Name: tc.Function.Name, + Arguments: tc.Function.Arguments, + }) + } + return result + case *schema.AgenticMessage: + var result []toolCallInfo + for i, block := range m.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolCall && block.FunctionToolCall != nil { + result = append(result, toolCallInfo{ + BlockIndex: i, + CallID: block.FunctionToolCall.CallID, + Name: block.FunctionToolCall.Name, + Arguments: block.FunctionToolCall.Arguments, + }) + } + } + return result + } + return nil +} + +// setToolCallArguments updates the arguments for a tool call at the given block index. +func setToolCallArguments[M adk.MessageType](msg M, blockIndex int, args string) { + switch m := any(msg).(type) { + case *schema.Message: + m.ToolCalls[blockIndex].Function.Arguments = args + case *schema.AgenticMessage: + if m.ContentBlocks[blockIndex].FunctionToolCall != nil { + m.ContentBlocks[blockIndex].FunctionToolCall.Arguments = args + } + } +} + +// toolResultFromMsgGeneric extracts tool result from a message as a *schema.ToolResult. +// For *schema.Message: delegates to existing toolResultFromMessage. +// For *schema.AgenticMessage: iterates FunctionToolResult blocks. +// The fromContent flag indicates whether the result came from simple content (true) +// or multi-part content (false), which affects how setToolResultContent writes it back. +func toolResultFromMsgGeneric[M adk.MessageType](msg M) (result *schema.ToolResult, fromContent bool, err error) { + switch m := any(msg).(type) { + case *schema.Message: + return toolResultFromMessage(m) + case *schema.AgenticMessage: + var found *schema.FunctionToolResult + for _, block := range m.ContentBlocks { + if block == nil || block.Type != schema.ContentBlockTypeFunctionToolResult || block.FunctionToolResult == nil { + continue + } + if found != nil { + return nil, false, fmt.Errorf("reduction: AgenticMessage contains multiple FunctionToolResult blocks; expected exactly one per message") + } + found = block.FunctionToolResult + } + if found == nil { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: ""}}}, true, nil + } + parts := toolResultToOutputParts(found) + if len(parts) == 0 { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: ""}}}, true, nil + } + isSimple := len(parts) == 1 && parts[0].Type == schema.ToolPartTypeText + return &schema.ToolResult{Parts: parts}, isSimple, nil + } + return nil, false, fmt.Errorf("unsupported message type") +} + +// setToolResultContent updates the tool result content in a message. +// For *schema.Message: sets msg.Content or msg.UserInputMultiContent. +// For *schema.AgenticMessage: reconstructs FunctionToolResult.Content. +func setToolResultContent[M adk.MessageType](msg M, toolResult *schema.ToolResult, fromContent bool) { + switch m := any(msg).(type) { + case *schema.Message: + if fromContent { + if len(toolResult.Parts) > 0 { + m.Content = toolResult.Parts[0].Text + } + } else { + convResult, convErr := toolResult.ToMessageInputParts() + if convErr == nil { + m.UserInputMultiContent = convResult + } + } + case *schema.AgenticMessage: + for _, block := range m.ContentBlocks { + if block == nil || block.Type != schema.ContentBlockTypeFunctionToolResult || block.FunctionToolResult == nil { + continue + } + setToolResultFromOutputParts(block.FunctionToolResult, toolResult.Parts) + return + } + } +} + +// copyMessagesGeneric deep-copies a slice of messages. +func copyMessagesGeneric[M adk.MessageType](msgs []M) []M { + var zero M + switch any(zero).(type) { + case *schema.Message: + origMsgs := any(msgs).([]*schema.Message) + copied := copyMessages(origMsgs) + return any(copied).([]M) + case *schema.AgenticMessage: + origMsgs := any(msgs).([]*schema.AgenticMessage) + copied := copyAgenticMessages(origMsgs) + return any(copied).([]M) + } + panic("unreachable") +} + +func copyAgenticMessages(msgs []*schema.AgenticMessage) []*schema.AgenticMessage { + resp := make([]*schema.AgenticMessage, len(msgs)) + for i, msg := range msgs { + if msg == nil { + continue + } + copied := &schema.AgenticMessage{ + Role: msg.Role, + ResponseMeta: msg.ResponseMeta, + } + if msg.ContentBlocks != nil { + copied.ContentBlocks = make([]*schema.ContentBlock, len(msg.ContentBlocks)) + for j, block := range msg.ContentBlocks { + if block == nil { + continue + } + cb := *block + // Deep copy mutable sub-fields + if block.FunctionToolCall != nil { + ftc := *block.FunctionToolCall + cb.FunctionToolCall = &ftc + } + if block.FunctionToolResult != nil { + ftr := *block.FunctionToolResult + if block.FunctionToolResult.Content != nil { + ftr.Content = make([]*schema.FunctionToolResultContentBlock, len(block.FunctionToolResult.Content)) + for k, rb := range block.FunctionToolResult.Content { + if rb != nil { + rbCopy := *rb // shallow copy: Image/Audio/Video/File sub-fields are not deep-copied. + // This is safe because the clear logic replaces entire blocks rather than + // mutating media fields in-place. Custom ClearHandlers should follow the same pattern. + if rb.Text != nil { + t := *rb.Text + rbCopy.Text = &t + } + ftr.Content[k] = &rbCopy + } + } + } + cb.FunctionToolResult = &ftr + } + if block.Extra != nil { + cb.Extra = make(map[string]any, len(block.Extra)) + for k, v := range block.Extra { + cb.Extra[k] = v + } + } + copied.ContentBlocks[j] = &cb + } + } + if msg.Extra != nil { + copied.Extra = make(map[string]any, len(msg.Extra)) + for k, v := range msg.Extra { + copied.Extra[k] = v + } + } + resp[i] = copied + } + return resp +} + +func copyMessages(msgs []*schema.Message) []*schema.Message { + resp := make([]*schema.Message, len(msgs)) + for i, msg := range msgs { + if msg == nil { + continue + } + copied := &schema.Message{ + Role: msg.Role, + Content: msg.Content, + MultiContent: msg.MultiContent, + UserInputMultiContent: msg.UserInputMultiContent, + AssistantGenMultiContent: msg.AssistantGenMultiContent, + Name: msg.Name, + ToolCalls: nil, + ToolCallID: msg.ToolCallID, + ToolName: msg.ToolName, + ResponseMeta: msg.ResponseMeta, + ReasoningContent: msg.ReasoningContent, + Extra: nil, + } + if msg.ToolCalls != nil { + copied.ToolCalls = append(make([]schema.ToolCall, 0, len(msg.ToolCalls)), msg.ToolCalls...) + } + if msg.Extra != nil { + copied.Extra = make(map[string]any, len(msg.Extra)) + for k, v := range msg.Extra { + copied.Extra[k] = v + } + } + resp[i] = copied + } + return resp +} + +// defaultTokenCounter estimates tokens, which treats one token as ~4 characters of text for common English text. +// github.com/tiktoken-go/tokenizer is highly recommended to replace it. +func defaultTokenCounter(_ context.Context, msgs []*schema.Message, tools []*schema.ToolInfo) (int64, error) { + var tokens int64 + for _, msg := range msgs { + if msg == nil { + continue + } + + var sb strings.Builder + sb.WriteString(string(msg.Role)) + sb.WriteString("\n") + sb.WriteString(msg.ReasoningContent) + sb.WriteString("\n") + sb.WriteString(msg.Content) + sb.WriteString("\n") + if msg.Role == schema.Assistant && len(msg.ToolCalls) > 0 { + for _, tc := range msg.ToolCalls { + sb.WriteString(tc.Function.Name) + sb.WriteString("\n") + sb.WriteString(tc.Function.Arguments) + } + } + + for _, mc := range msg.UserInputMultiContent { + switch mc.Type { + case schema.ChatMessagePartTypeText: + sb.WriteString(mc.Text) + sb.WriteString("\n") + default: + // do nothing for multi-modal content + } + } + + for _, mc := range msg.AssistantGenMultiContent { + switch mc.Type { + case schema.ChatMessagePartTypeText: + sb.WriteString(mc.Text) + sb.WriteString("\n") + default: + // do nothing for multi-modal content + } + } + + n := int64(len(sb.String()) / 4) + tokens += n + } + + for _, tl := range tools { + tl_ := *tl + tl_.Extra = nil + text, err := sonic.MarshalString(tl_) + if err != nil { + return 0, fmt.Errorf("failed to marshal tool info: %w", err) + } + + tokens += int64(len(text) / 4) + } + + return tokens, nil +} + +// defaultTruncHandler applies the shared buffered truncation strategy. The +// built-in default streaming wrappers bypass it so they can truncate +// incrementally with a streaming-specific notice. +// +// Processing steps: +// 1. Read and join tool output into a complete result: +// - Non-streaming: use ToolResult directly. +// - Streaming: consume the whole StreamToolResult, then concat all chunks. +// 2. If output is empty or total text length does not exceed truncMaxLength, +// return NeedTrunc=false. +// 3. If exceeded, replace oversized text parts with truncation notices and +// offload the full original content. +// +// Streaming-specific behavior: +// - Truncation is not incremental. The handler waits until the entire stream is read +// before deciding and producing output. +// - If stream Recv() returns a non-EOF error, getJointToolResult treats it as +// "skip processing" (needProcess=false, err=nil), so this handler returns +// NeedTrunc=false and does not propagate that recv error. +// - When truncation is applied to a streaming tool result, output is re-emitted as a +// buffered single-result stream (not original chunk-by-chunk streaming semantics). +// +// Tool-specific configurations that install this handler for streaming outputs +// keep the buffered behavior below. +func defaultTruncHandler( + genOffloadFilePathFn func(ctx context.Context, toolDetail *ToolDetail) (filePath string, err error), + truncMaxLength int, + readFileToolName string, +) func(ctx context.Context, detail *ToolDetail) (truncResult *TruncResult, err error) { + + return func(ctx context.Context, detail *ToolDetail) (offloadInfo *TruncResult, err error) { + isStreamResult := detail.StreamToolResult != nil + resultParts, needProcess, err := getJointToolResult(detail) + if err != nil { + return nil, err + } + if !needProcess { + return &TruncResult{NeedTrunc: false}, nil + } + + fullLength, textPartsCnt := 0, 0 + for _, part := range resultParts { + if part.Type == schema.ToolPartTypeText { + fullLength += len(part.Text) + textPartsCnt++ + } + } + if textPartsCnt == 0 || fullLength <= truncMaxLength { + return &TruncResult{NeedTrunc: false}, nil + } + + var ( + offloadContent = stringifyToolOutputParts(resultParts) + truncPartLength = truncMaxLength / textPartsCnt + previewSize = truncPartLength / 2 + ) + + filePath, err := genOffloadFilePathFn(ctx, detail) + if err != nil { + return nil, err + } + + for i, part := range resultParts { + text := part.Text + if part.Type != schema.ToolPartTypeText || + len(text) < truncPartLength { + continue + } + truncNotify, fmtErr := pyfmt.Fmt(getTruncFmt(), map[string]any{ + "original_size": len(part.Text), + "file_path": filePath, + "preview_size": previewSize, + "preview_first": clampPrefixToUTF8Boundary(text, previewSize), + "preview_last": clampSuffixToUTF8Boundary(text, previewSize), + "read_tool_name": readFileToolName, + }) + if fmtErr != nil { + return nil, fmtErr + } + resultParts[i].Text = truncNotify + } + + tr := &TruncResult{ + NeedTrunc: true, + NeedOffload: true, + OffloadFilePath: filePath, + OffloadContent: offloadContent, + } + if !isStreamResult { + tr.ToolResult = &schema.ToolResult{Parts: resultParts} + } else { + sr, sw := schema.Pipe[*schema.ToolResult](1) + sw.Send(&schema.ToolResult{Parts: resultParts}, nil) + sw.Close() + tr.StreamToolResult = sr + } + return tr, nil + } +} + +func clampPrefixToUTF8Boundary(s string, n int) string { + if n <= 0 { + return "" + } + if n >= len(s) { + return s + } + for n > 0 && !utf8.RuneStart(s[n]) { + n-- + } + return s[:n] +} + +func clampSuffixToUTF8Boundary(s string, n int) string { + if n <= 0 { + return "" + } + l := len(s) + if n >= l { + return s + } + start := l - n + for start < l && !utf8.RuneStart(s[start]) { + start++ + } + return s[start:] +} + +func defaultClearHandler( + genOffloadFilePathFn func(ctx context.Context, toolDetail *ToolDetail) (filePath string, err error), + needOffload bool, + readFileToolName string, +) func(ctx context.Context, detail *ToolDetail) (*ClearResult, error) { + + return func(ctx context.Context, detail *ToolDetail) (clearResult *ClearResult, err error) { + resultParts, needProcess, err := getJointToolResult(detail) + if err != nil { + return nil, err + } + if !needProcess { + return &ClearResult{NeedClear: false}, nil + } + + if needOffload { + filePath, err := genOffloadFilePathFn(ctx, detail) + if err != nil { + return nil, err + } + textPlaceHolder, fmtErr := pyfmt.Fmt(getClearWithOffloadingFmt(), map[string]any{ + "file_path": filePath, + "read_tool_name": readFileToolName, + }) + if fmtErr != nil { + return nil, fmtErr + } + + offloadContent := stringifyToolOutputParts(resultParts) + for i, part := range resultParts { + if part.Type != schema.ToolPartTypeText { + continue + } + resultParts[i].Text = textPlaceHolder + } + clearResult = &ClearResult{ + NeedClear: true, + ToolArgument: detail.ToolArgument, + ToolResult: &schema.ToolResult{Parts: resultParts}, + NeedOffload: true, + OffloadFilePath: filePath, + OffloadContent: offloadContent, + } + } else { + textPlaceHolder := getClearWithoutOffloadingFmt() + for i, part := range resultParts { + if part.Type != schema.ToolPartTypeText { + continue + } + resultParts[i].Text = textPlaceHolder + } + clearResult = &ClearResult{ + NeedClear: true, + ToolArgument: detail.ToolArgument, + ToolResult: &schema.ToolResult{Parts: resultParts}, + NeedOffload: false, + } + } + + return clearResult, nil + } +} + +func getJointToolResult(toolDetail *ToolDetail) (toolOutputParts []schema.ToolOutputPart, needProcess bool, err error) { + if toolDetail.ToolResult == nil && toolDetail.StreamToolResult == nil { + return nil, false, fmt.Errorf("ToolResult and StreamToolResult are both nil") + } + + if toolDetail.ToolResult != nil { + toolOutputParts = toolDetail.ToolResult.Parts + } else { + var toolResultChunks []*schema.ToolResult + for { + toolResultChunk, recvErr := toolDetail.StreamToolResult.Recv() + if recvErr != nil { + if recvErr == io.EOF { + break + } + // return original stream reader, not sending recvErr + return nil, false, nil + } + toolResultChunks = append(toolResultChunks, toolResultChunk) + } + toolResult, concatErr := schema.ConcatToolResults(toolResultChunks) + if concatErr != nil { + return nil, false, concatErr + } + toolOutputParts = toolResult.Parts + } + + if len(toolOutputParts) == 0 { + return nil, false, nil + } + + return toolOutputParts, true, nil +} + +func stringifyToolOutputParts(toolOutputParts []schema.ToolOutputPart) string { + if len(toolOutputParts) == 0 { + return "" + } else if len(toolOutputParts) == 1 && toolOutputParts[0].Type == schema.ToolPartTypeText { + return toolOutputParts[0].Text + } else { + b, _ := json.MarshalIndent(toolOutputParts, "", "\t") + return string(b) + } +} + +func getMsgClearedFlag(msg *schema.Message) (offloaded bool) { + if msg.Extra == nil { + return false + } + v, ok := msg.Extra[msgClearedFlag].(bool) + if !ok { + return false + } + return v +} + +func setMsgClearedFlag(msg *schema.Message) { + if msg.Extra == nil { + msg.Extra = make(map[string]any) + } + msg.Extra[msgClearedFlag] = true +} + +func toolResultFromMessage(msg *schema.Message) (result *schema.ToolResult, fromContent bool, err error) { + if msg.Role != schema.Tool { + return nil, false, fmt.Errorf("message role %s is not a tool", msg.Role) + } + if len(msg.UserInputMultiContent) > 0 { + result = &schema.ToolResult{Parts: make([]schema.ToolOutputPart, 0, len(msg.UserInputMultiContent))} + for _, part := range msg.UserInputMultiContent { + top, convErr := convMessageInputPartToToolOutputPart(part) + if convErr != nil { + return nil, false, convErr + } + result.Parts = append(result.Parts, top) + } + return result, false, nil + } + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: msg.Content}}}, true, nil +} + +func convMessageInputPartToToolOutputPart(msgPart schema.MessageInputPart) (schema.ToolOutputPart, error) { + switch msgPart.Type { + case schema.ChatMessagePartTypeText: + return schema.ToolOutputPart{ + Type: schema.ToolPartTypeText, + Text: msgPart.Text, + }, nil + case schema.ChatMessagePartTypeImageURL: + return schema.ToolOutputPart{ + Type: schema.ToolPartTypeImage, + Image: &schema.ToolOutputImage{ + MessagePartCommon: msgPart.Image.MessagePartCommon, + }, + }, nil + case schema.ChatMessagePartTypeAudioURL: + return schema.ToolOutputPart{ + Type: schema.ToolPartTypeAudio, + Audio: &schema.ToolOutputAudio{ + MessagePartCommon: msgPart.Audio.MessagePartCommon, + }, + }, nil + case schema.ChatMessagePartTypeVideoURL: + return schema.ToolOutputPart{ + Type: schema.ToolPartTypeVideo, + Video: &schema.ToolOutputVideo{ + MessagePartCommon: msgPart.Video.MessagePartCommon, + }, + }, nil + case schema.ChatMessagePartTypeFileURL: + return schema.ToolOutputPart{ + Type: schema.ToolPartTypeFile, + File: &schema.ToolOutputFile{ + MessagePartCommon: msgPart.File.MessagePartCommon, + }, + }, nil + default: + return schema.ToolOutputPart{}, fmt.Errorf("unknown msg part type: %v", msgPart.Type) + } +} + +// toolResultToOutputParts converts a FunctionToolResult's Content blocks to ToolOutputPart slice. +func toolResultToOutputParts(f *schema.FunctionToolResult) []schema.ToolOutputPart { + var parts []schema.ToolOutputPart + for _, block := range f.Content { + if block == nil { + continue + } + if block.Text != nil { + parts = append(parts, schema.ToolOutputPart{Type: schema.ToolPartTypeText, Text: block.Text.Text}) + } else if block.Image != nil { + parts = append(parts, schema.ToolOutputPart{ + Type: schema.ToolPartTypeImage, + Image: &schema.ToolOutputImage{MessagePartCommon: schema.MessagePartCommon{URL: strPtr(block.Image.URL), MIMEType: block.Image.MIMEType}}, + }) + } else if block.Audio != nil { + parts = append(parts, schema.ToolOutputPart{ + Type: schema.ToolPartTypeAudio, + Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{URL: strPtr(block.Audio.URL), MIMEType: block.Audio.MIMEType}}, + }) + } else if block.Video != nil { + parts = append(parts, schema.ToolOutputPart{ + Type: schema.ToolPartTypeVideo, + Video: &schema.ToolOutputVideo{MessagePartCommon: schema.MessagePartCommon{URL: strPtr(block.Video.URL), MIMEType: block.Video.MIMEType}}, + }) + } else if block.File != nil { + parts = append(parts, schema.ToolOutputPart{ + Type: schema.ToolPartTypeFile, + File: &schema.ToolOutputFile{MessagePartCommon: schema.MessagePartCommon{URL: strPtr(block.File.URL), MIMEType: block.File.MIMEType}}, + }) + } + } + return parts +} + +// setToolResultFromOutputParts converts ToolOutputPart slice back to FunctionToolResultContentBlock +// slice and sets f.Content. +func setToolResultFromOutputParts(f *schema.FunctionToolResult, parts []schema.ToolOutputPart) { + var newBlocks []*schema.FunctionToolResultContentBlock + for _, part := range parts { + switch part.Type { + case schema.ToolPartTypeText: + newBlocks = append(newBlocks, &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeText, + Text: &schema.UserInputText{Text: part.Text}, + }) + case schema.ToolPartTypeImage: + if part.Image != nil { + newBlocks = append(newBlocks, &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeImage, + Image: &schema.UserInputImage{URL: ptrStr(part.Image.URL), MIMEType: part.Image.MIMEType}, + }) + } + case schema.ToolPartTypeAudio: + if part.Audio != nil { + newBlocks = append(newBlocks, &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeAudio, + Audio: &schema.UserInputAudio{URL: ptrStr(part.Audio.URL), MIMEType: part.Audio.MIMEType}, + }) + } + case schema.ToolPartTypeVideo: + if part.Video != nil { + newBlocks = append(newBlocks, &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeVideo, + Video: &schema.UserInputVideo{URL: ptrStr(part.Video.URL), MIMEType: part.Video.MIMEType}, + }) + } + case schema.ToolPartTypeFile: + if part.File != nil { + newBlocks = append(newBlocks, &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeFile, + File: &schema.UserInputFile{URL: ptrStr(part.File.URL), MIMEType: part.File.MIMEType}, + }) + } + } + } + f.Content = newBlocks +} + +// strPtr returns a pointer to s, or nil if s is empty. +func strPtr(s string) *string { + if s == "" { + return nil + } + return &s +} + +// ptrStr safely dereferences a *string, returning "" if nil. +func ptrStr(p *string) string { + if p == nil { + return "" + } + return *p +} diff --git a/adk/middlewares/reduction/reduction_generic_test.go b/adk/middlewares/reduction/reduction_generic_test.go new file mode 100644 index 0000000..9953f47 --- /dev/null +++ b/adk/middlewares/reduction/reduction_generic_test.go @@ -0,0 +1,955 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reduction + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// --------------------------------------------------------------------------- +// Generic message construction helpers +// --------------------------------------------------------------------------- + +type testToolCall struct { + ID string + Name string + Arguments string +} + +func makeUserMsgG[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.UserMessage(content)).(M) + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(content)).(M) + } + panic("unreachable") +} + +func makeSystemMsgG[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{Role: schema.System, Content: content}).(M) + case *schema.AgenticMessage: + return any(schema.SystemAgenticMessage(content)).(M) + } + panic("unreachable") +} + +func makeAssistantMsgG[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(&schema.Message{Role: schema.Assistant, Content: content}).(M) + case *schema.AgenticMessage: + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.AssistantGenText{Text: content})}, + }).(M) + } + panic("unreachable") +} + +func makeAssistantMsgWithToolCallsG[M adk.MessageType](toolCalls []testToolCall) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + tcs := make([]schema.ToolCall, len(toolCalls)) + for i, tc := range toolCalls { + tcs[i] = schema.ToolCall{ + ID: tc.ID, + Type: "function", + Function: schema.FunctionCall{Name: tc.Name, Arguments: tc.Arguments}, + } + } + return any(schema.AssistantMessage("", tcs)).(M) + case *schema.AgenticMessage: + blocks := make([]*schema.ContentBlock, 0, len(toolCalls)) + for _, tc := range toolCalls { + blocks = append(blocks, schema.NewContentBlock(&schema.FunctionToolCall{ + CallID: tc.ID, + Name: tc.Name, + Arguments: tc.Arguments, + })) + } + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: blocks, + }).(M) + } + panic("unreachable") +} + +func makeToolResultMsgG[M adk.MessageType](content string, callID string, toolName string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.ToolMessage(content, callID) + msg.ToolName = toolName + return any(msg).(M) + case *schema.AgenticMessage: + return any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: callID, + Name: toolName, + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: content}}, + }, + }), + }, + }).(M) + } + panic("unreachable") +} + +func getMsgContentG[M adk.MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return v.Content + case *schema.AgenticMessage: + for _, block := range v.ContentBlocks { + if block == nil { + continue + } + if block.UserInputText != nil { + return block.UserInputText.Text + } + if block.AssistantGenText != nil { + return block.AssistantGenText.Text + } + if block.FunctionToolResult != nil { + for _, b := range block.FunctionToolResult.Content { + if b != nil && b.Text != nil { + return b.Text.Text + } + } + } + } + return "" + } + panic("unreachable") +} + +// --------------------------------------------------------------------------- +// Part 1: Helper function tests +// --------------------------------------------------------------------------- + +func testHelperFunctions[M adk.MessageType](t *testing.T) { + t.Run("isAssistantMsg", func(t *testing.T) { + assistant := makeAssistantMsgG[M]("hello") + user := makeUserMsgG[M]("hello") + assert.True(t, isAssistantMsg(assistant)) + assert.False(t, isAssistantMsg(user)) + }) + + t.Run("isSystemMsg", func(t *testing.T) { + sys := makeSystemMsgG[M]("system prompt") + user := makeUserMsgG[M]("hello") + assert.True(t, isSystemMsg(sys)) + assert.False(t, isSystemMsg(user)) + }) + + t.Run("isUserMsg", func(t *testing.T) { + user := makeUserMsgG[M]("hello") + assert.True(t, isUserMsg(user)) + + // A user message that only has tool results should return false. + toolResultOnly := makeToolResultMsgG[M]("result", "call_1", "my_tool") + assert.False(t, isUserMsg(toolResultOnly)) + }) + + t.Run("hasToolCalls", func(t *testing.T) { + withTC := makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "c1", Name: "tool1", Arguments: `{"a":1}`}, + }) + assert.True(t, hasToolCalls(withTC)) + + noTC := makeAssistantMsgG[M]("plain response") + assert.False(t, hasToolCalls(noTC)) + }) + + t.Run("isToolResultMsg", func(t *testing.T) { + tr := makeToolResultMsgG[M]("result content", "call_1", "my_tool") + assert.True(t, isToolResultMsg(tr)) + + user := makeUserMsgG[M]("not a tool result") + assert.False(t, isToolResultMsg(user)) + }) + + t.Run("isToolResultOnlyMsg", func(t *testing.T) { + trOnly := makeToolResultMsgG[M]("result content", "call_1", "my_tool") + assert.True(t, isToolResultOnlyMsg(trOnly)) + + // A normal user message is not a tool-result-only message. + user := makeUserMsgG[M]("hello") + assert.False(t, isToolResultOnlyMsg(user)) + + // For AgenticMessage, a mixed message (user text + tool result) should return false. + var zero M + if _, ok := any(zero).(*schema.AgenticMessage); ok { + mixed := any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.UserInputText{Text: "hello"}), + schema.NewContentBlock(&schema.FunctionToolResult{CallID: "c1", Name: "tool1", Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "result"}}, + }}), + }, + }).(M) + assert.False(t, isToolResultOnlyMsg(mixed)) + } + }) + + t.Run("getMsgClearedFlagGeneric_setMsgClearedFlagGeneric", func(t *testing.T) { + msg := makeAssistantMsgG[M]("test") + assert.False(t, getMsgClearedFlagGeneric(msg)) + + setMsgClearedFlagGeneric(msg) + assert.True(t, getMsgClearedFlagGeneric(msg)) + }) + + t.Run("getToolCallsGeneric", func(t *testing.T) { + tcs := []testToolCall{ + {ID: "call_a", Name: "tool_alpha", Arguments: `{"x":1}`}, + {ID: "call_b", Name: "tool_beta", Arguments: `{"y":2}`}, + } + msg := makeAssistantMsgWithToolCallsG[M](tcs) + got := getToolCallsGeneric(msg) + require.Len(t, got, 2) + + assert.Equal(t, "call_a", got[0].CallID) + assert.Equal(t, "tool_alpha", got[0].Name) + assert.Equal(t, `{"x":1}`, got[0].Arguments) + assert.Equal(t, 0, got[0].BlockIndex) + + assert.Equal(t, "call_b", got[1].CallID) + assert.Equal(t, "tool_beta", got[1].Name) + assert.Equal(t, `{"y":2}`, got[1].Arguments) + assert.Equal(t, 1, got[1].BlockIndex) + + // Empty assistant message returns nil. + noTC := makeAssistantMsgG[M]("plain") + assert.Nil(t, getToolCallsGeneric(noTC)) + }) + + t.Run("setToolCallArguments", func(t *testing.T) { + tcs := []testToolCall{ + {ID: "call_a", Name: "tool_alpha", Arguments: `{"old":"args"}`}, + } + msg := makeAssistantMsgWithToolCallsG[M](tcs) + setToolCallArguments(msg, 0, `{"new":"args"}`) + + got := getToolCallsGeneric(msg) + require.Len(t, got, 1) + assert.Equal(t, `{"new":"args"}`, got[0].Arguments) + + // Verify AgenticMessage path writes to the ContentBlock directly. + if am, ok := any(msg).(*schema.AgenticMessage); ok { + require.NotNil(t, am.ContentBlocks[0].FunctionToolCall) + assert.Equal(t, `{"new":"args"}`, am.ContentBlocks[0].FunctionToolCall.Arguments) + } + }) + + t.Run("copyMessagesGeneric", func(t *testing.T) { + original := []M{ + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "c1", Name: "t1", Arguments: `{"k":"v"}`}, + }), + makeUserMsgG[M]("user text"), + } + copied := copyMessagesGeneric(original) + require.Len(t, copied, 2) + + // Modify the copy's tool call arguments. + setToolCallArguments(copied[0], 0, `{"modified":"true"}`) + + // Original must be unchanged. + origTCs := getToolCallsGeneric(original[0]) + require.Len(t, origTCs, 1) + assert.Equal(t, `{"k":"v"}`, origTCs[0].Arguments, "original must not be affected by copy mutation") + + copiedTCs := getToolCallsGeneric(copied[0]) + assert.Equal(t, `{"modified":"true"}`, copiedTCs[0].Arguments) + }) + + t.Run("getToolResultCallID", func(t *testing.T) { + // Tool result message with matching callID + tr := makeToolResultMsgG[M]("result content", "call_123", "my_tool") + assert.Equal(t, "call_123", getToolResultCallID(tr)) + + // Non-tool-result message should return empty string + user := makeUserMsgG[M]("hello") + assert.Equal(t, "", getToolResultCallID(user)) + }) + + t.Run("findToolResultByCallID", func(t *testing.T) { + messages := []M{ + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "call_1", Name: "tool1", Arguments: `{}`}, + }), + makeToolResultMsgG[M]("result 1", "call_1", "tool1"), + makeToolResultMsgG[M]("result 2", "call_2", "tool2"), + } + + // Find existing tool result + idx, found := findToolResultByCallID(messages, 1, 3, "call_2") + assert.True(t, found) + assert.Equal(t, 2, idx) + + // Find non-existent tool result + idx, found = findToolResultByCallID(messages, 1, 3, "call_999") + assert.False(t, found) + assert.Equal(t, -1, idx) + + // Stop searching when encountering a non-tool-result message + messages2 := []M{ + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "call_3", Name: "tool3", Arguments: `{}`}, + }), + makeToolResultMsgG[M]("result 3", "call_3", "tool3"), + makeUserMsgG[M]("not a tool result"), + makeToolResultMsgG[M]("result 4", "call_4", "tool4"), + } + idx, found = findToolResultByCallID(messages2, 1, 4, "call_4") + assert.False(t, found) + assert.Equal(t, -1, idx) + }) +} + +// --------------------------------------------------------------------------- +// Part 2: Clear rewrite flow +// --------------------------------------------------------------------------- + +func testClearFlowGeneric[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + // Token counter that always returns a high count to trigger clearing. + highTokenCounter := func(_ context.Context, _ []M, _ []*schema.ToolInfo) (int64, error) { + return 999999, nil + } + + // ClearRetentionSuffixLimit defaults to 1 in copyAndFillDefaults when set to 0, + // so we explicitly set it to 1. This means the last tool-call group (call_new) + // is retained and only the older group (call_old) is cleared. + config := &TypedConfig[M]{ + SkipTruncation: true, + TokenCounter: highTokenCounter, + MaxTokensForClear: 100, + ClearRetentionSuffixLimit: 1, + } + + mw, err := NewTyped(ctx, config) + require.NoError(t, err) + + // Messages: system, user, assistant+toolcalls(old), tool_result(old), user, assistant+toolcalls(new) + msgs := []M{ + makeSystemMsgG[M]("you are helpful"), + makeUserMsgG[M]("what's the weather?"), + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "call_old", Name: "get_weather", Arguments: `{"location":"London"}`}, + }), + makeToolResultMsgG[M]("Sunny and warm", "call_old", "get_weather"), + makeUserMsgG[M]("set thermostat"), + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "call_new", Name: "set_thermostat", Arguments: `{"temp":20}`}, + }), + } + + state := &adk.TypedChatModelAgentState[M]{Messages: msgs} + _, resultState, err := mw.BeforeModelRewriteState(ctx, state, &adk.TypedModelContext[M]{}) + require.NoError(t, err) + require.Equal(t, 6, len(resultState.Messages)) + + // The default ClearHandler preserves tool call arguments (sets them to the original). + // Verify they are unchanged. + oldTCs := getToolCallsGeneric(resultState.Messages[2]) + require.Len(t, oldTCs, 1) + assert.Equal(t, `{"location":"London"}`, oldTCs[0].Arguments, "default handler preserves tool call arguments") + + // The old tool result (index 3) should have its content replaced with a placeholder. + // The placeholder text is locale-dependent, so just verify it changed from the original. + oldResultContent := getMsgContentG(resultState.Messages[3]) + assert.NotEqual(t, "Sunny and warm", oldResultContent, "old tool result content should be replaced with placeholder") + + // The cleared flag should be set on the old assistant message. + assert.True(t, getMsgClearedFlagGeneric(resultState.Messages[2]), "cleared flag should be set on old assistant msg") + + // System message (index 0) should be untouched. + assert.Equal(t, "you are helpful", getMsgContentG(resultState.Messages[0])) + + // Recent messages (index 4, 5) should not be affected: the new tool-call group + // is in the retention window. + newTCs := getToolCallsGeneric(resultState.Messages[5]) + require.Len(t, newTCs, 1) + assert.Equal(t, `{"temp":20}`, newTCs[0].Arguments, "recent tool calls must not be cleared") +} + +// --------------------------------------------------------------------------- +// Part 3: Truncation flow +// --------------------------------------------------------------------------- + +func testTruncationGeneric[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + callCount := 0 + // Token counter returns decreasing counts as messages shrink. + tokenCounter := func(_ context.Context, msgs []M, _ []*schema.ToolInfo) (int64, error) { + callCount++ + // First call: over limit. After truncation (fewer msgs), under limit. + return int64(len(msgs)) * 100, nil + } + + config := &TypedConfig[M]{ + SkipTruncation: true, + SkipClear: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 250, // 5 messages * 100 = 500 > 250 + ClearRetentionSuffixLimit: 0, + } + + mw, err := NewTyped(ctx, config) + require.NoError(t, err) + + msgs := []M{ + makeSystemMsgG[M]("system prompt"), + makeUserMsgG[M]("old user message"), + makeAssistantMsgG[M]("old assistant response"), + makeUserMsgG[M]("new user message"), + makeAssistantMsgG[M]("new assistant response"), + } + + state := &adk.TypedChatModelAgentState[M]{Messages: msgs} + _, resultState, err := mw.BeforeModelRewriteState(ctx, state, &adk.TypedModelContext[M]{}) + require.NoError(t, err) + + // Since SkipClear is true, the clear path is entirely skipped. + // The middleware should return the state unchanged because clear is skipped + // (truncation in BeforeModelRewriteState is the clear phase, not the tool-output truncation). + // The messages are returned as-is since the clearing loop is the only message-removal mechanism. + assert.Equal(t, len(msgs), len(resultState.Messages)) +} + +// --------------------------------------------------------------------------- +// Part 4: ClearPostProcess callback +// --------------------------------------------------------------------------- + +func testClearPostProcessGeneric[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + postProcessCalled := false + highTokenCounter := func(_ context.Context, _ []M, _ []*schema.ToolInfo) (int64, error) { + return 999999, nil + } + + // ClearRetentionSuffixLimit=0 defaults to 1 via copyAndFillDefaults. + // We need at least 2 tool-call groups so that the first one gets cleared + // while the second is retained by the suffix limit. + config := &TypedConfig[M]{ + SkipTruncation: true, + TokenCounter: highTokenCounter, + MaxTokensForClear: 100, + ClearRetentionSuffixLimit: 1, + ClearPostProcess: func(ctx context.Context, state *adk.TypedChatModelAgentState[M]) context.Context { + postProcessCalled = true + return ctx + }, + } + + mw, err := NewTyped(ctx, config) + require.NoError(t, err) + + msgs := []M{ + makeSystemMsgG[M]("system"), + makeUserMsgG[M]("user"), + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "call_1", Name: "tool1", Arguments: `{"a":"b"}`}, + }), + makeToolResultMsgG[M]("result", "call_1", "tool1"), + makeUserMsgG[M]("another request"), + makeAssistantMsgWithToolCallsG[M]([]testToolCall{ + {ID: "call_2", Name: "tool2", Arguments: `{"c":"d"}`}, + }), + makeToolResultMsgG[M]("result2", "call_2", "tool2"), + } + + state := &adk.TypedChatModelAgentState[M]{Messages: msgs} + _, _, err = mw.BeforeModelRewriteState(ctx, state, &adk.TypedModelContext[M]{}) + require.NoError(t, err) + assert.True(t, postProcessCalled, "ClearPostProcess should have been called") +} + +// --------------------------------------------------------------------------- +// Part 5: AgenticMessage-specific coverage +// --------------------------------------------------------------------------- + +func TestGetDefaultTokenCounter_AgenticMessage(t *testing.T) { + ctx := context.Background() + counter := getDefaultTokenCounter[*schema.AgenticMessage]() + + msgs := []*schema.AgenticMessage{ + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.UserInputText{Text: "Hello, world!"}), + }, + }, + { + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "Hi there!"}), + }, + }, + { + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolCall{CallID: "c1", Name: "my_tool", Arguments: `{"key":"value"}`}), + }, + }, + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: "c1", + Name: "my_tool", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "tool output text"}}, + }, + }), + }, + }, + nil, // nil message should be skipped + } + + tokens, err := counter(ctx, msgs, nil) + assert.NoError(t, err) + assert.Greater(t, tokens, int64(0), "should count tokens from content blocks") + + // Also test with tools + tools := []*schema.ToolInfo{ + {Name: "my_tool", Desc: "a test tool"}, + } + tokensWithTools, err := counter(ctx, msgs, tools) + assert.NoError(t, err) + assert.Greater(t, tokensWithTools, tokens, "tokens should increase with tool info") +} + +func TestCopyAgenticMessages_DeepCopy(t *testing.T) { + original := []*schema.AgenticMessage{ + { + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolCall{ + CallID: "call_1", + Name: "tool_a", + Arguments: `{"x":1}`, + }), + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "call_1", + Name: "tool_a", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "original result"}}, + }, + }, + Extra: map[string]any{"meta": "data"}, + }, + }, + Extra: map[string]any{"msg_key": "msg_value"}, + }, + } + + copied := copyMessagesGeneric(original) + require.Len(t, copied, 1) + + // Mutate the copy and verify original is unchanged. + copied[0].ContentBlocks[0].FunctionToolCall.Arguments = `{"modified":true}` + assert.Equal(t, `{"x":1}`, original[0].ContentBlocks[0].FunctionToolCall.Arguments, + "original FunctionToolCall.Arguments must not be affected") + + copied[0].ContentBlocks[1].FunctionToolResult.Content[0].Text.Text = "modified result" + assert.Equal(t, "original result", original[0].ContentBlocks[1].FunctionToolResult.Content[0].Text.Text, + "original FunctionToolResult text must not be affected") + + copied[0].ContentBlocks[1].Extra["meta"] = "changed" + assert.Equal(t, "data", original[0].ContentBlocks[1].Extra["meta"], + "original ContentBlock.Extra must not be affected") + + copied[0].Extra["msg_key"] = "changed" + assert.Equal(t, "msg_value", original[0].Extra["msg_key"], + "original AgenticMessage.Extra must not be affected") +} + +func TestToolResultFromMsgGeneric_AgenticMessage(t *testing.T) { + t.Run("single text block returns fromContent=true", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "tool1", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "hello result"}}, + }, + }, + }, + }, + } + + result, fromContent, err := toolResultFromMsgGeneric(msg) + assert.NoError(t, err) + assert.True(t, fromContent, "single text part should be fromContent=true") + require.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "hello result", result.Parts[0].Text) + }) + + t.Run("multiple blocks returns fromContent=false", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "tool1", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "text part"}}, + {Text: &schema.UserInputText{Text: "another text part"}}, + }, + }, + }, + }, + } + + result, fromContent, err := toolResultFromMsgGeneric(msg) + assert.NoError(t, err) + assert.False(t, fromContent, "multiple parts should be fromContent=false") + require.Len(t, result.Parts, 2) + assert.Equal(t, "text part", result.Parts[0].Text) + assert.Equal(t, "another text part", result.Parts[1].Text) + }) + + t.Run("empty blocks returns empty text", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "tool1", + Content: nil, + }, + }, + }, + } + + result, fromContent, err := toolResultFromMsgGeneric(msg) + assert.NoError(t, err) + assert.True(t, fromContent) + require.Len(t, result.Parts, 1) + assert.Equal(t, "", result.Parts[0].Text) + }) +} + +func TestSetToolResultContent_AgenticMessage(t *testing.T) { + t.Run("fromContent=true sets text", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "tool1", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "old"}}, + }, + }, + }, + }, + } + + newResult := &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "new content"}, + }, + } + + setToolResultContent(msg, newResult, true) + + // Verify the block was updated + blocks := msg.ContentBlocks[0].FunctionToolResult.Content + require.Len(t, blocks, 1) + assert.Equal(t, "new content", blocks[0].Text.Text) + }) + + t.Run("fromContent=false sets multi-part", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "tool1", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "old"}}, + }, + }, + }, + }, + } + + imgURL := "https://example.com/img.png" + newResult := &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "text part"}, + {Type: schema.ToolPartTypeImage, Image: &schema.ToolOutputImage{ + MessagePartCommon: schema.MessagePartCommon{URL: &imgURL, MIMEType: "image/png"}, + }}, + }, + } + + setToolResultContent(msg, newResult, false) + + blocks := msg.ContentBlocks[0].FunctionToolResult.Content + require.Len(t, blocks, 2) + assert.Equal(t, "text part", blocks[0].Text.Text) + require.NotNil(t, blocks[1].Image) + assert.Equal(t, "https://example.com/img.png", blocks[1].Image.URL) + assert.Equal(t, "image/png", blocks[1].Image.MIMEType) + }) +} + +func TestToolResultFromMsgGeneric_MediaBlocks(t *testing.T) { + imgURL := "https://example.com/img.png" + audioURL := "https://example.com/audio.wav" + videoURL := "https://example.com/video.mp4" + fileURL := "https://example.com/doc.pdf" + + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "media_tool", + Content: []*schema.FunctionToolResultContentBlock{ + {Image: &schema.UserInputImage{URL: imgURL, MIMEType: "image/png"}}, + {Audio: &schema.UserInputAudio{URL: audioURL, MIMEType: "audio/wav"}}, + {Video: &schema.UserInputVideo{URL: videoURL, MIMEType: "video/mp4"}}, + {File: &schema.UserInputFile{URL: fileURL, MIMEType: "application/pdf"}}, + }, + }, + }, + }, + } + + result, fromContent, err := toolResultFromMsgGeneric(msg) + assert.NoError(t, err) + assert.False(t, fromContent, "multi-media should be fromContent=false") + require.Len(t, result.Parts, 4) + + assert.Equal(t, schema.ToolPartTypeImage, result.Parts[0].Type) + require.NotNil(t, result.Parts[0].Image) + require.NotNil(t, result.Parts[0].Image.URL) + assert.Equal(t, imgURL, *result.Parts[0].Image.URL) + + assert.Equal(t, schema.ToolPartTypeAudio, result.Parts[1].Type) + require.NotNil(t, result.Parts[1].Audio) + require.NotNil(t, result.Parts[1].Audio.URL) + assert.Equal(t, audioURL, *result.Parts[1].Audio.URL) + + assert.Equal(t, schema.ToolPartTypeVideo, result.Parts[2].Type) + require.NotNil(t, result.Parts[2].Video) + require.NotNil(t, result.Parts[2].Video.URL) + assert.Equal(t, videoURL, *result.Parts[2].Video.URL) + + assert.Equal(t, schema.ToolPartTypeFile, result.Parts[3].Type) + require.NotNil(t, result.Parts[3].File) + require.NotNil(t, result.Parts[3].File.URL) + assert.Equal(t, fileURL, *result.Parts[3].File.URL) +} + +func TestSetToolResultContent_MediaBlocks(t *testing.T) { + audioURL := "https://example.com/speech.mp3" + videoURL := "https://example.com/clip.mp4" + fileURL := "https://example.com/report.pdf" + + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "c1", + Name: "tool1", + Content: []*schema.FunctionToolResultContentBlock{ + {Text: &schema.UserInputText{Text: "old"}}, + }, + }, + }, + }, + } + + newResult := &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{ + MessagePartCommon: schema.MessagePartCommon{URL: &audioURL, MIMEType: "audio/mp3"}, + }}, + {Type: schema.ToolPartTypeVideo, Video: &schema.ToolOutputVideo{ + MessagePartCommon: schema.MessagePartCommon{URL: &videoURL, MIMEType: "video/mp4"}, + }}, + {Type: schema.ToolPartTypeFile, File: &schema.ToolOutputFile{ + MessagePartCommon: schema.MessagePartCommon{URL: &fileURL, MIMEType: "application/pdf"}, + }}, + }, + } + + setToolResultContent(msg, newResult, false) + + blocks := msg.ContentBlocks[0].FunctionToolResult.Content + require.Len(t, blocks, 3) + + require.NotNil(t, blocks[0].Audio) + assert.Equal(t, "https://example.com/speech.mp3", blocks[0].Audio.URL) + assert.Equal(t, "audio/mp3", blocks[0].Audio.MIMEType) + + require.NotNil(t, blocks[1].Video) + assert.Equal(t, "https://example.com/clip.mp4", blocks[1].Video.URL) + assert.Equal(t, "video/mp4", blocks[1].Video.MIMEType) + + require.NotNil(t, blocks[2].File) + assert.Equal(t, "https://example.com/report.pdf", blocks[2].File.URL) + assert.Equal(t, "application/pdf", blocks[2].File.MIMEType) +} + +func TestAgenticURLToMPC(t *testing.T) { + t.Run("non-empty URL", func(t *testing.T) { + ftr := &schema.FunctionToolResult{ + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeFile, File: &schema.UserInputFile{URL: "https://example.com/file.pdf", MIMEType: "application/pdf"}}, + }, + } + parts := toolResultToOutputParts(ftr) + require.Len(t, parts, 1) + require.NotNil(t, parts[0].File) + require.NotNil(t, parts[0].File.URL) + assert.Equal(t, "https://example.com/file.pdf", *parts[0].File.URL) + assert.Equal(t, "application/pdf", parts[0].File.MIMEType) + }) + + t.Run("empty URL", func(t *testing.T) { + ftr := &schema.FunctionToolResult{ + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeFile, File: &schema.UserInputFile{URL: "", MIMEType: "text/plain"}}, + }, + } + parts := toolResultToOutputParts(ftr) + require.Len(t, parts, 1) + require.NotNil(t, parts[0].File) + assert.Nil(t, parts[0].File.URL) + assert.Equal(t, "text/plain", parts[0].File.MIMEType) + }) +} + +func TestMpcURLToString(t *testing.T) { + t.Run("non-nil URL", func(t *testing.T) { + urlStr := "https://example.com" + tr := &schema.FunctionToolResult{} + setToolResultFromOutputParts(tr, []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeFile, File: &schema.ToolOutputFile{MessagePartCommon: schema.MessagePartCommon{URL: &urlStr, MIMEType: "text/plain"}}}, + }) + require.Len(t, tr.Content, 1) + require.NotNil(t, tr.Content[0].File) + assert.Equal(t, "https://example.com", tr.Content[0].File.URL) + }) + + t.Run("nil URL", func(t *testing.T) { + tr := &schema.FunctionToolResult{} + setToolResultFromOutputParts(tr, []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeFile, File: &schema.ToolOutputFile{MessagePartCommon: schema.MessagePartCommon{URL: nil, MIMEType: "text/plain"}}}, + }) + require.Len(t, tr.Content, 1) + require.NotNil(t, tr.Content[0].File) + assert.Equal(t, "", tr.Content[0].File.URL) + }) +} + +// --------------------------------------------------------------------------- +// Top-level test +// --------------------------------------------------------------------------- + +func TestReductionGeneric(t *testing.T) { + t.Run("Message", func(t *testing.T) { + t.Run("Helpers", testHelperFunctions[*schema.Message]) + t.Run("ClearFlow", testClearFlowGeneric[*schema.Message]) + t.Run("Truncation", testTruncationGeneric[*schema.Message]) + t.Run("ClearPostProcess", testClearPostProcessGeneric[*schema.Message]) + t.Run("CopyNilMessage", testCopyNilMessage[*schema.Message]) + }) + t.Run("AgenticMessage", func(t *testing.T) { + t.Run("Helpers", testHelperFunctions[*schema.AgenticMessage]) + t.Run("ClearFlow", testClearFlowGeneric[*schema.AgenticMessage]) + t.Run("Truncation", testTruncationGeneric[*schema.AgenticMessage]) + t.Run("ClearPostProcess", testClearPostProcessGeneric[*schema.AgenticMessage]) + t.Run("CopyNilMessage", testCopyNilMessage[*schema.AgenticMessage]) + }) +} + +// testCopyNilMessage verifies that copyMessagesGeneric does not panic when +// the input slice contains nil message elements (regression test). +func testCopyNilMessage[M adk.MessageType](t *testing.T) { + var zero M + var msgs []M + switch any(zero).(type) { + case *schema.Message: + msgs = any([]*schema.Message{ + schema.UserMessage("hello"), + nil, + schema.UserMessage("world"), + }).([]M) + case *schema.AgenticMessage: + msgs = any([]*schema.AgenticMessage{ + schema.UserAgenticMessage("hello"), + nil, + schema.UserAgenticMessage("world"), + }).([]M) + } + + assert.NotPanics(t, func() { + copied := copyMessagesGeneric(msgs) + assert.Len(t, copied, 3) + assert.Nil(t, copied[1], "nil element should be preserved as nil") + }) +} diff --git a/adk/middlewares/reduction/reduction_test.go b/adk/middlewares/reduction/reduction_test.go new file mode 100644 index 0000000..dee9bb4 --- /dev/null +++ b/adk/middlewares/reduction/reduction_test.go @@ -0,0 +1,3566 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reduction + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" + "github.com/cloudwego/eino/schema" +) + +func testTruncOffloadPath(root string) func(context.Context, *ToolDetail) (string, error) { + return func(_ context.Context, detail *ToolDetail) (string, error) { + callID := "generated" + if detail != nil && detail.ToolContext != nil && detail.ToolContext.CallID != "" { + callID = detail.ToolContext.CallID + } + return fmt.Sprintf("%s/trunc/%s", root, callID), nil + } +} + +func testClearOffloadPath(root string) func(context.Context, *ToolDetail) (string, error) { + return func(_ context.Context, detail *ToolDetail) (string, error) { + callID := "generated" + if detail != nil && detail.ToolContext != nil && detail.ToolContext.CallID != "" { + callID = detail.ToolContext.CallID + } + return fmt.Sprintf("%s/clear/%s", root, callID), nil + } +} + +func stringStream(chunks ...string) *schema.StreamReader[string] { + sr, sw := schema.Pipe[string](len(chunks)) + go func() { + defer sw.Close() + for _, chunk := range chunks { + sw.Send(chunk, nil) + } + }() + return sr +} + +func stringStreamWithError(chunks []string, err error) *schema.StreamReader[string] { + sr, sw := schema.Pipe[string](len(chunks) + 1) + go func() { + defer sw.Close() + for _, chunk := range chunks { + sw.Send(chunk, nil) + } + var zero string + sw.Send(zero, err) + }() + return sr +} + +func toolResultStream(chunks ...*schema.ToolResult) *schema.StreamReader[*schema.ToolResult] { + sr, sw := schema.Pipe[*schema.ToolResult](len(chunks)) + go func() { + defer sw.Close() + for _, chunk := range chunks { + sw.Send(chunk, nil) + } + }() + return sr +} + +func toolResultStreamWithError(chunks []*schema.ToolResult, err error) *schema.StreamReader[*schema.ToolResult] { + sr, sw := schema.Pipe[*schema.ToolResult](len(chunks) + 1) + go func() { + defer sw.Close() + for _, chunk := range chunks { + sw.Send(chunk, nil) + } + var zero *schema.ToolResult + sw.Send(zero, err) + }() + return sr +} + +func sendStreamChunk[T any](t *testing.T, sw *schema.StreamWriter[T], chunk T) bool { + t.Helper() + + done := make(chan bool, 1) + go func() { + done <- sw.Send(chunk, nil) + }() + + select { + case closed := <-done: + return closed + case <-time.After(2 * time.Second): + t.Fatal("timed out sending stream chunk") + return true + } +} + +type writeFailBackend struct { + filesystem.Backend + err error +} + +func (b *writeFailBackend) Write(context.Context, *filesystem.WriteRequest) error { + return b.err +} + +func TestReductionMiddlewareTrunc(t *testing.T) { + ctx := context.Background() + it := mockInvokableTool() + st := mockStreamableTool() + + t.Run("test invokable max length trunc", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "mock_invokable_tool", + CallID: "12345", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "mock_invokable_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: defaultTruncHandler(testTruncOffloadPath("/tmp"), 70, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + exp := "\nOutput too large (199). Full output saved to: /tmp/trunc/12345\nPreview (first 35):\nhello worldhello worldhello worldhe\n\nPreview (last 35):\nldhello worldhello worldhello world\n\nUse read_file to view\n" + + edp, err := mw.WrapInvokableToolCall(ctx, it.InvokableRun, tCtx) + assert.NoError(t, err) + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + assert.Equal(t, exp, resp) + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/12345"}) + assert.NoError(t, err) + expOrigContent := `hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world +hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world` + assert.Equal(t, expOrigContent, content.Content) + }) + + t.Run("test default truncation notice includes configured read tool name", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "mock_invokable_tool", + CallID: "12345", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + MaxLengthForTrunc: 70, + ReadFileToolName: "cat_file", + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + edp, err := mw.WrapInvokableToolCall(ctx, it.InvokableRun, tCtx) + assert.NoError(t, err) + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + assert.Contains(t, resp, "Use cat_file to view") + }) + + t.Run("test streamable line and max length trunc", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "mock_streamable_tool", + CallID: "54321", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + ToolConfig: map[string]*ToolReductionConfig{ + "mock_streamable_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: defaultTruncHandler(testTruncOffloadPath("/tmp"), 70, "read_file"), + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + exp := "\nOutput too large (199). Full output saved to: /tmp/trunc/54321\nPreview (first 35):\nhello worldhello worldhello worldhe\n\nPreview (last 35):\nldhello worldhello worldhello world\n\nUse read_file to view\n" + + edp, err := mw.WrapStreamableToolCall(ctx, st.StreamableRun, tCtx) + assert.NoError(t, err) + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + s, err := resp.Recv() + assert.NoError(t, err) + resp.Close() + assert.Equal(t, exp, s) + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/54321"}) + assert.NoError(t, err) + expOrigContent := `hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world +hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world` + assert.Equal(t, expOrigContent, content.Content) + }) + + t.Run("test streamable default truncation is nonblocking before source EOF", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "mock_streamable_tool", + CallID: "stream_nonblocking", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + MaxLengthForTrunc: 30, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + releaseOverflow := make(chan struct{}) + releaseEOF := make(chan struct{}) + endpoint := func(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) { + sr, sw := schema.Pipe[string](0) + go func() { + sw.Send("first chunk", nil) + <-releaseOverflow + sw.Send(strings.Repeat("x", 80), nil) + <-releaseEOF + sw.Close() + }() + return sr, nil + } + + edp, err := mw.WrapStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + + respCh := make(chan *schema.StreamReader[string], 1) + errCh := make(chan error, 1) + go func() { + resp, runErr := edp(ctx, `{"value":"asd"}`) + if runErr != nil { + errCh <- runErr + return + } + respCh <- resp + }() + + var resp *schema.StreamReader[string] + select { + case runErr := <-errCh: + close(releaseEOF) + assert.NoError(t, runErr) + return + case resp = <-respCh: + case <-time.After(2 * time.Second): + close(releaseEOF) + assert.Fail(t, "streamable truncation wrapper blocked until source EOF") + return + } + defer resp.Close() + + first, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "first chunk", first) + + close(releaseOverflow) + noticeCh := make(chan string, 1) + noticeErrCh := make(chan error, 1) + go func() { + chunk, recvErr := resp.Recv() + if recvErr != nil { + noticeErrCh <- recvErr + return + } + noticeCh <- chunk + }() + + select { + case got := <-noticeCh: + close(releaseEOF) + assert.Failf(t, "streamable truncation notice returned before source EOF", "got %q", got) + return + case recvErr := <-noticeErrCh: + close(releaseEOF) + assert.NoError(t, recvErr) + return + case <-time.After(100 * time.Millisecond): + } + + close(releaseEOF) + var remaining string + select { + case remaining = <-noticeCh: + case recvErr := <-noticeErrCh: + assert.NoError(t, recvErr) + return + case <-time.After(2 * time.Second): + assert.Fail(t, "streamable truncation notice blocked after source EOF") + return + } + assert.Contains(t, remaining, "Output truncated after") + assert.NotContains(t, remaining, "Preview (first") + assert.NotContains(t, remaining, "Preview (last") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/stream_nonblocking"}) + assert.NoError(t, err) + assert.Equal(t, "first chunk"+strings.Repeat("x", 80), content.Content) + }) + + t.Run("test enhanced streamable default truncation is nonblocking before source EOF", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "mock_enhanced_streamable_tool", + CallID: "enhanced_stream_nonblocking", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + MaxLengthForTrunc: 30, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + releaseOverflow := make(chan struct{}) + releaseEOF := make(chan struct{}) + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr, sw := schema.Pipe[*schema.ToolResult](0) + go func() { + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "first chunk"}}}, nil) + <-releaseOverflow + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: strings.Repeat("x", 80)}}}, nil) + <-releaseEOF + sw.Close() + }() + return sr, nil + } + + edp, err := mw.WrapEnhancedStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + + respCh := make(chan *schema.StreamReader[*schema.ToolResult], 1) + errCh := make(chan error, 1) + go func() { + resp, runErr := edp(ctx, &schema.ToolArgument{Text: `{"value":"asd"}`}) + if runErr != nil { + errCh <- runErr + return + } + respCh <- resp + }() + + var resp *schema.StreamReader[*schema.ToolResult] + select { + case runErr := <-errCh: + close(releaseEOF) + assert.NoError(t, runErr) + return + case resp = <-respCh: + case <-time.After(2 * time.Second): + close(releaseEOF) + assert.Fail(t, "enhanced streamable truncation wrapper blocked until source EOF") + return + } + defer resp.Close() + + first, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "first chunk", first.Parts[0].Text) + + close(releaseOverflow) + noticeCh := make(chan string, 1) + noticeErrCh := make(chan error, 1) + go func() { + chunk, recvErr := resp.Recv() + if recvErr != nil { + noticeErrCh <- recvErr + return + } + noticeCh <- stringifyToolOutputParts(chunk.Parts) + }() + + select { + case got := <-noticeCh: + close(releaseEOF) + assert.Failf(t, "enhanced streamable truncation notice returned before source EOF", "got %q", got) + return + case recvErr := <-noticeErrCh: + close(releaseEOF) + assert.NoError(t, recvErr) + return + case <-time.After(100 * time.Millisecond): + } + + close(releaseEOF) + var remaining string + select { + case remaining = <-noticeCh: + case recvErr := <-noticeErrCh: + assert.NoError(t, recvErr) + return + case <-time.After(2 * time.Second): + assert.Fail(t, "enhanced streamable truncation notice blocked after source EOF") + return + } + assert.Contains(t, remaining, "Output truncated after") + assert.NotContains(t, remaining, "Preview (first") + assert.NotContains(t, remaining, "Preview (last") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/enhanced_stream_nonblocking"}) + assert.NoError(t, err) + assert.Contains(t, content.Content, `"text": "first chunk"`) + assert.Contains(t, content.Content, fmt.Sprintf(`"text": "%s"`, strings.Repeat("x", 80))) + }) + + t.Run("test default streamable truncation terminal cases", func(t *testing.T) { + tCtx := &adk.ToolContext{Name: "mock_streamable_tool", CallID: "stream_branches"} + backend := filesystem.NewInMemoryBackend() + mw := &typedToolReductionMiddleware[*schema.Message]{ + config: &TypedConfig[*schema.Message]{ + MaxLengthForTrunc: 10, + GenTruncOffloadFilePath: testTruncOffloadPath("/tmp"), + }, + } + + t.Run("short output forwards and does not truncate", func(t *testing.T) { + cfg := &ToolReductionConfig{} + resp := mw.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStream("short")) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "short", got) + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("source error is forwarded", func(t *testing.T) { + wantErr := errors.New("source stream error") + cfg := &ToolReductionConfig{} + resp := mw.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStreamWithError([]string{"ok"}, wantErr)) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "ok", got) + _, err = resp.Recv() + assert.Equal(t, wantErr, err) + }) + + t.Run("path generation error is rendered in truncation notice", func(t *testing.T) { + wantErr := errors.New("path generation error") + mwWithPathErr := &typedToolReductionMiddleware[*schema.Message]{ + config: &TypedConfig[*schema.Message]{ + MaxLengthForTrunc: 10, + GenTruncOffloadFilePath: func(context.Context, *ToolDetail) (string, error) { + return "", wantErr + }, + }, + } + cfg := &ToolReductionConfig{Backend: backend} + resp := mwWithPathErr.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStream(strings.Repeat("x", 20))) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Contains(t, got, strings.Repeat("x", 10)) + assert.Contains(t, got, "Output truncated after 10 bytes were streamed") + assert.Contains(t, got, "Failed to save full output: path generation error.") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("missing backend error is rendered in truncation notice", func(t *testing.T) { + cfg := &ToolReductionConfig{} + resp := mw.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStream(strings.Repeat("x", 20))) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Contains(t, got, strings.Repeat("x", 10)) + assert.Contains(t, got, "Output truncated after 10 bytes were streamed") + assert.Contains(t, got, "Failed to save full output: truncation: no backend for offload.") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("truncates on source EOF and writes full output", func(t *testing.T) { + cfg := &ToolReductionConfig{Backend: backend} + resp := mw.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStream(strings.Repeat("x", 20), "tail")) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Contains(t, got, strings.Repeat("x", 10)) + assert.Contains(t, got, "Output truncated after 10 bytes were streamed") + assert.Contains(t, got, "Full output saved to: /tmp/trunc/stream_branches.") + assert.NotContains(t, got, "Preview (first") + assert.NotContains(t, got, "Preview (last") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/stream_branches"}) + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 20)+"tail", content.Content) + }) + + t.Run("truncation notice includes configured read tool name", func(t *testing.T) { + mwWithReadTool := &typedToolReductionMiddleware[*schema.Message]{ + config: &TypedConfig[*schema.Message]{ + MaxLengthForTrunc: 10, + GenTruncOffloadFilePath: testTruncOffloadPath("/tmp"), + ReadFileToolName: "cat_file", + }, + } + cfg := &ToolReductionConfig{Backend: backend} + resp := mwWithReadTool.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStream(strings.Repeat("x", 20))) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Contains(t, got, "Use cat_file to view") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("write error is rendered in truncation notice after source EOF", func(t *testing.T) { + wantErr := errors.New("stream write error") + cfg := &ToolReductionConfig{ + Backend: &writeFailBackend{ + Backend: filesystem.NewInMemoryBackend(), + err: wantErr, + }, + } + resp := mw.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStream(strings.Repeat("x", 20))) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Contains(t, got, strings.Repeat("x", 10)) + assert.Contains(t, got, "Output truncated after 10 bytes were streamed") + assert.Contains(t, got, "Failed to save full output: stream write error.") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("source error after truncation replays swallowed chunks before error", func(t *testing.T) { + wantErr := errors.New("stream source error after truncation") + cfg := &ToolReductionConfig{Backend: backend} + resp := mw.wrapDefaultStreamableTruncation(ctx, cfg, tCtx, `{}`, stringStreamWithError( + []string{"abc", "defghijklmnop", "tail"}, + wantErr, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "abc", got) + + replayed, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "defghijklmnoptail", replayed) + _, err = resp.Recv() + assert.Equal(t, wantErr, err) + }) + }) + + t.Run("test default enhanced streamable truncation terminal cases", func(t *testing.T) { + tCtx := &adk.ToolContext{Name: "mock_enhanced_streamable_tool", CallID: "enhanced_stream_branches"} + toolArg := &schema.ToolArgument{Text: `{}`} + backend := filesystem.NewInMemoryBackend() + mw := &typedToolReductionMiddleware[*schema.Message]{ + config: &TypedConfig[*schema.Message]{ + MaxLengthForTrunc: 10, + GenTruncOffloadFilePath: testTruncOffloadPath("/tmp"), + }, + } + longResult := &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: strings.Repeat("x", 20)}}} + + t.Run("short output forwards and does not truncate", func(t *testing.T) { + cfg := &ToolReductionConfig{} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream( + &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "short"}}}, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "short", got.Parts[0].Text) + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("source error is forwarded", func(t *testing.T) { + wantErr := errors.New("enhanced source stream error") + cfg := &ToolReductionConfig{} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStreamWithError( + []*schema.ToolResult{{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "ok"}}}}, + wantErr, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "ok", got.Parts[0].Text) + _, err = resp.Recv() + assert.Equal(t, wantErr, err) + }) + + t.Run("conflicting media parts do not prevent truncation notice", func(t *testing.T) { + audioA := "YXVkaW8x" + audioB := "YXVkaW8y" + cfg := &ToolReductionConfig{Backend: backend} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream( + &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: strings.Repeat("x", 20)}, + {Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{Base64Data: &audioA}}}, + }}, + &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{Base64Data: &audioB}}}, + }}, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 10), got.Parts[0].Text) + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Output truncated after 10 bytes were streamed") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("conflicting media parts after forwarded chunk do not prevent truncation notice", func(t *testing.T) { + audioA := "YXVkaW8x" + audioB := "YXVkaW8y" + cfg := &ToolReductionConfig{Backend: backend} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream( + &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "ok"}, + {Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{Base64Data: &audioA}}}, + }}, + &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: strings.Repeat("x", 20)}, + {Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{Base64Data: &audioB}}}, + }}, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "ok", got.Parts[0].Text) + notice, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 8), notice.Parts[0].Text) + assert.Contains(t, stringifyToolOutputParts(notice.Parts), "Output truncated after 10 bytes were streamed") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("path generation error is rendered in truncation notice", func(t *testing.T) { + wantErr := errors.New("enhanced path generation error") + mwWithPathErr := &typedToolReductionMiddleware[*schema.Message]{ + config: &TypedConfig[*schema.Message]{ + MaxLengthForTrunc: 10, + GenTruncOffloadFilePath: func(context.Context, *ToolDetail) (string, error) { + return "", wantErr + }, + }, + } + cfg := &ToolReductionConfig{Backend: backend} + resp := mwWithPathErr.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream(longResult)) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 10), got.Parts[0].Text) + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Output truncated after 10 bytes were streamed") + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Failed to save full output: enhanced path generation error.") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("missing backend error is rendered in truncation notice", func(t *testing.T) { + cfg := &ToolReductionConfig{} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream(longResult)) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 10), got.Parts[0].Text) + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Output truncated after 10 bytes were streamed") + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Failed to save full output: truncation: no backend for offload.") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("truncates on source EOF and writes full output", func(t *testing.T) { + cfg := &ToolReductionConfig{Backend: backend} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream( + longResult, + &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "tail"}}}, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 10), got.Parts[0].Text) + gotText := stringifyToolOutputParts(got.Parts) + assert.Contains(t, gotText, "Output truncated after 10 bytes were streamed") + assert.Contains(t, gotText, "Full output saved to: /tmp/trunc/enhanced_stream_branches.") + assert.NotContains(t, gotText, "Preview (first") + assert.NotContains(t, gotText, "Preview (last") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/enhanced_stream_branches"}) + assert.NoError(t, err) + assert.Contains(t, content.Content, fmt.Sprintf(`"text": "%s"`, strings.Repeat("x", 20))) + assert.Contains(t, content.Content, `"text": "tail"`) + }) + + t.Run("truncation notice includes configured read tool name", func(t *testing.T) { + mwWithReadTool := &typedToolReductionMiddleware[*schema.Message]{ + config: &TypedConfig[*schema.Message]{ + MaxLengthForTrunc: 10, + GenTruncOffloadFilePath: testTruncOffloadPath("/tmp"), + ReadFileToolName: "cat_file", + }, + } + cfg := &ToolReductionConfig{Backend: backend} + resp := mwWithReadTool.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream(longResult)) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Use cat_file to view") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("write error is rendered in truncation notice after source EOF", func(t *testing.T) { + wantErr := errors.New("enhanced write error") + cfg := &ToolReductionConfig{ + Backend: &writeFailBackend{ + Backend: filesystem.NewInMemoryBackend(), + err: wantErr, + }, + } + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStream(longResult)) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, strings.Repeat("x", 10), got.Parts[0].Text) + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Output truncated after 10 bytes were streamed") + assert.Contains(t, stringifyToolOutputParts(got.Parts), "Failed to save full output: enhanced write error.") + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + }) + + t.Run("source error after truncation replays swallowed chunks before error", func(t *testing.T) { + wantErr := errors.New("enhanced source stream error after truncation") + cfg := &ToolReductionConfig{Backend: backend} + resp := mw.wrapDefaultEnhancedStreamableTruncation(ctx, cfg, tCtx, toolArg, toolResultStreamWithError( + []*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "abc"}}}, + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "defghijklmnop"}}}, + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "tail"}}}, + }, + wantErr, + )) + defer resp.Close() + + got, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "abc", got.Parts[0].Text) + + replayed, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "defghijklmnop", replayed.Parts[0].Text) + assert.Equal(t, "tail", replayed.Parts[1].Text) + _, err = resp.Recv() + assert.Equal(t, wantErr, err) + }) + }) + + t.Run("test streamable line and bypass error", func(t *testing.T) { + stWithErr := mockStreamableToolWithError() + tCtx := &adk.ToolContext{ + Name: "mock_streamable_tool", + CallID: "54321", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + ToolConfig: map[string]*ToolReductionConfig{ + "mock_streamable_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: defaultTruncHandler(testTruncOffloadPath("/tmp"), 70, "read_file"), + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + edp, err := mw.WrapStreamableToolCall(ctx, stWithErr.StreamableRun, tCtx) + assert.NoError(t, err) + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + cnt := 0 + gotError := false + for { + _, err := resp.Recv() + if err != nil { + if err == io.EOF { + break + } + assert.Equal(t, fmt.Errorf("mock error"), err) + gotError = true + break + } + cnt++ + } + assert.True(t, gotError) + assert.Equal(t, 10, cnt) + }) + + t.Run("test TruncExcludeTools with invokable tool", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "important_tool", + CallID: "12345", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + TruncExcludeTools: []string{"important_tool"}, + MaxLengthForTrunc: 70, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + edp, err := mw.WrapInvokableToolCall(ctx, it.InvokableRun, tCtx) + assert.NoError(t, err) + + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + + expOrigContent := `hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world +hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world` + assert.Equal(t, expOrigContent, resp) + + _, err = backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/12345"}) + assert.Error(t, err) + }) + + t.Run("test TruncExcludeTools with streamable tool", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "important_stream_tool", + CallID: "54321", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + TruncExcludeTools: []string{"important_stream_tool"}, + MaxLengthForTrunc: 70, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + edp, err := mw.WrapStreamableToolCall(ctx, st.StreamableRun, tCtx) + assert.NoError(t, err) + + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + + var fullOutput strings.Builder + for { + chunk, recvErr := resp.Recv() + if recvErr != nil { + if recvErr == io.EOF { + break + } + assert.Fail(t, "unexpected error", recvErr) + } + fullOutput.WriteString(chunk) + } + resp.Close() + + expOrigContent := `hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world +hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world` + assert.Equal(t, expOrigContent, fullOutput.String()) + + _, err = backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/54321"}) + assert.Error(t, err) + }) + + t.Run("test mixed tools with TruncExcludeTools", func(t *testing.T) { + excludedToolCtx := &adk.ToolContext{ + Name: "excluded_tool", + CallID: "excluded_123", + } + normalToolCtx := &adk.ToolContext{ + Name: "normal_tool", + CallID: "normal_456", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + TruncExcludeTools: []string{"excluded_tool"}, + MaxLengthForTrunc: 70, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + excludedEdp, err := mw.WrapInvokableToolCall(ctx, it.InvokableRun, excludedToolCtx) + assert.NoError(t, err) + excludedResp, err := excludedEdp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + expOrigContent := `hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world +hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world` + assert.Equal(t, expOrigContent, excludedResp) + + normalEdp, err := mw.WrapInvokableToolCall(ctx, it.InvokableRun, normalToolCtx) + assert.NoError(t, err) + normalResp, err := normalEdp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + assert.NotEqual(t, expOrigContent, normalResp) + assert.Contains(t, normalResp, "persisted-output") + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/normal_456"}) + assert.NoError(t, err) + assert.Equal(t, expOrigContent, content.Content) + }) + + t.Run("test GenTruncOffloadFilePath used by default trunc handler", func(t *testing.T) { + tCtx := &adk.ToolContext{ + Name: "mock_invokable_tool", + CallID: "custom_123", + } + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + MaxLengthForTrunc: 70, + RootDir: "/tmp/ignored", + GenTruncOffloadFilePath: func(_ context.Context, detail *ToolDetail) (string, error) { + assert.Equal(t, "custom_123", detail.ToolContext.CallID) + return "/custom/trunc/custom_123", nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + edp, err := mw.WrapInvokableToolCall(ctx, it.InvokableRun, tCtx) + assert.NoError(t, err) + + resp, err := edp(ctx, `{"value":"asd"}`) + assert.NoError(t, err) + assert.Contains(t, resp, "/custom/trunc/custom_123") + + _, err = backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/custom/trunc/custom_123"}) + assert.NoError(t, err) + }) +} + +func TestReductionMiddlewareClear(t *testing.T) { + ctx := context.Background() + it := mockInvokableTool() + st := mockStreamableTool() + tools := []tool.BaseTool{it, st} + var toolsInfo []*schema.ToolInfo + for _, bt := range tools { + ti, _ := bt.Info(ctx) + toolsInfo = append(toolsInfo, ti) + } + type OffloadContent struct { + Arguments map[string]string `json:"arguments"` + Result string `json:"result"` + } + + t.Run("test default clear", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + TokenCounter: defaultTokenCounter, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 0, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_987654321"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_123456789"), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[2].ToolCalls) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[4].ToolCalls) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_987654321\nUse read_file to view", s.Messages[3].Content) + fileContent, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_987654321", + }) + assert.NoError(t, err) + fileContentStr := strings.TrimPrefix(strings.TrimSpace(fileContent.Content), "1\t") + assert.Equal(t, "Sunny", fileContentStr) + }) + + t.Run("test GenClearOffloadFilePath used by default clear handler", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + SkipTruncation: true, + TokenCounter: func(context.Context, []adk.Message, []*schema.ToolInfo) (int64, error) { return 100, nil }, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 1, + RootDir: "/tmp/ignored", + GenClearOffloadFilePath: func(_ context.Context, detail *ToolDetail) (string, error) { + return "/custom/clear/" + detail.ToolContext.CallID, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("trigger clear"), + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "call_a", Type: "function", Function: schema.FunctionCall{Name: "get_weather", Arguments: `{}`}}, + }), + schema.ToolMessage("Sunny", "call_a"), + schema.AssistantMessage("", []schema.ToolCall{ + {ID: "call_b", Type: "function", Function: schema.FunctionCall{Name: "get_weather", Arguments: `{}`}}, + }), + schema.ToolMessage("Sunny", "call_b"), + }, + }, &adk.ModelContext{Tools: toolsInfo}) + assert.NoError(t, err) + + assert.Equal(t, "Tool result saved to: /custom/clear/call_a\nUse read_file to view", s.Messages[3].Content) + + fileContent, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/custom/clear/call_a", + }) + assert.NoError(t, err) + fileContentStr := strings.TrimPrefix(strings.TrimSpace(fileContent.Content), "1\t") + assert.Equal(t, "Sunny", fileContentStr) + }) + + t.Run("test default clear without offloading", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: defaultTokenCounter, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 0, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath(""), false, ""), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_987654321"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_123456789"), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[2].ToolCalls) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[4].ToolCalls) + assert.Equal(t, "[Old tool result content cleared]", s.Messages[3].Content) + }) + + t.Run("test clear with reordered tool results", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + TokenCounter: defaultTokenCounter, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 0, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("get weather for two cities"), + // Assistant message with two tool calls + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "two_call_one", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London"}`}, + }, + { + ID: "two_call_two", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "Paris"}`}, + }, + }), + // Intentionally put tool result two BEFORE tool result one + schema.ToolMessage("Cloudy in Paris", "two_call_two"), + schema.ToolMessage("Sunny in London", "two_call_one"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + + // Verify tool call arguments are preserved (not offloaded since offload is true) + assert.Equal(t, `{"location": "London"}`, s.Messages[2].ToolCalls[0].Function.Arguments) + assert.Equal(t, `{"location": "Paris"}`, s.Messages[2].ToolCalls[1].Function.Arguments) + + // Verify tool result two is correctly matched and offloaded (it's at index 3) + assert.Equal(t, "Tool result saved to: /tmp/clear/two_call_two\nUse read_file to view", s.Messages[3].Content) + fileContent2, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/two_call_two", + }) + assert.NoError(t, err) + fileContentStr2 := strings.TrimPrefix(strings.TrimSpace(fileContent2.Content), "1\t") + assert.Equal(t, "Cloudy in Paris", fileContentStr2) + + // Verify tool result one is correctly matched and offloaded (it's at index 4) + assert.Equal(t, "Tool result saved to: /tmp/clear/two_call_one\nUse read_file to view", s.Messages[4].Content) + fileContent1, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/two_call_one", + }) + assert.NoError(t, err) + fileContentStr1 := strings.TrimPrefix(strings.TrimSpace(fileContent1.Content), "1\t") + assert.Equal(t, "Sunny in London", fileContentStr1) + }) + + t.Run("test clear", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + handler := func(ctx context.Context, detail *ToolDetail) (*ClearResult, error) { + arguments := make(map[string]string) + if err := json.Unmarshal([]byte(detail.ToolArgument.Text), &arguments); err != nil { + return nil, err + } + offloadContent := &OffloadContent{ + Arguments: arguments, + Result: detail.ToolResult.Parts[0].Text, + } + replacedArguments := make(map[string]string, len(arguments)) + filePath := fmt.Sprintf("/tmp/%s", detail.ToolContext.CallID) + for k := range arguments { + replacedArguments[k] = "argument offloaded" + } + return &ClearResult{ + ToolArgument: &schema.ToolArgument{Text: toJson(replacedArguments)}, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "result offloaded, retrieve it from " + filePath}, + }, + }, + NeedClear: true, + NeedOffload: true, + OffloadFilePath: filePath, + OffloadContent: toJson(offloadContent), + }, nil + } + config := &Config{ + SkipTruncation: true, + TokenCounter: defaultTokenCounter, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 1, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: handler, + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_987654321"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_123456789"), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location":"argument offloaded","unit":"argument offloaded"}`}, + }, + }, s.Messages[2].ToolCalls) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[4].ToolCalls) + assert.Equal(t, "result offloaded, retrieve it from /tmp/call_987654321", s.Messages[3].Content) + fileContent, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/call_987654321", + }) + assert.NoError(t, err) + fileContentStr := strings.TrimPrefix(strings.TrimSpace(fileContent.Content), "1\t") + oc := &OffloadContent{} + err = json.Unmarshal([]byte(fileContentStr), oc) + assert.NoError(t, err) + assert.Equal(t, &OffloadContent{ + Arguments: map[string]string{ + "location": "London, UK", + "unit": "c", + }, + Result: "Sunny", + }, oc) + }) + + t.Run("test skip handled ones", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + TokenCounter: defaultTokenCounter, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 0, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + msgs := []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_987654321"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_123456789"), + } + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{Messages: msgs}, &adk.ModelContext{Tools: toolsInfo}) + assert.NoError(t, err) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[2].ToolCalls) + assert.NotNil(t, msgs[2].Extra[msgClearedFlag]) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[4].ToolCalls) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_987654321\nUse read_file to view", s.Messages[3].Content) + fileContent, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_987654321", + }) + assert.NoError(t, err) + fileContentStr := strings.TrimPrefix(strings.TrimSpace(fileContent.Content), "1\t") + assert.Equal(t, "Sunny", fileContentStr) + + msgs = append(msgs, []*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_8877665544", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_8877665544"), + }...) + _, s, err = mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{Messages: msgs}, &adk.ModelContext{Tools: toolsInfo}) + assert.NoError(t, err) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[2].ToolCalls) + assert.NotNil(t, msgs[2].Extra[msgClearedFlag]) + assert.Equal(t, []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }, s.Messages[4].ToolCalls) + assert.NotNil(t, msgs[4].Extra[msgClearedFlag]) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_987654321\nUse read_file to view", s.Messages[3].Content) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_123456789\nUse read_file to view", s.Messages[5].Content) + }) + + t.Run("test ClearExcludeTools", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + TokenCounter: defaultTokenCounter, + MaxTokensForClear: 20, + ClearRetentionSuffixLimit: 0, + ClearExcludeTools: []string{"get_important_data"}, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + "get_important_data": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + msgs := []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_987654321"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_important_data", Arguments: `{"id": "123"}`}, + }, + }), + schema.ToolMessage("Important Data Content", "call_123456789"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_999", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_999"), + } + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{Messages: msgs}, &adk.ModelContext{Tools: toolsInfo}) + assert.NoError(t, err) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_987654321\nUse read_file to view", s.Messages[3].Content) + assert.Equal(t, "Important Data Content", s.Messages[5].Content) + b, err := backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_987654321", + }) + assert.NoError(t, err) + assert.Equal(t, "Sunny", b.Content) + _, err = backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_123456789", + }) + assert.Error(t, err) + assert.Equal(t, "file not found: /tmp/clear/call_123456789", err.Error()) + }) + + t.Run("test ClearAtLeastTokens - not enough tokens cleared", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + TokenCounter: func(_ context.Context, msgs []adk.Message, _ []*schema.ToolInfo) (int64, error) { + var size int + for _, msg := range msgs { + size += len(msg.Content) + for _, tc := range msg.ToolCalls { + size += len(tc.Function.Name) + size += len(tc.Function.Arguments) + } + } + return int64(size), nil + }, + MaxTokensForClear: 50, + ClearRetentionSuffixLimit: 1, + ClearAtLeastTokens: 100, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + "get_important_data": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + msgs := []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny", "call_123456789"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_important_data", Arguments: `{"id": "123"}`}, + }, + }), + schema.ToolMessage("Important Data Content, qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe", "call_123456789"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_999", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_999"), + } + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{Messages: msgs}, &adk.ModelContext{Tools: toolsInfo}) + assert.NoError(t, err) + assert.Equal(t, "Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny", s.Messages[3].Content) + assert.Equal(t, "Important Data Content, qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe", s.Messages[5].Content) + _, err = backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_987654321", + }) + assert.Error(t, err) + }) + + t.Run("test ClearAtLeastTokens - enough tokens cleared", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + SkipTruncation: true, + TokenCounter: func(_ context.Context, msgs []adk.Message, _ []*schema.ToolInfo) (int64, error) { + var size int + for _, msg := range msgs { + size += len(msg.Content) + for _, tc := range msg.ToolCalls { + size += len(tc.Function.Name) + size += len(tc.Function.Arguments) + } + } + return int64(size), nil + }, + MaxTokensForClear: 50, + ClearRetentionSuffixLimit: 1, + ClearAtLeastTokens: 10, + ToolConfig: map[string]*ToolReductionConfig{ + "get_weather": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + "get_important_data": { + Backend: backend, + SkipClear: false, + ClearHandler: defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file"), + }, + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + msgs := []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("If it's warmer than 20°C in London, set the thermostat to 20°C, otherwise set it to 18°C."), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_987654321", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny Sunny", "call_987654321"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123456789", + Type: "function", + Function: schema.FunctionCall{Name: "get_important_data", Arguments: `{"id": "123"}`}, + }, + }), + schema.ToolMessage("Important Data Content, qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe", "call_123456789"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_999", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London, UK", "unit": "c"}`}, + }, + }), + schema.ToolMessage("Sunny", "call_999"), + } + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{Messages: msgs}, &adk.ModelContext{Tools: toolsInfo}) + assert.NoError(t, err) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_987654321\nUse read_file to view", s.Messages[3].Content) + _, err = backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_987654321", + }) + assert.NoError(t, err) + assert.Equal(t, "Tool result saved to: /tmp/clear/call_123456789\nUse read_file to view", s.Messages[5].Content) + _, err = backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: "/tmp/clear/call_987654321", + }) + assert.NoError(t, err) + }) +} + +func TestStreamTruncationHelpers(t *testing.T) { + t.Run("toolResultTextPrefix trims text and preserves leading non-text parts", func(t *testing.T) { + assert.Nil(t, toolResultTextPrefix(nil, 10)) + assert.Nil(t, toolResultTextPrefix(&schema.ToolResult{}, 10)) + assert.Nil(t, toolResultTextPrefix(&schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "abc"}, + }}, 0)) + + got := toolResultTextPrefix(&schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeImage}, + {Type: schema.ToolPartTypeText, Text: "abc"}, + {Type: schema.ToolPartTypeText, Text: "def"}, + }}, 5) + assert.NotNil(t, got) + assert.Equal(t, []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeImage}, + {Type: schema.ToolPartTypeText, Text: "abc"}, + {Type: schema.ToolPartTypeText, Text: "de"}, + }, got.Parts) + + got = toolResultTextPrefix(&schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "abc"}, + {Type: schema.ToolPartTypeText, Text: "def"}, + }}, 3) + assert.NotNil(t, got) + assert.Equal(t, []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "abc"}, + }, got.Parts) + }) + + t.Run("toolResultTextLength handles nil result", func(t *testing.T) { + assert.Equal(t, 0, toolResultTextLength(nil)) + }) + + t.Run("writeTruncOffload handles disabled missing and successful offload", func(t *testing.T) { + ctx := context.Background() + + err := writeTruncOffload(ctx, &ToolReductionConfig{}, &TruncResult{NeedOffload: false}) + assert.NoError(t, err) + + err = writeTruncOffload(ctx, &ToolReductionConfig{}, &TruncResult{NeedOffload: true}) + assert.EqualError(t, err, "truncation: no backend for offload") + + backend := filesystem.NewInMemoryBackend() + err = writeTruncOffload(ctx, &ToolReductionConfig{Backend: backend}, &TruncResult{ + NeedOffload: true, + OffloadFilePath: "/tmp/trunc/helper", + OffloadContent: "full output", + }) + assert.NoError(t, err) + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/helper"}) + assert.NoError(t, err) + assert.Equal(t, "full output", content.Content) + }) +} + +func TestGetJointToolResult(t *testing.T) { + t.Run("test with ToolResult", func(t *testing.T) { + detail := &ToolDetail{ + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "hello world"}}}, + } + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.True(t, needProcess) + assert.Len(t, toolOutputParts, 1) + assert.Equal(t, schema.ToolPartTypeText, toolOutputParts[0].Type) + assert.Equal(t, "hello world", toolOutputParts[0].Text) + }) + + t.Run("test with multiple ToolResult parts", func(t *testing.T) { + detail := &ToolDetail{ + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "hello "}, + {Type: schema.ToolPartTypeText, Text: "world"}, + }}, + } + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.True(t, needProcess) + assert.Len(t, toolOutputParts, 2) + assert.Equal(t, schema.ToolPartTypeText, toolOutputParts[0].Type) + assert.Equal(t, "hello ", toolOutputParts[0].Text) + assert.Equal(t, schema.ToolPartTypeText, toolOutputParts[1].Type) + assert.Equal(t, "world", toolOutputParts[1].Text) + }) + + t.Run("test with empty ToolResult parts", func(t *testing.T) { + detail := &ToolDetail{ + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{}}, + } + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.False(t, needProcess) + assert.Nil(t, toolOutputParts) + }) + + t.Run("test with ToolResult multimodal", func(t *testing.T) { + detail := &ToolDetail{ + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeImage, Text: "https://example.com/image.png"}}}, + } + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.True(t, needProcess) + assert.Len(t, toolOutputParts, 1) + assert.Equal(t, schema.ToolPartTypeImage, toolOutputParts[0].Type) + assert.Equal(t, "https://example.com/image.png", toolOutputParts[0].Text) + }) + + t.Run("test with StreamToolResult", func(t *testing.T) { + sr, sw := schema.Pipe[*schema.ToolResult](10) + go func() { + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "hello "}}}, nil) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "world"}}}, nil) + sw.Close() + }() + + detail := &ToolDetail{ + StreamToolResult: sr, + } + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.True(t, needProcess) + assert.Len(t, toolOutputParts, 1) + assert.Equal(t, schema.ToolPartTypeText, toolOutputParts[0].Type) + assert.Equal(t, "hello world", toolOutputParts[0].Text) + }) + + t.Run("test with StreamToolResult error", func(t *testing.T) { + sr, sw := schema.Pipe[*schema.ToolResult](10) + go func() { + sw.Send(nil, fmt.Errorf("stream error")) + sw.Close() + }() + + detail := &ToolDetail{ + StreamToolResult: sr, + } + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.False(t, needProcess) + assert.Nil(t, toolOutputParts) + }) + + t.Run("test with both ToolResult and StreamToolResult nil (should error)", func(t *testing.T) { + detail := &ToolDetail{} + toolOutputParts, needProcess, err := getJointToolResult(detail) + assert.Error(t, err) + assert.False(t, needProcess) + assert.Nil(t, toolOutputParts) + }) +} + +func TestDefaultTruncHandlerWithStreamToolResult(t *testing.T) { + ctx := context.Background() + + t.Run("test short stream result no trunc", func(t *testing.T) { + sr, sw := schema.Pipe[*schema.ToolResult](10) + go func() { + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "short text"}}}, nil) + sw.Close() + }() + + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: "test", + CallID: "call_id", + }, + ToolArgument: &schema.ToolArgument{Text: "{}"}, + StreamToolResult: sr, + } + + fn := defaultTruncHandler(testTruncOffloadPath("/tmp"), 100, "read_file") + result, err := fn(ctx, detail) + assert.NoError(t, err) + assert.False(t, result.NeedTrunc) + }) + + t.Run("test long stream result need trunc", func(t *testing.T) { + sr, sw := schema.Pipe[*schema.ToolResult](10) + go func() { + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: strings.Repeat("hello world", 20)}}}, nil) + sw.Close() + }() + + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: "test", + CallID: "call_id", + }, + ToolArgument: &schema.ToolArgument{Text: "{}"}, + StreamToolResult: sr, + } + + fn := defaultTruncHandler(testTruncOffloadPath("/tmp"), 100, "read_file") + result, err := fn(ctx, detail) + assert.NoError(t, err) + assert.True(t, result.NeedTrunc) + }) + + t.Run("test gen offload file path error", func(t *testing.T) { + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: "test", + CallID: "call_id", + }, + ToolArgument: &schema.ToolArgument{Text: "{}"}, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: strings.Repeat("x", 1000)}}, + }, + } + + fn := defaultTruncHandler(func(context.Context, *ToolDetail) (string, error) { + return "", fmt.Errorf("gen path error") + }, 10, "read_file") + _, err := fn(ctx, detail) + assert.Error(t, err) + assert.Contains(t, err.Error(), "gen path error") + }) +} + +func TestDefaultClearHandlerWithStreamToolResult(t *testing.T) { + ctx := context.Background() + + t.Run("test with stream result need offload", func(t *testing.T) { + sr, sw := schema.Pipe[*schema.ToolResult](10) + go func() { + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "streaming content"}}}, nil) + sw.Close() + }() + + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: "test", + CallID: "stream_call_id", + }, + ToolArgument: &schema.ToolArgument{Text: "{}"}, + StreamToolResult: sr, + } + + fn := defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file") + result, err := fn(ctx, detail) + assert.NoError(t, err) + assert.True(t, result.NeedClear) + assert.True(t, result.NeedOffload) + assert.Equal(t, "streaming content", result.OffloadContent) + }) + + t.Run("test gen offload file path error", func(t *testing.T) { + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: "test", + CallID: "call_id", + }, + ToolArgument: &schema.ToolArgument{Text: "{}"}, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "content"}}, + }, + } + fn := defaultClearHandler(func(context.Context, *ToolDetail) (string, error) { + return "", fmt.Errorf("gen path error") + }, true, "read_file") + _, err := fn(ctx, detail) + assert.Error(t, err) + assert.Contains(t, err.Error(), "gen path error") + }) +} + +func TestDefaultOffloadHandler(t *testing.T) { + ctx := context.Background() + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + Name: "mock_name", + CallID: "mock_call_id_12345", + }, + ToolArgument: &schema.ToolArgument{Text: "anything"}, + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "hello"}}}, + } + + fn := defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file") + info, err := fn(ctx, detail) + assert.NoError(t, err) + assert.Equal(t, &ClearResult{ + ToolArgument: &schema.ToolArgument{Text: "anything"}, + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{ + { + Type: schema.ToolPartTypeText, + Text: "Tool result saved to: /tmp/clear/mock_call_id_12345\nUse read_file to view", + }, + }}, + NeedClear: true, + NeedOffload: true, + OffloadFilePath: "/tmp/clear/mock_call_id_12345", + OffloadContent: "hello", + }, info) + +} + +func mockInvokableTool() tool.InvokableTool { + type ContentContainer struct { + Value string `json:"value"` + } + s1 := strings.Repeat("hello world", 10) + "\n" + s2 := strings.Repeat("hello world", 8) + s3 := s1 + s2 + t, _ := utils.InferTool("mock_invokable_tool", "test desc", func(ctx context.Context, input *ContentContainer) (output string, err error) { + return s3, nil + }) + return t +} + +func mockStreamableTool() tool.StreamableTool { + type ContentContainer struct { + Value string `json:"value"` + } + s1 := strings.Repeat("hello world", 10) + "\n" + s2 := strings.Repeat("hello world", 8) + s3 := s1 + s2 + t, _ := utils.InferStreamTool("mock_streamable_tool", "test desc", func(ctx context.Context, input ContentContainer) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](11) + for _, part := range splitStrings(s3, 10) { + sw.Send(part, nil) + } + sw.Close() + return sr, nil + }) + return t +} + +func mockStreamableToolWithError() tool.StreamableTool { + type ContentContainer struct { + Value string `json:"value"` + } + s1 := strings.Repeat("hello world", 10) + "\n" + s2 := strings.Repeat("hello world", 8) + s3 := s1 + s2 + t, _ := utils.InferStreamTool("mock_streamable_tool", "test desc", func(ctx context.Context, input ContentContainer) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](11) + for _, part := range splitStrings(s3, 10) { + sw.Send(part, nil) + } + sw.Send("", fmt.Errorf("mock error")) + sw.Close() + return sr, nil + }) + return t +} + +func splitStrings(s string, n int) []string { + if n <= 0 { + n = 1 + } + if n == 1 { + return []string{s} + } + if len(s) <= n { + parts := make([]string, n) + for i := 0; i < len(s); i++ { + parts[i] = string(s[i]) + } + return parts + } + baseLen := len(s) / n + extra := len(s) % n + parts := make([]string, 0, n) + start := 0 + for i := 0; i < n; i++ { + end := start + baseLen + if i < extra { + end++ + } + parts = append(parts, s[start:end]) + start = end + } + return parts +} + +func toJson(v any) string { + b, _ := json.Marshal(v) + return string(b) +} + +func TestToolResultFromMessage(t *testing.T) { + t.Run("test from content", func(t *testing.T) { + msg := schema.ToolMessage("test content", "call_123") + result, fromContent, err := toolResultFromMessage(msg) + assert.NoError(t, err) + assert.True(t, fromContent) + assert.NotNil(t, result) + assert.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "test content", result.Parts[0].Text) + }) + + t.Run("test from user input multi content", func(t *testing.T) { + msg := schema.ToolMessage("", "call_456") + msg.UserInputMultiContent = []schema.MessageInputPart{ + { + Type: schema.ChatMessagePartTypeText, + Text: "test text", + }, + } + result, fromContent, err := toolResultFromMessage(msg) + assert.NoError(t, err) + assert.False(t, fromContent) + assert.NotNil(t, result) + assert.Len(t, result.Parts, 1) + assert.Equal(t, schema.ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "test text", result.Parts[0].Text) + }) + + t.Run("test invalid role", func(t *testing.T) { + msg := schema.UserMessage("test user message") + _, _, err := toolResultFromMessage(msg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "message role") + }) +} + +func TestConvMessageInputPartToToolOutputPart(t *testing.T) { + t.Run("test text type", func(t *testing.T) { + part := schema.MessageInputPart{ + Type: schema.ChatMessagePartTypeText, + Text: "test text", + } + result, err := convMessageInputPartToToolOutputPart(part) + assert.NoError(t, err) + assert.Equal(t, schema.ToolPartTypeText, result.Type) + assert.Equal(t, "test text", result.Text) + }) + + t.Run("test image url type", func(t *testing.T) { + part := schema.MessageInputPart{ + Type: schema.ChatMessagePartTypeImageURL, + Image: &schema.MessageInputImage{}, + } + result, err := convMessageInputPartToToolOutputPart(part) + assert.NoError(t, err) + assert.Equal(t, schema.ToolPartTypeImage, result.Type) + assert.NotNil(t, result.Image) + }) + + t.Run("test audio url type", func(t *testing.T) { + part := schema.MessageInputPart{ + Type: schema.ChatMessagePartTypeAudioURL, + Audio: &schema.MessageInputAudio{}, + } + result, err := convMessageInputPartToToolOutputPart(part) + assert.NoError(t, err) + assert.Equal(t, schema.ToolPartTypeAudio, result.Type) + assert.NotNil(t, result.Audio) + }) + + t.Run("test video url type", func(t *testing.T) { + part := schema.MessageInputPart{ + Type: schema.ChatMessagePartTypeVideoURL, + Video: &schema.MessageInputVideo{}, + } + result, err := convMessageInputPartToToolOutputPart(part) + assert.NoError(t, err) + assert.Equal(t, schema.ToolPartTypeVideo, result.Type) + assert.NotNil(t, result.Video) + }) + + t.Run("test file url type", func(t *testing.T) { + part := schema.MessageInputPart{ + Type: schema.ChatMessagePartTypeFileURL, + File: &schema.MessageInputFile{}, + } + result, err := convMessageInputPartToToolOutputPart(part) + assert.NoError(t, err) + assert.Equal(t, schema.ToolPartTypeFile, result.Type) + assert.NotNil(t, result.File) + }) + + t.Run("test unknown type", func(t *testing.T) { + part := schema.MessageInputPart{ + Type: "unknown_type", + } + _, err := convMessageInputPartToToolOutputPart(part) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown msg part type") + }) +} + +func TestGetSetMsgOffloadedFlag(t *testing.T) { + t.Run("test get offloaded flag - not set", func(t *testing.T) { + msg := schema.UserMessage("test") + assert.False(t, getMsgClearedFlag(msg)) + }) + + t.Run("test get offloaded flag - set", func(t *testing.T) { + msg := schema.UserMessage("test") + setMsgClearedFlag(msg) + assert.True(t, getMsgClearedFlag(msg)) + }) + + t.Run("test set offloaded flag - nil extra", func(t *testing.T) { + msg := schema.UserMessage("test") + setMsgClearedFlag(msg) + assert.True(t, getMsgClearedFlag(msg)) + }) + + t.Run("test set offloaded flag - existing extra", func(t *testing.T) { + msg := schema.UserMessage("test") + msg.Extra = map[string]any{"existing": "value"} + setMsgClearedFlag(msg) + assert.True(t, getMsgClearedFlag(msg)) + assert.Equal(t, "value", msg.Extra["existing"]) + }) +} + +func TestNewErrors(t *testing.T) { + ctx := context.Background() + + t.Run("test nil config", func(t *testing.T) { + _, err := New(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config must not be nil") + }) + + t.Run("test no backend when not skipping truncation", func(t *testing.T) { + config := &Config{ + Backend: nil, + SkipTruncation: false, + } + _, err := New(ctx, config) + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend must be set") + }) +} + +func TestGetToolConfig(t *testing.T) { + ctx := context.Background() + backend := filesystem.NewInMemoryBackend() + + t.Run("test no tool config", func(t *testing.T) { + config := &Config{ + Backend: backend, + SkipTruncation: true, + SkipClear: true, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + trmw, ok := mw.(*typedToolReductionMiddleware[*schema.Message]) + assert.True(t, ok) + + cfg := trmw.getToolConfig("non_existent_tool", sceneTruncation) + assert.NotNil(t, cfg) + }) + + t.Run("test with tool config", func(t *testing.T) { + config := &Config{ + Backend: backend, + SkipTruncation: true, + SkipClear: true, + ToolConfig: map[string]*ToolReductionConfig{ + "test_tool": { + SkipTruncation: true, + SkipClear: true, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + trmw, ok := mw.(*typedToolReductionMiddleware[*schema.Message]) + assert.True(t, ok) + + cfg := trmw.getToolConfig("test_tool", sceneTruncation) + assert.NotNil(t, cfg) + assert.True(t, cfg.SkipTruncation) + }) + + t.Run("test with tool config needing default handler", func(t *testing.T) { + config := &Config{ + Backend: backend, + SkipTruncation: false, + ToolConfig: map[string]*ToolReductionConfig{ + "test_tool": { + SkipTruncation: false, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + trmw, ok := mw.(*typedToolReductionMiddleware[*schema.Message]) + assert.True(t, ok) + + cfg := trmw.getToolConfig("test_tool", sceneTruncation) + assert.NotNil(t, cfg) + assert.NotNil(t, cfg.TruncHandler) + }) +} + +func TestCopyAndFillDefaults(t *testing.T) { + t.Run("test empty config", func(t *testing.T) { + cfg := &Config{} + result, err := cfg.copyAndFillDefaults() + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "/tmp", result.RootDir) + assert.Equal(t, "read_file", result.ReadFileToolName) + assert.Equal(t, 50000, result.MaxLengthForTrunc) + assert.Equal(t, 1, result.ClearRetentionSuffixLimit) + assert.NotNil(t, result.TokenCounter) + assert.NotNil(t, result.GenTruncOffloadFilePath) + assert.NotNil(t, result.GenClearOffloadFilePath) + + truncPath, err := result.GenTruncOffloadFilePath(context.Background(), &ToolDetail{ + ToolContext: &adk.ToolContext{CallID: "call_1"}, + }) + assert.NoError(t, err) + assert.Equal(t, "/tmp/trunc/call_1", truncPath) + + clearPath, err := result.GenClearOffloadFilePath(context.Background(), &ToolDetail{ + ToolContext: &adk.ToolContext{CallID: "call_1"}, + }) + assert.NoError(t, err) + assert.Equal(t, "/tmp/clear/call_1", clearPath) + }) + + t.Run("test default offload path generators create uuid when CallID is empty", func(t *testing.T) { + cfg := &Config{} + result, err := cfg.copyAndFillDefaults() + assert.NoError(t, err) + + truncPath, err := result.GenTruncOffloadFilePath(context.Background(), &ToolDetail{ + ToolContext: &adk.ToolContext{CallID: ""}, + }) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(truncPath, "/tmp/trunc/")) + assert.NotEqual(t, "/tmp/trunc/", truncPath) + + clearPath, err := result.GenClearOffloadFilePath(context.Background(), &ToolDetail{ + ToolContext: &adk.ToolContext{CallID: ""}, + }) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(clearPath, "/tmp/clear/")) + assert.NotEqual(t, "/tmp/clear/", clearPath) + }) + + t.Run("test with tool config", func(t *testing.T) { + cfg := &Config{ + ToolConfig: map[string]*ToolReductionConfig{ + "test_tool": { + SkipTruncation: true, + }, + }, + } + result, err := cfg.copyAndFillDefaults() + assert.NoError(t, err) + assert.NotNil(t, result.ToolConfig) + assert.True(t, result.ToolConfig["test_tool"].SkipTruncation) + }) + + t.Run("test custom offload path generators preserved", func(t *testing.T) { + cfg := &Config{ + RootDir: "/tmp/ignored", + GenTruncOffloadFilePath: func(_ context.Context, detail *ToolDetail) (string, error) { + return "/custom/trunc/" + detail.ToolContext.CallID, nil + }, + GenClearOffloadFilePath: func(_ context.Context, detail *ToolDetail) (string, error) { + return "/custom/clear/" + detail.ToolContext.CallID, nil + }, + } + result, err := cfg.copyAndFillDefaults() + assert.NoError(t, err) + + filePath, err := result.GenClearOffloadFilePath(context.Background(), &ToolDetail{ + ToolContext: &adk.ToolContext{CallID: "call_custom"}, + }) + assert.NoError(t, err) + assert.Equal(t, "/custom/clear/call_custom", filePath) + }) +} + +func TestDefaultTokenCounter(t *testing.T) { + ctx := context.Background() + + t.Run("test with nil messages", func(t *testing.T) { + msgs := []*schema.Message{nil} + tokens, err := defaultTokenCounter(ctx, msgs, nil) + assert.NoError(t, err) + assert.GreaterOrEqual(t, tokens, int64(0)) + }) + + t.Run("test with tool info", func(t *testing.T) { + toolInfo := &schema.ToolInfo{ + Name: "test_tool", + Desc: "test description", + } + tokens, err := defaultTokenCounter(ctx, nil, []*schema.ToolInfo{toolInfo}) + assert.NoError(t, err) + assert.GreaterOrEqual(t, tokens, int64(0)) + }) +} + +func TestDefaultClearHandler(t *testing.T) { + ctx := context.Background() + + t.Run("test empty parts", func(t *testing.T) { + handler := defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file") + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + CallID: "test_call", + }, + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{}}, + } + result, err := handler(ctx, detail) + assert.NoError(t, err) + assert.False(t, result.NeedClear) + }) + + t.Run("test multimodal content", func(t *testing.T) { + handler := defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file") + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{ + CallID: "test_call", + }, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeImage}}, + }, + } + result, err := handler(ctx, detail) + assert.NoError(t, err) + assert.Equal(t, detail.ToolResult, result.ToolResult) + }) + + t.Run("test no call id", func(t *testing.T) { + handler := defaultClearHandler(testClearOffloadPath("/tmp"), true, "read_file") + detail := &ToolDetail{ + ToolContext: &adk.ToolContext{}, + ToolResult: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "test"}}, + }, + } + result, err := handler(ctx, detail) + assert.NoError(t, err) + assert.True(t, result.NeedClear) + assert.Equal(t, "/tmp/clear/generated", result.OffloadFilePath) + }) +} + +func TestStringifyToolOutputParts(t *testing.T) { + t.Run("test empty parts", func(t *testing.T) { + result := stringifyToolOutputParts([]schema.ToolOutputPart{}) + assert.Equal(t, "", result) + }) + + t.Run("test single text part", func(t *testing.T) { + result := stringifyToolOutputParts([]schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "hello world"}, + }) + assert.Equal(t, "hello world", result) + }) + + t.Run("test multiple text parts", func(t *testing.T) { + result := stringifyToolOutputParts([]schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "hello "}, + {Type: schema.ToolPartTypeText, Text: "world"}, + }) + expected := `[ + { + "type": "text", + "text": "hello " + }, + { + "type": "text", + "text": "world" + } +]` + assert.JSONEq(t, expected, result) + }) + + t.Run("test with image part", func(t *testing.T) { + result := stringifyToolOutputParts([]schema.ToolOutputPart{ + {Type: schema.ToolPartTypeImage, Text: "https://example.com/image.png"}, + }) + expected := `[ + { + "type": "image", + "text": "https://example.com/image.png" + } +]` + assert.JSONEq(t, expected, result) + }) + + t.Run("test mixed parts", func(t *testing.T) { + result := stringifyToolOutputParts([]schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "hello world"}, + {Type: schema.ToolPartTypeImage, Text: "https://example.com/image.png"}, + }) + expected := `[ + { + "type": "text", + "text": "hello world" + }, + { + "type": "image", + "text": "https://example.com/image.png" + } +]` + assert.JSONEq(t, expected, result) + }) +} + +func TestClampPrefixToUTF8Boundary(t *testing.T) { + t.Run("test n <= 0", func(t *testing.T) { + result := clampPrefixToUTF8Boundary("hello world", 0) + assert.Equal(t, "", result) + + result = clampPrefixToUTF8Boundary("hello world", -5) + assert.Equal(t, "", result) + }) + + t.Run("test n >= len(s)", func(t *testing.T) { + result := clampPrefixToUTF8Boundary("hello", 10) + assert.Equal(t, "hello", result) + }) + + t.Run("test ASCII string at rune boundary", func(t *testing.T) { + result := clampPrefixToUTF8Boundary("hello world", 5) + assert.Equal(t, "hello", result) + }) + + t.Run("test ASCII string not at rune boundary (should not happen)", func(t *testing.T) { + result := clampPrefixToUTF8Boundary("world", 3) + assert.Equal(t, "wor", result) + }) + + t.Run("test multi-byte UTF-8 characters at boundary", func(t *testing.T) { + s := "你好世界" + result := clampPrefixToUTF8Boundary(s, 3) + assert.Equal(t, "你", result) + }) + + t.Run("test multi-byte UTF-8 characters not at boundary", func(t *testing.T) { + s := "你好世界" + result := clampPrefixToUTF8Boundary(s, 2) + assert.Equal(t, "", result) + + result = clampPrefixToUTF8Boundary(s, 4) + assert.Equal(t, "你", result) + }) + + t.Run("test mixed ASCII and multi-byte characters", func(t *testing.T) { + s := "hello你好world" + result := clampPrefixToUTF8Boundary(s, 5+3) + assert.Equal(t, "hello你", result) + + result = clampPrefixToUTF8Boundary(s, 5+4) + assert.Equal(t, "hello你", result) + + result = clampPrefixToUTF8Boundary(s, 5+6) + assert.Equal(t, "hello你好", result) + }) +} + +func TestClampSuffixToUTF8Boundary(t *testing.T) { + t.Run("test n <= 0", func(t *testing.T) { + result := clampSuffixToUTF8Boundary("hello world", 0) + assert.Equal(t, "", result) + + result = clampSuffixToUTF8Boundary("hello world", -5) + assert.Equal(t, "", result) + }) + + t.Run("test n >= len(s)", func(t *testing.T) { + result := clampSuffixToUTF8Boundary("hello", 10) + assert.Equal(t, "hello", result) + }) + + t.Run("test ASCII string at rune boundary", func(t *testing.T) { + result := clampSuffixToUTF8Boundary("hello world", 5) + assert.Equal(t, "world", result) + }) + + t.Run("test ASCII string not at rune boundary (should not happen)", func(t *testing.T) { + result := clampSuffixToUTF8Boundary("hello", 3) + assert.Equal(t, "llo", result) + }) + + t.Run("test multi-byte UTF-8 characters at boundary", func(t *testing.T) { + s := "你好世界" + assert.Equal(t, 12, len(s)) + result := clampSuffixToUTF8Boundary(s, 3) + assert.Equal(t, "界", result) + }) + + t.Run("test multi-byte UTF-8 characters not at boundary", func(t *testing.T) { + s := "你好世界" + result := clampSuffixToUTF8Boundary(s, 2) + assert.Equal(t, "", result) + + result = clampSuffixToUTF8Boundary(s, 4) + assert.Equal(t, "界", result) + + result = clampSuffixToUTF8Boundary(s, 6) + assert.Equal(t, "世界", result) + }) + + t.Run("test mixed ASCII and multi-byte characters", func(t *testing.T) { + s := "hello你好world" + result := clampSuffixToUTF8Boundary(s, 5+3) + assert.Equal(t, "好world", result) + + result = clampSuffixToUTF8Boundary(s, 5+4) + assert.Equal(t, "好world", result) + + result = clampSuffixToUTF8Boundary(s, 5+6) + assert.Equal(t, "你好world", result) + }) +} + +func TestGetMsgClearedFlag(t *testing.T) { + t.Run("test non-bool value in extra", func(t *testing.T) { + msg := schema.UserMessage("test") + msg.Extra = map[string]any{msgClearedFlag: "not a bool"} + flag := getMsgClearedFlag(msg) + assert.False(t, flag) + }) +} + +func TestCopyMessages(t *testing.T) { + t.Run("test empty messages", func(t *testing.T) { + result := copyMessages(nil) + assert.Len(t, result, 0) + + result = copyMessages([]*schema.Message{}) + assert.Len(t, result, 0) + }) + + t.Run("test message with tool calls and extra", func(t *testing.T) { + msg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_123", + Type: "function", + Function: schema.FunctionCall{Name: "test_func", Arguments: `{"key": "value"}`}, + }, + }) + msg.Extra = map[string]any{"test_key": "test_value"} + msg.UserInputMultiContent = []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "test text"}, + } + msg.MultiContent = []schema.ChatMessagePart{ + {Type: schema.ChatMessagePartTypeText, Text: "multi content"}, + } + msg.AssistantGenMultiContent = []schema.MessageOutputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "assistant gen"}, + } + + result := copyMessages([]*schema.Message{msg}) + assert.Len(t, result, 1) + assert.NotEqual(t, &msg, result[0]) + assert.Len(t, result[0].ToolCalls, 1) + assert.Equal(t, "call_123", result[0].ToolCalls[0].ID) + assert.Equal(t, "test_value", result[0].Extra["test_key"]) + assert.Len(t, result[0].UserInputMultiContent, 1) + assert.Len(t, result[0].MultiContent, 1) + assert.Len(t, result[0].AssistantGenMultiContent, 1) + }) +} + +func TestReductionMiddlewareEnhancedTrunc(t *testing.T) { + ctx := context.Background() + + t.Run("enhanced invokable - passthrough when truncation skipped", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + SkipTruncation: true, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + + called := 0 + endpoint := func(_ context.Context, ta *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + called++ + assert.Same(t, toolArg, ta) + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "ok"}}}, nil + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + got, err := wrapped(ctx, toolArg) + assert.NoError(t, err) + assert.Equal(t, 1, called) + assert.Equal(t, "ok", got.Parts[0].Text) + }) + + t.Run("enhanced invokable - excluded tool bypasses truncation", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + TruncExcludeTools: []string{"enh_tool"}, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + t.Fatalf("trunc handler should not be called when tool is excluded") + return nil, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + got, err := wrapped(ctx, toolArg) + assert.NoError(t, err) + assert.Equal(t, "orig", got.Parts[0].Text) + }) + + t.Run("enhanced invokable - endpoint error returned", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + wantErr := errors.New("endpoint error") + + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + t.Fatalf("trunc handler should not be called on endpoint error") + return nil, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return nil, wantErr + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + _, err = wrapped(ctx, toolArg) + assert.Equal(t, wantErr, err) + }) + + t.Run("enhanced invokable - trunc handler error returned", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + wantErr := errors.New("handler error") + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + return nil, wantErr + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + _, err = wrapped(ctx, toolArg) + assert.Equal(t, wantErr, err) + }) + + t.Run("enhanced invokable - no trunc returns original result", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, detail *ToolDetail) (*TruncResult, error) { + assert.NotNil(t, detail.ToolResult) + assert.Equal(t, "orig", detail.ToolResult.Parts[0].Text) + return &TruncResult{NeedTrunc: false}, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + got, err := wrapped(ctx, toolArg) + assert.NoError(t, err) + assert.Equal(t, "orig", got.Parts[0].Text) + }) + + t.Run("enhanced invokable - offload without backend returns error", func(t *testing.T) { + // config.Backend must be non-nil to pass New(), but the per-tool cfg.Backend is nil. + configBackend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: configBackend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_tool": { + Backend: nil, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + return &TruncResult{ + NeedTrunc: true, + NeedOffload: true, + OffloadFilePath: "/tmp/trunc/cid", + OffloadContent: "big", + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "trunc"}}}, + }, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + _, err = wrapped(ctx, toolArg) + assert.EqualError(t, err, "truncation: no backend for offload") + }) + + t.Run("enhanced invokable - trunc + offload writes and returns trunc result", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, detail *ToolDetail) (*TruncResult, error) { + assert.NotNil(t, detail.ToolContext) + assert.NotNil(t, detail.ToolArgument) + assert.NotNil(t, detail.ToolResult) + return &TruncResult{ + NeedTrunc: true, + NeedOffload: true, + OffloadFilePath: "/tmp/trunc/cid", + OffloadContent: "full content", + ToolResult: &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "truncated"}}}, + }, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_tool", CallID: "cid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, ta *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + assert.Same(t, toolArg, ta) + return &schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil + } + + wrapped, err := mw.WrapEnhancedInvokableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + got, err := wrapped(ctx, toolArg) + assert.NoError(t, err) + assert.Equal(t, "truncated", got.Parts[0].Text) + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/cid"}) + assert.NoError(t, err) + assert.Equal(t, "full content", content.Content) + }) + + t.Run("enhanced streamable - no trunc returns original stream (Copy semantics)", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_stream_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, detail *ToolDetail) (*TruncResult, error) { + // Consume the "analysis" copy; the returned reader should still have the full stream. + parts, needProcess, err := getJointToolResult(detail) + assert.NoError(t, err) + assert.True(t, needProcess) + assert.Len(t, parts, 1) + assert.Equal(t, "hello world", parts[0].Text) + return &TruncResult{NeedTrunc: false}, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_stream_tool", CallID: "scid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr, sw := schema.Pipe[*schema.ToolResult](4) + go func() { + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "hello "}}}, nil) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "world"}}}, nil) + sw.Close() + }() + return sr, nil + } + + wrapped, err := mw.WrapEnhancedStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + resp, err := wrapped(ctx, toolArg) + assert.NoError(t, err) + defer resp.Close() + + var chunks []*schema.ToolResult + for { + tr, recvErr := resp.Recv() + if recvErr != nil { + assert.Equal(t, io.EOF, recvErr) + break + } + chunks = append(chunks, tr) + } + joined, err := schema.ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Equal(t, "hello world", joined.Parts[0].Text) + }) + + t.Run("enhanced streamable - trunc + offload writes and returns trunc stream", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_stream_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + sr, sw := schema.Pipe[*schema.ToolResult](1) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "truncated"}}}, nil) + sw.Close() + return &TruncResult{ + NeedTrunc: true, + NeedOffload: true, + OffloadFilePath: "/tmp/trunc/scid", + OffloadContent: "full content", + StreamToolResult: sr, + }, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_stream_tool", CallID: "scid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr, sw := schema.Pipe[*schema.ToolResult](2) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil) + sw.Close() + return sr, nil + } + + wrapped, err := mw.WrapEnhancedStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + resp, err := wrapped(ctx, toolArg) + assert.NoError(t, err) + defer resp.Close() + + tr, err := resp.Recv() + assert.NoError(t, err) + assert.Equal(t, "truncated", tr.Parts[0].Text) + + _, err = resp.Recv() + assert.Equal(t, io.EOF, err) + + content, err := backend.Read(ctx, &filesystem.ReadRequest{FilePath: "/tmp/trunc/scid"}) + assert.NoError(t, err) + assert.Equal(t, "full content", content.Content) + }) + + t.Run("enhanced streamable - endpoint error returned", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + wantErr := errors.New("endpoint error") + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_stream_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + t.Fatalf("trunc handler should not be called on endpoint error") + return nil, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_stream_tool", CallID: "scid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return nil, wantErr + } + + wrapped, err := mw.WrapEnhancedStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + _, err = wrapped(ctx, toolArg) + assert.Equal(t, wantErr, err) + }) + + t.Run("enhanced streamable - trunc handler error returned", func(t *testing.T) { + backend := filesystem.NewInMemoryBackend() + wantErr := errors.New("handler error") + config := &Config{ + Backend: backend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_stream_tool": { + Backend: backend, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + return nil, wantErr + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_stream_tool", CallID: "scid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr, sw := schema.Pipe[*schema.ToolResult](1) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil) + sw.Close() + return sr, nil + } + + wrapped, err := mw.WrapEnhancedStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + _, err = wrapped(ctx, toolArg) + assert.Equal(t, wantErr, err) + }) + + t.Run("enhanced streamable - offload without backend returns error", func(t *testing.T) { + // config.Backend must be non-nil to pass New(), but the per-tool cfg.Backend is nil. + configBackend := filesystem.NewInMemoryBackend() + config := &Config{ + Backend: configBackend, + ToolConfig: map[string]*ToolReductionConfig{ + "enh_stream_tool": { + Backend: nil, + SkipTruncation: false, + TruncHandler: func(_ context.Context, _ *ToolDetail) (*TruncResult, error) { + sr, sw := schema.Pipe[*schema.ToolResult](1) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "truncated"}}}, nil) + sw.Close() + return &TruncResult{ + NeedTrunc: true, + NeedOffload: true, + OffloadFilePath: "/tmp/trunc/scid", + OffloadContent: "full content", + StreamToolResult: sr, + }, nil + }, + }, + }, + } + mw, err := New(ctx, config) + assert.NoError(t, err) + + tCtx := &adk.ToolContext{Name: "enh_stream_tool", CallID: "scid"} + toolArg := &schema.ToolArgument{Text: `{"k":"v"}`} + endpoint := func(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr, sw := schema.Pipe[*schema.ToolResult](1) + sw.Send(&schema.ToolResult{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "orig"}}}, nil) + sw.Close() + return sr, nil + } + + wrapped, err := mw.WrapEnhancedStreamableToolCall(ctx, endpoint, tCtx) + assert.NoError(t, err) + _, err = wrapped(ctx, toolArg) + assert.EqualError(t, err, "truncation: no backend for offload") + }) +} + +func TestClearRewriteMessagesHandler(t *testing.T) { + ctx := context.Background() + it := mockInvokableTool() + st := mockStreamableTool() + tools := []tool.BaseTool{it, st} + var toolsInfo []*schema.ToolInfo + for _, bt := range tools { + ti, _ := bt.Info(ctx) + toolsInfo = append(toolsInfo, ti) + } + + tokenCounter := func(_ context.Context, msgs []adk.Message, _ []*schema.ToolInfo) (int64, error) { + return int64(1000), nil + } + + t.Run("test all tools hit - rewrite to system reminder", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{ + schema.UserMessage("write_file and edit_file executed successfully"), + }, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt", "content": "hello"}`}, + }, + { + ID: "call_2", + Type: "function", + Function: schema.FunctionCall{Name: "edit_file", Arguments: `{"file": "test.txt", "changes": "add"}`}, + }, + }), + schema.ToolMessage("write success", "call_1"), + schema.ToolMessage("edit success", "call_2"), + schema.AssistantMessage("done", nil), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Len(t, s.Messages, 5) + assert.Equal(t, schema.User, s.Messages[2].Role) + assert.Equal(t, "write_file and edit_file executed successfully", s.Messages[2].Content) + assert.Equal(t, schema.Assistant, s.Messages[3].Role) + assert.Equal(t, "done", s.Messages[3].Content) + }) + + t.Run("test remove tool call and tool responses", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{}, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt", "content": "hello"}`}, + }, + { + ID: "call_2", + Type: "function", + Function: schema.FunctionCall{Name: "edit_file", Arguments: `{"file": "test.txt", "changes": "add"}`}, + }, + }), + schema.ToolMessage("write success", "call_1"), + schema.ToolMessage("edit success", "call_2"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + schema.ToolMessage("dummy dummy", "dummy"), + schema.AssistantMessage("done", nil), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Len(t, s.Messages, 5) + assert.Equal(t, schema.Assistant, s.Messages[2].Role) + assert.Equal(t, schema.Tool, s.Messages[3].Role) + assert.Equal(t, "dummy dummy", s.Messages[3].Content) + assert.Equal(t, schema.Assistant, s.Messages[4].Role) + assert.Equal(t, "done", s.Messages[4].Content) + }) + + t.Run("test partial tools hit - keep unhit tools and rewrite hit ones", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearExcludeTools: []string{"get_weather"}, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + hitToolIDs := map[string]bool{"call_1": true} + + newToolCalls := make([]schema.ToolCall, 0) + newToolResponses := make([]adk.Message, 0) + + for i, tc := range assistantMessage.ToolCalls { + if !hitToolIDs[tc.ID] { + newToolCalls = append(newToolCalls, tc) + if i < len(toolResponses) { + newToolResponses = append(newToolResponses, toolResponses[i]) + } + } + } + + result := []adk.Message{ + schema.UserMessage("write_file executed successfully"), + } + if len(newToolCalls) > 0 { + result = append(result, schema.AssistantMessage("", newToolCalls)) + result = append(result, newToolResponses...) + } + return result, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt", "content": "hello"}`}, + }, + { + ID: "call_2", + Type: "function", + Function: schema.FunctionCall{Name: "get_weather", Arguments: `{"location": "London"}`}, + }, + }), + schema.ToolMessage("write success", "call_1"), + schema.ToolMessage("sunny", "call_2"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Len(t, s.Messages, 6) + assert.Equal(t, schema.User, s.Messages[2].Role) + assert.Equal(t, "write_file executed successfully", s.Messages[2].Content) + assert.Equal(t, schema.Assistant, s.Messages[3].Role) + assert.Len(t, s.Messages[3].ToolCalls, 1) + assert.Equal(t, "call_2", s.Messages[3].ToolCalls[0].ID) + assert.Equal(t, schema.Tool, s.Messages[4].Role) + assert.Equal(t, "sunny", s.Messages[4].Content) + }) + + t.Run("test mixed messages with system/user before", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{ + schema.UserMessage("tool executed"), + }, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("first request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt"}`}, + }, + }), + schema.ToolMessage("success", "call_1"), + schema.UserMessage("second request"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Len(t, s.Messages, 5) + assert.Equal(t, schema.System, s.Messages[0].Role) + assert.Equal(t, schema.User, s.Messages[1].Role) + assert.Equal(t, schema.User, s.Messages[2].Role) + assert.Equal(t, "tool executed", s.Messages[2].Content) + assert.Equal(t, schema.User, s.Messages[3].Role) + }) + + t.Run("test mixed messages with system/user before", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{ + schema.UserMessage("tool executed"), + }, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("first request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt"}`}, + }, + }), + schema.ToolMessage("success", "call_1"), + schema.UserMessage("second request"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Len(t, s.Messages, 5) + assert.Equal(t, schema.System, s.Messages[0].Role) + assert.Equal(t, schema.User, s.Messages[1].Role) + assert.Equal(t, schema.User, s.Messages[2].Role) + assert.Equal(t, "tool executed", s.Messages[2].Content) + assert.Equal(t, schema.User, s.Messages[3].Role) + }) + + t.Run("test assistant message without tool calls - keep as is", func(t *testing.T) { + handlerCalled := false + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + handlerCalled = true + return nil, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("regular response without tools", nil), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.False(t, handlerCalled) + assert.Len(t, s.Messages, 4) + assert.Equal(t, schema.Assistant, s.Messages[2].Role) + assert.Equal(t, "regular response without tools", s.Messages[2].Content) + }) + + t.Run("test clear not meeting threshold - should keep original messages", func(t *testing.T) { + originalTokenCount := int64(1000) + rewrittenTokenCount := int64(999) + callCount := 0 + + config := &Config{ + SkipTruncation: true, + TokenCounter: func(_ context.Context, msgs []adk.Message, _ []*schema.ToolInfo) (int64, error) { + callCount++ + if callCount == 1 { + return originalTokenCount, nil + } + return rewrittenTokenCount, nil + }, + MaxTokensForClear: 10, + ClearAtLeastTokens: 10, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{ + schema.UserMessage("tool executed"), + }, nil + }, + } + + originalMessages := []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt"}`}, + }, + }), + schema.ToolMessage("success", "call_1"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: originalMessages, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.Len(t, s.Messages, len(originalMessages)) + for i := range originalMessages { + assert.Equal(t, originalMessages[i].Role, s.Messages[i].Role) + assert.Equal(t, originalMessages[i].Content, s.Messages[i].Content) + } + }) + + t.Run("test applyClearRewrite with default role", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{}, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + unknownRoleMsg := &schema.Message{ + Role: "unknown_role", + Content: "unknown", + } + + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + unknownRoleMsg, + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.NotNil(t, s) + }) + + t.Run("test applyClearRewrite with rewrite error", func(t *testing.T) { + expectedErr := fmt.Errorf("rewrite error") + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return nil, expectedErr + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + + _, _, err = mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt"}`}, + }, + }), + schema.ToolMessage("success", "call_1"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("test applyClearRewrite with missing tool responses", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 0, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + assert.Nil(t, toolResponses) + return []adk.Message{ + schema.UserMessage("tool executed"), + }, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt"}`}, + }, + }), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.NotNil(t, s) + }) + + t.Run("test applyClearRewrite with clearAtLeastTokens > 0", func(t *testing.T) { + config := &Config{ + SkipTruncation: true, + TokenCounter: tokenCounter, + MaxTokensForClear: 10, + ClearAtLeastTokens: 10, + ClearMessageRewriter: func(ctx context.Context, assistantMessage adk.Message, toolResponses []adk.Message) (messagesAfterRewrite []adk.Message, err error) { + return []adk.Message{ + schema.UserMessage("tool executed"), + }, nil + }, + } + + mw, err := New(ctx, config) + assert.NoError(t, err) + _, s, err := mw.BeforeModelRewriteState(ctx, &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("you are a helpful assistant"), + schema.UserMessage("user request"), + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{Name: "write_file", Arguments: `{"file": "test.txt"}`}, + }, + }), + schema.ToolMessage("success", "call_1"), + schema.AssistantMessage("", []schema.ToolCall{{ID: "dummy", Type: "function", Function: schema.FunctionCall{Name: "dummy_tool"}}}), + }, + }, &adk.ModelContext{ + Tools: toolsInfo, + }) + assert.NoError(t, err) + assert.NotNil(t, s) + }) +} + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped(ctx, &TypedConfig[*schema.AgenticMessage]{ + SkipTruncation: true, + SkipClear: true, + }) + assert.NoError(t, err) + assert.NotNil(t, mw) + + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} diff --git a/adk/middlewares/skill/doc.go b/adk/middlewares/skill/doc.go new file mode 100644 index 0000000..9eea039 --- /dev/null +++ b/adk/middlewares/skill/doc.go @@ -0,0 +1,47 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package skill provides a Skill middleware, types, and a local filesystem backend. +// +// # Overview +// +// The Skill middleware is a ChatModelAgentMiddleware implementation that injects: +// - a system instruction (Skills System) +// - a tool (default name: "skill") to load and execute skills +// +// Skill definitions are stored in SKILL.md files with a YAML frontmatter. The frontmatter is +// parsed into FrontMatter (name/description/context/agent/model), and the remaining markdown +// is treated as the skill content. +// +// # Execution modes +// +// Skill execution is controlled by frontmatter "context": +// - inline (default): returns the skill content as the tool result in the current agent +// - fork: runs the skill with a new sub-agent without parent message history +// - fork_with_context: runs the skill with a new sub-agent carrying parent message history +// +// # Extension points +// +// - CustomToolParams customizes the tool parameter schema. +// - BuildContent customizes how skill content is generated from raw tool arguments. +// - BuildForkMessages customizes the initial messages passed to the sub-agent in fork modes. +// - FormatForkResult customizes how sub-agent outputs are formatted back to the caller. +// +// # Filesystem backend +// +// NewBackendFromFilesystem loads skills from a filesystem backend. It scans only first-level +// subdirectories under BaseDir and reads each /SKILL.md as a skill definition. +package skill diff --git a/adk/middlewares/skill/filesystem_backend.go b/adk/middlewares/skill/filesystem_backend.go new file mode 100644 index 0000000..a337a5a --- /dev/null +++ b/adk/middlewares/skill/filesystem_backend.go @@ -0,0 +1,186 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package skill + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/cloudwego/eino/adk/filesystem" +) + +const skillFileName = "SKILL.md" + +type filesystemBackend struct { + backend filesystem.Backend + baseDir string +} + +// BackendFromFilesystemConfig contains configuration for NewBackendFromFilesystem. +type BackendFromFilesystemConfig struct { + // Backend is the filesystem.Backend implementation used for file operations. + Backend filesystem.Backend + // BaseDir is the base directory where skill directories are located. + // Each skill should be in a subdirectory containing a SKILL.md file. + BaseDir string +} + +// NewBackendFromFilesystem creates a new Backend implementation that reads skills from a filesystem. +// It searches for SKILL.md files in immediate subdirectories of the configured BaseDir. +// Only first-level subdirectories are scanned; deeply nested SKILL.md files are ignored. +func NewBackendFromFilesystem(_ context.Context, config *BackendFromFilesystemConfig) (Backend, error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if config.Backend == nil { + return nil, fmt.Errorf("backend is required") + } + if config.BaseDir == "" { + return nil, fmt.Errorf("baseDir is required") + } + + return &filesystemBackend{ + backend: config.Backend, + baseDir: config.BaseDir, + }, nil +} + +func (b *filesystemBackend) List(ctx context.Context) ([]FrontMatter, error) { + skills, err := b.list(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list skills: %w", err) + } + + matters := make([]FrontMatter, 0, len(skills)) + for _, skill := range skills { + matters = append(matters, skill.FrontMatter) + } + + return matters, nil +} + +func (b *filesystemBackend) Get(ctx context.Context, name string) (Skill, error) { + skills, err := b.list(ctx) + if err != nil { + return Skill{}, fmt.Errorf("failed to list skills: %w", err) + } + + for _, skill := range skills { + if skill.Name == name { + return skill, nil + } + } + + return Skill{}, fmt.Errorf("skill not found: %s", name) +} + +func (b *filesystemBackend) list(ctx context.Context) ([]Skill, error) { + var skills []Skill + + pattern := "*/" + skillFileName + entries, err := b.backend.GlobInfo(ctx, &filesystem.GlobInfoRequest{ + Pattern: pattern, + Path: b.baseDir, + }) + if err != nil { + return nil, fmt.Errorf("failed to glob skill files: %w", err) + } + + for _, entry := range entries { + filePath := entry.Path + if !filepath.IsAbs(filePath) { + filePath = filepath.Join(b.baseDir, filePath) + } + skill, loadErr := b.loadSkillFromFile(ctx, filePath) + if loadErr != nil { + return nil, fmt.Errorf("failed to load skill from %s: %w", filePath, loadErr) + } + + skills = append(skills, skill) + } + + return skills, nil +} + +func (b *filesystemBackend) loadSkillFromFile(ctx context.Context, path string) (Skill, error) { + fileContent, err := b.backend.Read(ctx, &filesystem.ReadRequest{ + FilePath: path, + }) + if err != nil { + return Skill{}, fmt.Errorf("failed to read file: %w", err) + } + + data := stripLineNumbers(fileContent.Content) + + frontmatter, content, err := parseFrontmatter(data) + if err != nil { + return Skill{}, fmt.Errorf("failed to parse frontmatter: %w", err) + } + + var fm FrontMatter + if err = yaml.Unmarshal([]byte(frontmatter), &fm); err != nil { + return Skill{}, fmt.Errorf("failed to unmarshal frontmatter: %w", err) + } + + absDir := filepath.Dir(path) + + return Skill{ + FrontMatter: fm, + Content: strings.TrimSpace(content), + BaseDirectory: absDir, + }, nil +} + +func stripLineNumbers(data string) string { + lines := strings.Split(data, "\n") + result := make([]string, 0, len(lines)) + for _, line := range lines { + idx := strings.Index(line, "\t") + if idx != -1 { + line = line[idx+1:] + } + result = append(result, line) + } + return strings.Join(result, "\n") +} + +func parseFrontmatter(data string) (frontmatter string, content string, err error) { + const delimiter = "---" + + data = strings.TrimSpace(data) + + if !strings.HasPrefix(data, delimiter) { + return "", "", fmt.Errorf("file does not start with frontmatter delimiter") + } + + rest := data[len(delimiter):] + endIdx := strings.Index(rest, "\n"+delimiter) + if endIdx == -1 { + return "", "", fmt.Errorf("frontmatter closing delimiter not found") + } + + frontmatter = strings.TrimSpace(rest[:endIdx]) + content = rest[endIdx+len("\n"+delimiter):] + + content = strings.TrimPrefix(content, "\n") + + return frontmatter, content, nil +} diff --git a/adk/middlewares/skill/filesystem_backend_test.go b/adk/middlewares/skill/filesystem_backend_test.go new file mode 100644 index 0000000..3b8743c --- /dev/null +++ b/adk/middlewares/skill/filesystem_backend_test.go @@ -0,0 +1,500 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package skill + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk/filesystem" +) + +func TestNewBackendFromFilesystem(t *testing.T) { + ctx := context.Background() + + t.Run("nil config returns error", func(t *testing.T) { + backend, err := NewBackendFromFilesystem(ctx, nil) + assert.Nil(t, backend) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") + }) + + t.Run("nil backend returns error", func(t *testing.T) { + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + BaseDir: "/skills", + }) + assert.Nil(t, backend) + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend is required") + }) + + t.Run("empty baseDir returns error", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "", + }) + assert.Nil(t, backend) + assert.Error(t, err) + assert.Contains(t, err.Error(), "baseDir is required") + }) + + t.Run("valid config succeeds", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + assert.NoError(t, err) + assert.NotNil(t, backend) + }) +} + +func TestFilesystemBackend_List(t *testing.T) { + ctx := context.Background() + + t.Run("empty directory returns empty list", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/.keep", + Content: "", + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + assert.Empty(t, skills) + }) + + t.Run("directory with no SKILL.md files returns empty list", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/subdir/other.txt", + Content: "some content", + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + assert.Empty(t, skills) + }) + + t.Run("files in root directory are ignored", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/SKILL.md", + Content: `--- +name: root-skill +description: Root skill +--- +Content`, + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + assert.Empty(t, skills) + }) + + t.Run("valid skill directory returns skill", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/my-skill/SKILL.md", + Content: `--- +name: pdf-processing +description: Extract text and tables from PDF files, fill forms, merge documents. +license: Apache-2.0 +metadata: + author: example-org + version: "1.0" +--- +This is the skill content.`, + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + require.Len(t, skills, 1) + assert.Equal(t, "pdf-processing", skills[0].Name) + assert.Equal(t, "Extract text and tables from PDF files, fill forms, merge documents.", skills[0].Description) + }) + + t.Run("multiple skill directories returns all skills", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/skill-1/SKILL.md", + Content: `--- +name: skill-1 +description: First skill +--- +Content 1`, + }) + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/skill-2/SKILL.md", + Content: `--- +name: skill-2 +description: Second skill +--- +Content 2`, + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + assert.Len(t, skills, 2) + + names := []string{skills[0].Name, skills[1].Name} + assert.Contains(t, names, "skill-1") + assert.Contains(t, names, "skill-2") + }) + + t.Run("invalid SKILL.md returns error", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/invalid-skill/SKILL.md", + Content: `No frontmatter here`, + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.Error(t, err) + assert.Nil(t, skills) + assert.Contains(t, err.Error(), "failed to load skill") + }) + + t.Run("non-existent baseDir returns empty list", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/path/that/does/not/exist", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + assert.Empty(t, skills) + }) + + t.Run("deeply nested SKILL.md is ignored", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/valid-skill/SKILL.md", + Content: `--- +name: valid-skill +description: Valid skill +--- +Content`, + }) + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/deep/nested/SKILL.md", + Content: `--- +name: nested-skill +description: Nested skill +--- +Content`, + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skills, err := backend.List(ctx) + assert.NoError(t, err) + assert.Len(t, skills, 1) + assert.Equal(t, "valid-skill", skills[0].Name) + }) +} + +func TestFilesystemBackend_Get(t *testing.T) { + ctx := context.Background() + + t.Run("skill not found returns error", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/.keep", + Content: "", + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skill, err := backend.Get(ctx, "non-existent") + assert.Error(t, err) + assert.Empty(t, skill) + assert.Contains(t, err.Error(), "skill not found") + }) + + t.Run("existing skill is returned", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/test-skill/SKILL.md", + Content: `--- +name: test-skill +description: Test skill description +--- +Test content here.`, + }) + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skill, err := backend.Get(ctx, "test-skill") + assert.NoError(t, err) + assert.Equal(t, "test-skill", skill.Name) + assert.Equal(t, "Test skill description", skill.Description) + assert.Equal(t, "Test content here.", skill.Content) + }) + + t.Run("get specific skill from multiple", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + for _, name := range []string{"alpha", "beta", "gamma"} { + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/" + name + "/SKILL.md", + Content: `--- +name: ` + name + ` +description: Skill ` + name + ` +--- +Content for ` + name, + }) + } + + backend, err := NewBackendFromFilesystem(ctx, &BackendFromFilesystemConfig{ + Backend: fsBackend, + BaseDir: "/skills", + }) + require.NoError(t, err) + + skill, err := backend.Get(ctx, "beta") + assert.NoError(t, err) + assert.Equal(t, "beta", skill.Name) + assert.Equal(t, "Skill beta", skill.Description) + assert.Equal(t, "Content for beta", skill.Content) + }) +} + +func TestParseFrontmatter(t *testing.T) { + t.Run("valid frontmatter", func(t *testing.T) { + data := `--- +name: test +description: test description +--- +This is the content.` + + fm, content, err := parseFrontmatter(data) + assert.NoError(t, err) + assert.Equal(t, "name: test\ndescription: test description", fm) + assert.Equal(t, "This is the content.", content) + }) + + t.Run("frontmatter with multiline content", func(t *testing.T) { + data := `--- +name: test +--- +Line 1 +Line 2 +Line 3` + + fm, content, err := parseFrontmatter(data) + assert.NoError(t, err) + assert.Equal(t, "name: test", fm) + assert.Equal(t, "Line 1\nLine 2\nLine 3", content) + }) + + t.Run("frontmatter with leading/trailing whitespace", func(t *testing.T) { + data := ` +--- +name: test +--- +Content ` + + fm, content, err := parseFrontmatter(data) + assert.NoError(t, err) + assert.Equal(t, "name: test", fm) + assert.Equal(t, "Content", content) + }) + + t.Run("missing opening delimiter returns error", func(t *testing.T) { + data := `name: test +--- +Content` + + fm, content, err := parseFrontmatter(data) + assert.Error(t, err) + assert.Empty(t, fm) + assert.Empty(t, content) + assert.Contains(t, err.Error(), "does not start with frontmatter delimiter") + }) + + t.Run("missing closing delimiter returns error", func(t *testing.T) { + data := `--- +name: test +Content without closing` + + fm, content, err := parseFrontmatter(data) + assert.Error(t, err) + assert.Empty(t, fm) + assert.Empty(t, content) + assert.Contains(t, err.Error(), "closing delimiter not found") + }) + + t.Run("empty frontmatter", func(t *testing.T) { + data := `--- +--- +Content only` + + fm, content, err := parseFrontmatter(data) + assert.NoError(t, err) + assert.Empty(t, fm) + assert.Equal(t, "Content only", content) + }) + + t.Run("empty content", func(t *testing.T) { + data := `--- +name: test +---` + + fm, content, err := parseFrontmatter(data) + assert.NoError(t, err) + assert.Equal(t, "name: test", fm) + assert.Empty(t, content) + }) + + t.Run("content with --- inside", func(t *testing.T) { + data := `--- +name: test +--- +Content with --- in the middle` + + fm, content, err := parseFrontmatter(data) + assert.NoError(t, err) + assert.Equal(t, "name: test", fm) + assert.Equal(t, "Content with --- in the middle", content) + }) +} + +func TestLoadSkillFromFile(t *testing.T) { + ctx := context.Background() + + t.Run("valid skill file", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/SKILL.md", + Content: `--- +name: file-skill +description: Skill from file +--- +File skill content.`, + }) + + backend := &filesystemBackend{backend: fsBackend, baseDir: "/skills"} + skill, err := backend.loadSkillFromFile(ctx, "/skills/SKILL.md") + assert.NoError(t, err) + assert.Equal(t, "file-skill", skill.Name) + assert.Equal(t, "Skill from file", skill.Description) + assert.Equal(t, "File skill content.", skill.Content) + assert.Equal(t, "/skills", skill.BaseDirectory) + }) + + t.Run("non-existent file returns error", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + backend := &filesystemBackend{backend: fsBackend, baseDir: "/tmp"} + skill, err := backend.loadSkillFromFile(ctx, "/path/to/nonexistent/SKILL.md") + assert.Error(t, err) + assert.Empty(t, skill) + assert.Contains(t, err.Error(), "failed to read file") + }) + + t.Run("invalid yaml in frontmatter returns error", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/SKILL.md", + Content: `--- +name: [invalid yaml +--- +Content`, + }) + + backend := &filesystemBackend{backend: fsBackend, baseDir: "/skills"} + skill, err := backend.loadSkillFromFile(ctx, "/skills/SKILL.md") + assert.Error(t, err) + assert.Empty(t, skill) + assert.Contains(t, err.Error(), "failed to unmarshal frontmatter") + }) + + t.Run("content with extra whitespace is trimmed", func(t *testing.T) { + fsBackend := filesystem.NewInMemoryBackend() + _ = fsBackend.Write(ctx, &filesystem.WriteRequest{ + FilePath: "/skills/SKILL.md", + Content: `--- +name: trimmed-skill +description: desc +--- + + Content with whitespace + +`, + }) + + backend := &filesystemBackend{backend: fsBackend, baseDir: "/skills"} + skill, err := backend.loadSkillFromFile(ctx, "/skills/SKILL.md") + assert.NoError(t, err) + assert.Equal(t, "Content with whitespace", skill.Content) + }) +} diff --git a/adk/middlewares/skill/prompt.go b/adk/middlewares/skill/prompt.go new file mode 100644 index 0000000..87ae617 --- /dev/null +++ b/adk/middlewares/skill/prompt.go @@ -0,0 +1,154 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package skill + +const ( + systemPrompt = ` +# Skills System + +**How to Use Skills (Progressive Disclosure):** + +Skills follow a **progressive disclosure** pattern - you see their name and description above, but only read full instructions when needed: + +1. **Recognize when a skill applies**: Check if the user's task matches a skill's description +2. **Read the skill's full instructions**: Use the '{tool_name}' tool to load skill +3. **Follow the skill's instructions**: tool result contains step-by-step workflows, best practices, and examples +4. **Access supporting files**: Skills may include helper scripts, configs, or reference docs - use absolute paths + +**When to Use Skills:** +- User's request matches a skill's domain (e.g., "research X" -> web-research skill) +- You need specialized knowledge or structured workflows +- A skill provides proven patterns for complex tasks + +**Executing Skill Scripts:** +Skills may contain Python scripts or other executable files. Always use absolute paths. + +**Example Workflow:** + +User: "Can you research the latest developments in quantum computing?" + +1. Check available skills -> See "web-research" skill +2. Call '{tool_name}' tool to read the full skill instructions +3. Follow the skill's research workflow (search -> organize -> synthesize) +4. Use any helper scripts with absolute paths + +Remember: Skills make you more capable and consistent. When in doubt, check if a skill exists for the task! +` + + systemPromptChinese = ` +# Skill 系统 + +**如何使用 Skill(技能)(渐进式展示):** + +Skill 遵循**渐进式展示**模式 - 你可以在上方看到 Skill 的名称和描述,但只在需要时才阅读完整说明: + +1. **识别 Skill 适用场景**:检查用户的任务是否匹配某个 Skill 的描述 +2. **阅读 Skill 的完整说明**:使用 '{tool_name}' 工具加载 Skill +3. **遵循 Skill 说明操作**:工具结果包含逐步工作流程、最佳实践和示例 +4. **访问支持文件**:Skill 可能包含辅助脚本、配置或参考文档 - 使用绝对路径访问 + +**何时使用 Skill:** +- 用户请求匹配某个 Skill 的领域(例如"研究 X" -> web-research Skill) +- 你需要专业知识或结构化工作流程 +- 某个 Skill 为复杂任务提供了经过验证的模式 + +**执行 Skill 脚本:** +Skill 可能包含 Python 脚本或其他可执行文件。始终使用绝对路径。 + +**示例工作流程:** + +用户:"你能研究一下量子计算的最新发展吗?" + +1. 检查可用 Skill -> 发现 "web-research" Skill +2. 调用 '{tool_name}' 工具读取完整的 Skill 说明 +3. 遵循 Skill 的研究工作流程(搜索 -> 整理 -> 综合) +4. 使用绝对路径运行任何辅助脚本 + +记住:Skill 让你更加强大和稳定。如有疑问,请检查是否存在适用于该任务的 Skill! +` + + toolDescriptionBase = `Execute a skill within the main conversation + + +When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge. + +How to invoke: +- Use the exact string inside tag as the skill name (no arguments) +- Examples: + - ` + "`" + `skill: "pdf"` + "`" + ` - invoke the pdf skill + - ` + "`" + `skill: "xlsx"` + "`" + ` - invoke the xlsx skill + - ` + "`" + `skill: "ms-office-suite:pdf"` + "`" + ` - invoke using fully qualified name + +Important: +- When a skill is relevant, you must invoke this tool IMMEDIATELY as your first action +- NEVER just announce or mention a skill in your text response without actually calling this tool +- This is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task +- Only use skills listed in below +- Do not invoke a skill that is already running +- Skill content may contain relative paths. Convert them to absolute paths using the base directory provided in the tool result + + +` + toolDescriptionBaseChinese = `在主对话中执行 Skill(技能) + + +当用户要求你执行任务时,检查下方可用 Skill 列表中是否有 Skill 可以更有效地完成任务。Skill 提供专业能力和领域知识。 + +如何调用: +- 使用 标签内的完整字符串作为 Skill 名称(无需其他参数) +- 示例: + - ` + "`" + `skill: "pdf"` + "`" + ` - 调用 pdf Skill + - ` + "`" + `skill: "xlsx"` + "`" + ` - 调用 xlsx Skill + - ` + "`" + `skill: "ms-office-suite:pdf"` + "`" + ` - 使用完全限定名称调用 + +重要说明: +- 当 Skill 相关时,你必须立即调用此工具作为第一个动作 +- 切勿仅在文本回复中提及 Skill 而不实际调用此工具 +- 这是阻塞性要求:在生成任何关于任务的其他响应之前,先调用相关的 Skill 工具 +- 仅使用 中列出的 Skill +- 不要调用已经运行中的 Skill +- Skill 内容中可能包含相对路径,需使用工具返回的 base directory 将其转换为绝对路径 + + +` + toolDescriptionTemplate = ` + +{{- range .Matters }} + + +{{ .Name }} + + +{{ .Description }} + + +{{- end }} + +` + toolResult = "Launching skill: %s\n" + toolResultChinese = "正在启动 Skill:%s\n" + userContent = `Base directory for this skill: %s + +%s` + userContentChinese = `此 Skill 的目录:%s + +%s` + toolName = "skill" + + subAgentResultFormat = "Skill \"%s\" completed (sub-agent execution).\n\nResult:\n%s" + subAgentResultFormatChinese = "Skill \"%s\" 已完成(子 Agent 执行)。\n\n结果:\n%s" +) diff --git a/adk/middlewares/skill/skill.go b/adk/middlewares/skill/skill.go new file mode 100644 index 0000000..8f8b2ca --- /dev/null +++ b/adk/middlewares/skill/skill.go @@ -0,0 +1,711 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package skill + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + "text/template" + + "github.com/slongfield/pyfmt" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// ContextMode defines the execution mode of a skill. +type ContextMode string + +const ( + // ContextModeFork creates a new sub-agent without parent history + ContextModeFork ContextMode = "fork" + // ContextModeForkWithContext creates a new sub-agent with parent history + ContextModeForkWithContext ContextMode = "fork_with_context" +) + +// FrontMatter defines the YAML frontmatter schema parsed from a SKILL.md file. +type FrontMatter struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Context ContextMode `yaml:"context"` + Agent string `yaml:"agent"` + Model string `yaml:"model"` +} + +// Skill represents a skill loaded from a backend. +type Skill struct { + FrontMatter + // Content is the markdown body after the frontmatter contains the skill instructions of a SKILL.md file. + Content string + // BaseDirectory is the absolute directory path where the SKILL.md file is located (e.g., "/absolute/path/to/skills/my-skill"). + BaseDirectory string +} + +// Backend loads skills and provides metadata for tool description rendering. +type Backend interface { + List(ctx context.Context) ([]FrontMatter, error) + Get(ctx context.Context, name string) (Skill, error) +} + +// TypedAgentHubOptions contains options passed to TypedAgentHub.Get when creating an agent for skill execution. +type TypedAgentHubOptions[M adk.MessageType] struct { + // Model is the resolved model instance when a skill specifies a "model" field in frontmatter. + // nil means the skill did not specify a model override; implementations should use their default. + Model model.BaseModel[M] +} + +// AgentHubOptions is a backward-compatible alias for TypedAgentHubOptions instantiated with *schema.Message. +type AgentHubOptions = TypedAgentHubOptions[*schema.Message] + +// TypedAgentHub provides agent instances for context mode (fork/fork_with_context) execution. +type TypedAgentHub[M adk.MessageType] interface { + // Get returns an Agent by name. When name is empty, implementations should return a default agent. + // The opts parameter carries skill-level overrides (e.g., model) resolved by the framework. + Get(ctx context.Context, name string, opts *TypedAgentHubOptions[M]) (adk.TypedAgent[M], error) +} + +// AgentHub is a backward-compatible alias for TypedAgentHub instantiated with *schema.Message. +type AgentHub = TypedAgentHub[*schema.Message] + +// TypedModelHub resolves model instances by name for skills that specify a "model" field in frontmatter. +type TypedModelHub[M adk.MessageType] interface { + Get(ctx context.Context, name string) (model.BaseModel[M], error) +} + +// ModelHub is a backward-compatible alias for TypedModelHub instantiated with *schema.Message. +type ModelHub = TypedModelHub[*schema.Message] + +// SystemPromptFunc is a function that returns a custom system prompt. +// The toolName parameter is the name of the skill tool (default: "skill"). +type SystemPromptFunc func(ctx context.Context, toolName string) string + +// ToolDescriptionFunc is a function that returns a custom tool description. +// The skills parameter contains all available skill front matters. +type ToolDescriptionFunc func(ctx context.Context, skills []FrontMatter) string + +// TypedSubAgentInput contains the context available when building the sub-agent's +// initial messages in fork/fork_with_context mode. +type TypedSubAgentInput[M adk.MessageType] struct { + Skill Skill + Mode ContextMode + RawArguments string + SkillContent string + History []M + ToolCallID string +} + +// SubAgentInput is a backward-compatible alias for TypedSubAgentInput instantiated with *schema.Message. +type SubAgentInput = TypedSubAgentInput[*schema.Message] + +// TypedSubAgentOutput contains the sub-agent's execution results, available when +// formatting the final tool response. +type TypedSubAgentOutput[M adk.MessageType] struct { + Skill Skill + Mode ContextMode + RawArguments string + Messages []M + Results []string +} + +// SubAgentOutput is a backward-compatible alias for TypedSubAgentOutput instantiated with *schema.Message. +type SubAgentOutput = TypedSubAgentOutput[*schema.Message] + +// TypedConfig is the configuration for the skill middleware. +type TypedConfig[M adk.MessageType] struct { + // Backend is the backend for retrieving skills. + Backend Backend + // SkillToolName is the custom name for the skill tool. If nil, the default name "skill" is used. + SkillToolName *string + // Deprecated: Use adk.SetLanguage(adk.LanguageChinese) instead to enable Chinese prompts globally. + // This field will be removed in a future version. + UseChinese bool + // AgentHub provides agent instances for context mode (fork/fork_with_context) execution. + // Required when skills use "context: fork" or "context: fork_with_context" in frontmatter. + // The agent factory is retrieved by agent name (skill.Agent) from this hub. + // When skill.Agent is empty, AgentHub.Get is called with an empty string, + // allowing the hub implementation to return a default agent. + AgentHub TypedAgentHub[M] + // ModelHub provides model instances for skills that specify a "model" field in frontmatter. + // Used in two scenarios: + // - With context mode (fork/fork_with_context): The model is passed to the AgentHub + // - Without context mode (inline): The model becomes active for subsequent ChatModel requests + // If nil, skills with model specification will be ignored in inline mode, + // or return an error in context mode. + ModelHub TypedModelHub[M] + + // CustomSystemPrompt allows customizing the system prompt injected into the agent. + // If nil, the default system prompt is used. + // The function receives the skill tool name as a parameter. + CustomSystemPrompt SystemPromptFunc + // CustomToolDescription allows customizing the tool description for the skill tool. + // If nil, the default tool description is used. + // The function receives all available skill front matters as a parameter. + CustomToolDescription ToolDescriptionFunc + + // CustomToolParams customizes tool parameters for the skill tool. + // defaults is the default schema with only the required "skill" field. + // optional + CustomToolParams func(ctx context.Context, defaults map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) + + // BuildContent customizes the skill content generated for this invocation. + // rawArgs contains the original tool call arguments in JSON form. + // optional + BuildContent func(ctx context.Context, skill Skill, rawArgs string) (string, error) + + // BuildForkMessages customizes the messages passed to the forked sub-agent. + // When nil, fork uses [UserMessage(skillContent)] and fork_with_context uses + // [history..., ToolMessage(skillContent, toolCallID)]. + // optional + BuildForkMessages func(ctx context.Context, in TypedSubAgentInput[M]) ([]M, error) + + // FormatForkResult customizes the final text returned from the forked sub-agent results. + // When nil, assistant message contents emitted by the sub-agent are concatenated and returned + // in a default formatted string. + // optional + FormatForkResult func(ctx context.Context, in TypedSubAgentOutput[M]) (string, error) +} + +// Config is a backward-compatible alias for TypedConfig instantiated with *schema.Message. +type Config = TypedConfig[*schema.Message] + +// NewTyped creates a generic skill middleware handler for TypedChatModelAgent. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +// For *schema.AgenticMessage, tool execution is message-type-independent; the model override +// via ModelHub only takes effect when M is *schema.Message (for other types it is a no-op). +// +// See NewMiddleware for full usage documentation. +func NewTyped[M adk.MessageType](ctx context.Context, config *TypedConfig[M]) (adk.TypedChatModelAgentMiddleware[M], error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if config.Backend == nil { + return nil, fmt.Errorf("backend is required") + } + + name := toolName + if config.SkillToolName != nil { + name = *config.SkillToolName + } + + var instruction string + if config.CustomSystemPrompt != nil { + instruction = config.CustomSystemPrompt(ctx, name) + } else { + var err error + instruction, err = buildSystemPrompt(name, config.UseChinese) + if err != nil { + return nil, err + } + } + + return &typedSkillHandler[M]{ + instruction: instruction, + tool: &typedSkillTool[M]{ + b: config.Backend, + toolName: name, + useChinese: config.UseChinese, + agentHub: config.AgentHub, + modelHub: config.ModelHub, + customToolDesc: config.CustomToolDescription, + customToolParams: config.CustomToolParams, + buildContent: config.BuildContent, + buildForkMessages: config.BuildForkMessages, + formatForkResult: config.FormatForkResult, + }, + }, nil +} + +// NewMiddleware creates a new skill middleware handler for ChatModelAgent. +// +// The handler provides a skill tool that allows agents to load and execute skills +// defined in SKILL.md files. Skills can run in different modes based on their +// frontmatter configuration: +// +// - Inline mode (default): Skill content is returned directly as tool result +// - Fork mode (context: fork): Forks a new agent with a clean context, discarding message history +// - Fork with context mode (context: fork_with_context): Forks a new agent carrying over message history +// +// Example usage: +// +// handler, err := skill.NewMiddleware(ctx, &skill.Config{ +// Backend: backend, +// AgentHub: myAgentHub, +// ModelHub: myModelHub, +// }) +// if err != nil { +// return err +// } +// +// agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ +// // ... +// Handlers: []adk.ChatModelAgentMiddleware{handler}, +// }) +func NewMiddleware(ctx context.Context, config *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped(ctx, config) +} + +type typedSkillHandler[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + instruction string + tool *typedSkillTool[M] +} + +func (h *typedSkillHandler[M]) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) (context.Context, *adk.ChatModelAgentContext, error) { + runCtx.Instruction = runCtx.Instruction + "\n" + h.instruction + runCtx.Tools = append(runCtx.Tools, h.tool) + return ctx, runCtx, nil +} + +func (h *typedSkillHandler[M]) WrapModel(ctx context.Context, m model.BaseModel[M], _ *adk.TypedModelContext[M]) (model.BaseModel[M], error) { + if h.tool.modelHub == nil { + return m, nil + } + modelName, found, err := adk.GetRunLocalValue(ctx, activeModelKey) + if err != nil { + return nil, fmt.Errorf("failed to get active model from run local value: %w", err) + } + if !found { + return m, nil + } + name, ok := modelName.(string) + if !ok || name == "" { + return m, nil + } + newModel, err := h.tool.modelHub.Get(ctx, name) + if err != nil { + return nil, fmt.Errorf("failed to get model '%s' from ModelHub: %w", name, err) + } + return newModel, nil +} + +const activeModelKey = "__skill_active_model__" + +// New creates a new skill middleware. +// It provides a tool for the agent to use skills. +// +// Deprecated: Use NewMiddleware instead. New does not support fork mode execution +// because AgentMiddleware cannot save message history for fork mode. +func New(ctx context.Context, config *Config) (adk.AgentMiddleware, error) { + if config == nil { + return adk.AgentMiddleware{}, fmt.Errorf("config is required") + } + if config.Backend == nil { + return adk.AgentMiddleware{}, fmt.Errorf("backend is required") + } + + name := toolName + if config.SkillToolName != nil { + name = *config.SkillToolName + } + + var sp string + if config.CustomSystemPrompt != nil { + sp = config.CustomSystemPrompt(ctx, name) + } else { + var err error + sp, err = buildSystemPrompt(name, config.UseChinese) + if err != nil { + return adk.AgentMiddleware{}, err + } + } + + return adk.AgentMiddleware{ + AdditionalInstruction: sp, + AdditionalTools: []tool.BaseTool{&typedSkillTool[*schema.Message]{ + b: config.Backend, + toolName: name, + useChinese: config.UseChinese, + customToolDesc: config.CustomToolDescription, + }}, + }, nil +} + +func buildSystemPrompt(skillToolName string, useChinese bool) (string, error) { + var prompt string + if useChinese { + prompt = systemPromptChinese + } else { + prompt = internal.SelectPrompt(internal.I18nPrompts{ + English: systemPrompt, + Chinese: systemPromptChinese, + }) + } + return pyfmt.Fmt(prompt, map[string]string{ + "tool_name": skillToolName, + }) +} + +type typedSkillTool[M adk.MessageType] struct { + b Backend + toolName string + + useChinese bool + agentHub TypedAgentHub[M] + modelHub TypedModelHub[M] + + customToolDesc ToolDescriptionFunc + + customToolParams func(ctx context.Context, defaults map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) + buildContent func(ctx context.Context, skill Skill, rawArgs string) (string, error) + + buildForkMessages func(ctx context.Context, in TypedSubAgentInput[M]) ([]M, error) + formatForkResult func(ctx context.Context, in TypedSubAgentOutput[M]) (string, error) +} + +type descriptionTemplateHelper struct { + Matters []FrontMatter +} + +func (s *typedSkillTool[M]) Info(ctx context.Context) (*schema.ToolInfo, error) { + skills, err := s.b.List(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list skills: %w", err) + } + + var fullDesc string + if s.customToolDesc != nil { + fullDesc = s.customToolDesc(ctx, skills) + } else { + desc, renderErr := renderToolDescription(skills) + if renderErr != nil { + return nil, fmt.Errorf("failed to render skill tool description: %w", renderErr) + } + + descBase := internal.SelectPrompt(internal.I18nPrompts{ + English: toolDescriptionBase, + Chinese: toolDescriptionBaseChinese, + }) + fullDesc = descBase + desc + } + + oneOf, err := s.buildParamsOneOf(ctx) + if err != nil { + return nil, fmt.Errorf("failed to build skill tool params: %w", err) + } + + return &schema.ToolInfo{ + Name: s.toolName, + Desc: fullDesc, + ParamsOneOf: oneOf, + }, nil +} + +type inputArguments struct { + Skill string `json:"skill"` +} + +func (s *typedSkillTool[M]) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + args := &inputArguments{} + err := json.Unmarshal([]byte(argumentsInJSON), args) + if err != nil { + return "", fmt.Errorf("failed to unmarshal arguments: %w", err) + } + skill, err := s.b.Get(ctx, args.Skill) + if err != nil { + return "", fmt.Errorf("failed to get skill: %w", err) + } + + switch skill.Context { + case ContextModeForkWithContext: + return s.runAgentMode(ctx, skill, true, argumentsInJSON) + case ContextModeFork: + return s.runAgentMode(ctx, skill, false, argumentsInJSON) + default: + if skill.Model != "" { + s.setActiveModel(ctx, skill.Model) + } + return s.buildSkillResult(ctx, skill, argumentsInJSON) + } +} + +func (s *typedSkillTool[M]) setActiveModel(ctx context.Context, modelName string) { + _ = adk.SetRunLocalValue(ctx, activeModelKey, modelName) +} + +func defaultToolParams() map[string]*schema.ParameterInfo { + skillParamDesc := internal.SelectPrompt(internal.I18nPrompts{ + English: "The skill name (no arguments). E.g., \"pdf\" or \"xlsx\"", + Chinese: "Skill 名称(无需其他参数)。例如:\"pdf\" 或 \"xlsx\"", + }) + return map[string]*schema.ParameterInfo{ + "skill": { + Type: schema.String, + Desc: skillParamDesc, + Required: true, + }, + } +} + +func (s *typedSkillTool[M]) buildParamsOneOf(ctx context.Context) (*schema.ParamsOneOf, error) { + defaults := defaultToolParams() + if s.customToolParams == nil { + return schema.NewParamsOneOfByParams(defaults), nil + } + + params, err := s.customToolParams(ctx, defaults) + if err != nil { + return nil, err + } + if params == nil { + params = defaults + } + + if _, ok := params["skill"]; !ok { + params["skill"] = defaults["skill"] + } + + if p := params["skill"]; p != nil { + p.Required = true + } + + return schema.NewParamsOneOfByParams(params), nil +} + +func (s *typedSkillTool[M]) buildSkillResult(ctx context.Context, skill Skill, rawArguments string) (string, error) { + if s.buildContent == nil { + return s.defaultSkillContent(skill), nil + } + content, err := s.buildContent(ctx, skill, rawArguments) + if err != nil { + return "", fmt.Errorf("failed to build skill result: %w", err) + } + return content, nil +} + +func (s *typedSkillTool[M]) defaultSkillContent(skill Skill) string { + resultFmt := internal.SelectPrompt(internal.I18nPrompts{ + English: toolResult, + Chinese: toolResultChinese, + }) + contentFmt := internal.SelectPrompt(internal.I18nPrompts{ + English: userContent, + Chinese: userContentChinese, + }) + + return fmt.Sprintf(resultFmt, skill.Name) + fmt.Sprintf(contentFmt, skill.BaseDirectory, skill.Content) +} + +func (s *typedSkillTool[M]) runAgentMode(ctx context.Context, skill Skill, forkHistory bool, rawArguments string) (string, error) { + if s.agentHub == nil { + return "", fmt.Errorf("skill '%s' requires context:%s but AgentHub is not configured", skill.Name, skill.Context) + } + + opts := &TypedAgentHubOptions[M]{} + if skill.Model != "" { + if s.modelHub == nil { + return "", fmt.Errorf("skill '%s' requires model '%s' but ModelHub is not configured", skill.Name, skill.Model) + } + m, err := s.modelHub.Get(ctx, skill.Model) + if err != nil { + return "", fmt.Errorf("failed to get model '%s' from ModelHub: %w", skill.Model, err) + } + opts.Model = m + } + + agent, err := s.agentHub.Get(ctx, skill.Agent, opts) + if err != nil { + return "", fmt.Errorf("failed to get agent '%s' from AgentHub: %w", skill.Agent, err) + } + + var messages []M + skillContent, err := s.buildSkillResult(ctx, skill, rawArguments) + if err != nil { + return "", fmt.Errorf("failed to build skill result: %w", err) + } + + var history []M + var toolCallID string + if forkHistory { + history, err = s.getMessagesFromState(ctx) + if err != nil { + return "", fmt.Errorf("failed to get messages from state: %w", err) + } + toolCallID = compose.GetToolCallID(ctx) + } + + if s.buildForkMessages != nil { + messages, err = s.buildForkMessages(ctx, TypedSubAgentInput[M]{ + Skill: skill, + Mode: skill.Context, + RawArguments: rawArguments, + SkillContent: skillContent, + History: history, + ToolCallID: toolCallID, + }) + if err != nil { + return "", fmt.Errorf("failed to build fork messages: %w", err) + } + } else { + var zero M + if forkHistory { + var toolMsg M + switch any(zero).(type) { + case *schema.Message: + toolMsg = any(schema.ToolMessage(skillContent, toolCallID)).(M) + case *schema.AgenticMessage: + toolMsg = any(&schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: toolCallID, + Name: "", + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: skillContent}}, + }, + }), + }, + }).(M) + } + messages = append(history, toolMsg) + } else { + var userMsg M + switch any(zero).(type) { + case *schema.Message: + userMsg = any(schema.UserMessage(skillContent)).(M) + case *schema.AgenticMessage: + userMsg = any(schema.UserAgenticMessage(skillContent)).(M) + } + messages = []M{userMsg} + } + } + + input := &adk.TypedAgentInput[M]{ + Messages: messages, + EnableStreaming: false, + } + + iter := agent.Run(ctx, input) + + var msgList []M + var results []string + for { + event, ok := iter.Next() + if !ok { + break + } + + if event.Err != nil { + return "", fmt.Errorf("failed to run agent event: %w", event.Err) + } + + if event.Output == nil || event.Output.MessageOutput == nil { + continue + } + + msg, msgErr := event.Output.MessageOutput.GetMessage() + if msgErr != nil { + return "", fmt.Errorf("failed to get message from event: %w", msgErr) + } + + if !isNilMessage(msg) { + msgList = append(msgList, msg) + var content string + switch m := any(msg).(type) { + case *schema.Message: + content = m.Content + case *schema.AgenticMessage: + var parts []string + for _, block := range m.ContentBlocks { + if block == nil { + continue + } + if block.AssistantGenText != nil { + parts = append(parts, block.AssistantGenText.Text) + } + } + content = strings.Join(parts, "\n") + } + if content != "" { + results = append(results, content) + } + } + } + + if s.formatForkResult != nil { + out, err := s.formatForkResult(ctx, TypedSubAgentOutput[M]{ + Skill: skill, + Mode: skill.Context, + RawArguments: rawArguments, + Messages: msgList, + Results: results, + }) + if err != nil { + return "", fmt.Errorf("failed to format fork result: %w", err) + } + return out, nil + } + + resultFmt := internal.SelectPrompt(internal.I18nPrompts{ + English: subAgentResultFormat, + Chinese: subAgentResultFormatChinese, + }) + + return fmt.Sprintf(resultFmt, skill.Name, strings.Join(results, "\n")), nil +} + +func isNilMessage[M adk.MessageType](msg M) bool { + var zero M + return any(msg) == any(zero) +} + +func (s *typedSkillTool[M]) getMessagesFromState(ctx context.Context) ([]M, error) { + var messages []M + var zero M + switch any(zero).(type) { + case *schema.Message: + err := compose.ProcessState(ctx, func(_ context.Context, st *adk.State) error { + messages = make([]M, len(st.Messages)) + for i, m := range st.Messages { + messages[i] = any(m).(M) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to process state: %w", err) + } + case *schema.AgenticMessage: + // Fork mode is not supported for AgenticMessage because the internal + // agent state type (agenticState) is unexported from the adk package, + // making it inaccessible via compose.ProcessState from middleware packages. + // Agent mode (the default) works normally for AgenticMessage. + return nil, fmt.Errorf("fork mode is not supported for AgenticMessage; use agent mode instead") + } + return messages, nil +} + +func renderToolDescription(matters []FrontMatter) (string, error) { + tpl, err := template.New("skills").Parse(toolDescriptionTemplate) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = tpl.Execute(&buf, descriptionTemplateHelper{Matters: matters}) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/adk/middlewares/skill/skill_generic_test.go b/adk/middlewares/skill/skill_generic_test.go new file mode 100644 index 0000000..4e5a8e7 --- /dev/null +++ b/adk/middlewares/skill/skill_generic_test.go @@ -0,0 +1,466 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package skill + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +// --- Generic mock types --- + +type mockGenericModel[M adk.MessageType] struct { + generateFunc func(ctx context.Context, input []M, opts ...model.Option) (M, error) +} + +func (m *mockGenericModel[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + if m.generateFunc != nil { + return m.generateFunc(ctx, input, opts...) + } + var zero M + return zero, nil +} + +func (m *mockGenericModel[M]) Stream(_ context.Context, _ []M, _ ...model.Option) (*schema.StreamReader[M], error) { + return nil, nil +} + +type mockGenericModelHub[M adk.MessageType] struct { + models map[string]model.BaseModel[M] +} + +func (h *mockGenericModelHub[M]) Get(_ context.Context, name string) (model.BaseModel[M], error) { + m, ok := h.models[name] + if !ok { + return nil, assert.AnError + } + return m, nil +} + +type mockGenericAgent[M adk.MessageType] struct { + events []*adk.TypedAgentEvent[M] + lastIn *adk.TypedAgentInput[M] +} + +func (a *mockGenericAgent[M]) Name(_ context.Context) string { return "mock-generic-agent" } +func (a *mockGenericAgent[M]) Description(_ context.Context) string { return "mock generic agent" } +func (a *mockGenericAgent[M]) Run(_ context.Context, in *adk.TypedAgentInput[M], _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.TypedAgentEvent[M]] { + a.lastIn = in + iter, gen := adk.NewAsyncIteratorPair[*adk.TypedAgentEvent[M]]() + go func() { + defer gen.Close() + for _, e := range a.events { + gen.Send(e) + } + }() + return iter +} + +type mockGenericAgentHub[M adk.MessageType] struct { + agent adk.TypedAgent[M] + lastOpts *TypedAgentHubOptions[M] +} + +func (h *mockGenericAgentHub[M]) Get(_ context.Context, _ string, opts *TypedAgentHubOptions[M]) (adk.TypedAgent[M], error) { + h.lastOpts = opts + return h.agent, nil +} + +// --- Helper to build an assistant event for each message type --- + +func assistantEvent[M adk.MessageType](text string) *adk.TypedAgentEvent[M] { + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.AssistantMessage(text, nil) + return any(&adk.TypedAgentEvent[*schema.Message]{ + Output: &adk.TypedAgentOutput[*schema.Message]{ + MessageOutput: &adk.TypedMessageVariant[*schema.Message]{ + Message: msg, + }, + }, + }).(*adk.TypedAgentEvent[M]) + case *schema.AgenticMessage: + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: text}), + }, + } + return any(&adk.TypedAgentEvent[*schema.AgenticMessage]{ + Output: &adk.TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &adk.TypedMessageVariant[*schema.AgenticMessage]{ + Message: msg, + }, + }, + }).(*adk.TypedAgentEvent[M]) + } + panic("unreachable") +} + +// --- Part 1: WrapModel test --- + +func testWrapModel[M adk.MessageType](t *testing.T) { + ctx := context.Background() + mockModel := &mockGenericModel[M]{} + hub := &mockGenericModelHub[M]{ + models: map[string]model.BaseModel[M]{ + "test-model": mockModel, + }, + } + + mw, err := NewTyped(ctx, &TypedConfig[M]{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: hub, + }) + require.NoError(t, err) + require.NotNil(t, mw) + + t.Run("nil ModelHub keeps base model", func(t *testing.T) { + handler, err := NewTyped(ctx, &TypedConfig[M]{ + Backend: &inMemoryBackend{m: []Skill{}}, + }) + require.NoError(t, err) + h := handler.(*typedSkillHandler[M]) + base := &mockGenericModel[M]{} + got, err := h.WrapModel(ctx, base, &adk.TypedModelContext[M]{}) + require.NoError(t, err) + assert.Equal(t, base, got) + }) +} + +// --- Part 2: Agent mode tests --- + +func testAgentMode[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + t.Run("successful agent run", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + assistantEvent[M]("agent answer"), + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "test-skill", Context: ContextModeFork}, + Content: "skill content", + BaseDirectory: "/skills/test", + }, + }}, + toolName: "skill", + agentHub: hub, + } + + result, err := st.InvokableRun(ctx, `{"skill": "test-skill"}`) + require.NoError(t, err) + assert.Contains(t, result, "test-skill") + assert.Contains(t, result, "agent answer") + + // Verify agent received a user message (fork mode, no history). + require.NotNil(t, agent.lastIn) + require.Len(t, agent.lastIn.Messages, 1) + }) + + t.Run("fork mode constructs user message", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + assistantEvent[M]("ok"), + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, + Content: "c1", + BaseDirectory: "/d", + }, + }}, + toolName: "skill", + agentHub: hub, + } + + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.NoError(t, err) + require.NotNil(t, agent.lastIn) + require.Len(t, agent.lastIn.Messages, 1) + + // Verify the message is a user-role message. + msg := agent.lastIn.Messages[0] + var zero M + switch any(zero).(type) { + case *schema.Message: + m := any(msg).(*schema.Message) + assert.Equal(t, schema.User, m.Role) + case *schema.AgenticMessage: + m := any(msg).(*schema.AgenticMessage) + assert.Equal(t, schema.AgenticRoleTypeUser, m.Role) + } + }) + + t.Run("custom buildForkMessages called", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + assistantEvent[M]("ok"), + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + var captured TypedSubAgentInput[M] + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, + Content: "c1", + BaseDirectory: "/d", + }, + }}, + toolName: "skill", + agentHub: hub, + buildForkMessages: func(_ context.Context, in TypedSubAgentInput[M]) ([]M, error) { + captured = in + // Build a simple user message. + var zero M + switch any(zero).(type) { + case *schema.Message: + return []M{any(schema.UserMessage("custom")).(M)}, nil + case *schema.AgenticMessage: + return []M{any(schema.UserAgenticMessage("custom")).(M)}, nil + } + return nil, nil + }, + } + + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.NoError(t, err) + assert.Equal(t, "s1", captured.Skill.Name) + assert.Equal(t, ContextModeFork, captured.Mode) + }) + + t.Run("custom formatForkResult called", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + assistantEvent[M]("p1"), + assistantEvent[M]("p2"), + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, + Content: "c1", + BaseDirectory: "/d", + }, + }}, + toolName: "skill", + agentHub: hub, + formatForkResult: func(_ context.Context, in TypedSubAgentOutput[M]) (string, error) { + assert.Equal(t, ContextModeFork, in.Mode) + assert.Equal(t, []string{"p1", "p2"}, in.Results) + assert.Len(t, in.Messages, 2) + return "formatted:" + strings.Join(in.Results, ","), nil + }, + } + + result, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.NoError(t, err) + assert.Equal(t, "formatted:p1,p2", result) + }) + + t.Run("agent error event propagates", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + {Err: assert.AnError}, + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, + Content: "c1", + BaseDirectory: "/d", + }, + }}, + toolName: "skill", + agentHub: hub, + } + + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to run agent event") + }) + + t.Run("multiple events concatenated", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + assistantEvent[M]("part1"), + {Output: nil}, // skipped + assistantEvent[M]("part2"), + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, + Content: "c1", + BaseDirectory: "/d", + }, + }}, + toolName: "skill", + agentHub: hub, + } + + result, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.NoError(t, err) + assert.Contains(t, result, "part1") + assert.Contains(t, result, "part2") + }) + + t.Run("model passed to agent hub", func(t *testing.T) { + mdl := &mockGenericModel[M]{} + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{ + assistantEvent[M]("ok"), + }, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork, Model: "m1"}, + Content: "c1", + BaseDirectory: "/d", + }, + }}, + toolName: "skill", + agentHub: hub, + modelHub: &mockGenericModelHub[M]{ + models: map[string]model.BaseModel[M]{"m1": mdl}, + }, + } + + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.NoError(t, err) + require.NotNil(t, hub.lastOpts) + assert.Equal(t, mdl, hub.lastOpts.Model) + }) + + t.Run("no AgentHub returns error", func(t *testing.T) { + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1"}, + }}, + toolName: "skill", + } + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.Error(t, err) + assert.Contains(t, err.Error(), "AgentHub is not configured") + }) + + t.Run("no ModelHub with model returns error", func(t *testing.T) { + agent := &mockGenericAgent[M]{ + events: []*adk.TypedAgentEvent[M]{assistantEvent[M]("ok")}, + } + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork, Model: "m1"}, Content: "c1"}, + }}, + toolName: "skill", + agentHub: hub, + } + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.Error(t, err) + assert.Contains(t, err.Error(), "ModelHub is not configured") + }) + + t.Run("empty content events still produce result", func(t *testing.T) { + var emptyEvent *adk.TypedAgentEvent[M] + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.AssistantMessage("", nil) + emptyEvent = any(&adk.TypedAgentEvent[*schema.Message]{ + Output: &adk.TypedAgentOutput[*schema.Message]{ + MessageOutput: &adk.TypedMessageVariant[*schema.Message]{ + Message: msg, + }, + }, + }).(*adk.TypedAgentEvent[M]) + case *schema.AgenticMessage: + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{}, + } + emptyEvent = any(&adk.TypedAgentEvent[*schema.AgenticMessage]{ + Output: &adk.TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &adk.TypedMessageVariant[*schema.AgenticMessage]{ + Message: msg, + }, + }, + }).(*adk.TypedAgentEvent[M]) + } + + agent := &mockGenericAgent[M]{events: []*adk.TypedAgentEvent[M]{emptyEvent}} + hub := &mockGenericAgentHub[M]{agent: agent} + + st := &typedSkillTool[M]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + } + + result, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + require.NoError(t, err) + assert.Contains(t, result, "s1") + }) +} + +// --- Top-level test --- + +func TestSkillGeneric(t *testing.T) { + t.Run("Message", func(t *testing.T) { + t.Run("WrapModel", testWrapModel[*schema.Message]) + t.Run("AgentMode", testAgentMode[*schema.Message]) + }) + t.Run("AgenticMessage", func(t *testing.T) { + t.Run("WrapModel", testWrapModel[*schema.AgenticMessage]) + t.Run("AgentMode", testAgentMode[*schema.AgenticMessage]) + }) +} diff --git a/adk/middlewares/skill/skill_test.go b/adk/middlewares/skill/skill_test.go new file mode 100644 index 0000000..3cc536a --- /dev/null +++ b/adk/middlewares/skill/skill_test.go @@ -0,0 +1,1353 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package skill + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +type inMemoryBackend struct { + m []Skill +} + +func (i *inMemoryBackend) List(ctx context.Context) ([]FrontMatter, error) { + matters := make([]FrontMatter, 0, len(i.m)) + for _, skill := range i.m { + matters = append(matters, skill.FrontMatter) + } + return matters, nil +} + +func (i *inMemoryBackend) Get(ctx context.Context, name string) (Skill, error) { + for _, skill := range i.m { + if skill.Name == name { + return skill, nil + } + } + return Skill{}, errors.New("skill not found") +} + +func TestTool(t *testing.T) { + backend := &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{ + Name: "name1", + Description: "desc1", + }, + Content: "content1", + BaseDirectory: "basedir1", + }, + { + FrontMatter: FrontMatter{ + Name: "name2", + Description: "desc2", + }, + Content: "content2", + BaseDirectory: "basedir2", + }, + }} + + ctx := context.Background() + m, err := New(ctx, &Config{Backend: backend}) + assert.NoError(t, err) + assert.Len(t, m.AdditionalTools, 1) + + to := m.AdditionalTools[0].(tool.InvokableTool) + + info, err := to.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "skill", info.Name) + desc := strings.TrimPrefix(info.Desc, toolDescriptionBase) + assert.Equal(t, ` + + + +name1 + + +desc1 + + + + +name2 + + +desc2 + + + +`, desc) + + result, err := to.InvokableRun(ctx, `{"skill": "name1"}`) + assert.NoError(t, err) + assert.Equal(t, `Launching skill: name1 +Base directory for this skill: basedir1 + +content1`, result) + + // chinese + internal.SetLanguage(internal.LanguageChinese) + defer internal.SetLanguage(internal.LanguageEnglish) + m, err = New(ctx, &Config{Backend: backend}) + assert.NoError(t, err) + assert.Len(t, m.AdditionalTools, 1) + + to = m.AdditionalTools[0].(tool.InvokableTool) + + info, err = to.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "skill", info.Name) + desc = strings.TrimPrefix(info.Desc, toolDescriptionBaseChinese) + assert.Equal(t, ` + + + +name1 + + +desc1 + + + + +name2 + + +desc2 + + + +`, desc) + + result, err = to.InvokableRun(ctx, `{"skill": "name1"}`) + assert.NoError(t, err) + assert.Equal(t, `正在启动 Skill:name1 +此 Skill 的目录:basedir1 + +content1`, result) +} + +func TestSkillToolName(t *testing.T) { + ctx := context.Background() + + // default + m, err := New(ctx, &Config{Backend: &inMemoryBackend{m: []Skill{}}}) + assert.NoError(t, err) + // instruction + assert.Contains(t, m.AdditionalInstruction, "'skill'") + // tool name + info, err := m.AdditionalTools[0].Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "skill", info.Name) + + // customized + name := "load_skill" + m, err = New(ctx, &Config{Backend: &inMemoryBackend{m: []Skill{}}, SkillToolName: &name}) + assert.NoError(t, err) + assert.Contains(t, m.AdditionalInstruction, "'load_skill'") + info, err = m.AdditionalTools[0].Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "load_skill", info.Name) +} + +func TestBuildParamsOneOf_CustomParams(t *testing.T) { + internal.SetLanguage(internal.LanguageEnglish) + ctx := context.Background() + + st := &typedSkillTool[*schema.Message]{ + customToolParams: func(context.Context, map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) { + return map[string]*schema.ParameterInfo{ + "foo": { + Type: schema.String, + Desc: "foo desc", + Required: true, + }, + "bar": { + Type: schema.Integer, + Desc: "bar desc", + Required: false, + }, + "skill": { + Type: schema.String, + Desc: "custom skill desc", + Required: false, + }, + }, nil + }, + } + + oneOf, err := st.buildParamsOneOf(ctx) + require.NoError(t, err) + js, err := oneOf.ToJSONSchema() + require.NoError(t, err) + require.NotNil(t, js) + require.NotNil(t, js.Properties) + + skillSchema, ok := js.Properties.Get("skill") + require.True(t, ok) + require.NotNil(t, skillSchema) + assert.Equal(t, string(schema.String), skillSchema.Type) + assert.Equal(t, "custom skill desc", skillSchema.Description) + + _, ok = js.Properties.Get("foo") + assert.True(t, ok) + _, ok = js.Properties.Get("bar") + assert.True(t, ok) + + assert.Equal(t, []string{"foo", "skill"}, js.Required) +} + +func TestBuildParamsOneOf_CustomParamsNilFallsBackToDefault(t *testing.T) { + ctx := context.Background() + st := &typedSkillTool[*schema.Message]{ + customToolParams: func(context.Context, map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) { + return nil, nil + }, + } + + oneOf, err := st.buildParamsOneOf(ctx) + require.NoError(t, err) + js, err := oneOf.ToJSONSchema() + require.NoError(t, err) + require.NotNil(t, js) + require.NotNil(t, js.Properties) + _, ok := js.Properties.Get("skill") + require.True(t, ok) + assert.Contains(t, js.Required, "skill") +} + +// --- Mock types for NewMiddleware tests --- + +type mockModel struct { + model.BaseModel[*schema.Message] + name string +} + +type mockModelHub struct { + models map[string]model.BaseModel[*schema.Message] +} + +func (h *mockModelHub) Get(_ context.Context, name string) (model.BaseModel[*schema.Message], error) { + m, ok := h.models[name] + if !ok { + return nil, fmt.Errorf("model not found: %s", name) + } + return m, nil +} + +type fakeToolCallingModel struct { + id string + calls int +} + +func (m *fakeToolCallingModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + m.calls++ + return schema.AssistantMessage(m.id, nil), nil +} + +func (m *fakeToolCallingModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, io.EOF +} + +func (m *fakeToolCallingModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +type runLocalSetterHandler struct { + *adk.BaseChatModelAgentMiddleware + key string + val any +} + +func (h *runLocalSetterHandler) BeforeModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, _ *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) { + if err := adk.SetRunLocalValue(ctx, h.key, h.val); err != nil { + return nil, nil, err + } + return ctx, state, nil +} + +type stateMessagesCaptureHandler struct { + *adk.BaseChatModelAgentMiddleware + st *typedSkillTool[*schema.Message] + captured []adk.Message +} + +func (h *stateMessagesCaptureHandler) AfterModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, _ *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) { + msgs, err := h.st.getMessagesFromState(ctx) + if err != nil { + return nil, nil, err + } + h.captured = msgs + return ctx, state, nil +} + +type mockAgent struct { + events []*adk.AgentEvent + lastIn *adk.AgentInput +} + +func (a *mockAgent) Name(_ context.Context) string { return "mock-agent" } +func (a *mockAgent) Description(_ context.Context) string { return "mock agent for testing" } +func (a *mockAgent) Run(_ context.Context, in *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + a.lastIn = in + iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + go func() { + defer gen.Close() + for _, e := range a.events { + gen.Send(e) + } + }() + return iter +} + +type mockAgentHub struct { + agents map[string]adk.Agent + lastOpts *AgentHubOptions + defaultAgent adk.Agent +} + +func (h *mockAgentHub) Get(_ context.Context, name string, opts *AgentHubOptions) (adk.Agent, error) { + h.lastOpts = opts + if name == "" && h.defaultAgent != nil { + return h.defaultAgent, nil + } + a, ok := h.agents[name] + if !ok { + return nil, fmt.Errorf("agent not found: %s", name) + } + return a, nil +} + +type errorBackend struct { + listErr error + getErr error +} + +func (b *errorBackend) List(_ context.Context) ([]FrontMatter, error) { + return nil, b.listErr +} +func (b *errorBackend) Get(_ context.Context, _ string) (Skill, error) { + return Skill{}, b.getErr +} + +// --- NewMiddleware tests --- + +func TestNewMiddleware(t *testing.T) { + ctx := context.Background() + + t.Run("nil config returns error", func(t *testing.T) { + handler, err := NewMiddleware(ctx, nil) + assert.Nil(t, handler) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") + }) + + t.Run("nil backend returns error", func(t *testing.T) { + handler, err := NewMiddleware(ctx, &Config{}) + assert.Nil(t, handler) + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend is required") + }) + + t.Run("custom tool params error surfaces in Info", func(t *testing.T) { + backend := &inMemoryBackend{m: []Skill{}} + handler, err := NewMiddleware(ctx, &Config{ + Backend: backend, + CustomToolParams: func(context.Context, map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) { + return nil, errors.New("bad params") + }, + }) + require.NoError(t, err) + h := handler.(*typedSkillHandler[*schema.Message]) + _, err = h.tool.Info(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to build skill tool params") + assert.Contains(t, err.Error(), "bad params") + }) + + t.Run("valid config succeeds", func(t *testing.T) { + backend := &inMemoryBackend{m: []Skill{}} + handler, err := NewMiddleware(ctx, &Config{Backend: backend}) + assert.NoError(t, err) + assert.NotNil(t, handler) + }) + + t.Run("custom tool name", func(t *testing.T) { + backend := &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Description: "d1"}, Content: "c1"}, + }} + name := "load_skill" + handler, err := NewMiddleware(ctx, &Config{Backend: backend, SkillToolName: &name}) + require.NoError(t, err) + + h := handler.(*typedSkillHandler[*schema.Message]) + assert.Contains(t, h.instruction, "'load_skill'") + assert.Equal(t, "load_skill", h.tool.toolName) + }) + + t.Run("custom system prompt", func(t *testing.T) { + backend := &inMemoryBackend{m: []Skill{}} + handler, err := NewMiddleware(ctx, &Config{ + Backend: backend, + CustomSystemPrompt: func(_ context.Context, toolName string) string { + return "custom prompt for " + toolName + }, + }) + require.NoError(t, err) + + h := handler.(*typedSkillHandler[*schema.Message]) + assert.Equal(t, "custom prompt for skill", h.instruction) + }) + + t.Run("custom tool description", func(t *testing.T) { + backend := &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Description: "d1"}, Content: "c1"}, + }} + handler, err := NewMiddleware(ctx, &Config{ + Backend: backend, + CustomToolDescription: func(_ context.Context, skills []FrontMatter) string { + return fmt.Sprintf("custom desc with %d skills", len(skills)) + }, + }) + require.NoError(t, err) + + h := handler.(*typedSkillHandler[*schema.Message]) + info, err := h.tool.Info(ctx) + require.NoError(t, err) + assert.Equal(t, "custom desc with 1 skills", info.Desc) + }) +} + +func TestBeforeAgent(t *testing.T) { + ctx := context.Background() + backend := &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Description: "d1"}, Content: "c1"}, + }} + handler, err := NewMiddleware(ctx, &Config{Backend: backend}) + require.NoError(t, err) + + runCtx := &adk.ChatModelAgentContext{ + Instruction: "base instruction", + Tools: []tool.BaseTool{}, + } + _, newRunCtx, err := handler.BeforeAgent(ctx, runCtx) + assert.NoError(t, err) + assert.Contains(t, newRunCtx.Instruction, "base instruction") + assert.Contains(t, newRunCtx.Instruction, "Skills System") + assert.Len(t, newRunCtx.Tools, 1) + + // verify the added tool is the skill tool + info, err := newRunCtx.Tools[0].Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "skill", info.Name) +} + +func TestWrapModel_SwitchesModelWhenRunLocalIsSet(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + other := &fakeToolCallingModel{id: "other"} + + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{"other": other}}, + }) + require.NoError(t, err) + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "t", + Description: "t", + Model: base, + Handlers: []adk.ChatModelAgentMiddleware{ + &runLocalSetterHandler{key: activeModelKey, val: "other"}, + handler, + }, + MaxIterations: 1, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &adk.AgentInput{Messages: []adk.Message{schema.UserMessage("hi")}}) + var last string + for { + ev, ok := iter.Next() + if !ok { + break + } + require.NoError(t, ev.Err) + if ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + msg, err := ev.Output.MessageOutput.GetMessage() + require.NoError(t, err) + if msg != nil { + last = msg.Content + } + } + + assert.Equal(t, "other", last) + assert.Equal(t, 0, base.calls) + assert.Equal(t, 1, other.calls) +} + +func TestWrapModel_OutsideAgentContextReturnsError(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + other := &fakeToolCallingModel{id: "other"} + + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{"other": other}}, + }) + require.NoError(t, err) + + h := handler.(*typedSkillHandler[*schema.Message]) + _, err = h.WrapModel(ctx, base, &adk.ModelContext{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get active model from run local value") +} + +func TestWrapModel_IgnoresNonStringRunLocalValue(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + other := &fakeToolCallingModel{id: "other"} + + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{"other": other}}, + }) + require.NoError(t, err) + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "t", + Description: "t", + Model: base, + Handlers: []adk.ChatModelAgentMiddleware{ + &runLocalSetterHandler{key: activeModelKey, val: 123}, + handler, + }, + MaxIterations: 1, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &adk.AgentInput{Messages: []adk.Message{schema.UserMessage("hi")}}) + var last string + for { + ev, ok := iter.Next() + if !ok { + break + } + require.NoError(t, ev.Err) + if ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + msg, err := ev.Output.MessageOutput.GetMessage() + require.NoError(t, err) + if msg != nil { + last = msg.Content + } + } + + assert.Equal(t, "base", last) + assert.Equal(t, 1, base.calls) + assert.Equal(t, 0, other.calls) +} + +func TestWrapModel_ModelHubGetError(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{}}, + }) + require.NoError(t, err) + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "t", + Description: "t", + Model: base, + Handlers: []adk.ChatModelAgentMiddleware{ + &runLocalSetterHandler{key: activeModelKey, val: "missing"}, + handler, + }, + MaxIterations: 1, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &adk.AgentInput{Messages: []adk.Message{schema.UserMessage("hi")}}) + var gotErr error + for { + ev, ok := iter.Next() + if !ok { + break + } + if ev.Err != nil { + gotErr = ev.Err + break + } + } + require.Error(t, gotErr) + assert.Contains(t, gotErr.Error(), "failed to get model") + assert.Equal(t, 0, base.calls) +} + +func TestWrapModel_ModelHubNilKeepsBase(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + }) + require.NoError(t, err) + + h := handler.(*typedSkillHandler[*schema.Message]) + m, err := h.WrapModel(ctx, base, &adk.ModelContext{}) + require.NoError(t, err) + assert.Equal(t, base, m) +} + +func TestWrapModel_RunLocalNotFoundKeepsBase(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + other := &fakeToolCallingModel{id: "other"} + + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{"other": other}}, + }) + require.NoError(t, err) + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "t", + Description: "t", + Model: base, + Handlers: []adk.ChatModelAgentMiddleware{ + handler, + }, + MaxIterations: 1, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &adk.AgentInput{Messages: []adk.Message{schema.UserMessage("hi")}}) + var last string + for { + ev, ok := iter.Next() + if !ok { + break + } + require.NoError(t, ev.Err) + if ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + msg, err := ev.Output.MessageOutput.GetMessage() + require.NoError(t, err) + if msg != nil { + last = msg.Content + } + } + + assert.Equal(t, "base", last) + assert.Equal(t, 1, base.calls) + assert.Equal(t, 0, other.calls) +} + +func TestWrapModel_IgnoresEmptyStringRunLocalValue(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + other := &fakeToolCallingModel{id: "other"} + + handler, err := NewMiddleware(ctx, &Config{ + Backend: &inMemoryBackend{m: []Skill{}}, + ModelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{"other": other}}, + }) + require.NoError(t, err) + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "t", + Description: "t", + Model: base, + Handlers: []adk.ChatModelAgentMiddleware{ + &runLocalSetterHandler{key: activeModelKey, val: ""}, + handler, + }, + MaxIterations: 1, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &adk.AgentInput{Messages: []adk.Message{schema.UserMessage("hi")}}) + var last string + for { + ev, ok := iter.Next() + if !ok { + break + } + require.NoError(t, ev.Err) + if ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + msg, err := ev.Output.MessageOutput.GetMessage() + require.NoError(t, err) + if msg != nil { + last = msg.Content + } + } + + assert.Equal(t, "base", last) + assert.Equal(t, 1, base.calls) + assert.Equal(t, 0, other.calls) +} + +func TestGetMessagesFromState_InAgentContext(t *testing.T) { + ctx := context.Background() + + base := &fakeToolCallingModel{id: "base"} + st := &typedSkillTool[*schema.Message]{} + capture := &stateMessagesCaptureHandler{st: st} + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "t", + Description: "t", + Model: base, + Handlers: []adk.ChatModelAgentMiddleware{ + capture, + }, + MaxIterations: 1, + }) + require.NoError(t, err) + + iter := agent.Run(ctx, &adk.AgentInput{Messages: []adk.Message{schema.UserMessage("hi")}}) + for { + ev, ok := iter.Next() + if !ok { + break + } + require.NoError(t, ev.Err) + } + + require.NotNil(t, capture.captured) + require.NotEmpty(t, capture.captured) +} + +func TestSkillToolInfo(t *testing.T) { + ctx := context.Background() + + t.Run("list error propagates", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &errorBackend{listErr: errors.New("list failed")}, + toolName: "skill", + } + info, err := st.Info(ctx) + assert.Nil(t, info) + assert.Error(t, err) + assert.Contains(t, err.Error(), "list failed") + }) + + t.Run("description contains all skills", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "alpha", Description: "desc-alpha"}}, + {FrontMatter: FrontMatter{Name: "beta", Description: "desc-beta"}}, + }}, + toolName: "skill", + } + info, err := st.Info(ctx) + require.NoError(t, err) + assert.Contains(t, info.Desc, "alpha") + assert.Contains(t, info.Desc, "desc-alpha") + assert.Contains(t, info.Desc, "beta") + assert.Contains(t, info.Desc, "desc-beta") + }) + + t.Run("custom tool params is used", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "alpha", Description: "desc-alpha"}}, + }}, + toolName: "skill", + customToolParams: func(_ context.Context, _ map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) { + return map[string]*schema.ParameterInfo{ + "foo": {Type: schema.String, Desc: "foo-desc", Required: true}, + "skill": {Type: schema.String, Desc: "custom-skill-desc", Required: false}, + }, nil + }, + } + info, err := st.Info(ctx) + require.NoError(t, err) + js, err := info.ParamsOneOf.ToJSONSchema() + require.NoError(t, err) + _, ok := js.Properties.Get("foo") + require.True(t, ok) + v, ok := js.Properties.Get("skill") + require.True(t, ok) + assert.Equal(t, "custom-skill-desc", v.Description) + assert.Contains(t, js.Required, "skill") + assert.Contains(t, js.Required, "foo") + }) +} + +func TestInvokableRun_InlineMode(t *testing.T) { + ctx := context.Background() + + t.Run("invalid json returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{}}, + toolName: "skill", + } + _, err := st.InvokableRun(ctx, "not json") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmarshal") + }) + + t.Run("skill not found returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{}}, + toolName: "skill", + } + _, err := st.InvokableRun(ctx, `{"skill": "nonexistent"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get skill") + }) + + t.Run("inline mode returns skill content", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "pdf", Description: "PDF processing"}, + Content: "Process PDF files here", + BaseDirectory: "/skills/pdf", + }, + }}, + toolName: "skill", + } + result, err := st.InvokableRun(ctx, `{"skill": "pdf"}`) + assert.NoError(t, err) + assert.Contains(t, result, "pdf") + assert.Contains(t, result, "/skills/pdf") + assert.Contains(t, result, "Process PDF files here") + }) + + t.Run("inline mode with model triggers setActiveModel", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "pdf", Description: "PDF processing", Model: "m1"}, + Content: "Process PDF files here", + BaseDirectory: "/skills/pdf", + }, + }}, + toolName: "skill", + } + result, err := st.InvokableRun(ctx, `{"skill": "pdf"}`) + assert.NoError(t, err) + assert.Contains(t, result, "pdf") + }) + + t.Run("custom skill content is used", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "pdf", Description: "PDF processing"}, + Content: "Process PDF files here", + BaseDirectory: "/skills/pdf", + }, + }}, + toolName: "skill", + buildContent: func(_ context.Context, _ Skill, rawArgs string) (string, error) { + var raw map[string]any + require.NoError(t, json.Unmarshal([]byte(rawArgs), &raw)) + assert.Equal(t, "pdf", raw["skill"]) + return "custom-content", nil + }, + } + result, err := st.InvokableRun(ctx, `{"skill":"pdf"}`) + assert.NoError(t, err) + assert.Equal(t, "custom-content", result) + }) + + t.Run("custom tool params with decoder is used", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "pdf", Description: "PDF processing"}, + Content: "Process PDF files here", + BaseDirectory: "/skills/pdf", + }, + }}, + toolName: "skill", + customToolParams: func(_ context.Context, _ map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) { + return map[string]*schema.ParameterInfo{ + "skill": {Type: schema.String, Desc: "custom", Required: false}, + "task": {Type: schema.String, Desc: "custom", Required: false}, + "x": {Type: schema.Integer, Desc: "custom", Required: false}, + }, nil + }, + buildContent: func(_ context.Context, _ Skill, rawArgs string) (string, error) { + var raw struct { + Skill string `json:"skill"` + Task string `json:"task"` + X int `json:"x"` + } + require.NoError(t, json.Unmarshal([]byte(rawArgs), &raw)) + assert.Equal(t, "pdf", raw.Skill) + assert.Equal(t, "t", raw.Task) + assert.Equal(t, 1, raw.X) + return "decoded", nil + }, + } + result, err := st.InvokableRun(ctx, `{"skill":"pdf","task":"t","x":1}`) + assert.NoError(t, err) + assert.Equal(t, "decoded", result) + }) + + t.Run("custom skill content returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "pdf", Description: "PDF processing"}, + Content: "Process PDF files here", + BaseDirectory: "/skills/pdf", + }, + }}, + toolName: "skill", + buildContent: func(context.Context, Skill, string) (string, error) { + return "", errors.New("boom") + }, + } + _, err := st.InvokableRun(ctx, `{"skill":"pdf"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to build skill result") + }) +} + +func TestInvokableRun_AgentMode(t *testing.T) { + ctx := context.Background() + + t.Run("fork mode without AgentHub returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1"}, + }}, + toolName: "skill", + } + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "AgentHub is not configured") + }) + + t.Run("fork_with_context mode without AgentHub returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeForkWithContext}, Content: "c1"}, + }}, + toolName: "skill", + } + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "AgentHub is not configured") + }) + + t.Run("fork_with_context mode without state returns error", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("agent response", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeForkWithContext}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + } + + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get messages from state") + }) + + t.Run("model specified without ModelHub returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork, Model: "gpt-4"}, Content: "c1"}, + }}, + toolName: "skill", + agentHub: &mockAgentHub{}, + } + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "ModelHub is not configured") + }) + + t.Run("model not found in ModelHub returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork, Model: "gpt-4"}, Content: "c1"}, + }}, + toolName: "skill", + agentHub: &mockAgentHub{}, + modelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{}}, + } + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get model") + }) + + t.Run("agent not found in AgentHub returns error", func(t *testing.T) { + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork, Agent: "nonexistent"}, Content: "c1"}, + }}, + toolName: "skill", + agentHub: &mockAgentHub{agents: map[string]adk.Agent{}}, + } + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get agent") + }) + + t.Run("fork mode runs agent and returns result", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("agent response", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "test-skill", Context: ContextModeFork}, + Content: "skill content", + BaseDirectory: "/skills/test", + }, + }}, + toolName: "skill", + agentHub: hub, + } + + result, err := st.InvokableRun(ctx, `{"skill": "test-skill"}`) + assert.NoError(t, err) + assert.Contains(t, result, "test-skill") + assert.Contains(t, result, "agent response") + assert.Contains(t, result, "completed") + // verify no model was passed in opts + assert.NotNil(t, hub.lastOpts) + assert.Nil(t, hub.lastOpts.Model) + }) + + t.Run("fork mode with model passes model to AgentHub", func(t *testing.T) { + m := &mockModel{name: "test-model"} + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("response", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + { + FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork, Model: "test-model"}, + Content: "c1", + BaseDirectory: "/skills/s1", + }, + }}, + toolName: "skill", + agentHub: hub, + modelHub: &mockModelHub{models: map[string]model.BaseModel[*schema.Message]{"test-model": m}}, + } + + result, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.NoError(t, err) + assert.Contains(t, result, "s1") + // verify model was passed + assert.NotNil(t, hub.lastOpts) + assert.Equal(t, m, hub.lastOpts.Model) + }) + + t.Run("agent returns multiple events", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("part1", nil), + }, + }, + }, + {Output: nil}, // nil output should be skipped + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("part2", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + } + + result, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.NoError(t, err) + assert.Contains(t, result, "part1") + assert.Contains(t, result, "part2") + }) + + t.Run("agent returns empty content events", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + } + + result, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.NoError(t, err) + // result should contain skill name but no extra content + assert.Contains(t, result, "s1") + }) + + t.Run("agent event error returns error", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + {Err: errors.New("boom")}, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + } + + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to run agent event") + }) + + t.Run("custom fork messages is used", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("ok", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + buildForkMessages: func(_ context.Context, in SubAgentInput) ([]adk.Message, error) { + assert.Equal(t, ContextModeFork, in.Mode) + assert.Equal(t, "s1", in.Skill.Name) + return []adk.Message{schema.UserMessage("custom")}, nil + }, + } + + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.NoError(t, err) + require.NotNil(t, agent.lastIn) + require.Len(t, agent.lastIn.Messages, 1) + msg := agent.lastIn.Messages[0] + assert.Equal(t, "custom", msg.Content) + }) + + t.Run("custom fork messages returns error", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("ok", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + buildForkMessages: func(context.Context, SubAgentInput) ([]adk.Message, error) { + return nil, errors.New("build msg fail") + }, + } + + _, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to build fork messages") + }) + + t.Run("custom fork result prompts from results is used", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("p1", nil), + }, + }, + }, + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("p2", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + formatForkResult: func(_ context.Context, in SubAgentOutput) (string, error) { + assert.Equal(t, ContextModeFork, in.Mode) + assert.Equal(t, []string{"p1", "p2"}, in.Results) + return "E:" + strings.Join(in.Results, ","), nil + }, + } + + result, err := st.InvokableRun(ctx, `{"skill": "s1"}`) + assert.NoError(t, err) + assert.Equal(t, "E:p1,p2", result) + }) + + t.Run("custom fork result prompts returns error", func(t *testing.T) { + agent := &mockAgent{ + events: []*adk.AgentEvent{ + { + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Message: schema.AssistantMessage("p1", nil), + }, + }, + }, + }, + } + hub := &mockAgentHub{defaultAgent: agent} + + st := &typedSkillTool[*schema.Message]{ + b: &inMemoryBackend{m: []Skill{ + {FrontMatter: FrontMatter{Name: "s1", Context: ContextModeFork}, Content: "c1", BaseDirectory: "/d"}, + }}, + toolName: "skill", + agentHub: hub, + formatForkResult: func(context.Context, SubAgentOutput) (string, error) { + return "", errors.New("format fail") + }, + } + + _, err := st.InvokableRun(ctx, `{"skill":"s1"}`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to format fork result") + assert.Contains(t, err.Error(), "format fail") + }) +} + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + mw, err := NewTyped(ctx, &TypedConfig[*schema.AgenticMessage]{ + Backend: &inMemoryBackend{m: []Skill{}}, + }) + assert.NoError(t, err) + assert.NotNil(t, mw) + + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} diff --git a/adk/middlewares/summarization/consts.go b/adk/middlewares/summarization/consts.go new file mode 100644 index 0000000..ae9e117 --- /dev/null +++ b/adk/middlewares/summarization/consts.go @@ -0,0 +1,39 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package summarization + +type ActionType string + +const ( + ActionTypeBeforeSummarize ActionType = "before_summarize" + ActionTypeAfterSummarize ActionType = "after_summarize" + ActionTypeGenerateSummary ActionType = "generate_summary" +) + +const ( + extraKeyContentType = "_eino_summarization_content_type" + preserveUserMsgsMaxTokens = 30000 +) + +type summarizationContentType string + +const ( + contentTypeSummary summarizationContentType = "summary" + contentTypeSkills summarizationContentType = "skills" +) + +type ctxKeyModelInput struct{} diff --git a/adk/middlewares/summarization/customized_action.go b/adk/middlewares/summarization/customized_action.go new file mode 100644 index 0000000..61000f5 --- /dev/null +++ b/adk/middlewares/summarization/customized_action.go @@ -0,0 +1,101 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package summarization + +import ( + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// TypedCustomizedAction is the generic customized action for summarization events. +type TypedCustomizedAction[M adk.MessageType] struct { + // Type is the action type. + Type ActionType `json:"type"` + + // Before is set when Type is ActionTypeBeforeSummarize. + // Emitted after trigger condition is met, before calling model to generate summary. + Before *TypedBeforeSummarizeAction[M] `json:"before,omitempty"` + + // After is set when Type is ActionTypeAfterSummarize. + // Emitted after summarization. + After *TypedAfterSummarizeAction[M] `json:"after,omitempty"` + + // GenerateSummary is set when Type is ActionTypeGenerateSummary. + // Emitted on each summary generation attempt, including retries and failovers. + GenerateSummary *TypedGenerateSummaryAction[M] `json:"generate_summary,omitempty"` +} + +// CustomizedAction is the default action type using *schema.Message. +type CustomizedAction = TypedCustomizedAction[*schema.Message] + +// TypedBeforeSummarizeAction contains the state messages before summarization. +type TypedBeforeSummarizeAction[M adk.MessageType] struct { + // Messages is the original state messages before summarization. + Messages []M `json:"messages,omitempty"` +} + +// BeforeSummarizeAction is the default type using *schema.Message. +type BeforeSummarizeAction = TypedBeforeSummarizeAction[*schema.Message] + +// TypedAfterSummarizeAction contains the state messages after summarization. +type TypedAfterSummarizeAction[M adk.MessageType] struct { + // Messages is the final state messages after summarization. + Messages []M `json:"messages,omitempty"` +} + +// AfterSummarizeAction is the default type using *schema.Message. +type AfterSummarizeAction = TypedAfterSummarizeAction[*schema.Message] + +// GenerateSummaryPhase indicates which phase a model generate attempt belongs to during summarization. +type GenerateSummaryPhase string + +const ( + // GenerateSummaryPhasePrimary indicates an attempt using the primary model. + // Attempt=1 is the initial call; Attempt>1 indicates a retry. + GenerateSummaryPhasePrimary GenerateSummaryPhase = "primary" + + // GenerateSummaryPhaseFailover indicates an attempt using a failover model + // after the primary model exhausted all retries or was deemed unrecoverable. + GenerateSummaryPhaseFailover GenerateSummaryPhase = "failover" +) + +// TypedGenerateSummaryAction contains details of a single model generate attempt during summarization. +// Emitted on every attempt, whether it succeeds or fails. +type TypedGenerateSummaryAction[M adk.MessageType] struct { + // Attempt is the 1-based attempt number within the current phase. + // For primary phase, Attempt=1 is the initial call and Attempt>1 indicates retries. + // For failover phase, Attempt counts the failover rounds (1, 2, 3, ...). + Attempt int `json:"attempt"` + + // Phase indicates which phase this generate attempt belongs to. + Phase GenerateSummaryPhase `json:"phase"` + + // ModelResponse is the raw response returned by the model. + // It may be nil when the model call fails without returning a response. + ModelResponse M `json:"model_response,omitempty"` + + // err is the error returned by the model call, if any. Use GetError to access it. + err error +} + +// GenerateSummaryAction is the default type using *schema.Message. +type GenerateSummaryAction = TypedGenerateSummaryAction[*schema.Message] + +// GetError returns the error from the model call, if any. +func (a *TypedGenerateSummaryAction[M]) GetError() error { + return a.err +} diff --git a/adk/middlewares/summarization/finalizer_builder.go b/adk/middlewares/summarization/finalizer_builder.go new file mode 100644 index 0000000..7e79b2b --- /dev/null +++ b/adk/middlewares/summarization/finalizer_builder.go @@ -0,0 +1,442 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package summarization + +import ( + "context" + "fmt" + "strings" + "unicode/utf8" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// DefaultFinalize is the default TypedFinalizeFunc implementation, providing the same +// summary post-processing as the middleware does internally: +// 1. Replaces the ... section in the model-generated +// summary with recent original user messages from the conversation (up to ~30k tokens). +// 2. Adds a preamble and a postamble around the summary content. +// 3. Converts the summary into a user message, prepended with the original system messages. +func DefaultFinalize[M adk.MessageType](ctx context.Context, originalMessages []M, summary M) ([]M, error) { + systemMsgs, contextMsgs := splitSystemAndContextMsgs(originalMessages) + + processed, err := postProcessSummary(ctx, &postProcessSummaryParams[M]{ + contextMsgs: contextMsgs, + summaryContent: getAssistantTextContent(summary), + }) + if err != nil { + return nil, err + } + + result := make([]M, 0, len(systemMsgs)+1) + result = append(result, systemMsgs...) + result = append(result, processed) + return result, nil +} + +// TypedFinalizerBuilder builds a TypedFinalizeFunc by chaining handlers, +// generic over message type M. +type TypedFinalizerBuilder[M adk.MessageType] struct { + handlers []TypedFinalizeFunc[M] + errs []error +} + +// FinalizerBuilder is a backward-compatible alias for TypedFinalizerBuilder +// specialized with *schema.Message. +type FinalizerBuilder = TypedFinalizerBuilder[*schema.Message] + +// NewTypedFinalizer creates a new TypedFinalizerBuilder. +// +// Handlers run in registration order, and the summary message is post-processed +// as DefaultFinalize does after all handlers have run. For example, with +// PreserveSkills and a system message in originalMessages, the final output is: +// +// [system message, preserved skill message, processed summary] +// +// Example: +// +// finalizer, err := NewTypedFinalizer[*schema.Message](). +// PreserveSkills(&PreserveSkillsConfig{}). +// Build() +// +// cfg := &Config{ +// Finalize: finalizer, +// // ... +// } +func NewTypedFinalizer[M adk.MessageType]() *TypedFinalizerBuilder[M] { + return &TypedFinalizerBuilder[M]{} +} + +// NewFinalizer creates a new FinalizerBuilder that builds a FinalizeFunc +// by chaining handlers. +func NewFinalizer() *FinalizerBuilder { + return &FinalizerBuilder{} +} + +// Build constructs the final TypedFinalizeFunc by chaining all registered handlers +// and post-processing the summary message as DefaultFinalize does. +// For example, with PreserveSkills and a system message in +// originalMessages, the final output is: +// +// [system message, preserved skill message, processed summary] +func (b *TypedFinalizerBuilder[M]) Build() (TypedFinalizeFunc[M], error) { + if len(b.errs) > 0 { + msgs := make([]string, len(b.errs)) + for i, e := range b.errs { + msgs[i] = e.Error() + } + return nil, fmt.Errorf("failed to build finalizer:\n%s", strings.Join(msgs, "\n")) + } + + if len(b.handlers) == 0 { + return nil, fmt.Errorf("at least one handler is required") + } + + handlers := make([]TypedFinalizeFunc[M], len(b.handlers)) + copy(handlers, b.handlers) + + return func(ctx context.Context, originalMessages []M, summary M) ([]M, error) { + var extraMessages []M + for _, fn := range handlers { + result, err := fn(ctx, originalMessages, summary) + if err != nil { + return nil, err + } + if len(result) == 0 { + return nil, fmt.Errorf("finalizer handler returned no messages") + } + extraMessages = append(extraMessages, result[:len(result)-1]...) + summary = result[len(result)-1] + } + + systemMsgs, contextMsgs := splitSystemAndContextMsgs(originalMessages) + + processed, err := postProcessSummary(ctx, &postProcessSummaryParams[M]{ + contextMsgs: contextMsgs, + summaryContent: getAssistantTextContent(summary), + }) + if err != nil { + return nil, err + } + + result := make([]M, 0, len(systemMsgs)+len(extraMessages)+1) + result = append(result, systemMsgs...) + result = append(result, extraMessages...) + result = append(result, processed) + return result, nil + }, nil +} + +type PreserveSkillsConfig struct { + // SkillToolName is the tool name used for loading skills. + // Must match the tool name configured in the ADK skill middleware. + // Optional. Defaults to "skill". + SkillToolName string + + // MaxSkills limits the maximum number of skills to preserve. + // = 0 means do not preserve any skills (disabled). + // > 0 means preserve up to this many most recent skills. + // Optional. Defaults to 5. + MaxSkills *int + + // MaxTokensPerSkill limits the maximum token count for a single preserved skill. + // Skills exceeding this limit are truncated, with the truncated portion replaced + // by a short marker text (e.g. "[... skill content truncated ...]"). + // Note: if this value is set smaller than the token count of the marker text itself, + // the skill will contain only the marker text with no original content preserved. + // Optional. Defaults to 5000. + MaxTokensPerSkill *int + + // SkillsTokenBudget limits the total token count for all preserved skills combined. + // Skills are preserved from most recent to oldest; once the budget is exhausted, + // remaining skills are dropped. + // Optional. Defaults to 25000. + SkillsTokenBudget *int +} + +// PreserveSkills preserves skill contents loaded by the ADK skill middleware. +// It scans the conversation for matching skill tool calls and returns the preserved +// skill content as a user message before the summary. +// +// Example: +// +// messages: [assistant(tool_call: skill "foo"), tool(content: "bar")] +// summary: S +// +// When skill content is found, PreserveSkills returns: +// +// []M{user(""), S} +func (b *TypedFinalizerBuilder[M]) PreserveSkills(config *PreserveSkillsConfig) *TypedFinalizerBuilder[M] { + if err := config.check(); err != nil { + b.errs = append(b.errs, fmt.Errorf("PreserveSkills: %w", err)) + return b + } + b.handlers = append(b.handlers, func(ctx context.Context, originalMessages []M, summary M) ([]M, error) { + messages := originalMessages + + modelInput, ok := ctx.Value(ctxKeyModelInput{}).([]M) + if ok && len(modelInput) > 0 { + messages = modelInput + } + + if len(messages) == 0 { + return []M{summary}, nil + } + + skillText, err := buildPreservedSkillsText(ctx, messages, config) + if err != nil { + return nil, err + } + + if skillText == "" { + return []M{summary}, nil + } + + preserved := makeUserMsg[M](skillText) + setMsgExtra(preserved, extraKeyContentType, string(contentTypeSkills)) + return []M{preserved, summary}, nil + }) + return b +} + +func (c *PreserveSkillsConfig) check() error { + if c == nil { + return fmt.Errorf("PreserveSkillsConfig is required") + } + if c.MaxSkills != nil && *c.MaxSkills < 0 { + return fmt.Errorf("MaxSkills must be non-negative") + } + if c.MaxTokensPerSkill != nil && *c.MaxTokensPerSkill < 0 { + return fmt.Errorf("MaxTokensPerSkill must be non-negative") + } + if c.SkillsTokenBudget != nil && *c.SkillsTokenBudget < 0 { + return fmt.Errorf("SkillsTokenBudget must be non-negative") + } + return nil +} + +type skillInfo struct { + Name string + Content string +} + +func extractSkillInfos[M adk.MessageType](messages []M, skillTool string) ([]*skillInfo, error) { + var skills []*skillInfo + argsMap := make(map[string]string) + + for _, msg := range messages { + switch m := any(msg).(type) { + case *schema.Message: + if m.Role == schema.Assistant { + for _, tc := range m.ToolCalls { + if tc.Function.Name == skillTool { + argsMap[tc.ID] = tc.Function.Arguments + } + } + } else if m.Role == schema.Tool { + arguments, ok := argsMap[m.ToolCallID] + if !ok { + continue + } + var arg struct { + Skill string `json:"skill"` + } + if err := sonic.UnmarshalString(arguments, &arg); err != nil { + return nil, fmt.Errorf("failed to parse skill arguments from tool call %s: %w", m.ToolCallID, err) + } + skills = append(skills, &skillInfo{ + Name: arg.Skill, + Content: messageUserTextContent(m), + }) + } + + case *schema.AgenticMessage: + for _, block := range m.ContentBlocks { + if block == nil { + continue + } + if block.Type == schema.ContentBlockTypeFunctionToolCall && block.FunctionToolCall != nil { + if block.FunctionToolCall.Name == skillTool { + argsMap[block.FunctionToolCall.CallID] = block.FunctionToolCall.Arguments + } + } + if block.Type == schema.ContentBlockTypeFunctionToolResult && block.FunctionToolResult != nil { + arguments, ok := argsMap[block.FunctionToolResult.CallID] + if !ok { + continue + } + var arg struct { + Skill string `json:"skill"` + } + if err := sonic.UnmarshalString(arguments, &arg); err != nil { + return nil, fmt.Errorf("failed to parse skill arguments from tool call %s: %w", block.FunctionToolResult.CallID, err) + } + var contentParts []string + for _, cb := range block.FunctionToolResult.Content { + if cb != nil && cb.Type == schema.FunctionToolResultContentBlockTypeText && cb.Text != nil { + contentParts = append(contentParts, cb.Text.Text) + } + } + skills = append(skills, &skillInfo{ + Name: arg.Skill, + Content: strings.Join(contentParts, "\n"), + }) + } + } + } + } + + return skills, nil +} + +func buildPreservedSkillsText[M adk.MessageType](_ context.Context, messages []M, config *PreserveSkillsConfig) (string, error) { + const ( + defaultSkillTool = "skill" + defaultMaxTokensPerSkill = 5000 + defaultSkillsTokenBudget = 25000 + ) + + if config == nil { + config = &PreserveSkillsConfig{} + } + + maxSkills := 5 + if config.MaxSkills != nil { + maxSkills = *config.MaxSkills + } + if maxSkills <= 0 { + return "", nil + } + + skillTool := defaultSkillTool + if config.SkillToolName != "" { + skillTool = config.SkillToolName + } + + maxTokensPerSkill := defaultMaxTokensPerSkill + if config.MaxTokensPerSkill != nil { + maxTokensPerSkill = *config.MaxTokensPerSkill + } + + skillsTokenBudget := defaultSkillsTokenBudget + if config.SkillsTokenBudget != nil { + skillsTokenBudget = *config.SkillsTokenBudget + } + + skills, err := extractSkillInfos(messages, skillTool) + if err != nil { + return "", err + } + + if len(skills) == 0 { + return "", nil + } + + uniqueSkills := make([]*skillInfo, 0, len(skills)) + seenNames := make(map[string]bool) + for i := len(skills) - 1; i >= 0; i-- { + skill := skills[i] + if !seenNames[skill.Name] { + seenNames[skill.Name] = true + uniqueSkills = append(uniqueSkills, skill) + } + } + + for i, j := 0, len(uniqueSkills)-1; i < j; i, j = i+1, j-1 { + uniqueSkills[i], uniqueSkills[j] = uniqueSkills[j], uniqueSkills[i] + } + skills = uniqueSkills + + if len(skills) > maxSkills { + skills = skills[len(skills)-maxSkills:] + } + + totalTokens := 0 + var budgetedSkills []*skillInfo + for i := len(skills) - 1; i >= 0; i-- { + skill := skills[i] + tokens := estimateTokenCount(len(skill.Content)) + + if tokens > maxTokensPerSkill { + skill = &skillInfo{ + Name: skill.Name, + Content: truncateSkillContent(skill.Content, maxTokensPerSkill), + } + tokens = maxTokensPerSkill + } + + if totalTokens+tokens > skillsTokenBudget { + break + } + + totalTokens += tokens + budgetedSkills = append(budgetedSkills, skill) + } + + if len(budgetedSkills) == 0 { + return "", nil + } + + // Reverse to restore chronological order. + for i, j := 0, len(budgetedSkills)-1; i < j; i, j = i+1, j-1 { + budgetedSkills[i], budgetedSkills[j] = budgetedSkills[j], budgetedSkills[i] + } + + var parts []string + for _, skill := range budgetedSkills { + parts = append(parts, fmt.Sprintf(skillSectionFormat, skill.Name, skill.Content)) + } + + skillsText := strings.Join(parts, "\n\n---\n\n") + skillsText = fmt.Sprintf(getSkillPreamble(), skillsText) + skillsText = fmt.Sprintf("\n%s"+"\n", skillsText) + + return skillsText, nil +} + +// truncateSkillContent truncates skill content to fit within maxTokens. +// It keeps the first portion of the content and appends a truncation marker +// (e.g. "[... skill content truncated ...]") to indicate the omission. +// If maxTokens is smaller than the marker itself, only the marker is returned. +func truncateSkillContent(content string, maxTokens int) string { + if len(content) == 0 { + return content + } + + if estimateTokenCount(len(content)) <= maxTokens { + return content + } + + marker := getSkillTruncationMarker() + targetBytes := estimateTokenBytes(maxTokens) - len(marker) + if targetBytes < 0 { + targetBytes = 0 + } + if targetBytes > len(content) { + targetBytes = len(content) + } + + // Back up to a valid UTF-8 rune boundary. + for targetBytes > 0 && targetBytes < len(content) && !utf8.RuneStart(content[targetBytes]) { + targetBytes-- + } + + return content[:targetBytes] + marker +} diff --git a/adk/middlewares/summarization/finalizer_builder_test.go b/adk/middlewares/summarization/finalizer_builder_test.go new file mode 100644 index 0000000..a9290a0 --- /dev/null +++ b/adk/middlewares/summarization/finalizer_builder_test.go @@ -0,0 +1,528 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package summarization + +import ( + "context" + "errors" + "strings" + "testing" + "unicode/utf8" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +func TestNewFinalizer(t *testing.T) { + b := NewFinalizer() + assert.NotNil(t, b) + assert.Empty(t, b.handlers) + + tb := NewTypedFinalizer[*schema.Message]() + assert.NotNil(t, tb) + assert.Empty(t, tb.handlers) +} + +func TestBuildEmpty(t *testing.T) { + finalizer, err := NewFinalizer().Build() + assert.Error(t, err) + assert.Nil(t, finalizer) + assert.Contains(t, err.Error(), "at least one handler is required") +} + +func TestBuildConfigError(t *testing.T) { + ptr := func(i int) *int { return &i } + + t.Run("nil config", func(t *testing.T) { + finalizer, err := NewFinalizer(). + PreserveSkills(nil). + Build() + assert.Error(t, err) + assert.Nil(t, finalizer) + assert.Contains(t, err.Error(), "PreserveSkills:") + assert.Contains(t, err.Error(), "PreserveSkillsConfig is required") + }) + + t.Run("negative max skills", func(t *testing.T) { + finalizer, err := NewFinalizer(). + PreserveSkills(&PreserveSkillsConfig{MaxSkills: ptr(-1)}). + Build() + assert.Error(t, err) + assert.Nil(t, finalizer) + assert.Contains(t, err.Error(), "PreserveSkills:") + assert.Contains(t, err.Error(), "MaxSkills must be non-negative") + }) +} + +func TestDefaultFinalizeBasic(t *testing.T) { + ctx := context.Background() + + result, err := DefaultFinalize(ctx, []adk.Message{ + schema.SystemMessage("system prompt"), + schema.UserMessage("original user"), + }, schema.AssistantMessage("raw summary", nil)) + assert.NoError(t, err) + assert.Len(t, result, 2) + + assert.Equal(t, schema.System, result[0].Role) + assert.Equal(t, schema.User, result[1].Role) + assert.Equal(t, contentTypeSummary, typedGetContentType(result[1])) + assert.Contains(t, result[1].Content, "raw summary") +} + +func TestBuildStepChaining(t *testing.T) { + ctx := context.Background() + + b := NewFinalizer() + b.handlers = append(b.handlers, func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + summary.Content = summary.Content + " | step1" + return []adk.Message{summary}, nil + }) + b.handlers = append(b.handlers, func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + summary.Content = summary.Content + " | step2" + return []adk.Message{summary}, nil + }) + + finalizer, err := b.Build() + assert.NoError(t, err) + + summary := schema.AssistantMessage("start", nil) + result, err := finalizer(ctx, []adk.Message{}, summary) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, schema.User, result[0].Role) + assert.Contains(t, result[0].Content, "start | step1 | step2") +} + +func TestBuildStepError(t *testing.T) { + ctx := context.Background() + + b := NewFinalizer() + b.handlers = append(b.handlers, func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + return nil, errors.New("step failed") + }) + + finalizer, err := b.Build() + assert.NoError(t, err) + + summary := schema.UserMessage("test") + _, err = finalizer(ctx, []adk.Message{}, summary) + assert.Error(t, err) + assert.Contains(t, err.Error(), "step failed") +} + +func TestBuildHandlerReturnsEmpty(t *testing.T) { + ctx := context.Background() + + b := NewFinalizer() + b.handlers = append(b.handlers, func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + return []adk.Message{}, nil + }) + + finalizer, err := b.Build() + assert.NoError(t, err) + + _, err = finalizer(ctx, []adk.Message{}, schema.AssistantMessage("test", nil)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "finalizer handler returned no messages") +} + +func TestBuildPostProcessError(t *testing.T) { + ctx := context.Background() + + b := NewFinalizer() + b.handlers = append(b.handlers, func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + return []adk.Message{schema.UserMessage("not assistant")}, nil + }) + + finalizer, err := b.Build() + assert.NoError(t, err) + + _, err = finalizer(ctx, []adk.Message{}, schema.AssistantMessage("test", nil)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "summary content is empty") +} + +func TestDefaultFinalizeError(t *testing.T) { + ctx := context.Background() + + _, err := DefaultFinalize(ctx, []adk.Message{ + schema.UserMessage("original"), + }, schema.UserMessage("not an assistant message")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "summary content is empty") +} + +func TestPreserveSkillsConfigCheck(t *testing.T) { + ptr := func(i int) *int { return &i } + + t.Run("nil config", func(t *testing.T) { + var c *PreserveSkillsConfig + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "PreserveSkillsConfig is required") + }) + + t.Run("valid config", func(t *testing.T) { + c := &PreserveSkillsConfig{ + MaxSkills: ptr(5), + SkillToolName: "load_skill", + } + assert.NoError(t, c.check()) + }) + + t.Run("zero max skills", func(t *testing.T) { + c := &PreserveSkillsConfig{ + MaxSkills: ptr(0), + } + assert.NoError(t, c.check()) + }) + + t.Run("negative max skills", func(t *testing.T) { + c := &PreserveSkillsConfig{ + MaxSkills: ptr(-1), + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "MaxSkills must be non-negative") + }) + + t.Run("nil max skills", func(t *testing.T) { + c := &PreserveSkillsConfig{} + err := c.check() + assert.NoError(t, err) + }) + + t.Run("negative max tokens per skill", func(t *testing.T) { + c := &PreserveSkillsConfig{ + MaxTokensPerSkill: ptr(-1), + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "MaxTokensPerSkill must be non-negative") + }) + + t.Run("negative skills token budget", func(t *testing.T) { + c := &PreserveSkillsConfig{ + SkillsTokenBudget: ptr(-1), + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "SkillsTokenBudget must be non-negative") + }) +} + +func TestPreserveSkillsViaBuilder(t *testing.T) { + ptr := func(i int) *int { return &i } + ctx := context.Background() + + finalizer, err := NewFinalizer(). + PreserveSkills(&PreserveSkillsConfig{ + MaxSkills: ptr(2), + SkillToolName: "load_skill", + }). + Build() + assert.NoError(t, err) + + originalMessages := []adk.Message{ + schema.SystemMessage("system prompt"), + schema.UserMessage("original"), + } + modelInput := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "load_skill", + Arguments: `{"skill": "test-skill"}`, + }, + }, + }, + }, + { + Role: schema.Tool, + ToolCallID: "call_1", + Content: "skill content 1", + }, + } + ctx = context.WithValue(ctx, ctxKeyModelInput{}, modelInput) + + summary := schema.AssistantMessage("test summary", nil) + + result, err := finalizer(ctx, originalMessages, summary) + assert.NoError(t, err) + assert.Len(t, result, 3) + + assert.Equal(t, schema.System, result[0].Role) + assert.Equal(t, "system prompt", result[0].Content) + + assert.Equal(t, schema.User, result[1].Role) + assert.Equal(t, contentTypeSkills, typedGetContentType(result[1])) + assert.Contains(t, result[1].Content, "test-skill") + assert.Contains(t, result[1].Content, "skill content 1") + + assert.Equal(t, schema.User, result[2].Role) + assert.Equal(t, contentTypeSummary, typedGetContentType(result[2])) + assert.Contains(t, result[2].Content, "test summary") +} + +func TestBuildPreservedSkillsText(t *testing.T) { + ptr := func(i int) *int { return &i } + ctx := context.Background() + + t.Run("nil config", func(t *testing.T) { + text, err := buildPreservedSkillsText[*schema.Message](ctx, nil, nil) + assert.NoError(t, err) + assert.Empty(t, text) + }) + + t.Run("zero max skills", func(t *testing.T) { + text, err := buildPreservedSkillsText[*schema.Message](ctx, nil, &PreserveSkillsConfig{MaxSkills: ptr(0)}) + assert.NoError(t, err) + assert.Empty(t, text) + }) + + t.Run("no matching skills", func(t *testing.T) { + text, err := buildPreservedSkillsText(ctx, []adk.Message{ + schema.UserMessage("hi"), + }, &PreserveSkillsConfig{ + MaxSkills: ptr(5), + SkillToolName: "load_skill", + }) + assert.NoError(t, err) + assert.Empty(t, text) + }) + + t.Run("with default skill tool name", func(t *testing.T) { + messages := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "skill", + Arguments: `{"skill": "test-skill"}`, + }, + }, + }, + }, + { + Role: schema.Tool, + ToolCallID: "call_1", + Content: "skill content 1", + }, + } + + config := &PreserveSkillsConfig{ + MaxSkills: ptr(2), + } + + text, err := buildPreservedSkillsText(ctx, messages, config) + assert.NoError(t, err) + assert.Contains(t, text, "test-skill") + assert.Contains(t, text, "skill content 1") + }) + + t.Run("parse error", func(t *testing.T) { + messages := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "load_skill", + Arguments: `invalid json`, + }, + }, + }, + }, + { + Role: schema.Tool, + ToolCallID: "call_1", + Content: "content", + }, + } + + _, err := buildPreservedSkillsText(ctx, messages, &PreserveSkillsConfig{ + MaxSkills: ptr(2), + SkillToolName: "load_skill", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse skill arguments") + }) + + t.Run("max skills truncation and deduplication", func(t *testing.T) { + messages := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill1"}`}}, + {ID: "call_2", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill2"}`}}, + {ID: "call_3", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill1"}`}}, + {ID: "call_4", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill3"}`}}, + }, + }, + {Role: schema.Tool, ToolCallID: "call_1", Content: "c1"}, + {Role: schema.Tool, ToolCallID: "call_2", Content: "c2"}, + {Role: schema.Tool, ToolCallID: "call_3", Content: "c3"}, + {Role: schema.Tool, ToolCallID: "call_4", Content: "c4"}, + } + + text, err := buildPreservedSkillsText(ctx, messages, &PreserveSkillsConfig{ + MaxSkills: ptr(2), + SkillToolName: "load_skill", + }) + assert.NoError(t, err) + assert.Contains(t, text, "skill1") + assert.Contains(t, text, "c3") + assert.Contains(t, text, "skill3") + assert.Contains(t, text, "c4") + assert.NotContains(t, text, "c1") + assert.NotContains(t, text, "skill2") + assert.NotContains(t, text, "c2") + }) + + t.Run("per skill token limit truncates large skills", func(t *testing.T) { + // estimateTokenCount = (len+3)/4 + // "short" = 5 chars → 2 tokens + // strings.Repeat("x", 100) = 100 chars → 25 tokens + largeContent := strings.Repeat("x", 100) + messages := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "small"}`}}, + {ID: "call_2", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "large"}`}}, + }, + }, + {Role: schema.Tool, ToolCallID: "call_1", Content: "short"}, + {Role: schema.Tool, ToolCallID: "call_2", Content: largeContent}, + } + + // MaxTokensPerSkill=10: "short"→2 tokens (ok), largeContent→25 tokens (truncated) + text, err := buildPreservedSkillsText(ctx, messages, &PreserveSkillsConfig{ + MaxSkills: ptr(10), + MaxTokensPerSkill: ptr(10), + SkillToolName: "load_skill", + }) + assert.NoError(t, err) + // small skill preserved as-is + assert.Contains(t, text, "small") + assert.Contains(t, text, "short") + // large skill is truncated, not dropped — name still present, full content gone + assert.Contains(t, text, "large") + assert.NotContains(t, text, largeContent) + assert.Contains(t, text, "skill content truncated for compaction") + }) + + t.Run("total token budget drops excess skills", func(t *testing.T) { + // Each content is 40 chars → (40+3)/4 = 10 tokens + content := strings.Repeat("a", 40) + messages := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill1"}`}}, + {ID: "call_2", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill2"}`}}, + {ID: "call_3", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "skill3"}`}}, + }, + }, + {Role: schema.Tool, ToolCallID: "call_1", Content: content}, + {Role: schema.Tool, ToolCallID: "call_2", Content: content}, + {Role: schema.Tool, ToolCallID: "call_3", Content: content}, + } + + // Budget=15: skill3=10 tokens fits, skill2=10 tokens → 10+10=20 > 15, stop. + text, err := buildPreservedSkillsText(ctx, messages, &PreserveSkillsConfig{ + MaxSkills: ptr(10), + SkillsTokenBudget: ptr(15), + SkillToolName: "load_skill", + }) + assert.NoError(t, err) + assert.Contains(t, text, "skill3") + assert.NotContains(t, text, "skill1") + assert.NotContains(t, text, "skill2") + }) + + t.Run("token budget and per-skill limit combined", func(t *testing.T) { + // s1: 16 chars → 4 tokens + // s2: 200 chars → 50 tokens (exceeds per-skill limit of 20, gets truncated) + // s3: 24 chars → 6 tokens + // s4: 24 chars → 6 tokens + messages := []adk.Message{ + { + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "s1"}`}}, + {ID: "call_2", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "s2"}`}}, + {ID: "call_3", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "s3"}`}}, + {ID: "call_4", Function: schema.FunctionCall{Name: "load_skill", Arguments: `{"skill": "s4"}`}}, + }, + }, + {Role: schema.Tool, ToolCallID: "call_1", Content: strings.Repeat("a", 16)}, + {Role: schema.Tool, ToolCallID: "call_2", Content: strings.Repeat("b", 200)}, + {Role: schema.Tool, ToolCallID: "call_3", Content: strings.Repeat("c", 24)}, + {Role: schema.Tool, ToolCallID: "call_4", Content: strings.Repeat("d", 24)}, + } + + // Per-skill limit: 20 (s2 with 50 tokens is truncated to 20) + // Budget: 30 (from most recent: s4=6, s3=6, s2=20, total=32 > 30, so s2 cannot fit) + // Result: s4 and s3 preserved + text, err := buildPreservedSkillsText(ctx, messages, &PreserveSkillsConfig{ + MaxSkills: ptr(10), + MaxTokensPerSkill: ptr(20), + SkillsTokenBudget: ptr(30), + SkillToolName: "load_skill", + }) + assert.NoError(t, err) + assert.Contains(t, text, "s3") + assert.Contains(t, text, "s4") + assert.NotContains(t, text, "\"s1\"") + assert.NotContains(t, text, "\"s2\"") + }) + + t.Run("truncated skill content preserves only prefix", func(t *testing.T) { + // Use a long content and generous maxTokens so the prefix is clearly visible. + content := strings.Repeat("abcdefghij", 100) // 1000 bytes → 250 tokens + // maxTokens=125 → targetBytes = 500, minus ~101 marker bytes → ~399 prefix bytes + truncated := truncateSkillContent(content, 125) + assert.True(t, strings.HasPrefix(truncated, "abcdefghij")) // prefix preserved + assert.Contains(t, truncated, "skill content truncated for compaction") + assert.NotEqual(t, content, truncated) + // Ends with marker, not with original content suffix + assert.True(t, strings.HasSuffix(truncated, "]")) + // No suffix from original content + assert.False(t, strings.HasSuffix(truncated, "abcdefghij]")) + }) + + t.Run("truncated multibyte content does not produce invalid utf8", func(t *testing.T) { + // Each Chinese char is 3 bytes. 334 chars = 1002 bytes → 251 tokens + content := strings.Repeat("中", 334) + // maxTokens=125 → targetBytes=500, minus marker ~101 bytes → ~399 bytes + // 399 / 3 = 133 full Chinese chars, no partial rune + truncated := truncateSkillContent(content, 125) + assert.True(t, utf8.ValidString(truncated)) + assert.True(t, strings.HasPrefix(truncated, "中中中")) + assert.Contains(t, truncated, "skill content truncated for compaction") + }) +} diff --git a/adk/middlewares/summarization/prompt.go b/adk/middlewares/summarization/prompt.go new file mode 100644 index 0000000..086017e --- /dev/null +++ b/adk/middlewares/summarization/prompt.go @@ -0,0 +1,340 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package summarization + +import ( + "regexp" + + "github.com/cloudwego/eino/adk/internal" +) + +var allUserMessagesTagRegex = regexp.MustCompile(`(?s).*`) + +func getSystemInstruction() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: systemInstruction, + Chinese: systemInstructionZh, + }) +} + +func getUserSummaryInstruction() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: userSummaryInstruction, + Chinese: userSummaryInstructionZh, + }) +} + +func getSummaryPreamble() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: summaryPreamble, + Chinese: summaryPreambleZh, + }) +} + +func getContinueInstruction() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: continueInstruction, + Chinese: continueInstructionZh, + }) +} + +func getTranscriptPathInstruction() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: transcriptPathInstruction, + Chinese: transcriptPathInstructionZh, + }) +} + +func getTruncatedMarkerFormat() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: truncatedMarkerFormat, + Chinese: truncatedMarkerFormatZh, + }) +} + +func getUserMessagesReplacedNote() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: userMessagesReplacedNote, + Chinese: userMessagesReplacedNoteZh, + }) +} + +const systemInstruction = `You are a helpful AI assistant tasked with summarizing conversations.` + +const systemInstructionZh = `你是一个负责总结对话的 AI 助手。` + +const userSummaryInstruction = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools. + +- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool. +- You already have all the context you need in the conversation above. +- Tool calls will be REJECTED and will waste your only turn — you will fail the task. +- Your entire response must be plain text: an block followed by a block. + +Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. +This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context. + +Before providing your final summary, wrap your analysis in tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process: + +1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify: + - The user's explicit requests and intents + - Your approach to addressing the user's requests + - Key decisions, technical concepts and code patterns + - Specific details like: + - file names + - full code snippets + - function signatures + - file edits + - Errors that you ran into and how you fixed them + - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently. +2. Double-check for technical accuracy and completeness, addressing each required element thoroughly. + +Your summary should include the following sections: + +1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail +2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed. +3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important. +4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently. +5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts. +6. All user messages: List ALL user messages that are not tool results, and wrap them in the ... block. These are critical for understanding the users' feedback and changing intent. +7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on. +8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable. +9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first. + If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation. + +Here's an example of how your output should be structured: + + + +[Your thought process, ensuring all points are covered thoroughly and accurately] + + + +1. Primary Request and Intent: + [Detailed description] + +2. Key Technical Concepts: + - [Concept 1] + - [Concept 2] + - [...] + +3. Files and Code Sections: + - [File Name 1] + - [Summary of why this file is important] + - [Summary of the changes made to this file, if any] + - [Important Code Snippet] + - [File Name 2] + - [Important Code Snippet] + - [...] + +4. Errors and fixes: + - [Detailed description of error 1]: + - [How you fixed the error] + - [User feedback on the error if any] + - [...] + +5. Problem Solving: + [Description of solved problems and ongoing troubleshooting] + +6. All user messages: + + - [Detailed non tool use user message] + - [...] + + +7. Pending Tasks: + - [Task 1] + - [Task 2] + - [...] + +8. Current Work: + [Precise description of current work] + +9. Optional Next Step: + [Optional Next step to take] + + + + +Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response. + +There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include: + +## Compact Instructions +When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them. + + + +# Summary instructions +When you are using compact - please focus on test output and code changes. Include file reads verbatim. + + + +REMINDER: Do NOT call any tools. Respond with plain text only — an block followed by a block. Tool calls will be rejected and you will fail the task. +` + +const userSummaryInstructionZh = `关键:仅以文本响应。不要调用任何工具。 + +- 不要使用 Read、Bash、Grep、Glob、Edit、Write 或任何其他工具。 +- 你已经拥有上述对话中所需的全部上下文。 +- 工具调用将被拒绝,并且会浪费你唯一的一次回复机会——你将无法完成任务。 +- 你的整个回复必须是纯文本:先是一个 代码块,后面紧跟一个 代码块。 + +你的任务是对目前为止的对话创建一份详细的总结,需要密切关注用户的明确请求和你之前的操作。 +这份总结应该全面捕捉技术细节、代码模式和架构决策,以确保继续开发工作时不丢失上下文。 + +在提供最终总结之前,请将你的分析过程包裹在 标签中,以组织思路并确保涵盖所有必要的要点。在分析过程中: + +1. 按时间顺序分析对话中的每条消息和每个部分。对于每个部分,需要全面识别: + - 用户的明确请求和意图 + - 你处理用户请求的方法 + - 关键决策、技术概念和代码模式 + - 具体细节,例如: + - 文件名 + - 完整代码片段 + - 函数签名 + - 文件编辑 + - 你遇到的错误以及如何修复它们 + - 特别注意你收到的具体用户反馈,尤其是用户要求你以不同方式处理的情况 +2. 仔细检查技术准确性和完整性,彻底处理每个必需的元素。 + +你的总结应包含以下部分: + +1. 主要请求和意图:详细捕捉用户所有的明确请求和意图 +2. 关键技术概念:列出讨论过的所有重要技术概念、技术和框架 +3. 文件和代码部分:列举检查、修改或创建的具体文件和代码部分。特别注意最近的消息,在适用的地方包含完整的代码片段,并总结为什么这个文件的读取或编辑很重要 +4. 错误和修复:列出你遇到的所有错误以及如何修复它们。特别注意你收到的具体用户反馈,尤其是用户要求你以不同方式处理的情况 +5. 问题解决:记录已解决的问题和任何正在进行的故障排除工作 +6. 所有用户消息:列出所有非工具结果的用户消息,并将它们包裹在 ... 块中。这些对于理解用户的反馈和变化的意图至关重要 +7. 待处理任务:列出明确要求你处理的任何待处理任务 +8. 当前工作:详细描述在此总结请求之前正在进行的工作,特别注意用户和助手的最近消息。在适用的地方包含文件名和代码片段 +9. 可选的下一步:列出与你最近工作相关的下一步操作。重要提示:确保这一步与用户最近的明确请求以及你在此总结请求之前正在处理的任务直接相关。如果你的上一个任务已经完成,则只有在与用户请求明确相关时才列出下一步。不要在未与用户确认的情况下开始处理无关的请求或已经完成的旧请求。 + 如果有下一步,请包含最近对话中的直接引用,准确显示你正在处理的任务以及你停止的位置。这应该是逐字引用,以确保任务理解不会偏离。 + +以下是输出结构的示例: + + + +[你的思考过程,确保全面准确地涵盖所有要点] + + + +1. 主要请求和意图: + [详细描述] + +2. 关键技术概念: + - [概念 1] + - [概念 2] + - [...] + +3. 文件和代码部分: + - [文件名 1] + - [该文件为何重要的总结] + - [对该文件所做更改的总结(如有)] + - [重要代码片段] + - [文件名 2] + - [重要代码片段] + - [...] + +4. 错误与修复: + - [错误 1 的详细描述]: + - [你如何修复该错误] + - [与该错误相关的用户反馈(如有)] + - [...] + +5. 问题解决: + [对已解决问题和正在进行中的排查工作的描述] + +6. 所有用户消息: + + - [详细的非工具调用的用户消息] + - [...] + + +7. 待处理任务: + - [任务 1] + - [任务 2] + - [...] + +8. 当前工作: + [当前工作的精确描述] + +9. 可选的下一步: + [可选的下一步操作] + + + + +请根据目前为止的对话提供你的总结,遵循此结构并确保回复的精确性和全面性。 + +上下文中可能包含额外的总结指令。如果有,请在创建上述总结时记得遵循这些指令。指令示例包括: + +## 压缩指令 +在总结对话时,重点关注 typescript 代码更改,并记住你犯的错误以及如何修复它们。 + + + +# 总结指令 +当你使用压缩时,请重点关注测试输出和代码更改。逐字包含文件读取内容。 + + + +提醒:不要调用任何工具。仅以纯文本响应——一个 代码块后面跟一个 代码块。工具调用将被拒绝,你将无法完成任务。 +` + +const summaryPreamble = `This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.` + +const summaryPreambleZh = `此会话延续自此前一段因上下文耗尽而终止的对话。以下总结概述了此前对话的内容。` + +const continueInstruction = `Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.` + +const continueInstructionZh = `从对话中断的地方继续,不要再问用户任何问题。直接继续——不要提及总结,不要复述之前的经过,不要以"我会继续"之类的话作为开场白。就像中断从未发生过一样,继续上次的任务。` + +const transcriptPathInstruction = `If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: %s` + +const transcriptPathInstructionZh = `如果你需要压缩之前的具体细节(如精确的代码片段、错误消息或你生成的内容),完整的对话记录位于:%s` + +const truncatedMarkerFormat = "…%d characters truncated…" + +const truncatedMarkerFormatZh = "…已截断 %d 个字符…" + +const userMessagesReplacedNote = "Some earlier user messages have been cleared. Below are the most recent user messages:" + +const userMessagesReplacedNoteZh = "部分较早的用户消息已被清除,以下是保留的最近用户消息:" + +const skillSectionFormat = "### Skill: %s\n\n%s" + +const skillPreamble = "The following skills were invoked in this session. Continue to follow these guidelines:\n\n%s" + +const skillPreambleZh = "以下 Skill 已在本会话中被调用,请继续遵循这些指导原则:\n\n%s" + +func getSkillPreamble() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: skillPreamble, + Chinese: skillPreambleZh, + }) +} + +const skillTruncationMarker = "\n\n[... skill content truncated for compaction; use Read on the skill path if you need the full text]" + +const skillTruncationMarkerZh = "\n\n[... skill 内容已在压缩时截断,如需完整内容请通过 Read 读取 skill 对应的文件路径]" + +func getSkillTruncationMarker() string { + return internal.SelectPrompt(internal.I18nPrompts{ + English: skillTruncationMarker, + Chinese: skillTruncationMarkerZh, + }) +} diff --git a/adk/middlewares/summarization/summarization.go b/adk/middlewares/summarization/summarization.go new file mode 100644 index 0000000..6110107 --- /dev/null +++ b/adk/middlewares/summarization/summarization.go @@ -0,0 +1,1289 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package summarization provides a middleware that automatically summarizes +// conversation history when token count exceeds the configured threshold. +package summarization + +import ( + "context" + "fmt" + "math/rand" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +func init() { + schema.RegisterName[*TypedCustomizedAction[*schema.Message]]("_eino_adk_summarization_mw_customized_action") + schema.RegisterName[*TypedCustomizedAction[*schema.AgenticMessage]]("_eino_adk_summarization_mw_customized_action_agentic") +} + +type TypedTokenCounterFunc[M adk.MessageType] func(ctx context.Context, input *TypedTokenCounterInput[M]) (int, error) +type TypedGenModelInputFunc[M adk.MessageType] func(ctx context.Context, sysInstruction, userInstruction M, originalMsgs []M) ([]M, error) +type TypedGetFailoverModelFunc[M adk.MessageType] func(ctx context.Context, failoverCtx *TypedFailoverContext[M]) (failoverModel model.BaseModel[M], failoverModelInputMsgs []M, failoverErr error) +type TypedFinalizeFunc[M adk.MessageType] func(ctx context.Context, originalMessages []M, summary M) ([]M, error) +type TypedCallbackFunc[M adk.MessageType] func(ctx context.Context, before, after adk.TypedChatModelAgentState[M]) error + +type TokenCounterFunc = TypedTokenCounterFunc[*schema.Message] +type GenModelInputFunc = TypedGenModelInputFunc[*schema.Message] +type GetFailoverModelFunc = TypedGetFailoverModelFunc[*schema.Message] +type FinalizeFunc = TypedFinalizeFunc[*schema.Message] +type CallbackFunc = TypedCallbackFunc[*schema.Message] + +// TypedConfig defines the configuration for the summarization middleware, +// generic over message type M. +type TypedConfig[M adk.MessageType] struct { + // Model is the chat model used to generate summaries. + Model model.BaseModel[M] + + // ModelOptions specifies options passed to the model when generating summaries. + // Optional. + ModelOptions []model.Option + + // TokenCounter calculates the token count for given messages and tools. + // + // Parameters: + // - input: contains the messages and tools to count tokens for. + // + // Returns: + // - int: the total token count. + // + // Optional. Defaults to using the total tokens reported in the last assistant + // message as baseline, with incremental messages estimated at ~4 chars/token. + TokenCounter TypedTokenCounterFunc[M] + + // Trigger specifies the conditions that activate summarization. + // Optional. Defaults to triggering when total tokens exceed 160k. + Trigger *TriggerCondition + + // EmitInternalEvents indicates whether internal events should be emitted during summarization, + // allowing external observers to track the summarization process. + // + // Event Scoping: + // - ActionTypeBeforeSummarize: emitted before calling model to generate summary + // - ActionTypeGenerateSummary: emitted after each model generate attempt + // - ActionTypeAfterSummarize: emitted after summary generation completes + // + // Optional. Defaults to false. + EmitInternalEvents bool + + // UserInstruction serves as the user-level instruction to guide the model on how to summarize the context. + // It is appended to the message history as a User message. + // If provided, it overrides the default user summarization instruction. + // Optional. + UserInstruction string + + // TranscriptFilePath is the path to the file containing the full conversation history. + // It is appended to the summary to remind the model where to read the original context. + // This field takes effect only when Finalize is not set. + // Optional but strongly recommended. + TranscriptFilePath string + + // GenModelInput allows full control over the summarization model input construction. + // + // Parameters: + // - sysInstruction: System message defining the model's role. It is set + // internally by the middleware and is not configurable. + // - userInstruction: User message with the task instruction. + // - originalMsgs: original complete message list. + // + // Returns: + // - []M: the constructed model input messages. + // + // Typical model input order: systemInstruction -> contextMessages -> userInstruction. + // + // Optional. + GenModelInput TypedGenModelInputFunc[M] + + // Finalize is called after summary generation. + // The returned messages are used as the final conversation history. + // When set, the middleware does not perform any post-processing on the summary. + // Use DefaultFinalize to apply the same post-processing as the default path. + // + // Parameters: + // - originalMessages: the original conversation messages before summarization. + // - summary: the model-generated summary message. + // + // Returns: + // - []M: the new conversation history to replace the original messages. + // + // Optional. + Finalize TypedFinalizeFunc[M] + + // Callback is called after Finalize, before exiting the middleware. + // Read-only, do not modify state. + // + // Parameters: + // - before: the agent state before summarization. + // - after: the agent state after summarization. + // + // Optional. + Callback TypedCallbackFunc[M] + + // Retry configures retry behavior for summary generation on the primary model. + // Optional. Defaults to no retries. + Retry *TypedRetryConfig[M] + + // Failover configures fallback behavior when summary generation on the primary model fails. + // Optional. + Failover *TypedFailoverConfig[M] +} + +// Config is a backward-compatible alias for TypedConfig specialized with *schema.Message. +type Config = TypedConfig[*schema.Message] + +// TypedTokenCounterInput is the input for TypedTokenCounterFunc. +type TypedTokenCounterInput[M adk.MessageType] struct { + // Messages is the list of messages to count tokens for. + Messages []M + // Tools is the list of tools to count tokens for. + Tools []*schema.ToolInfo +} + +// TokenCounterInput is a backward-compatible alias for TypedTokenCounterInput specialized with *schema.Message. +type TokenCounterInput = TypedTokenCounterInput[*schema.Message] + +// TriggerCondition specifies when summarization should be activated. +// Summarization triggers if ANY of the set conditions is met. +type TriggerCondition struct { + // ContextTokens triggers summarization when total token count exceeds this threshold. + ContextTokens int + // ContextMessages triggers summarization when total messages count exceeds this threshold. + ContextMessages int +} + +type TypedRetryConfig[M adk.MessageType] struct { + // MaxRetries specifies the maximum number of retry attempts. + // Optional. Defaults to 3. + MaxRetries *int + + // ShouldRetry determines whether a failed summary generation attempt should be retried. + // It is called after each failed attempt with the model response and error. + // Optional. Defaults to retrying when err is non-nil. + ShouldRetry func(ctx context.Context, resp M, err error) bool + + // BackoffFunc calculates the delay before the next retry attempt. + // The attempt parameter starts at 1 for the first retry. + // Optional. Defaults to a default exponential backoff with jitter. + BackoffFunc func(ctx context.Context, attempt int, resp M, err error) time.Duration +} + +// RetryConfig is a backward-compatible alias for TypedRetryConfig specialized with *schema.Message. +type RetryConfig = TypedRetryConfig[*schema.Message] + +type TypedFailoverConfig[M adk.MessageType] struct { + // MaxRetries specifies the maximum number of failover attempts. + // Optional. Defaults to 3. + MaxRetries *int + + // ShouldFailover determines whether another failover attempt should be made. + // It is called after each failover attempt with the model response and error. + // Optional. Defaults to failing over when err is non-nil. + ShouldFailover func(ctx context.Context, resp M, err error) bool + + // BackoffFunc calculates the delay before the next failover attempt. + // The attempt parameter starts at 1 for the first failover attempt. + // Optional. Defaults to a default exponential backoff with jitter. + BackoffFunc func(ctx context.Context, attempt int, resp M, err error) time.Duration + + // GetFailoverModel selects the model and input messages for the current failover attempt. + // + // Parameters: + // - failoverCtx: contains the context for the current failover attempt. + // + // Returns: + // - failoverModel: the model to use for this failover attempt. + // - failoverModelInputMsgs: the input messages to send to failoverModel. + // - failoverErr: an error encountered while preparing the failover model or input. + // + // Constraints: + // - When provided, it must return a non-nil model and a non-empty input message list. + // + // Optional. Defaults to reusing the primary model with the default input messages. + GetFailoverModel TypedGetFailoverModelFunc[M] +} + +// FailoverConfig is a backward-compatible alias for TypedFailoverConfig specialized with *schema.Message. +type FailoverConfig = TypedFailoverConfig[*schema.Message] + +// TypedFailoverContext contains the state for a failover attempt. +type TypedFailoverContext[M adk.MessageType] struct { + // Attempt is the current failover attempt number, starting at 1. + Attempt int + + // SystemInstruction is the system instruction used for summary generation. + // It is set internally by the middleware and is not configurable. + SystemInstruction M + + // UserInstruction is the user instruction used for summary generation. + UserInstruction M + + // OriginalMessages is the full original conversation before summarization. + OriginalMessages []M + + // LastModelResponse is the response returned by the previous attempt, if any. + LastModelResponse M + + // LastErr is the error returned by the previous attempt, if any. + LastErr error +} + +// FailoverContext is a backward-compatible alias for TypedFailoverContext specialized with *schema.Message. +type FailoverContext = TypedFailoverContext[*schema.Message] + +// NewTyped creates a generic summarization middleware that automatically summarizes +// conversation history when trigger conditions are met. +// +// This is the generic constructor that supports both *schema.Message and *schema.AgenticMessage. +func NewTyped[M adk.MessageType](_ context.Context, cfg *TypedConfig[M]) (adk.TypedChatModelAgentMiddleware[M], error) { + if err := cfg.check(); err != nil { + return nil, err + } + mw := &TypedMiddleware[M]{ + cfg: cfg, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[M]{}, + } + return mw, nil +} + +// New creates a summarization middleware that automatically summarizes conversation history +// when trigger conditions are met. +func New(ctx context.Context, cfg *Config) (adk.ChatModelAgentMiddleware, error) { + return NewTyped(ctx, cfg) +} + +type TypedMiddleware[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + cfg *TypedConfig[M] +} + +func (m *TypedMiddleware[M]) Summarize(ctx context.Context, state *adk.TypedChatModelAgentState[M]) ([]M, error) { + beforeState := *state + + if m.cfg.EmitInternalEvents { + err := m.emitEvent(ctx, &TypedCustomizedAction[M]{ + Type: ActionTypeBeforeSummarize, + Before: &TypedBeforeSummarizeAction[M]{ + Messages: beforeState.Messages, + }, + }) + if err != nil { + return nil, err + } + } + + rawSummary, modelInput, err := m.summarize(ctx, beforeState.Messages) + if err != nil { + return nil, err + } + + finalizeCtx := context.WithValue(ctx, ctxKeyModelInput{}, modelInput) + + finalizer := m.cfg.Finalize + if finalizer == nil { + finalizer = buildInternalFinalizer(m.cfg) + } + finalMsgs, err := finalizer(finalizeCtx, beforeState.Messages, rawSummary) + if err != nil { + return nil, err + } + + if m.cfg.Callback != nil { + afterState := beforeState + afterState.Messages = finalMsgs + if err = m.cfg.Callback(ctx, beforeState, afterState); err != nil { + return nil, err + } + } + + if m.cfg.EmitInternalEvents { + err = m.emitEvent(ctx, &TypedCustomizedAction[M]{ + Type: ActionTypeAfterSummarize, + After: &TypedAfterSummarizeAction[M]{ + Messages: finalMsgs, + }, + }) + if err != nil { + return nil, err + } + } + + return finalMsgs, nil +} + +func (m *TypedMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *adk.TypedChatModelAgentState[M], + _ *adk.TypedModelContext[M]) (context.Context, *adk.TypedChatModelAgentState[M], error) { + + triggered, err := m.shouldSummarize(ctx, &TypedTokenCounterInput[M]{ + Messages: state.Messages, + Tools: state.ToolInfos, + }) + if err != nil { + return nil, nil, err + } + if !triggered { + return ctx, state, nil + } + + finalMsgs, err := m.Summarize(ctx, state) + if err != nil { + return nil, nil, err + } + + afterState := *state + afterState.Messages = finalMsgs + + return ctx, &afterState, nil +} + +func (m *TypedMiddleware[M]) shouldSummarize(ctx context.Context, input *TypedTokenCounterInput[M]) (bool, error) { + if m.cfg.Trigger != nil && m.cfg.Trigger.ContextMessages > 0 { + if len(input.Messages) > m.cfg.Trigger.ContextMessages { + return true, nil + } + } + tokens, err := m.countTokens(ctx, input) + if err != nil { + return false, fmt.Errorf("failed to count tokens: %w", err) + } + return tokens > m.getTriggerContextTokens(), nil +} + +func (m *TypedMiddleware[M]) getTriggerContextTokens() int { + const defaultTriggerContextTokens = 160000 + if m.cfg.Trigger != nil { + return m.cfg.Trigger.ContextTokens + } + return defaultTriggerContextTokens +} + +func (m *TypedMiddleware[M]) emitEvent(ctx context.Context, action *TypedCustomizedAction[M]) error { + err := adk.TypedSendEvent(ctx, &adk.TypedAgentEvent[M]{ + Action: &adk.AgentAction{ + CustomizedAction: action, + }, + }) + if err != nil { + return fmt.Errorf("failed to send internal event: %w", err) + } + return nil +} + +func (m *TypedMiddleware[M]) emitGenerateSummaryEvent(ctx context.Context, attempt int, phase GenerateSummaryPhase, + resp M, err error) error { + + if !m.cfg.EmitInternalEvents { + return nil + } + + action := &TypedGenerateSummaryAction[M]{ + Attempt: attempt, + Phase: phase, + ModelResponse: resp, + err: err, + } + + return m.emitEvent(ctx, &TypedCustomizedAction[M]{ + Type: ActionTypeGenerateSummary, + GenerateSummary: action, + }) +} + +func (m *TypedMiddleware[M]) countTokens(ctx context.Context, input *TypedTokenCounterInput[M]) (int, error) { + if m.cfg.TokenCounter != nil { + return m.cfg.TokenCounter(ctx, input) + } + return defaultTypedTokenCounter(ctx, input) +} + +func defaultTypedTokenCounter[M adk.MessageType](_ context.Context, input *TypedTokenCounterInput[M]) (int, error) { + var ( + baseTokens int + incrementStart int + ) + + for i := len(input.Messages) - 1; i >= 0; i-- { + if tokens := getAssistantTotalTokens(input.Messages[i]); tokens > 0 { + baseTokens = tokens + incrementStart = i + 1 + break + } + } + + var incrementTokens int + for _, msg := range input.Messages[incrementStart:] { + switch m := any(msg).(type) { + case *schema.Message: + incrementTokens += estimateMessageTokens(m) + case *schema.AgenticMessage: + incrementTokens += estimateAgenticMessageTokens(m) + } + } + + for _, tl := range input.Tools { + tl_ := *tl + tl_.Extra = nil + text, err := sonic.MarshalString(tl_) + if err != nil { + return 0, fmt.Errorf("failed to marshal tool info: %w", err) + } + incrementTokens += estimateTokenCount(len(text)) + } + + return baseTokens + incrementTokens, nil +} + +func getAssistantTotalTokens[M adk.MessageType](msg M) int { + if msg == nil { + return 0 + } + switch m := any(msg).(type) { + case *schema.Message: + if m.Role != schema.Assistant || m.ResponseMeta == nil || m.ResponseMeta.Usage == nil { + return 0 + } + return m.ResponseMeta.Usage.TotalTokens + case *schema.AgenticMessage: + if m.Role != schema.AgenticRoleTypeAssistant || m.ResponseMeta == nil || m.ResponseMeta.TokenUsage == nil { + return 0 + } + return m.ResponseMeta.TokenUsage.TotalTokens + } + return 0 +} + +func estimateTokenCount(charLen int) int { + return charLen / 4 +} + +func estimateTokenBytes(tokens int) int { + return tokens * 4 +} + +func (m *TypedMiddleware[M]) summarize(ctx context.Context, originalMsgs []M) (M, []M, error) { + var zero M + _, contextMsgs := splitSystemAndContextMsgs(originalMsgs) + + modelInput, err := m.buildSummarizationModelInput(ctx, originalMsgs, contextMsgs) + if err != nil { + return zero, nil, err + } + + rawSummary, err := m.generateWithRetry(ctx, m.cfg.Model, modelInput, m.cfg.ModelOptions, m.cfg.Retry) + if typedShouldFailover(ctx, m.cfg.Failover, rawSummary, err) { + rawSummary, modelInput, err = m.runFailover(ctx, originalMsgs, modelInput, rawSummary, err) + if err != nil { + return zero, nil, err + } + } else if err != nil { + return zero, nil, fmt.Errorf("failed to generate summary: %w", err) + } + + return rawSummary, modelInput, nil +} + +func splitSystemAndContextMsgs[M adk.MessageType](msgs []M) ([]M, []M) { + var systemMsgs []M + for _, msg := range msgs { + if isSystemRole(msg) { + systemMsgs = append(systemMsgs, msg) + } else { + break + } + } + contextMsgs := msgs[len(systemMsgs):] + return systemMsgs, contextMsgs +} + +func (m *TypedMiddleware[M]) runFailover(ctx context.Context, originalMsgs, defaultInput []M, lastResp M, + lastErr error) (M, []M, error) { + + var zero M + const defaultMaxRetries = 3 + + sysInstruction, userInstruction := m.getModelInstructions() + + maxRetries := defaultMaxRetries + if m.cfg.Failover.MaxRetries != nil { + maxRetries = *m.cfg.Failover.MaxRetries + } + + backoff := m.cfg.Failover.BackoffFunc + if backoff == nil { + backoff = defaultTypedBackoffFunc[M] + } + + modelInput := defaultInput + + if maxRetries <= 0 { + return lastResp, modelInput, lastErr + } + + for attempt := 1; ; attempt++ { + fctx := &TypedFailoverContext[M]{ + Attempt: attempt, + SystemInstruction: sysInstruction, + UserInstruction: userInstruction, + OriginalMessages: originalMsgs, + LastModelResponse: lastResp, + LastErr: lastErr, + } + + failoverModel, nextInput, failoverErr := m.getFailoverModel(ctx, fctx, defaultInput) + if failoverErr != nil { + lastResp = zero + lastErr = failoverErr + if emitErr := m.emitGenerateSummaryEvent(ctx, attempt, GenerateSummaryPhaseFailover, zero, failoverErr); emitErr != nil { + return zero, nil, emitErr + } + } else { + modelInput = nextInput + lastResp, lastErr = m.generateAndEmit(ctx, failoverModel, modelInput, m.cfg.ModelOptions, attempt, GenerateSummaryPhaseFailover) + } + + if !typedShouldFailover(ctx, m.cfg.Failover, lastResp, lastErr) { + return lastResp, modelInput, lastErr + } + if attempt == maxRetries { + if lastErr != nil { + return zero, nil, fmt.Errorf("exceeds max failover attempts: %w", lastErr) + } + return zero, nil, fmt.Errorf("exceeds max failover attempts") + } + + select { + case <-time.After(backoff(ctx, attempt, lastResp, lastErr)): + case <-ctx.Done(): + return zero, nil, ctx.Err() + } + } +} + +func (m *TypedMiddleware[M]) getFailoverModel(ctx context.Context, failoverCtx *TypedFailoverContext[M], defaultInput []M) (model.BaseModel[M], []M, error) { + if m.cfg.Failover == nil { + return nil, nil, fmt.Errorf("failover config is required") + } + if m.cfg.Failover.GetFailoverModel == nil { + return m.cfg.Model, defaultInput, nil + } + + failoverModel, nextModelInput, err := m.cfg.Failover.GetFailoverModel(ctx, failoverCtx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get failover model: %w", err) + } + + if failoverModel == nil { + return nil, nil, fmt.Errorf("failover model is required") + } + if len(nextModelInput) == 0 { + return nil, nil, fmt.Errorf("failover model input messages are required") + } + + return failoverModel, nextModelInput, nil +} + +func (m *TypedMiddleware[M]) buildSummarizationModelInput(ctx context.Context, originMsgs, contextMsgs []M) ([]M, error) { + sysInstruction, userInstruction := m.getModelInstructions() + + if m.cfg.GenModelInput != nil { + input, err := m.cfg.GenModelInput(ctx, sysInstruction, userInstruction, originMsgs) + if err != nil { + return nil, fmt.Errorf("failed to generate model input: %w", err) + } + return input, nil + } + + input := make([]M, 0, len(contextMsgs)+2) + input = append(input, sysInstruction) + input = append(input, contextMsgs...) + input = append(input, userInstruction) + + return input, nil +} + +func (m *TypedMiddleware[M]) getModelInstructions() (M, M) { + userInstruction := m.cfg.UserInstruction + if userInstruction == "" { + userInstruction = getUserSummaryInstruction() + } + + return makeSystemMsg[M](getSystemInstruction()), makeUserMsg[M](userInstruction) +} + +func buildInternalFinalizer[M adk.MessageType](cfg *TypedConfig[M]) TypedFinalizeFunc[M] { + return func(ctx context.Context, originalMessages []M, rawSummary M) ([]M, error) { + systemMsgs, contextMsgs := splitSystemAndContextMsgs(originalMessages) + + processed, err := postProcessSummary(ctx, &postProcessSummaryParams[M]{ + contextMsgs: contextMsgs, + summaryContent: getAssistantTextContent(rawSummary), + transcriptPath: cfg.TranscriptFilePath, + }) + if err != nil { + return nil, err + } + + return append(systemMsgs, processed), nil + } +} + +func getAssistantTextContent[M adk.MessageType](msg M) string { + switch r := any(msg).(type) { + case *schema.Message: + if r.Role != schema.Assistant { + return "" + } + var parts []string + for _, part := range r.AssistantGenMultiContent { + if part.Type == schema.ChatMessagePartTypeText && part.Text != "" { + parts = append(parts, part.Text) + } + } + if len(parts) > 0 { + return strings.Join(parts, "\n") + } + return r.Content + case *schema.AgenticMessage: + if r.Role != schema.AgenticRoleTypeAssistant { + return "" + } + var parts []string + for _, block := range r.ContentBlocks { + if block != nil && block.AssistantGenText != nil { + parts = append(parts, block.AssistantGenText.Text) + } + } + return strings.Join(parts, "\n") + default: + return "" + } +} + +type postProcessSummaryParams[M adk.MessageType] struct { + contextMsgs []M + summaryContent string + transcriptPath string +} + +func postProcessSummary[M adk.MessageType](ctx context.Context, p *postProcessSummaryParams[M]) (processed M, err error) { + content := p.summaryContent + if content == "" { + return processed, fmt.Errorf("summary content is empty") + } + + if len(p.contextMsgs) > 0 { + newContent, err := replaceUserMessagesInSummary(ctx, &replaceUserMessagesInSummaryParams[M]{ + contextMsgs: p.contextMsgs, + summaryText: content, + }) + if err != nil { + return processed, fmt.Errorf("failed to populate user messages in summary: %w", err) + } + content = newContent + } + + if p.transcriptPath != "" { + content = appendSection(content, fmt.Sprintf(getTranscriptPathInstruction(), p.transcriptPath)) + } + + content = appendSection(getSummaryPreamble(), content) + content = appendSection(content, getContinueInstruction()) + + processed = newTypedSummaryMessage[M](content) + + return processed, nil +} + +type replaceUserMessagesInSummaryParams[M adk.MessageType] struct { + contextMsgs []M + summaryText string +} + +func replaceUserMessagesInSummary[M adk.MessageType](ctx context.Context, p *replaceUserMessagesInSummaryParams[M]) (string, error) { + var userMsgs []M + var hasUserMsgs bool + for _, msg := range p.contextMsgs { + if isInternalUserMessage(msg) { + continue + } + if isUserRole(msg) { + hasUserMsgs = true + userMsgs = append(userMsgs, msg) + } + } + + if !hasUserMsgs { + return p.summaryText, nil + } + + var selected []M + if len(userMsgs) == 1 { + selected = userMsgs + } else { + var totalTokens int + for i := len(userMsgs) - 1; i >= 0; i-- { + msg := userMsgs[i] + + tokens, err := defaultTypedTokenCounter(ctx, &TypedTokenCounterInput[M]{ + Messages: []M{msg}, + }) + if err != nil { + return "", fmt.Errorf("failed to count tokens: %w", err) + } + + remaining := preserveUserMsgsMaxTokens - totalTokens + if tokens <= remaining { + totalTokens += tokens + selected = append(selected, msg) + continue + } + + trimmedMsg := defaultTypedTrimUserMessage(msg, remaining) + var zero M + if any(trimmedMsg) != any(zero) { + selected = append(selected, trimmedMsg) + } + + break + } + + for i, j := 0, len(selected)-1; i < j; i, j = i+1, j-1 { + selected[i], selected[j] = selected[j], selected[i] + } + } + + var msgLines []string + for _, msg := range selected { + text := getUserMsgTextContent(msg) + if text != "" { + msgLines = append(msgLines, " - "+text) + } + } + userMsgsText := strings.Join(msgLines, "\n") + + lastMatch := findLastMatch(allUserMessagesTagRegex, p.summaryText) + if lastMatch == nil { + return p.summaryText, nil + } + + var replacement string + if len(selected) < len(userMsgs) { + replacement = "\n" + getUserMessagesReplacedNote() + "\n" + userMsgsText + "\n" + } else { + replacement = "\n" + userMsgsText + "\n" + } + + newSummaryText := p.summaryText[:lastMatch[0]] + replacement + p.summaryText[lastMatch[1]:] + + return newSummaryText, nil +} + +func findLastMatch(re *regexp.Regexp, s string) []int { + matches := re.FindAllStringIndex(s, -1) + if len(matches) == 0 { + return nil + } + return matches[len(matches)-1] +} + +func appendSection(base, section string) string { + if base == "" { + return section + } + if section == "" { + return base + } + return base + "\n\n" + section +} + +func (m *TypedMiddleware[M]) generateAndEmit(ctx context.Context, chatModel model.BaseModel[M], input []M, + opts []model.Option, attempt int, phase GenerateSummaryPhase) (M, error) { + + resp, err := chatModel.Generate(ctx, input, opts...) + if emitErr := m.emitGenerateSummaryEvent(ctx, attempt, phase, resp, err); emitErr != nil { + var zero M + return zero, emitErr + } + return resp, err +} + +func (m *TypedMiddleware[M]) generateWithRetry(ctx context.Context, chatModel model.BaseModel[M], input []M, + opts []model.Option, retryCfg *TypedRetryConfig[M]) (M, error) { + + const defaultMaxRetries = 3 + + if retryCfg == nil { + return m.generateAndEmit(ctx, chatModel, input, opts, 1, GenerateSummaryPhasePrimary) + } + + shouldRetry := retryCfg.ShouldRetry + if shouldRetry == nil { + shouldRetry = defaultTypedShouldRetry[M] + } + backoffFunc := retryCfg.BackoffFunc + if backoffFunc == nil { + backoffFunc = defaultTypedBackoffFunc[M] + } + + maxRetries := defaultMaxRetries + if retryCfg.MaxRetries != nil { + maxRetries = *retryCfg.MaxRetries + } + totalAttempts := maxRetries + 1 + + var ( + lastModelResp M + lastErr error + ) + for attempt := 1; attempt <= totalAttempts; attempt++ { + resp, err := m.generateAndEmit(ctx, chatModel, input, opts, attempt, GenerateSummaryPhasePrimary) + if !shouldRetry(ctx, resp, err) { + return resp, err + } + + lastModelResp = resp + lastErr = err + if attempt < totalAttempts { + select { + case <-time.After(backoffFunc(ctx, attempt, resp, err)): + case <-ctx.Done(): + var zero M + return zero, ctx.Err() + } + } + } + + if maxRetries > 0 { + return lastModelResp, fmt.Errorf("exceeds max retries: %w", lastErr) + } + + return lastModelResp, lastErr +} + +func truncateTextByChars(text string) string { + const maxRunes = 2000 + + if text == "" { + return "" + } + + if utf8.RuneCountInString(text) <= maxRunes { + return text + } + + halfRunes := maxRunes / 2 + runes := []rune(text) + totalRunes := len(runes) + + prefix := string(runes[:halfRunes]) + suffix := string(runes[totalRunes-halfRunes:]) + removedChars := totalRunes - maxRunes + + marker := fmt.Sprintf(getTruncatedMarkerFormat(), removedChars) + + return prefix + marker + suffix +} + +func (c *TypedConfig[M]) check() error { + if c == nil { + return fmt.Errorf("config is required") + } + if c.Model == nil { + return fmt.Errorf("model is required") + } + if c.Trigger != nil { + if err := c.Trigger.check(); err != nil { + return err + } + } + if c.Retry != nil { + if err := c.Retry.check(); err != nil { + return err + } + } + if c.Failover != nil { + if err := c.Failover.check(); err != nil { + return err + } + } + return nil +} + +func (c *TypedRetryConfig[M]) check() error { + if c.MaxRetries != nil && *c.MaxRetries < 0 { + return fmt.Errorf("retry.MaxRetries must be non-negative") + } + return nil +} + +func (c *TypedFailoverConfig[M]) check() error { + if c.MaxRetries != nil && *c.MaxRetries < 0 { + return fmt.Errorf("failover.MaxRetries must be non-negative") + } + return nil +} + +func (c *TriggerCondition) check() error { + if c.ContextTokens < 0 { + return fmt.Errorf("contextTokens must be non-negative") + } + if c.ContextMessages < 0 { + return fmt.Errorf("contextMessages must be non-negative") + } + if c.ContextTokens == 0 && c.ContextMessages == 0 { + return fmt.Errorf("at least one of contextTokens or contextMessages must be non-negative") + } + return nil +} + +// ============================================================================ +// Generic helper functions +// ============================================================================ + +func isSystemRole[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.System + case *schema.AgenticMessage: + return m.Role == schema.AgenticRoleTypeSystem + } + panic("unreachable") +} + +func isUserRole[M adk.MessageType](msg M) bool { + switch m := any(msg).(type) { + case *schema.Message: + return m.Role == schema.User + case *schema.AgenticMessage: + return m.Role == schema.AgenticRoleTypeUser + } + panic("unreachable") +} + +func messageUserTextContent(m *schema.Message) string { + if m == nil { + return "" + } + var parts []string + for _, part := range m.UserInputMultiContent { + if part.Type == schema.ChatMessagePartTypeText && part.Text != "" { + parts = append(parts, part.Text) + } + } + if len(parts) > 0 { + return strings.Join(parts, "\n") + } + return m.Content +} + +func getUserMsgTextContent[M adk.MessageType](msg M) string { + switch m := any(msg).(type) { + case *schema.Message: + return messageUserTextContent(m) + + case *schema.AgenticMessage: + if m == nil { + return "" + } + var parts []string + for _, block := range m.ContentBlocks { + if block == nil { + continue + } + if block.UserInputText != nil { + parts = append(parts, block.UserInputText.Text) + } + } + return strings.Join(parts, "\n") + + default: + panic("unreachable") + } +} + +const multimodalTokenEstimate = 2000 + +func estimateMessageTokens(msg *schema.Message) int { + if msg == nil { + return 0 + } + var totalLen int + var multimodalTokens int + + if msg.Role == schema.Assistant { + if len(msg.AssistantGenMultiContent) > 0 { + hasReasoning := false + for _, part := range msg.AssistantGenMultiContent { + switch part.Type { + case schema.ChatMessagePartTypeText: + totalLen += len(part.Text) + case schema.ChatMessagePartTypeReasoning: + hasReasoning = true + if part.Reasoning != nil { + totalLen += len(part.Reasoning.Text) + } + case schema.ChatMessagePartTypeImageURL, schema.ChatMessagePartTypeAudioURL, + schema.ChatMessagePartTypeVideoURL, schema.ChatMessagePartTypeFileURL: + multimodalTokens += multimodalTokenEstimate + } + } + if !hasReasoning { + totalLen += len(msg.ReasoningContent) + } + } else { + totalLen += len(msg.Content) + len(msg.ReasoningContent) + } + for _, tc := range msg.ToolCalls { + totalLen += len(tc.Function.Name) + len(tc.Function.Arguments) + } + } else { + if len(msg.UserInputMultiContent) > 0 { + for _, part := range msg.UserInputMultiContent { + switch part.Type { + case schema.ChatMessagePartTypeText: + totalLen += len(part.Text) + case schema.ChatMessagePartTypeToolSearchResult: + if part.ToolSearchResult != nil { + for _, tl := range part.ToolSearchResult.Tools { + totalLen += len(tl.Name) + len(tl.Desc) + if b, err := sonic.Marshal(tl.ParamsOneOf); err == nil { + totalLen += len(b) + } + } + } + case schema.ChatMessagePartTypeImageURL, schema.ChatMessagePartTypeAudioURL, + schema.ChatMessagePartTypeVideoURL, schema.ChatMessagePartTypeFileURL: + multimodalTokens += multimodalTokenEstimate + } + } + } else { + totalLen += len(msg.Content) + } + } + + return estimateTokenCount(totalLen) + multimodalTokens +} + +func estimateAgenticMessageTokens(msg *schema.AgenticMessage) int { + if msg == nil { + return 0 + } + var totalLen int + var multimodalTokens int + + if msg.Role == schema.AgenticRoleTypeAssistant { + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + switch block.Type { + case schema.ContentBlockTypeAssistantGenText: + totalLen += len(block.AssistantGenText.Text) + case schema.ContentBlockTypeFunctionToolCall: + totalLen += len(block.FunctionToolCall.Name) + len(block.FunctionToolCall.Arguments) + case schema.ContentBlockTypeReasoning: + totalLen += len(block.Reasoning.Text) + case schema.ContentBlockTypeAssistantGenImage, schema.ContentBlockTypeAssistantGenAudio, + schema.ContentBlockTypeAssistantGenVideo: + multimodalTokens += multimodalTokenEstimate + } + } + } else { + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + switch block.Type { + case schema.ContentBlockTypeUserInputText: + totalLen += len(block.UserInputText.Text) + case schema.ContentBlockTypeFunctionToolResult: + for _, cb := range block.FunctionToolResult.Content { + if cb == nil { + continue + } + switch cb.Type { + case schema.FunctionToolResultContentBlockTypeText: + if cb.Text != nil { + totalLen += len(cb.Text.Text) + } + case schema.FunctionToolResultContentBlockTypeImage, schema.FunctionToolResultContentBlockTypeAudio, + schema.FunctionToolResultContentBlockTypeVideo, schema.FunctionToolResultContentBlockTypeFile: + multimodalTokens += multimodalTokenEstimate + } + } + case schema.ContentBlockTypeToolSearchResult: + if block.ToolSearchFunctionToolResult != nil && block.ToolSearchFunctionToolResult.Result != nil { + for _, tl := range block.ToolSearchFunctionToolResult.Result.Tools { + totalLen += len(tl.Name) + len(tl.Desc) + if b, err := sonic.Marshal(tl.ParamsOneOf); err == nil { + totalLen += len(b) + } + } + } + case schema.ContentBlockTypeUserInputImage, schema.ContentBlockTypeUserInputFile, + schema.ContentBlockTypeUserInputAudio, schema.ContentBlockTypeUserInputVideo: + multimodalTokens += multimodalTokenEstimate + } + } + } + + return estimateTokenCount(totalLen) + multimodalTokens +} + +func getMsgExtra[M adk.MessageType](msg M) map[string]any { + switch m := any(msg).(type) { + case *schema.Message: + return m.Extra + case *schema.AgenticMessage: + return m.Extra + default: + panic("unreachable") + } +} + +func setMsgExtra[M adk.MessageType](msg M, key string, value any) { + switch m := any(msg).(type) { + case *schema.Message: + if m.Extra == nil { + m.Extra = map[string]any{} + } + m.Extra[key] = value + case *schema.AgenticMessage: + if m.Extra == nil { + m.Extra = map[string]any{} + } + m.Extra[key] = value + } +} + +func makeSystemMsg[M adk.MessageType](text string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.SystemMessage(text)).(M) + case *schema.AgenticMessage: + return any(schema.SystemAgenticMessage(text)).(M) + default: + panic("unreachable") + } +} + +func makeUserMsg[M adk.MessageType](text string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.UserMessage(text)).(M) + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(text)).(M) + default: + panic("unreachable") + } +} + +func newTypedSummaryMessage[M adk.MessageType](content string) M { + msg := makeUserMsg[M](content) + setMsgExtra(msg, extraKeyContentType, string(contentTypeSummary)) + return msg +} + +func typedGetContentType[M adk.MessageType](msg M) summarizationContentType { + extra := getMsgExtra(msg) + if extra == nil { + return "" + } + ct, ok := extra[extraKeyContentType].(string) + if !ok { + return "" + } + return summarizationContentType(ct) +} + +func isInternalUserMessage[M adk.MessageType](msg M) bool { + return typedGetContentType(msg) == contentTypeSummary || isPreservedMessage(msg) +} + +func isPreservedMessage[M adk.MessageType](msg M) bool { + return typedGetContentType(msg) == contentTypeSkills +} + +func typedShouldFailover[M adk.MessageType](ctx context.Context, cfg *TypedFailoverConfig[M], resp M, err error) bool { + if cfg == nil { + return false + } + if cfg.ShouldFailover == nil { + return err != nil + } + return cfg.ShouldFailover(ctx, resp, err) +} + +func defaultTypedShouldRetry[M adk.MessageType](_ context.Context, _ M, err error) bool { + return err != nil +} + +func defaultTypedBackoffFunc[M adk.MessageType](_ context.Context, attempt int, _ M, _ error) time.Duration { + return defaultBackoffDuration(attempt) +} + +func defaultBackoffDuration(attempt int) time.Duration { + const ( + baseDelay = time.Second + maxDelay = 10 * time.Second + ) + + if attempt <= 0 { + return baseDelay + } + + if attempt > 7 { + return maxDelay + time.Duration(rand.Int63n(int64(maxDelay/2))) + } + + delay := baseDelay * time.Duration(1< maxDelay { + delay = maxDelay + } + + jitter := time.Duration(rand.Int63n(int64(delay / 2))) + + return delay + jitter +} + +func defaultTypedTrimUserMessage[M adk.MessageType](msg M, remainingTokens int) M { + var zero M + if remainingTokens <= 0 { + return zero + } + + textContent := getUserMsgTextContent(msg) + if len(textContent) == 0 { + return zero + } + + trimmed := truncateTextByChars(textContent) + if trimmed == "" { + return zero + } + + return makeUserMsg[M](trimmed) +} diff --git a/adk/middlewares/summarization/summarization_test.go b/adk/middlewares/summarization/summarization_test.go new file mode 100644 index 0000000..d70f396 --- /dev/null +++ b/adk/middlewares/summarization/summarization_test.go @@ -0,0 +1,2245 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package summarization + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +func intPtr(v int) *int { + return &v +} + +func TestNew(t *testing.T) { + ctx := context.Background() + + t.Run("valid config", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + cfg := &Config{ + Model: cm, + } + + mw, err := New(ctx, cfg) + assert.NoError(t, err) + assert.NotNil(t, mw) + }) + + t.Run("nil config returns error", func(t *testing.T) { + mw, err := New(ctx, nil) + assert.Error(t, err) + assert.Nil(t, mw) + }) + + t.Run("nil model returns error", func(t *testing.T) { + mw, err := New(ctx, &Config{}) + assert.Error(t, err) + assert.Nil(t, mw) + }) +} + +func TestMiddlewareBeforeModelRewriteState(t *testing.T) { + ctx := context.Background() + mtx := &adk.ModelContext{} + + t.Run("no summarization when under threshold", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 1000}, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage("hello"), + schema.AssistantMessage("hi", nil), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 2) + assert.Equal(t, "hello", newState.Messages[0].Content) + }) + + t.Run("summarization triggered when over threshold", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary content", + }, nil).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 10}, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + schema.AssistantMessage(strings.Repeat("b", 100), nil), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 1) + assert.Equal(t, schema.User, newState.Messages[0].Role) + }) + + t.Run("preserves system messages after summarization", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + for i, msg := range msgs { + if i == 0 { + assert.Equal(t, schema.System, msg.Role) + } else { + assert.NotEqual(t, schema.System, msg.Role) + } + } + return &schema.Message{ + Role: schema.Assistant, + Content: "Summary content", + }, nil + }).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 10}, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("You are a helpful assistant"), + schema.UserMessage(strings.Repeat("a", 100)), + schema.AssistantMessage(strings.Repeat("b", 100), nil), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 2) + assert.Equal(t, schema.System, newState.Messages[0].Role) + assert.Equal(t, "You are a helpful assistant", newState.Messages[0].Content) + assert.Equal(t, schema.User, newState.Messages[1].Role) + }) + + t.Run("preserves multiple system messages", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 10}, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("System 1"), + schema.SystemMessage("System 2"), + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 3) + assert.Equal(t, schema.System, newState.Messages[0].Role) + assert.Equal(t, "System 1", newState.Messages[0].Content) + assert.Equal(t, schema.System, newState.Messages[1].Role) + assert.Equal(t, "System 2", newState.Messages[1].Content) + assert.Equal(t, schema.User, newState.Messages[2].Role) + }) + + t.Run("custom finalize function", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 10}, + Finalize: func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + return []adk.Message{ + schema.SystemMessage("system prompt"), + summary, + }, nil + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 2) + assert.Equal(t, schema.System, newState.Messages[0].Role) + assert.Equal(t, "system prompt", newState.Messages[0].Content) + }) + + t.Run("retry succeeds after transient error", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + callCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + callCount++ + if callCount == 1 { + return nil, fmt.Errorf("transient error") + } + return &schema.Message{ + Role: schema.Assistant, + Content: "Summary after retry", + }, nil + }).Times(2) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 10}, + Retry: &RetryConfig{ + MaxRetries: intPtr(2), + BackoffFunc: func(_ context.Context, _ int, _ adk.Message, _ error) time.Duration { return 0 }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 1) + assert.Equal(t, 2, callCount) + }) + + t.Run("retry uses default max retries when MaxRetries is nil", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + callCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + callCount++ + return nil, fmt.Errorf("transient error") + }).Times(4) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 10}, + Retry: &RetryConfig{ + BackoffFunc: func(_ context.Context, _ int, _ adk.Message, _ error) time.Duration { return 0 }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, _, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to generate summary") + assert.Equal(t, 4, callCount) + }) + + t.Run("failover succeeds after primary failure", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + failover := mockModel.NewMockBaseChatModel(ctrl) + + primary.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("primary error")).Times(1) + failover.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...interface{}) (*schema.Message, error) { + assert.Len(t, msgs, 1) + assert.Equal(t, "failover input", msgs[0].Content) + return &schema.Message{ + Role: schema.Assistant, + Content: "Summary from failover", + }, nil + }).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: primary, + Trigger: &TriggerCondition{ContextTokens: 10}, + Failover: &FailoverConfig{ + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + assert.Equal(t, 1, failoverCtx.Attempt) + assert.Equal(t, schema.System, failoverCtx.SystemInstruction.Role) + assert.Equal(t, schema.User, failoverCtx.UserInstruction.Role) + assert.Len(t, failoverCtx.OriginalMessages, 1) + assert.Nil(t, failoverCtx.LastModelResponse) + assert.EqualError(t, failoverCtx.LastErr, "primary error") + return failover, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 1) + assert.Equal(t, schema.User, newState.Messages[0].Role) + }) + + t.Run("failover context last err is retry exhausted error when retries exhausted", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + failover := mockModel.NewMockBaseChatModel(ctrl) + + primary.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("primary error")).Times(2) + failover.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary from failover", + }, nil).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: primary, + Trigger: &TriggerCondition{ContextTokens: 10}, + Retry: &RetryConfig{ + MaxRetries: intPtr(1), + BackoffFunc: func(_ context.Context, _ int, _ adk.Message, _ error) time.Duration { return 0 }, + }, + Failover: &FailoverConfig{ + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + assert.ErrorContains(t, failoverCtx.LastErr, "exceeds max retries") + return failover, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, _, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + }) + + t.Run("returns failover exhausted error when failover model fails", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + failover := mockModel.NewMockBaseChatModel(ctrl) + + primary.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("primary error")).Times(1) + failover.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("failover error")).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: primary, + Trigger: &TriggerCondition{ContextTokens: 10}, + Failover: &FailoverConfig{ + MaxRetries: intPtr(1), + BackoffFunc: func(_ context.Context, _ int, _ adk.Message, _ error) time.Duration { return 0 }, + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return failover, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, _, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.Error(t, err) + assert.ErrorContains(t, err, "exceeds max failover attempts") + }) + + t.Run("failover retries with max retries and succeeds on second attempt", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + failover1 := mockModel.NewMockBaseChatModel(ctrl) + failover2 := mockModel.NewMockBaseChatModel(ctrl) + + primary.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("primary error")).Times(1) + failover1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("failover error 1")).Times(1) + failover2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary from second failover", + }, nil).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: primary, + Trigger: &TriggerCondition{ContextTokens: 10}, + Failover: &FailoverConfig{ + MaxRetries: intPtr(2), + BackoffFunc: func(_ context.Context, _ int, _ adk.Message, _ error) time.Duration { return 0 }, + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + if failoverCtx.Attempt == 1 { + assert.EqualError(t, failoverCtx.LastErr, "primary error") + return failover1, []*schema.Message{schema.UserMessage("failover input 1")}, nil + } + assert.Equal(t, 2, failoverCtx.Attempt) + assert.EqualError(t, failoverCtx.LastErr, "failover error 1") + return failover2, []*schema.Message{schema.UserMessage("failover input 2")}, nil + }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + assert.Len(t, newState.Messages, 1) + }) + + t.Run("failover context carries generate resp as last output message", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + failover := mockModel.NewMockBaseChatModel(ctrl) + + primary.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "partial output", + }, fmt.Errorf("primary error")).Times(1) + failover.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary from failover", + }, nil).Times(1) + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: primary, + Trigger: &TriggerCondition{ContextTokens: 10}, + Failover: &FailoverConfig{ + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + if assert.NotNil(t, failoverCtx.LastModelResponse) { + assert.Equal(t, "partial output", failoverCtx.LastModelResponse.Content) + } + return failover, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + }, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + _, _, err := mw.BeforeModelRewriteState(ctx, state, mtx) + assert.NoError(t, err) + }) + +} + +func TestMiddlewareShouldSummarize(t *testing.T) { + ctx := context.Background() + + t.Run("returns true when over messages threshold", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Trigger: &TriggerCondition{ContextMessages: 1}, + }, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{ + schema.UserMessage("msg1"), + schema.UserMessage("msg2"), + }, + } + + triggered, err := mw.shouldSummarize(ctx, input) + assert.NoError(t, err) + assert.True(t, triggered) + }) + + t.Run("returns false when under messages threshold", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Trigger: &TriggerCondition{ + ContextMessages: 3, + ContextTokens: 1000, + }, + }, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{ + schema.UserMessage("msg1"), + schema.UserMessage("msg2"), + }, + } + + triggered, err := mw.shouldSummarize(ctx, input) + assert.NoError(t, err) + assert.False(t, triggered) + }) + + t.Run("returns true when over threshold", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Trigger: &TriggerCondition{ContextTokens: 10}, + }, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{ + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + triggered, err := mw.shouldSummarize(ctx, input) + assert.NoError(t, err) + assert.True(t, triggered) + }) + + t.Run("returns false when under threshold", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Trigger: &TriggerCondition{ContextTokens: 1000}, + }, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{ + schema.UserMessage("short message"), + }, + } + + triggered, err := mw.shouldSummarize(ctx, input) + assert.NoError(t, err) + assert.False(t, triggered) + }) + + t.Run("uses default threshold when trigger is nil", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{}, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{ + schema.UserMessage("short message"), + }, + } + + triggered, err := mw.shouldSummarize(ctx, input) + assert.NoError(t, err) + assert.False(t, triggered) + }) +} + +func TestMiddlewareCountTokens(t *testing.T) { + ctx := context.Background() + + t.Run("uses custom token counter", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + TokenCounter: func(ctx context.Context, input *TokenCounterInput) (int, error) { + return 42, nil + }, + }, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{schema.UserMessage("test")}, + } + tokens, err := mw.countTokens(ctx, input) + assert.NoError(t, err) + assert.Equal(t, 42, tokens) + }) + + t.Run("uses default token counter when nil", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{}, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{schema.UserMessage("test")}, + } + tokens, err := mw.countTokens(ctx, input) + assert.NoError(t, err) + assert.Greater(t, tokens, 0) + }) + + t.Run("custom token counter error", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + TokenCounter: func(ctx context.Context, input *TokenCounterInput) (int, error) { + return 0, errors.New("token count error") + }, + }, + } + + input := &TokenCounterInput{ + Messages: []adk.Message{schema.UserMessage("test")}, + } + _, err := mw.countTokens(ctx, input) + assert.Error(t, err) + }) +} + +func TestGetUserMsgTextContent(t *testing.T) { + t.Run("Message extracts from Content field", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + Content: "hello world", + } + assert.Equal(t, "hello world", getUserMsgTextContent(msg)) + }) + + t.Run("Message extracts from UserInputMultiContent", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "part1"}, + {Type: schema.ChatMessagePartTypeText, Text: "part2"}, + }, + } + assert.Equal(t, "part1\npart2", getUserMsgTextContent(msg)) + }) + + t.Run("Message prefers UserInputMultiContent over Content", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + Content: "content field", + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "multi content"}, + }, + } + assert.Equal(t, "multi content", getUserMsgTextContent(msg)) + }) + + t.Run("Message nil returns empty", func(t *testing.T) { + assert.Equal(t, "", getUserMsgTextContent[*schema.Message](nil)) + }) + + t.Run("AgenticMessage extracts UserInputText", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + {UserInputText: &schema.UserInputText{Text: "user input"}}, + }, + } + assert.Equal(t, "user input", getUserMsgTextContent(msg)) + }) + + t.Run("AgenticMessage nil returns empty", func(t *testing.T) { + assert.Equal(t, "", getUserMsgTextContent[*schema.AgenticMessage](nil)) + }) +} + +func TestTruncateTextByChars(t *testing.T) { + t.Run("returns empty for empty string", func(t *testing.T) { + result := truncateTextByChars("") + assert.Equal(t, "", result) + }) + + t.Run("returns original if under limit", func(t *testing.T) { + result := truncateTextByChars("short") + assert.Equal(t, "short", result) + }) + + t.Run("truncates long text", func(t *testing.T) { + longText := strings.Repeat("a", 3000) + result := truncateTextByChars(longText) + assert.Less(t, len(result), len(longText)) + assert.Contains(t, result, "truncated") + }) + + t.Run("preserves prefix and suffix", func(t *testing.T) { + longText := strings.Repeat("a", 1000) + strings.Repeat("b", 1000) + strings.Repeat("c", 1000) + result := truncateTextByChars(longText) + assert.True(t, strings.HasPrefix(result, strings.Repeat("a", 1000))) + assert.True(t, strings.HasSuffix(result, strings.Repeat("c", 1000))) + }) +} + +func TestAppendSection(t *testing.T) { + tests := []struct { + name string + base string + section string + expected string + }{ + { + name: "both empty", + base: "", + section: "", + expected: "", + }, + { + name: "base empty", + base: "", + section: "section", + expected: "section", + }, + { + name: "section empty", + base: "base", + section: "", + expected: "base", + }, + { + name: "both non-empty", + base: "base", + section: "section", + expected: "base\n\nsection", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := appendSection(tt.base, tt.section) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestAllUserMessagesTagRegex(t *testing.T) { + t.Run("matches tag", func(t *testing.T) { + text := ` + - msg1 + - msg2 +` + assert.True(t, allUserMessagesTagRegex.MatchString(text)) + }) + + t.Run("replaces tag content", func(t *testing.T) { + text := `before + + - old msg + +after` + replacement := "\n - new msg\n" + result := allUserMessagesTagRegex.ReplaceAllString(text, replacement) + assert.Contains(t, result, "new msg") + assert.NotContains(t, result, "old msg") + assert.Contains(t, result, "before") + assert.Contains(t, result, "after") + }) +} + +func TestConfigCheck(t *testing.T) { + t.Run("nil config", func(t *testing.T) { + var c *Config + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") + }) + + t.Run("nil model", func(t *testing.T) { + c := &Config{} + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "model is required") + }) + + t.Run("valid config", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + } + err := c.check() + assert.NoError(t, err) + }) + + t.Run("invalid trigger max tokens", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: -1}, + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be non-negative") + }) + + t.Run("invalid trigger max messages", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextMessages: -1}, + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be non-negative") + }) + + t.Run("both trigger conditions are zero", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Trigger: &TriggerCondition{ContextTokens: 0, ContextMessages: 0}, + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be non-negative") + }) + + t.Run("negative retry max retries", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Retry: &RetryConfig{MaxRetries: intPtr(-1)}, + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "retry.MaxRetries must be non-negative") + }) + + t.Run("failover getFailoverModel is optional", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Failover: &FailoverConfig{}, + } + err := c.check() + assert.NoError(t, err) + }) + + t.Run("failover max retries accepts int value", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + failover := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Failover: &FailoverConfig{ + MaxRetries: intPtr(1), + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return failover, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + } + err := c.check() + assert.NoError(t, err) + }) + + t.Run("failover max retries must be non-negative", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + failover := mockModel.NewMockBaseChatModel(ctrl) + + c := &Config{ + Model: cm, + Failover: &FailoverConfig{ + MaxRetries: intPtr(-1), + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return failover, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + } + err := c.check() + assert.Error(t, err) + assert.Contains(t, err.Error(), "failover.MaxRetries must be non-negative") + }) +} + +func TestSetGetContentType(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + Content: "test", + } + + setMsgExtra(msg, extraKeyContentType, string(contentTypeSummary)) + + ct := typedGetContentType(msg) + assert.Equal(t, contentTypeSummary, ct) +} + +func TestSetGetExtra(t *testing.T) { + t.Run("set and get", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + Content: "test", + } + + setMsgExtra(msg, "key", "value") + + extra := getMsgExtra(msg) + v, ok := extra["key"].(string) + assert.True(t, ok) + assert.Equal(t, "value", v) + }) + + t.Run("get non-existent key", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + Content: "test", + } + + extra := getMsgExtra(msg) + assert.Nil(t, extra) + }) +} + +func TestMiddlewareBuildSummarizationModelInput(t *testing.T) { + ctx := context.Background() + + t.Run("message structure", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{}, + } + + testMsg := []adk.Message{schema.UserMessage("test")} + input, err := mw.buildSummarizationModelInput(ctx, testMsg, testMsg) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(input), 3) + assert.Equal(t, schema.System, input[0].Role) + assert.Equal(t, schema.User, input[len(input)-1].Role) + }) + + t.Run("uses context messages", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{}, + } + + contextMsgs := []adk.Message{ + schema.UserMessage("context message"), + } + input, err := mw.buildSummarizationModelInput(ctx, contextMsgs, contextMsgs) + assert.NoError(t, err) + + found := false + for _, msg := range input { + if msg.Content == "context message" { + found = true + break + } + } + assert.True(t, found, "should contain context message") + }) + + t.Run("uses GenModelInput", func(t *testing.T) { + expectedInput := []adk.Message{ + schema.UserMessage("custom input"), + } + + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + GenModelInput: func(ctx context.Context, defaultSystemInstruction, userInstruction adk.Message, originalMsgs []adk.Message) ([]adk.Message, error) { + return expectedInput, nil + }, + }, + } + + testMsg := []adk.Message{schema.UserMessage("test")} + input, err := mw.buildSummarizationModelInput(ctx, testMsg, testMsg) + assert.NoError(t, err) + assert.Len(t, input, 1) + assert.Equal(t, "custom input", input[0].Content) + }) + + t.Run("GenModelInput error", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + GenModelInput: func(ctx context.Context, defaultSystemInstruction, userInstruction adk.Message, originalMsgs []adk.Message) ([]adk.Message, error) { + return nil, errors.New("gen input error") + }, + }, + } + + testMsg := []adk.Message{schema.UserMessage("test")} + _, err := mw.buildSummarizationModelInput(ctx, testMsg, testMsg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "gen input error") + }) + + t.Run("uses custom instruction", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + UserInstruction: "custom instruction", + }, + } + + testMsg := []adk.Message{schema.UserMessage("test")} + input, err := mw.buildSummarizationModelInput(ctx, testMsg, testMsg) + assert.NoError(t, err) + + lastMsg := input[len(input)-1] + assert.Equal(t, schema.User, lastMsg.Role) + assert.Contains(t, lastMsg.Content, "custom instruction") + }) +} + +func TestMiddlewareSummarize(t *testing.T) { + ctx := context.Background() + + t.Run("generates summary", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "summary", + }, nil).Times(1) + + input := []adk.Message{schema.UserMessage("test")} + resp, err := cm.Generate(ctx, input) + assert.NoError(t, err) + assert.NotNil(t, resp) + summary := newTypedSummaryMessage[*schema.Message](resp.Content) + assert.NotNil(t, summary) + assert.Equal(t, "summary", summary.Content) + }) + + t.Run("model generate error", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errors.New("generate error")).Times(1) + + input := []adk.Message{schema.UserMessage("test")} + _, err := cm.Generate(ctx, input) + assert.Error(t, err) + }) +} + +func TestMiddlewareGenerateWithRetry(t *testing.T) { + ctx := context.Background() + + t.Run("retries until success", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{}, + } + + callCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(context.Context, []*schema.Message, ...any) (*schema.Message, error) { + callCount++ + if callCount == 1 { + return schema.AssistantMessage("partial output", nil), errors.New("transient error") + } + return schema.AssistantMessage("final summary", nil), nil + }).Times(2) + + resp, err := mw.generateWithRetry(ctx, cm, []adk.Message{schema.UserMessage("test")}, nil, &RetryConfig{}) + + assert.NoError(t, err) + if assert.NotNil(t, resp) { + assert.Equal(t, "final summary", resp.Content) + } + }) + + t.Run("delegates to generateAndEmit without retry config", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{}, + } + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("partial output", nil), errors.New("generate error")).Times(1) + + resp, err := mw.generateWithRetry(ctx, cm, []adk.Message{schema.UserMessage("test")}, nil, nil) + + assert.EqualError(t, err, "generate error") + if assert.NotNil(t, resp) { + assert.Equal(t, "partial output", resp.Content) + } + }) +} + +func TestPopulateUserMessagesInternal(t *testing.T) { + ctx := context.Background() + + t.Run("replaces user messages section", func(t *testing.T) { + msgs := []adk.Message{ + schema.UserMessage("msg1"), + schema.AssistantMessage("response1", nil), + schema.UserMessage("msg2"), + } + + summary := `1. Primary Request: + test + +6. All user messages: + + - [old message] + + +7. Pending Tasks: + - task1` + + result, err := replaceUserMessagesInSummary(ctx, &replaceUserMessagesInSummaryParams[*schema.Message]{ + contextMsgs: msgs, + summaryText: summary, + }) + assert.NoError(t, err) + assert.Contains(t, result, "msg1") + assert.Contains(t, result, "msg2") + assert.NotContains(t, result, "old message") + assert.Contains(t, result, "7. Pending Tasks:") + }) + + t.Run("returns original if no matching sections", func(t *testing.T) { + msgs := []adk.Message{ + schema.UserMessage("test"), + } + + summary := "summary without sections" + result, err := replaceUserMessagesInSummary(ctx, &replaceUserMessagesInSummaryParams[*schema.Message]{ + contextMsgs: msgs, + summaryText: summary, + }) + assert.NoError(t, err) + assert.Equal(t, summary, result) + }) + + t.Run("skips summary messages", func(t *testing.T) { + summaryMsg := &schema.Message{ + Role: schema.User, + Content: "summary", + } + setMsgExtra(summaryMsg, extraKeyContentType, string(contentTypeSummary)) + + msgs := []adk.Message{ + summaryMsg, + schema.UserMessage("regular message"), + } + + summary := `6. All user messages: + + - [old] + + +7. Pending Tasks: + - task` + + result, err := replaceUserMessagesInSummary(ctx, &replaceUserMessagesInSummaryParams[*schema.Message]{ + contextMsgs: msgs, + summaryText: summary, + }) + assert.NoError(t, err) + assert.Contains(t, result, "regular message") + assert.NotContains(t, result, " - summary") + }) + + t.Run("returns original if empty user messages", func(t *testing.T) { + msgs := []adk.Message{ + schema.AssistantMessage("response", nil), + } + + summary := `6. All user messages: + - [old] + +7. Pending Tasks: + - task` + + result, err := replaceUserMessagesInSummary(ctx, &replaceUserMessagesInSummaryParams[*schema.Message]{ + contextMsgs: msgs, + summaryText: summary, + }) + assert.NoError(t, err) + assert.Equal(t, summary, result) + }) +} + +func TestAllUserMessagesTagRegexMatch(t *testing.T) { + t.Run("matches xml tag", func(t *testing.T) { + text := "\n - msg\n" + assert.True(t, allUserMessagesTagRegex.MatchString(text)) + }) + + t.Run("does not match without tag", func(t *testing.T) { + text := "6. All user messages:\n - msg" + assert.False(t, allUserMessagesTagRegex.MatchString(text)) + }) +} + +func TestDefaultTrimUserMessage(t *testing.T) { + t.Run("returns nil for zero remaining tokens", func(t *testing.T) { + msg := schema.UserMessage("test") + result := defaultTypedTrimUserMessage(msg, 0) + assert.Nil(t, result) + }) + + t.Run("returns nil for empty content", func(t *testing.T) { + msg := schema.UserMessage("") + result := defaultTypedTrimUserMessage(msg, 100) + assert.Nil(t, result) + }) + + t.Run("trims long message", func(t *testing.T) { + longText := strings.Repeat("a", 3000) + msg := schema.UserMessage(longText) + result := defaultTypedTrimUserMessage(msg, 100) + assert.NotNil(t, result) + assert.Less(t, len(result.Content), len(longText)) + }) +} + +func TestDefaultTokenCounter(t *testing.T) { + ctx := context.Background() + + t.Run("counts tool tokens", func(t *testing.T) { + input := &TokenCounterInput{ + Messages: []adk.Message{}, + Tools: []*schema.ToolInfo{ + {Name: "test_tool", Desc: "description"}, + }, + } + count, err := defaultTypedTokenCounter(ctx, input) + assert.NoError(t, err) + assert.Greater(t, count, 0) + }) + + t.Run("reuses latest assistant total tokens as baseline", func(t *testing.T) { + input := &TokenCounterInput{ + Messages: []adk.Message{ + schema.UserMessage("earlier context"), + { + Role: schema.Assistant, + Content: "baseline", + ResponseMeta: &schema.ResponseMeta{ + Usage: &schema.TokenUsage{TotalTokens: 100}, + }, + }, + schema.UserMessage("later context"), + }, + } + + count, err := defaultTypedTokenCounter(ctx, input) + require.NoError(t, err) + assert.Equal(t, 100+estimateMessageTokens(schema.UserMessage("later context")), count) + }) +} + +func TestGetAssistantTotalTokens(t *testing.T) { + t.Run("returns zero for nil message", func(t *testing.T) { + assert.Zero(t, getAssistantTotalTokens[*schema.Message](nil)) + assert.Zero(t, getAssistantTotalTokens[*schema.AgenticMessage](nil)) + }) + + t.Run("reads total tokens from assistant messages only", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + ResponseMeta: &schema.ResponseMeta{ + Usage: &schema.TokenUsage{TotalTokens: 42}, + }, + } + assert.Equal(t, 42, getAssistantTotalTokens(msg)) + assert.Zero(t, getAssistantTotalTokens(schema.UserMessage("ignored"))) + }) + + t.Run("reads total tokens from agentic assistant messages only", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ResponseMeta: &schema.AgenticResponseMeta{ + TokenUsage: &schema.TokenUsage{TotalTokens: 64}, + }, + } + assert.Equal(t, 64, getAssistantTotalTokens(msg)) + assert.Zero(t, getAssistantTotalTokens(schema.UserAgenticMessage("ignored"))) + }) +} + +func TestEstimateMessageTokens(t *testing.T) { + t.Run("returns zero for nil message", func(t *testing.T) { + assert.Zero(t, estimateMessageTokens(nil)) + }) + + t.Run("counts assistant text reasoning and tool calls", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + ReasoningContent: "reason", + ToolCalls: []schema.ToolCall{ + { + Function: schema.FunctionCall{ + Name: "tool", + Arguments: `{"k":"v"}`, + }, + }, + }, + AssistantGenMultiContent: []schema.MessageOutputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "answer"}, + }, + } + + expectedLen := len("answer") + len("reason") + len("tool") + len(`{"k":"v"}`) + assert.Equal(t, estimateTokenCount(expectedLen), estimateMessageTokens(msg)) + }) + + t.Run("adds multimodal estimate for user content", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.User, + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "hello"}, + {Type: schema.ChatMessagePartTypeImageURL}, + }, + } + + assert.Equal(t, estimateTokenCount(len("hello"))+multimodalTokenEstimate, estimateMessageTokens(msg)) + }) +} + +func TestEstimateAgenticMessageTokens(t *testing.T) { + t.Run("returns zero for nil message", func(t *testing.T) { + assert.Zero(t, estimateAgenticMessageTokens(nil)) + }) + + t.Run("counts assistant blocks and multimodal outputs", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "answer"}), + schema.NewContentBlock(&schema.Reasoning{Text: "reason"}), + schema.NewContentBlock(&schema.FunctionToolCall{Name: "tool", Arguments: `{"k":"v"}`}), + schema.NewContentBlock(&schema.AssistantGenImage{}), + }, + } + + expectedLen := len("answer") + len("reason") + len("tool") + len(`{"k":"v"}`) + assert.Equal(t, estimateTokenCount(expectedLen)+multimodalTokenEstimate, estimateAgenticMessageTokens(msg)) + }) +} + +func TestPostProcessSummary(t *testing.T) { + ctx := context.Background() + + t.Run("with transcript path", func(t *testing.T) { + result, err := postProcessSummary(ctx, &postProcessSummaryParams[*schema.Message]{ + contextMsgs: []adk.Message{}, + summaryContent: "summary content", + transcriptPath: "/path/to/transcript.txt", + }) + assert.NoError(t, err) + assert.Contains(t, result.Content, "/path/to/transcript.txt") + assert.Contains(t, result.Content, getContinueInstruction()) + }) +} + +func TestEventHelpers(t *testing.T) { + ctx := context.Background() + + t.Run("emitEvent returns wrapped error outside execution context", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{cfg: &Config{}} + err := mw.emitEvent(ctx, &CustomizedAction{Type: ActionTypeBeforeSummarize}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to send internal event") + }) + + t.Run("emitGenerateSummaryEvent is skipped when internal events are disabled", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{cfg: &Config{EmitInternalEvents: false}} + err := mw.emitGenerateSummaryEvent(ctx, 1, GenerateSummaryPhasePrimary, schema.AssistantMessage("ok", nil), nil) + assert.NoError(t, err) + }) + + t.Run("emitGenerateSummaryEvent returns wrapped error when enabled outside execution context", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{cfg: &Config{EmitInternalEvents: true}} + err := mw.emitGenerateSummaryEvent(ctx, 1, GenerateSummaryPhasePrimary, schema.AssistantMessage("ok", nil), nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to send internal event") + }) +} + +func TestGetFailoverModel(t *testing.T) { + ctx := context.Background() + defaultInput := []adk.Message{schema.UserMessage("default")} + fctx := &FailoverContext{Attempt: 1} + + t.Run("requires failover config", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{cfg: &Config{}} + mdl, input, err := mw.getFailoverModel(ctx, fctx, defaultInput) + assert.Nil(t, mdl) + assert.Nil(t, input) + assert.ErrorContains(t, err, "failover config is required") + }) + + t.Run("uses primary model and default input when callback is not provided", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Model: primary, + Failover: &FailoverConfig{}, + }, + } + + mdl, input, err := mw.getFailoverModel(ctx, fctx, defaultInput) + assert.NoError(t, err) + assert.Same(t, primary, mdl) + assert.Equal(t, defaultInput, input) + }) + + t.Run("wraps callback error", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Failover: &FailoverConfig{ + GetFailoverModel: func(context.Context, *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return nil, nil, errors.New("boom") + }, + }, + }, + } + + mdl, input, err := mw.getFailoverModel(ctx, fctx, defaultInput) + assert.Nil(t, mdl) + assert.Nil(t, input) + assert.ErrorContains(t, err, "failed to get failover model") + }) + + t.Run("requires non nil failover model", func(t *testing.T) { + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Failover: &FailoverConfig{ + GetFailoverModel: func(context.Context, *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return nil, []*schema.Message{schema.UserMessage("input")}, nil + }, + }, + }, + } + + mdl, input, err := mw.getFailoverModel(ctx, fctx, defaultInput) + assert.Nil(t, mdl) + assert.Nil(t, input) + assert.ErrorContains(t, err, "failover model is required") + }) + + t.Run("requires non empty failover input", func(t *testing.T) { + ctrl := gomock.NewController(t) + failoverModel := mockModel.NewMockBaseChatModel(ctrl) + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Failover: &FailoverConfig{ + GetFailoverModel: func(context.Context, *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return failoverModel, nil, nil + }, + }, + }, + } + + mdl, input, err := mw.getFailoverModel(ctx, fctx, defaultInput) + assert.Nil(t, mdl) + assert.Nil(t, input) + assert.ErrorContains(t, err, "failover model input messages are required") + }) + + t.Run("returns custom failover model and input", func(t *testing.T) { + ctrl := gomock.NewController(t) + failoverModel := mockModel.NewMockBaseChatModel(ctrl) + customInput := []*schema.Message{schema.UserMessage("custom")} + mw := &TypedMiddleware[*schema.Message]{ + cfg: &Config{ + Failover: &FailoverConfig{ + GetFailoverModel: func(context.Context, *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return failoverModel, customInput, nil + }, + }, + }, + } + + mdl, input, err := mw.getFailoverModel(ctx, fctx, defaultInput) + assert.NoError(t, err) + assert.Same(t, failoverModel, mdl) + if assert.Len(t, input, 1) { + assert.Equal(t, "custom", input[0].Content) + } + }) +} + +func TestHelperBranches(t *testing.T) { + t.Run("should failover branches", func(t *testing.T) { + assert.False(t, typedShouldFailover(context.Background(), (*FailoverConfig)(nil), nil, errors.New("x"))) + assert.False(t, typedShouldFailover(context.Background(), &FailoverConfig{}, nil, nil)) + assert.True(t, typedShouldFailover(context.Background(), &FailoverConfig{}, nil, errors.New("x"))) + + cfg := &FailoverConfig{ + ShouldFailover: func(ctx context.Context, resp adk.Message, err error) bool { + return resp != nil && err == nil + }, + } + assert.True(t, typedShouldFailover(context.Background(), cfg, schema.AssistantMessage("ok", nil), nil)) + }) + + t.Run("config check branches", func(t *testing.T) { + assert.ErrorContains(t, (&RetryConfig{MaxRetries: intPtr(-1)}).check(), "retry.MaxRetries must be non-negative") + assert.ErrorContains(t, (&FailoverConfig{MaxRetries: intPtr(-1)}).check(), "failover.MaxRetries must be non-negative") + assert.ErrorContains(t, (&TriggerCondition{}).check(), "at least one of contextTokens or contextMessages") + assert.ErrorContains(t, (&TriggerCondition{ContextTokens: -1}).check(), "contextTokens must be non-negative") + assert.ErrorContains(t, (&TriggerCondition{ContextMessages: -1, ContextTokens: 1}).check(), "contextMessages must be non-negative") + }) + + t.Run("default backoff branches", func(t *testing.T) { + assert.Equal(t, time.Second, defaultBackoffDuration(0)) + + delay := defaultBackoffDuration(8) + assert.GreaterOrEqual(t, delay, 10*time.Second) + assert.Less(t, delay, 15*time.Second) + }) + + t.Run("user messages replaced note is present", func(t *testing.T) { + note := getUserMessagesReplacedNote() + assert.NotEmpty(t, note) + assert.Contains(t, []string{userMessagesReplacedNote, userMessagesReplacedNoteZh}, note) + }) +} + +func TestSummarize(t *testing.T) { + ctx := context.Background() + + newMW := func(cfg *Config) *TypedMiddleware[*schema.Message] { + return &TypedMiddleware[*schema.Message]{ + cfg: cfg, + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } + } + + t.Run("basic summarization", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary content", + }, nil).Times(1) + + mw := newMW(&Config{Model: cm}) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("You are a helpful assistant"), + schema.UserMessage(strings.Repeat("a", 100)), + schema.AssistantMessage(strings.Repeat("b", 100), nil), + }, + } + + result, err := mw.Summarize(ctx, state) + assert.NoError(t, err) + assert.NotEmpty(t, result) + assert.Equal(t, schema.System, result[0].Role) + }) + + t.Run("model error propagates", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("model error")).Times(1) + + mw := newMW(&Config{Model: cm}) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.Error(t, err) + assert.Nil(t, result) + }) + + t.Run("retry works", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + + callCount := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...any) (*schema.Message, error) { + callCount++ + if callCount == 1 { + return nil, fmt.Errorf("transient error") + } + return &schema.Message{ + Role: schema.Assistant, + Content: "Summary after retry", + }, nil + }).Times(2) + + mw := newMW(&Config{ + Model: cm, + Retry: &RetryConfig{ + MaxRetries: intPtr(2), + BackoffFunc: func(_ context.Context, _ int, _ adk.Message, _ error) time.Duration { return 0 }, + }, + }) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.NoError(t, err) + assert.NotEmpty(t, result) + assert.Equal(t, 2, callCount) + }) + + t.Run("failover works", func(t *testing.T) { + ctrl := gomock.NewController(t) + primary := mockModel.NewMockBaseChatModel(ctrl) + failoverModel := mockModel.NewMockBaseChatModel(ctrl) + + primary.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("primary error")).Times(1) + failoverModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary from failover", + }, nil).Times(1) + + mw := newMW(&Config{ + Model: primary, + Failover: &FailoverConfig{ + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext) (model.BaseChatModel, []*schema.Message, error) { + return failoverModel, []*schema.Message{schema.UserMessage("failover input")}, nil + }, + }, + }) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.NoError(t, err) + assert.NotEmpty(t, result) + }) + + t.Run("callback is invoked", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + callbackCalled := false + mw := newMW(&Config{ + Model: cm, + Callback: func(ctx context.Context, before, after adk.ChatModelAgentState) error { + callbackCalled = true + assert.Len(t, before.Messages, 1) + assert.NotEmpty(t, after.Messages) + return nil + }, + }) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, callbackCalled) + }) + + t.Run("custom finalize is used", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + mw := newMW(&Config{ + Model: cm, + Finalize: func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + return []adk.Message{ + schema.SystemMessage("custom system"), + summary, + }, nil + }, + }) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, schema.System, result[0].Role) + assert.Equal(t, "custom system", result[0].Content) + }) + + t.Run("callback error propagates", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + mw := newMW(&Config{ + Model: cm, + Callback: func(ctx context.Context, before, after adk.ChatModelAgentState) error { + return fmt.Errorf("callback error") + }, + }) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "callback error") + }) + + t.Run("finalize error propagates", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + mw := newMW(&Config{ + Model: cm, + Finalize: func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) { + return nil, fmt.Errorf("finalize error") + }, + }) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{schema.UserMessage("hello")}, + } + + result, err := mw.Summarize(ctx, state) + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "finalize error") + }) + + t.Run("preserves system messages", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockBaseChatModel(ctrl) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.Message{ + Role: schema.Assistant, + Content: "Summary", + }, nil).Times(1) + + mw := newMW(&Config{Model: cm}) + + state := &adk.ChatModelAgentState{ + Messages: []adk.Message{ + schema.SystemMessage("System 1"), + schema.SystemMessage("System 2"), + schema.UserMessage(strings.Repeat("a", 100)), + }, + } + + result, err := mw.Summarize(ctx, state) + assert.NoError(t, err) + assert.Len(t, result, 3) + assert.Equal(t, schema.System, result[0].Role) + assert.Equal(t, "System 1", result[0].Content) + assert.Equal(t, schema.System, result[1].Role) + assert.Equal(t, "System 2", result[1].Content) + }) +} + +func TestNewTypedAgenticMessage(t *testing.T) { + ctx := context.Background() + + // TypedConfig requires a Model, so passing an empty config will return an error. + // This test verifies that NewTyped[*schema.AgenticMessage] compiles correctly. + mw, err := NewTyped(ctx, &TypedConfig[*schema.AgenticMessage]{}) + assert.Error(t, err) + assert.Nil(t, mw) + + // Verify the return type is correct at compile time. + var _ adk.TypedChatModelAgentMiddleware[*schema.AgenticMessage] = mw +} + +// ============================================================================ +// Generic message helpers (prefixed with 's' to avoid conflicts) +// ============================================================================ + +func smakeUserMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.UserMessage(content)).(M) + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(content)).(M) + } + panic("unreachable") +} + +func smakeSystemMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.SystemMessage(content)).(M) + case *schema.AgenticMessage: + return any(schema.SystemAgenticMessage(content)).(M) + } + panic("unreachable") +} + +func smakeAssistantMsg[M adk.MessageType](content string) M { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.AssistantMessage(content, nil)).(M) + case *schema.AgenticMessage: + am := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: content}), + }, + } + return any(am).(M) + } + panic("unreachable") +} + +// ============================================================================ +// Generic mock model +// ============================================================================ + +type genericMockModel[M adk.MessageType] struct { + response M + err error +} + +func (m *genericMockModel[M]) Generate(_ context.Context, _ []M, _ ...model.Option) (M, error) { + return m.response, m.err +} + +func (m *genericMockModel[M]) Stream(_ context.Context, _ []M, _ ...model.Option) (*schema.StreamReader[M], error) { + return nil, fmt.Errorf("not implemented") +} + +// ============================================================================ +// Generic tests +// ============================================================================ + +func TestSummarizationGeneric(t *testing.T) { + t.Run("Message", func(t *testing.T) { + t.Run("Helpers", testSummarizationHelpers[*schema.Message]) + t.Run("Flow", testSummarizationFlow[*schema.Message]) + t.Run("TokenCounterUsesStateToolInfos", testTokenCounterReceivesStateToolInfos[*schema.Message]) + }) + t.Run("AgenticMessage", func(t *testing.T) { + t.Run("Helpers", testSummarizationHelpers[*schema.AgenticMessage]) + t.Run("Flow", testSummarizationFlow[*schema.AgenticMessage]) + t.Run("TokenCounterUsesStateToolInfos", testTokenCounterReceivesStateToolInfos[*schema.AgenticMessage]) + }) +} + +func TestEmitInternalEvents_AgenticMessage_RequiresExecContext(t *testing.T) { + ctx := context.Background() + + longContent := strings.Repeat("x", 800000) + msgs := []*schema.AgenticMessage{ + { + Role: schema.AgenticRoleTypeSystem, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "system"}), + }, + }, + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.UserInputText{Text: longContent}), + }, + }, + } + + mockResp := smakeAssistantMsg[*schema.AgenticMessage]("This is the summary.") + mw, err := NewTyped(ctx, &TypedConfig[*schema.AgenticMessage]{ + Model: &genericMockModel[*schema.AgenticMessage]{response: mockResp}, + EmitInternalEvents: true, + Trigger: &TriggerCondition{ + ContextTokens: 1, + }, + }) + require.NoError(t, err) + + state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{Messages: msgs} + _, _, err = mw.BeforeModelRewriteState(ctx, state, nil) + assert.Error(t, err, "should error without exec context when EmitInternalEvents is true") + assert.Contains(t, err.Error(), "send internal event") +} + +func testSummarizationHelpers[M adk.MessageType](t *testing.T) { + t.Run("isSystemRole", func(t *testing.T) { + sys := smakeSystemMsg[M]("hello") + usr := smakeUserMsg[M]("hello") + assert.True(t, isSystemRole(sys)) + assert.False(t, isSystemRole(usr)) + }) + + t.Run("isUserRole", func(t *testing.T) { + usr := smakeUserMsg[M]("hello") + sys := smakeSystemMsg[M]("hello") + assert.True(t, isUserRole(usr)) + assert.False(t, isUserRole(sys)) + }) + + t.Run("getUserMsgTextContent", func(t *testing.T) { + usr := smakeUserMsg[M]("hello world") + assert.Equal(t, "hello world", getUserMsgTextContent(usr)) + }) + + t.Run("getMsgExtra_setMsgExtra", func(t *testing.T) { + msg := smakeUserMsg[M]("test") + extra := getMsgExtra(msg) + assert.Nil(t, extra) + + setMsgExtra(msg, "key1", "value1") + extra = getMsgExtra(msg) + assert.Equal(t, "value1", extra["key1"]) + }) + + t.Run("makeSystemMsg", func(t *testing.T) { + msg := makeSystemMsg[M]("system prompt") + assert.True(t, isSystemRole(msg)) + switch m := any(msg).(type) { + case *schema.Message: + assert.Equal(t, "system prompt", m.Content) + case *schema.AgenticMessage: + require.Len(t, m.ContentBlocks, 1) + assert.Equal(t, "system prompt", m.ContentBlocks[0].UserInputText.Text) + } + }) + + t.Run("makeUserMsg", func(t *testing.T) { + msg := makeUserMsg[M]("user input") + assert.True(t, isUserRole(msg)) + assert.Equal(t, "user input", getUserMsgTextContent(msg)) + }) + + t.Run("newTypedSummaryMessage", func(t *testing.T) { + msg := newTypedSummaryMessage[M]("summary content") + + assert.True(t, isUserRole(msg)) + + switch m := any(msg).(type) { + case *schema.Message: + assert.Equal(t, schema.User, m.Role) + assert.Equal(t, "summary content", m.Content) + case *schema.AgenticMessage: + assert.Equal(t, schema.AgenticRoleTypeUser, m.Role) + require.Len(t, m.ContentBlocks, 1) + assert.Equal(t, "summary content", m.ContentBlocks[0].UserInputText.Text) + } + }) + + t.Run("isInnerMessage", func(t *testing.T) { + summaryMsg := newTypedSummaryMessage[M]("summary content") + assert.True(t, isInternalUserMessage(summaryMsg)) + assert.False(t, isPreservedMessage(summaryMsg)) + + skillsMsg := makeUserMsg[M]("skills content") + setMsgExtra(skillsMsg, extraKeyContentType, string(contentTypeSkills)) + assert.True(t, isInternalUserMessage(skillsMsg)) + assert.True(t, isPreservedMessage(skillsMsg)) + + normalMsg := makeUserMsg[M]("normal content") + assert.False(t, isInternalUserMessage(normalMsg)) + assert.False(t, isPreservedMessage(normalMsg)) + }) +} + +func testSummarizationFlow[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + summaryText := "This is a summary of the conversation." + mockModel := &genericMockModel[M]{ + response: smakeAssistantMsg[M](summaryText), + } + + tokenCounter := func(_ context.Context, input *TypedTokenCounterInput[M]) (int, error) { + total := 0 + for _, msg := range input.Messages { + total += len(getUserMsgTextContent(msg)) + } + return total, nil + } + + cfg := &TypedConfig[M]{ + Model: mockModel, + TokenCounter: tokenCounter, + Trigger: &TriggerCondition{ + ContextTokens: 20, + }, + } + + mw, err := NewTyped(ctx, cfg) + require.NoError(t, err) + + msgs := []M{ + smakeSystemMsg[M]("You are a helpful assistant."), + smakeUserMsg[M]("Tell me a very long story about dragons and castles"), + smakeAssistantMsg[M]("Once upon a time there was a magnificent dragon"), + smakeUserMsg[M]("What happened next?"), + } + + state := &adk.TypedChatModelAgentState[M]{Messages: msgs} + mtx := &adk.TypedModelContext[M]{} + + _, newState, err := mw.BeforeModelRewriteState(ctx, state, mtx) + require.NoError(t, err) + + require.GreaterOrEqual(t, len(newState.Messages), 2, + "should have at least system + summary messages") + + assert.True(t, isSystemRole(newState.Messages[0]), + "first message should be system") + + foundSummary := false + for _, msg := range newState.Messages { + extra := getMsgExtra(msg) + if extra != nil { + if ct, ok := extra[extraKeyContentType]; ok && ct == string(contentTypeSummary) { + foundSummary = true + break + } + } + if strings.Contains(getUserMsgTextContent(msg), summaryText) { + foundSummary = true + break + } + } + assert.True(t, foundSummary, "should have a summary message") +} + +func testTokenCounterReceivesStateToolInfos[M adk.MessageType](t *testing.T) { + ctx := context.Background() + + stateTools := []*schema.ToolInfo{ + {Name: "state_tool_a"}, + {Name: "state_tool_b"}, + } + mcTools := []*schema.ToolInfo{ + {Name: "mc_tool_should_not_appear"}, + } + + var receivedTools []*schema.ToolInfo + tokenCounter := func(_ context.Context, input *TypedTokenCounterInput[M]) (int, error) { + receivedTools = input.Tools + return 0, nil + } + + cfg := &TypedConfig[M]{ + Model: &genericMockModel[M]{ + response: smakeAssistantMsg[M]("unused"), + }, + TokenCounter: tokenCounter, + Trigger: &TriggerCondition{ + ContextTokens: 9999, + }, + } + + mw, err := NewTyped(ctx, cfg) + require.NoError(t, err) + + state := &adk.TypedChatModelAgentState[M]{ + Messages: []M{smakeUserMsg[M]("hello")}, + ToolInfos: stateTools, + } + mc := &adk.TypedModelContext[M]{Tools: mcTools} + + _, _, err = mw.BeforeModelRewriteState(ctx, state, mc) + require.NoError(t, err) + + require.NotNil(t, receivedTools, "token counter should have been called") + require.Len(t, receivedTools, 2) + assert.Equal(t, "state_tool_a", receivedTools[0].Name) + assert.Equal(t, "state_tool_b", receivedTools[1].Name) +} + +func TestGetAssistantTextContent(t *testing.T) { + t.Run("schema.Message with MultiContent", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + Content: "fallback content", + AssistantGenMultiContent: []schema.MessageOutputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "hello"}, + {Type: schema.ChatMessagePartTypeText, Text: "world"}, + }, + } + got := getAssistantTextContent(msg) + assert.Equal(t, "hello\nworld", got) + }) + + t.Run("schema.Message with MultiContent skips non-text parts", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + Content: "fallback", + AssistantGenMultiContent: []schema.MessageOutputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "text part"}, + {Type: schema.ChatMessagePartTypeImageURL}, + {Type: schema.ChatMessagePartTypeText, Text: ""}, + }, + } + got := getAssistantTextContent(msg) + assert.Equal(t, "text part", got) + }) + + t.Run("schema.Message falls back to Content when MultiContent is empty", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + Content: "plain content", + } + got := getAssistantTextContent(msg) + assert.Equal(t, "plain content", got) + }) + + t.Run("schema.Message falls back to Content when MultiContent has no text", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + Content: "fallback", + AssistantGenMultiContent: []schema.MessageOutputPart{ + {Type: schema.ChatMessagePartTypeImageURL}, + }, + } + got := getAssistantTextContent(msg) + assert.Equal(t, "fallback", got) + }) + + t.Run("schema.AgenticMessage with multiple text blocks", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: "first"}), + schema.NewContentBlock(&schema.AssistantGenText{Text: "second"}), + }, + } + got := getAssistantTextContent(msg) + assert.Equal(t, "first\nsecond", got) + }) + + t.Run("schema.AgenticMessage with nil blocks", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + nil, + schema.NewContentBlock(&schema.AssistantGenText{Text: "only"}), + nil, + }, + } + got := getAssistantTextContent(msg) + assert.Equal(t, "only", got) + }) + + t.Run("schema.AgenticMessage with no text blocks", func(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{}, + } + got := getAssistantTextContent(msg) + assert.Equal(t, "", got) + }) +} diff --git a/adk/prebuilt/deep/checkpoint_compat_resume_test.go b/adk/prebuilt/deep/checkpoint_compat_resume_test.go new file mode 100644 index 0000000..1a4f8ba --- /dev/null +++ b/adk/prebuilt/deep/checkpoint_compat_resume_test.go @@ -0,0 +1,203 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package deep + +import ( + "context" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type compatCheckpointStore struct { + data map[string][]byte +} + +func newCompatCheckpointStore() *compatCheckpointStore { + return &compatCheckpointStore{data: make(map[string][]byte)} +} + +func (s *compatCheckpointStore) Set(_ context.Context, key string, value []byte) error { + s.data[key] = append([]byte(nil), value...) + return nil +} + +func (s *compatCheckpointStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.data[key] + if !ok { + return nil, false, nil + } + return append([]byte(nil), v...), true, nil +} + +type interruptingSubAgentTool struct { + name string +} + +func (t *interruptingSubAgentTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "interrupts on first call and resumes from stored state", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "action": {Type: schema.String}, + }), + }, nil +} + +func (t *interruptingSubAgentTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + _, _, _ = tool.GetInterruptState[string](ctx) + return "resumed", nil +} + +func readTestdataBytes(t *testing.T, filename string) []byte { + t.Helper() + _, file, _, ok := runtime.Caller(0) + assert.True(t, ok) + p := filepath.Join(filepath.Dir(file), "testdata", filename) + b, err := os.ReadFile(p) + assert.NoError(t, err) + assert.NotEmpty(t, b) + return b +} + +func runDeepAgentCheckpointCompat(t *testing.T, checkpointID string, filename string) { + t.Helper() + ctx := context.Background() + + data := readTestdataBytes(t, filename) + + store := newCompatCheckpointStore() + assert.NoError(t, store.Set(ctx, checkpointID, data)) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + interruptToolName := "interrupt_in_subagent_tool" + subTool := &interruptingSubAgentTool{name: interruptToolName} + + deepModel := mockModel.NewMockBaseChatModel(ctrl) + subModel := mockModel.NewMockBaseChatModel(ctrl) + + deepModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("deep done", nil), nil + }).AnyTimes() + + subModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("sub done", nil), nil + }).AnyTimes() + + subAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "sub_chatmodel_agent", + Description: "sub agent", + Model: subModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{subTool}, + }, + }, + MaxIterations: 4, + }) + assert.NoError(t, err) + + deepAgent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: deepModel, + SubAgents: []adk.Agent{subAgent}, + MaxIteration: 4, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + assert.NoError(t, err) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: deepAgent, + CheckPointStore: store, + }) + + it, err := runner.Resume(ctx, checkpointID) + assert.NoError(t, err) + + var sawDeepDone bool + var sawAnyOutput bool + for { + ev, ok := it.Next() + if !ok { + break + } + assert.NoError(t, ev.Err) + if ev.Output != nil && ev.Output.MessageOutput != nil && ev.Output.MessageOutput.Message != nil { + sawAnyOutput = true + msg := ev.Output.MessageOutput.Message + if msg.Role == schema.Assistant && strings.Contains(msg.Content, "deep done") { + sawDeepDone = true + } + } + } + + assert.True(t, sawAnyOutput) + assert.True(t, sawDeepDone) +} + +func TestDeepAgentCheckpointCompat_V0_8_Resume(t *testing.T) { + tests := []struct { + name string + checkpointID string + filename string + }{ + { + name: "v0.7.37", + checkpointID: "checkpoint_compat_v0_7_37", + filename: "checkpoint_data_v0.7.37.bin", + }, + { + name: "v0.8.2", + checkpointID: "checkpoint_compat_v0_8_2", + filename: "checkpoint_data_v0.8.2.bin", + }, + { + name: "v0.8.3", + checkpointID: "checkpoint_compat_v0_8_3", + filename: "checkpoint_data_v0.8.3.bin", + }, + { + name: "v0.8.4", + checkpointID: "checkpoint_compat_v0_8_4", + filename: "checkpoint_data_v0.8.4.bin", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runDeepAgentCheckpointCompat(t, tc.checkpointID, tc.filename) + }) + } +} diff --git a/adk/prebuilt/deep/deep.go b/adk/prebuilt/deep/deep.go new file mode 100644 index 0000000..5315113 --- /dev/null +++ b/adk/prebuilt/deep/deep.go @@ -0,0 +1,267 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package deep provides a prebuilt agent with deep task orchestration. +package deep + +import ( + "context" + "fmt" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/filesystem" + "github.com/cloudwego/eino/adk/internal" + filesystem2 "github.com/cloudwego/eino/adk/middlewares/filesystem" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool/utils" + "github.com/cloudwego/eino/schema" +) + +func init() { + schema.RegisterName[TODO]("_eino_adk_prebuilt_deep_todo") + schema.RegisterName[[]TODO]("_eino_adk_prebuilt_deep_todo_slice") +} + +// TypedConfig defines the configuration for creating a DeepAgent parameterized by message type. +// An Agentic DeepAgent (M = *schema.AgenticMessage) only supports Agentic sub-agents, +// and a standard DeepAgent (M = *schema.Message) only supports standard sub-agents. +// This is enforced by the type system through the SubAgents field. +type TypedConfig[M adk.MessageType] struct { + // Name is the identifier for the Deep agent. + Name string + // Description provides a brief explanation of the agent's purpose. + Description string + + // ChatModel is the model used by DeepAgent for reasoning and task execution. + // If the agent uses any tools, this model must support the model.WithTools call option, + // as that's how the agent configures the model with tool information. + ChatModel model.BaseModel[M] + // Instruction contains the system prompt that guides the agent's behavior. + // When empty, a built-in default system prompt will be used, which includes general assistant + // behavior guidelines, security policies, coding style guidelines, and tool usage policies. + Instruction string + // SubAgents are specialized agents that can be invoked by the agent. + // For M = *schema.AgenticMessage, only agentic sub-agents are accepted. + SubAgents []adk.TypedAgent[M] + // ToolsConfig provides the tools and tool-calling configurations available for the agent to invoke. + ToolsConfig adk.ToolsConfig + // MaxIteration limits the maximum number of reasoning iterations the agent can perform. + MaxIteration int + + // Backend provides filesystem operations used by tools and offloading. + // If set, filesystem tools (read_file, write_file, edit_file, glob, grep) will be registered. + // Optional. + Backend filesystem.Backend + // Shell provides shell command execution capability. + // If set, an execute tool will be registered to support shell command execution. + // Optional. Mutually exclusive with StreamingShell. + Shell filesystem.Shell + // StreamingShell provides streaming shell command execution capability. + // If set, a streaming execute tool will be registered to support streaming shell command execution. + // Optional. Mutually exclusive with Shell. + StreamingShell filesystem.StreamingShell + + // WithoutWriteTodos disables the built-in write_todos tool when set to true. + WithoutWriteTodos bool + // WithoutGeneralSubAgent disables the general-purpose subagent when set to true. + WithoutGeneralSubAgent bool + // TaskToolDescriptionGenerator allows customizing the description for the task tool. + // If provided, this function generates the tool description based on available subagents. + TaskToolDescriptionGenerator func(ctx context.Context, availableAgents []adk.TypedAgent[M]) (string, error) + + Middlewares []adk.AgentMiddleware + + // Handlers configures interface-based handlers for extending agent behavior. + // Unlike Middlewares (struct-based), Handlers allow users to: + // - Add custom methods to their handler implementations + // - Return modified context from handler methods + // - Centralize configuration in struct fields instead of closures + // + // Handlers are processed after Middlewares, in registration order. + // See adk.ChatModelAgentMiddleware documentation for when to use Handlers vs Middlewares. + Handlers []adk.TypedChatModelAgentMiddleware[M] + + ModelRetryConfig *adk.TypedModelRetryConfig[M] + // ModelFailoverConfig configures failover behavior for the ChatModel. + // When set, the agent will automatically fail over to alternative models on errors. + // This config is also propagated to the general sub-agent. + ModelFailoverConfig *adk.ModelFailoverConfig[M] + + // OutputKey stores the agent's response in the session. + // Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content). + OutputKey string +} + +// Config defines the configuration for creating a standard DeepAgent. +type Config = TypedConfig[*schema.Message] + +// NewTyped creates a new typed Deep agent instance with the provided configuration. +// This function initializes built-in tools, creates a task tool for subagent orchestration, +// and returns a fully configured TypedChatModelAgent ready for execution. +func NewTyped[M adk.MessageType](ctx context.Context, cfg *TypedConfig[M]) (adk.TypedResumableAgent[M], error) { + handlers, err := buildTypedBuiltinAgentMiddlewares(ctx, cfg) + if err != nil { + return nil, err + } + + instruction := cfg.Instruction + if len(instruction) == 0 { + instruction = internal.SelectPrompt(internal.I18nPrompts{ + English: baseAgentInstruction, + Chinese: baseAgentInstructionChinese, + }) + } + + if !cfg.WithoutGeneralSubAgent || len(cfg.SubAgents) > 0 { + tt, err := typedTaskToolMiddleware( + ctx, + cfg.TaskToolDescriptionGenerator, + cfg.SubAgents, + + cfg.WithoutGeneralSubAgent, + cfg.ChatModel, + instruction, + cfg.ToolsConfig, + cfg.MaxIteration, + cfg.Middlewares, + append(handlers, cfg.Handlers...), + cfg.ModelFailoverConfig, + ) + if err != nil { + return nil, fmt.Errorf("failed to new task tool: %w", err) + } + handlers = append(handlers, tt) + } + + return adk.NewTypedChatModelAgent(ctx, &adk.TypedChatModelAgentConfig[M]{ + Name: cfg.Name, + Description: cfg.Description, + Instruction: instruction, + Model: cfg.ChatModel, + ToolsConfig: cfg.ToolsConfig, + MaxIterations: cfg.MaxIteration, + Middlewares: cfg.Middlewares, + Handlers: append(handlers, cfg.Handlers...), + + GenModelInput: typedGenModelInput[M], + ModelRetryConfig: cfg.ModelRetryConfig, + ModelFailoverConfig: cfg.ModelFailoverConfig, + OutputKey: cfg.OutputKey, + }) +} + +// New creates a new Deep agent instance with the provided configuration. +// This function initializes built-in tools, creates a task tool for subagent orchestration, +// and returns a fully configured ChatModelAgent ready for execution. +func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) { + return NewTyped(ctx, cfg) +} + +func typedGenModelInput[M adk.MessageType](_ context.Context, instruction string, input *adk.TypedAgentInput[M]) ([]M, error) { + var zero M + switch any(zero).(type) { + case *schema.Message: + msgs := make([]*schema.Message, 0, len(input.Messages)+1) + if instruction != "" { + msgs = append(msgs, schema.SystemMessage(instruction)) + } + // Type assertion is safe here because M = *schema.Message. + for _, m := range input.Messages { + msgs = append(msgs, any(m).(*schema.Message)) + } + result := make([]M, len(msgs)) + for i, m := range msgs { + result[i] = any(m).(M) + } + return result, nil + case *schema.AgenticMessage: + msgs := make([]*schema.AgenticMessage, 0, len(input.Messages)+1) + if instruction != "" { + msgs = append(msgs, schema.SystemAgenticMessage(instruction)) + } + for _, m := range input.Messages { + msgs = append(msgs, any(m).(*schema.AgenticMessage)) + } + result := make([]M, len(msgs)) + for i, m := range msgs { + result[i] = any(m).(M) + } + return result, nil + } + panic("unreachable") +} + +func buildTypedBuiltinAgentMiddlewares[M adk.MessageType](ctx context.Context, cfg *TypedConfig[M]) ([]adk.TypedChatModelAgentMiddleware[M], error) { + var ms []adk.TypedChatModelAgentMiddleware[M] + if !cfg.WithoutWriteTodos { + t, err := typedNewWriteTodos[M]() + if err != nil { + return nil, err + } + ms = append(ms, t) + } + + if cfg.Backend != nil || cfg.Shell != nil || cfg.StreamingShell != nil { + fm, err := filesystem2.NewTyped[M](ctx, &filesystem2.MiddlewareConfig{ + Backend: cfg.Backend, + Shell: cfg.Shell, + StreamingShell: cfg.StreamingShell, + }) + if err != nil { + return nil, err + } + ms = append(ms, fm) + } + + return ms, nil +} + +type TODO struct { + Content string `json:"content"` + ActiveForm string `json:"activeForm"` + Status string `json:"status" jsonschema:"enum=pending,enum=in_progress,enum=completed"` +} + +type writeTodosArguments struct { + Todos []TODO `json:"todos"` +} + +func typedNewWriteTodos[M adk.MessageType]() (adk.TypedChatModelAgentMiddleware[M], error) { + toolDesc := internal.SelectPrompt(internal.I18nPrompts{ + English: writeTodosToolDescription, + Chinese: writeTodosToolDescriptionChinese, + }) + resultMsg := internal.SelectPrompt(internal.I18nPrompts{ + English: "Updated todo list to %s", + Chinese: "已更新待办列表为 %s", + }) + + t, err := utils.InferTool("write_todos", toolDesc, func(ctx context.Context, input writeTodosArguments) (output string, err error) { + adk.AddSessionValue(ctx, SessionKeyTodos, input.Todos) + todos, err := sonic.MarshalString(input.Todos) + if err != nil { + return "", err + } + return fmt.Sprintf(resultMsg, todos), nil + }) + if err != nil { + return nil, err + } + + return typedBuildAppendPromptTool[M]("", t), nil +} diff --git a/adk/prebuilt/deep/deep_test.go b/adk/prebuilt/deep/deep_test.go new file mode 100644 index 0000000..b39cfe9 --- /dev/null +++ b/adk/prebuilt/deep/deep_test.go @@ -0,0 +1,700 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package deep + +import ( + "context" + "fmt" + "io" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/prebuilt/planexecute" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type sequentialAgenticModel struct { + responses []*schema.AgenticMessage + callCount int32 +} + +func (m *sequentialAgenticModel) Generate(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + idx := atomic.AddInt32(&m.callCount, 1) - 1 + if int(idx) >= len(m.responses) { + return nil, fmt.Errorf("sequentialAgenticModel: no more responses (call #%d)", idx) + } + return m.responses[idx], nil +} + +func (m *sequentialAgenticModel) Stream(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + result, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.AgenticMessage{result}), nil +} + +func agenticText(text string) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.AssistantGenText{Text: text}), + }, + } +} + +func agenticToolCall(toolName, callID, args string) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeAssistant, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolCall{Name: toolName, CallID: callID, Arguments: args}), + }, + } +} + +func readAgenticText(msg *schema.AgenticMessage) string { + if msg == nil { + return "" + } + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + if block.AssistantGenText != nil { + return block.AssistantGenText.Text + } + } + return "" +} + +type mockSearchTool struct{} + +func (m *mockSearchTool) Info(context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "search", + Desc: "search latest news", + }, nil +} + +func (m *mockSearchTool) InvokableRun(context.Context, string, ...tool.Option) (string, error) { + return "latest news search result", nil +} + +func TestGenModelInput(t *testing.T) { + ctx := context.Background() + + t.Run("WithInstruction", func(t *testing.T) { + input := &adk.AgentInput{ + Messages: []*schema.Message{ + schema.UserMessage("hello"), + }, + } + + msgs, err := typedGenModelInput(ctx, "You are a helpful assistant", input) + assert.NoError(t, err) + assert.Len(t, msgs, 2) + assert.Equal(t, schema.System, msgs[0].Role) + assert.Equal(t, "You are a helpful assistant", msgs[0].Content) + assert.Equal(t, schema.User, msgs[1].Role) + assert.Equal(t, "hello", msgs[1].Content) + }) + + t.Run("WithoutInstruction", func(t *testing.T) { + input := &adk.AgentInput{ + Messages: []*schema.Message{ + schema.UserMessage("hello"), + }, + } + + msgs, err := typedGenModelInput(ctx, "", input) + assert.NoError(t, err) + assert.Len(t, msgs, 1) + assert.Equal(t, schema.User, msgs[0].Role) + assert.Equal(t, "hello", msgs[0].Content) + }) +} + +func TestWriteTodos(t *testing.T) { + m, err := buildTypedBuiltinAgentMiddlewares(context.Background(), &Config{WithoutWriteTodos: false}) + assert.NoError(t, err) + + wt := m[0].(*typedAppendPromptTool[*schema.Message]).t.(tool.InvokableTool) + + todos := `[{"content":"content1","activeForm":"","status":"pending"},{"content":"content2","activeForm":"","status":"pending"}]` + args := fmt.Sprintf(`{"todos": %s}`, todos) + + result, err := wt.InvokableRun(context.Background(), args) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("Updated todo list to %s", todos), result) +} + +func TestDeepSubAgentSharesSessionValues(t *testing.T) { + ctx := context.Background() + spy := &spySubAgent{} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + calls := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + calls++ + if calls == 1 { + c := schema.ToolCall{ID: "id-1", Type: "function"} + c.Function.Name = taskToolName + c.Function.Arguments = fmt.Sprintf(`{"subagent_type":"%s","description":"from_parent"}`, spy.Name(ctx)) + return schema.AssistantMessage("", []schema.ToolCall{c}), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + + agent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: cm, + Instruction: "you are deep agent", + SubAgents: []adk.Agent{spy}, + ToolsConfig: adk.ToolsConfig{}, + MaxIteration: 2, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + assert.NoError(t, err) + + r := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) + it := r.Run(ctx, []adk.Message{schema.UserMessage("hi")}, adk.WithSessionValues(map[string]any{"parent_key": "parent_val"})) + for { + if _, ok := it.Next(); !ok { + break + } + } + + assert.Equal(t, "parent_val", spy.seenParentValue) +} + +func TestDeepSubAgentFollowsStreamingMode(t *testing.T) { + ctx := context.Background() + spy := &spyStreamingSubAgent{} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + subName := spy.Name(ctx) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "id-1", + Type: "function", + Function: schema.FunctionCall{ + Name: taskToolName, + Arguments: fmt.Sprintf(`{"subagent_type":"%s","description":"from_parent"}`, subName), + }, + }, + }), + }), nil). + Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("done", nil), + }), nil). + Times(1) + + agent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: cm, + Instruction: "you are deep agent", + SubAgents: []adk.Agent{spy}, + ToolsConfig: adk.ToolsConfig{}, + MaxIteration: 2, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + assert.NoError(t, err) + + r := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []adk.Message{schema.UserMessage("hi")}) + for { + if _, ok := it.Next(); !ok { + break + } + } + + assert.True(t, spy.seenEnableStreaming) +} + +type spySubAgent struct { + seenParentValue any +} + +func (s *spySubAgent) Name(context.Context) string { return "spy-subagent" } +func (s *spySubAgent) Description(context.Context) string { return "spy" } +func (s *spySubAgent) Run(ctx context.Context, _ *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + s.seenParentValue, _ = adk.GetSessionValue(ctx, "parent_key") + it, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + gen.Send(adk.EventFromMessage(schema.AssistantMessage("ok", nil), nil, schema.Assistant, "")) + gen.Close() + return it +} + +type spyStreamingSubAgent struct { + seenEnableStreaming bool +} + +func (s *spyStreamingSubAgent) Name(context.Context) string { return "spy-streaming-subagent" } +func (s *spyStreamingSubAgent) Description(context.Context) string { return "spy" } +func (s *spyStreamingSubAgent) Run(_ context.Context, input *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + if input != nil { + s.seenEnableStreaming = input.EnableStreaming + } + it, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + gen.Send(adk.EventFromMessage(schema.AssistantMessage("ok", nil), nil, schema.Assistant, "")) + gen.Close() + return it +} + +func TestDeepAgentWithPlanExecuteSubAgent_InternalEventsEmitted(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + deepModel := mockModel.NewMockToolCallingChatModel(ctrl) + plannerModel := mockModel.NewMockToolCallingChatModel(ctrl) + executorModel := mockModel.NewMockToolCallingChatModel(ctrl) + replannerModel := mockModel.NewMockToolCallingChatModel(ctrl) + + deepModel.EXPECT().WithTools(gomock.Any()).Return(deepModel, nil).AnyTimes() + plannerModel.EXPECT().WithTools(gomock.Any()).Return(plannerModel, nil).AnyTimes() + executorModel.EXPECT().WithTools(gomock.Any()).Return(executorModel, nil).AnyTimes() + replannerModel.EXPECT().WithTools(gomock.Any()).Return(replannerModel, nil).AnyTimes() + + plannerModel.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input []*schema.Message, opts ...interface{}) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + go func() { + defer sw.Close() + planJSON := `{"steps":["step1"]}` + msg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "plan_call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "plan", + Arguments: planJSON, + }, + }, + }) + sw.Send(msg, nil) + }() + return sr, nil + }, + ).Times(1) + + executorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("executed step1", nil), nil + }, + ).Times(1) + + replannerModel.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input []*schema.Message, opts ...interface{}) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + go func() { + defer sw.Close() + responseJSON := `{"response":"final response"}` + msg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "respond_call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "respond", + Arguments: responseJSON, + }, + }, + }) + sw.Send(msg, nil) + }() + return sr, nil + }, + ).Times(1) + + planner, err := planexecute.NewPlanner(ctx, &planexecute.PlannerConfig{ + ToolCallingChatModel: plannerModel, + }) + assert.NoError(t, err) + + executor, err := planexecute.NewExecutor(ctx, &planexecute.ExecutorConfig{ + Model: executorModel, + }) + assert.NoError(t, err) + + replanner, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{ + ChatModel: replannerModel, + }) + assert.NoError(t, err) + + planExecuteAgent, err := planexecute.New(ctx, &planexecute.Config{ + Planner: planner, + Executor: executor, + Replanner: replanner, + }) + assert.NoError(t, err) + + namedPlanExecuteAgent := &namedPlanExecuteAgent{ + ResumableAgent: planExecuteAgent, + name: "plan_execute_subagent", + description: "a plan execute subagent", + } + + deepModelCalls := 0 + deepModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + deepModelCalls++ + if deepModelCalls == 1 { + c := schema.ToolCall{ID: "id-1", Type: "function"} + c.Function.Name = taskToolName + c.Function.Arguments = fmt.Sprintf(`{"subagent_type":"%s","description":"execute the plan"}`, namedPlanExecuteAgent.name) + return schema.AssistantMessage("", []schema.ToolCall{c}), nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + + deepAgent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: deepModel, + Instruction: "you are deep agent", + SubAgents: []adk.Agent{namedPlanExecuteAgent}, + ToolsConfig: adk.ToolsConfig{EmitInternalEvents: true}, + MaxIteration: 5, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + assert.NoError(t, err) + + r := adk.NewRunner(ctx, adk.RunnerConfig{Agent: deepAgent}) + it := r.Run(ctx, []adk.Message{schema.UserMessage("hi")}) + + var events []*adk.AgentEvent + for { + event, ok := it.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Greater(t, len(events), 0, "should have at least one event") + + var deepAgentEvents []*adk.AgentEvent + var plannerEvents []*adk.AgentEvent + var executorEvents []*adk.AgentEvent + var replannerEvents []*adk.AgentEvent + var planExecuteEvents []*adk.AgentEvent + + for _, event := range events { + switch event.AgentName { + case "deep": + deepAgentEvents = append(deepAgentEvents, event) + case "planner": + plannerEvents = append(plannerEvents, event) + case "executor": + executorEvents = append(executorEvents, event) + case "replanner": + replannerEvents = append(replannerEvents, event) + case "plan_execute_replan", "execute_replan": + planExecuteEvents = append(planExecuteEvents, event) + } + } + + assert.Greater(t, len(deepAgentEvents), 0, "should have events from deep agent") + + assert.Greater(t, len(plannerEvents), 0, "planner internal events should be emitted when EmitInternalEvents is true") + assert.Greater(t, len(executorEvents), 0, "executor internal events should be emitted when EmitInternalEvents is true") + assert.Greater(t, len(replannerEvents), 0, "replanner internal events should be emitted when EmitInternalEvents is true") + + t.Logf("Total events: %d", len(events)) + t.Logf("Deep agent events: %d", len(deepAgentEvents)) + t.Logf("Planner events: %d", len(plannerEvents)) + t.Logf("Executor events: %d", len(executorEvents)) + t.Logf("Replanner events: %d", len(replannerEvents)) + t.Logf("PlanExecute events: %d", len(planExecuteEvents)) +} + +type namedPlanExecuteAgent struct { + adk.ResumableAgent + name string + description string +} + +func (n *namedPlanExecuteAgent) Name(_ context.Context) string { + return n.name +} + +func (n *namedPlanExecuteAgent) Description(_ context.Context) string { + return n.description +} + +func TestAgenticDeepAgentEmitInternalEventsFromSubAgent(t *testing.T) { + ctx := context.Background() + + subAgentModel := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCall("search", "search-call-1", `{"query":"latest news"}`), + agenticText("search result from subagent"), + }, + } + webSearchAgent, err := adk.NewTypedChatModelAgent(ctx, &adk.TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "web_searcher", + Description: "agent dedicated to web search", + Model: subAgentModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&mockSearchTool{}}, + }, + }, + }) + assert.NoError(t, err) + + deepModel := &sequentialAgenticModel{ + responses: []*schema.AgenticMessage{ + agenticToolCall(taskToolName, "task-call-1", `{"subagent_type":"web_searcher","description":"search latest news"}`), + agenticText("deep final answer"), + }, + } + deepAgent, err := NewTyped(ctx, &TypedConfig[*schema.AgenticMessage]{ + Name: "deep", + Description: "deep agent", + ChatModel: deepModel, + SubAgents: []adk.TypedAgent[*schema.AgenticMessage]{webSearchAgent}, + ToolsConfig: adk.ToolsConfig{EmitInternalEvents: true}, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + assert.NoError(t, err) + + runner := adk.NewTypedRunner(adk.TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: deepAgent, + EnableStreaming: true, + }) + iter := runner.Run(ctx, []*schema.AgenticMessage{ + schema.UserAgenticMessage("search latest news"), + }) + + var emittedTexts []string + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output == nil || event.Output.MessageOutput == nil { + continue + } + if event.Output.MessageOutput.IsStreaming { + stream := event.Output.MessageOutput.MessageStream + for { + chunk, err := stream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + if text := readAgenticText(chunk); text != "" { + emittedTexts = append(emittedTexts, fmt.Sprintf("%s:%s", event.AgentName, text)) + } + } + continue + } + if text := readAgenticText(event.Output.MessageOutput.Message); text != "" { + emittedTexts = append(emittedTexts, fmt.Sprintf("%s:%s", event.AgentName, text)) + } + } + + t.Logf("emitted texts: %v", emittedTexts) + assert.Contains(t, emittedTexts, "deep:deep final answer", "expected DeepAgent final result to be emitted") + assert.Contains(t, emittedTexts, "web_searcher:search result from subagent", "expected DeepAgent to emit Agentic sub-agent events when EmitInternalEvents is true") +} + +func TestDeepAgentOutputKey(t *testing.T) { + t.Run("OutputKeyStoresInSession", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello from DeepAgent", nil), nil). + Times(1) + + agent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: cm, + Instruction: "you are deep agent", + MaxIteration: 2, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + OutputKey: "deep_output", + }) + assert.NoError(t, err) + + var capturedSessionValues map[string]any + wrappedAgent := &sessionCaptureAgent{ + Agent: agent, + captureSession: func(values map[string]any) { capturedSessionValues = values }, + } + + r := adk.NewRunner(ctx, adk.RunnerConfig{Agent: wrappedAgent}) + it := r.Run(ctx, []adk.Message{schema.UserMessage("hi")}) + for { + if _, ok := it.Next(); !ok { + break + } + } + + assert.Contains(t, capturedSessionValues, "deep_output") + assert.Equal(t, "Hello from DeepAgent", capturedSessionValues["deep_output"]) + }) + + t.Run("OutputKeyWithStreamingStoresInSession", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("Hello", nil), + schema.AssistantMessage(" from", nil), + schema.AssistantMessage(" DeepAgent", nil), + }), nil). + Times(1) + + agent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: cm, + Instruction: "you are deep agent", + MaxIteration: 2, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + OutputKey: "deep_output", + }) + assert.NoError(t, err) + + var capturedSessionValues map[string]any + wrappedAgent := &sessionCaptureAgent{ + Agent: agent, + captureSession: func(values map[string]any) { capturedSessionValues = values }, + } + + r := adk.NewRunner(ctx, adk.RunnerConfig{Agent: wrappedAgent, EnableStreaming: true}) + it := r.Run(ctx, []adk.Message{schema.UserMessage("hi")}) + for { + if _, ok := it.Next(); !ok { + break + } + } + + assert.Contains(t, capturedSessionValues, "deep_output") + assert.Equal(t, "Hello from DeepAgent", capturedSessionValues["deep_output"]) + }) + + t.Run("OutputKeyNotSetWhenEmpty", func(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cm := mockModel.NewMockToolCallingChatModel(ctrl) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("Hello from DeepAgent", nil), nil). + Times(1) + + agent, err := New(ctx, &Config{ + Name: "deep", + Description: "deep agent", + ChatModel: cm, + Instruction: "you are deep agent", + MaxIteration: 2, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + assert.NoError(t, err) + + var capturedSessionValues map[string]any + wrappedAgent := &sessionCaptureAgent{ + Agent: agent, + captureSession: func(values map[string]any) { capturedSessionValues = values }, + } + + r := adk.NewRunner(ctx, adk.RunnerConfig{Agent: wrappedAgent}) + it := r.Run(ctx, []adk.Message{schema.UserMessage("hi")}) + for { + if _, ok := it.Next(); !ok { + break + } + } + + assert.NotContains(t, capturedSessionValues, "deep_output") + }) +} + +type sessionCaptureAgent struct { + adk.Agent + captureSession func(map[string]any) +} + +func (s *sessionCaptureAgent) Run(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + innerIt := s.Agent.Run(ctx, input, opts...) + it, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + go func() { + defer gen.Close() + for { + event, ok := innerIt.Next() + if !ok { + break + } + gen.Send(event) + } + s.captureSession(adk.GetSessionValues(ctx)) + }() + return it +} diff --git a/adk/prebuilt/deep/prompt.go b/adk/prebuilt/deep/prompt.go new file mode 100644 index 0000000..aba5fd3 --- /dev/null +++ b/adk/prebuilt/deep/prompt.go @@ -0,0 +1,685 @@ +/* + * Copyright (c) 2025 Harrison Chase + * Copyright (c) 2025 CloudWeGo Authors + * SPDX-License-Identifier: MIT + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package deep + +// This file contains prompt templates and tool descriptions adapted from the DeepAgents project and ClaudeCode. +// Original source: https://github.com/langchain-ai/deepagents https://claude.com/product/claude-code +// +// These prompts are used under the terms of the original project's open source license. +// When using this code in your own open source project, ensure compliance with the original license requirements. + +const ( + taskPrompt = ` +# 'task' (subagent spawner) + +You have access to a 'task' tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result. +You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description. + +When to use the task tool: +- When a task is complex and multi-step, and can be fully delegated in isolation +- When a task is independent of other tasks and can run in parallel +- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread +- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting) +- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.) + +Subagent lifecycle: +1. **Spawn** → Provide clear role, instructions, and expected output +2. **Run** → The subagent completes the task autonomously +3. **Return** → The subagent provides a single structured result +4. **Reconcile** → Incorporate or synthesize the result into the main thread + +When NOT to use the task tool: +- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them) +- If the task is trivial (a few tool calls or simple lookup) +- If delegating does not reduce token usage, complexity, or context switching +- If splitting would add latency without benefit + +## Important Task Tool Usage Notes to Remember +- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important. +- Remember to use the 'task' tool to silo independent tasks within a multi-part objective. +- You should use the 'task' tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient. +` + + baseAgentInstruction = ` +You are a helpful assistant. Use the instructions below and the tools available to you to assist the user. + +IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases. +IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files. + +# Tone and style +- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked. +- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification. +- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session. +- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files. +- Do not use a colon before tool calls. Your tool calls may not be shown directly in the output, so text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period. + +# Professional objectivity +Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if agent honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases. + +# Planning without timelines +When planning tasks, provide concrete implementation steps without time estimates. Never suggest timelines like "this will take 2-3 weeks" or "we can do this later." Focus on what needs to be done, not when. Break work into actionable steps and let users decide scheduling. + +# Doing tasks +The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: +- NEVER propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications. +- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. +- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused. + - Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident. + - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code. + - Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task—three similar lines of code is better than a premature abstraction. +- Avoid backwards-compatibility hacks like renaming unused '_vars', re-exporting types, adding '// removed' comments for removed code, etc. If something is unused, delete it completely. + +- Tool results and user messages may include tags. tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear. +- The conversation has unlimited context through automatic summarization. + +IMPORTANT: Complete tasks fully. Do not stop mid-task or leave work incomplete. Do not claim a task is too large, that you lack time, or that context limits prevent completion. You have unlimited context through summarization. Continue working until the task is done or the user stops you. + +# Tool usage policy +- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls. +- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls. +- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead. + + +IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases. + + +# Code References + +When referencing specific functions or pieces of code include the pattern 'file_path:line_number' to allow the user to easily navigate to the source code location. + + +user: Where are errors from the client handled? +assistant: Clients are marked as failed in the 'connectToServer' function in src/services/process.ts:712. + +` + generalAgentDescription = `general-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)` + taskToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. + +The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it. + +Available agent types and the tools they have access to: +{other_agents} + +When using the Task tool, you must specify a subagent_type parameter to select which agent type to use. + +When NOT to use the Task tool: +- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly +- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly +- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly +- Other tasks that are not related to the agent descriptions above + + +Usage notes: +- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses +- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. +- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need. +- The agent's outputs should generally be trusted +- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent +- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. +- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls. + +Example usage: + + +"code-reviewer": use this agent after you are done writing a significant piece of code +"greeting-responder": use this agent when to respond to user greetings with a friendly joke + + + +user: "Please write a function that checks if a number is prime" +assistant: Sure let me write a function that checks if a number is prime +assistant: First let me use the Write tool to write a function that checks if a number is prime +assistant: I'm going to use the Write tool to write the following code: + +function isPrime(n) {{ + if (n <= 1) return false + for (let i = 2; i * i <= n; i++) {{ + if (n %!i(MISSING) === 0) return false + }} + return true +}} + + +Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code + +assistant: Now let me use the code-reviewer agent to review the code +assistant: Uses the Task tool to launch the code-reviewer agent + + + +user: "Hello" + +Since the user is greeting, use the greeting-responder agent to respond with a friendly joke + +assistant: "I'm going to use the Task tool to launch the greeting-responder agent" + +` + writeTodosToolDescription = `Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user. +It also helps the user understand the progress of the task and overall progress of their requests. + +## When to Use This Tool +Use this tool proactively in these scenarios: + +1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions +2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations +3. User explicitly requests todo list - When the user directly asks you to use the todo list +4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) +5. After receiving new instructions - Immediately capture user requirements as todos +6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time +7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation + +## When NOT to Use This Tool + +Skip using this tool when: +1. There is only a single, straightforward task +2. The task is trivial and tracking it provides no organizational benefit +3. The task can be completed in less than 3 trivial steps +4. The task is purely conversational or informational + +NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly. + +## Examples of When to Use the Todo List + + +User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done! +Assistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation. +*Creates todo list with the following items:* +1. Creating dark mode toggle component in Settings page +2. Adding dark mode state management (context/store) +3. Implementing CSS-in-JS styles for dark theme +4. Updating existing components to support theme switching +5. Running tests and build process, addressing any failures or errors that occur +*Begins working on the first task* + + +The assistant used the todo list because: +1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes +2. The user explicitly requested tests and build be run afterward +3. The assistant inferred that tests and build need to pass by adding "Ensure tests and build succeed" as the final task + + + + +User: Help me rename the function getCwd to getCurrentWorkingDirectory across my project +Assistant: Let me first search through your codebase to find all occurrences of 'getCwd'. +*Uses grep or search tools to locate all instances of getCwd in the codebase* +Assistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes. +*Creates todo list with specific items for each file that needs updating* + + +The assistant used the todo list because: +1. First, the assistant searched to understand the scope of the task +2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps +3. The todo list helps ensure every instance is tracked and updated systematically +4. This approach prevents missing any occurrences and maintains code consistency + + + + + +User: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow. +Assistant: I'll help implement these features. First, let's add all the features to the todo list. +*Creates a todo list breaking down each feature into specific tasks based on the project architecture* +Assistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system. + + +The assistant used the todo list because: +1. The user provided multiple complex features to implement in a comma separated list +2. The todo list helps organize these large features into manageable tasks +3. This approach allows for tracking progress across the entire implementation + + + + +User: Can you help optimize my React application? It's rendering slowly and has performance issues. +Assistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks. +*Reviews component structure, render patterns, state management, and data fetching* +Assistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts. +*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting* +Let's start by implementing memoization for the expensive calculations in your ProductList component. + + +The assistant used the todo list because: +1. First, the assistant examined the codebase to identify specific performance issues +2. Based on this analysis, it identified multiple optimization opportunities +3. Performance optimization is a non-trivial task requiring multiple steps +4. The todo list helps methodically track improvements across different components +5. This systematic approach ensures all performance bottlenecks are addressed + + + +## Examples of When NOT to Use the Todo List + + +User: How do I print 'Hello World' in Python? +Assistant: In Python, you can print "Hello World" with this simple code: + +python +print("Hello World") + +This will output the text "Hello World" to the console when executed. + + +The assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request. + + + + +User: What does the git status command do? +Assistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits. + + +The assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks. + + + + +User: Can you add a comment to the calculateTotal function to explain what it does? +Assistant: Sure, let me add a comment to the calculateTotal function to explain what it does. +* Uses the Edit tool to add a comment to the calculateTotal function * + + +The assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization. + + + + +User: Run npm install for me and tell me what happens. +Assistant: I'll run the npm install command for you. + +*Executes: npm install* + +The command completed successfully. Here's the output: +[Output of npm install command] + +All dependencies have been installed according to your package.json file. + + +The assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task. + + + +## Task States and Management + +1. **Task States**: Use these states to track progress: + - pending: Task not yet started + - in_progress: Currently working on (limit to ONE task at a time) + - completed: Task finished successfully + + **IMPORTANT**: Task descriptions must have two forms: + - content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project") + - activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project") + +2. **Task Management**: + - Update task status in real-time as you work + - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) + - Exactly ONE task must be in_progress at any time (not less, not more) + - Complete current tasks before starting new ones + - Remove tasks that are no longer relevant from the list entirely + +3. **Task Completion Requirements**: + - ONLY mark a task as completed when you have FULLY accomplished it + - If you encounter errors, blockers, or cannot finish, keep the task as in_progress + - When blocked, create a new task describing what needs to be resolved + - Never mark a task as completed if: + - Tests are failing + - Implementation is partial + - You encountered unresolved errors + - You couldn't find necessary files or dependencies + +4. **Task Breakdown**: + - Create specific, actionable items + - Break complex tasks into smaller, manageable steps + - Use clear, descriptive task names + - Always provide both forms: + - content: "Fix authentication bug" + - activeForm: "Fixing authentication bug" + +When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully. +` + + taskPromptChinese = ` +# 'task'(子代理生成器) + +你可以使用 'task' 工具来启动处理独立任务的短期子代理。这些代理是临时的——它们只在任务持续期间存在,并返回单个结果。 +当手头的任务与代理的描述匹配时,你应该主动使用带有专门代理的 'task' 工具。 + +何时使用 task 工具: +- 当任务复杂且包含多个步骤,并且可以完全独立委托时 +- 当任务独立于其他任务并且可以并行运行时 +- 当任务需要集中推理或大量 token/上下文使用,这会使编排器线程膨胀时 +- 当沙箱化提高可靠性时(例如代码执行、结构化搜索、数据格式化) +- 当你只关心子代理的输出,而不关心中间步骤时(例如执行大量研究然后返回综合报告,执行一系列计算或查找以获得简洁、相关的答案) + +子代理生命周期: +1. **生成** → 提供清晰的角色、指令和预期输出 +2. **运行** → 子代理自主完成任务 +3. **返回** → 子代理提供单个结构化结果 +4. **协调** → 将结果合并或综合到主线程中 + +何时不使用 task 工具: +- 如果你需要在子代理完成后查看中间推理或步骤(task 工具会隐藏它们) +- 如果任务很简单(几个工具调用或简单查找) +- 如果委托不会减少 token 使用、复杂性或上下文切换 +- 如果拆分会增加延迟而没有好处 + +## 重要的 Task 工具使用注意事项 +- 尽可能并行化你所做的工作。这对于 tool_calls 和 tasks 都适用。每当你有独立的步骤要完成时——进行 tool_calls,或并行启动任务(子代理)以更快地完成它们。这为用户节省了时间,这非常重要。 +- 记住使用 'task' 工具在多部分目标中隔离独立任务。 +- 每当你有一个需要多个步骤的复杂任务,并且独立于代理需要完成的其他任务时,你应该使用 'task' 工具。这些代理非常有能力且高效。 +` + + baseAgentInstructionChinese = ` +你是一个有帮助的助手。使用以下说明和可用的工具来协助用户。 + +重要:协助授权的安全测试、防御性安全、CTF 挑战和教育环境。拒绝破坏性技术、DoS 攻击、大规模目标、供应链攻击或用于恶意目的的检测规避请求。双用途安全工具(C2 框架、凭证测试、漏洞开发)需要明确的授权上下文:渗透测试参与、CTF 竞赛、安全研究或防御用例。 +重要:除非你确信 URL 是为了帮助用户编程,否则你绝不能为用户生成或猜测 URL。你可以使用用户在其消息或本地文件中提供的 URL。 + +# 语气和风格 +- 仅在用户明确要求时使用表情符号。除非被要求,否则避免在所有通信中使用表情符号。 +- 你的输出将显示在命令行界面上。你的回复应该简短而简洁。你可以使用 Github 风格的 markdown 进行格式化,并将使用 CommonMark 规范以等宽字体呈现。 +- 输出文本与用户交流;你在工具使用之外输出的所有文本都会显示给用户。仅使用工具来完成任务。永远不要使用 Bash 或代码注释等工具作为在会话期间与用户交流的手段。 +- 除非绝对必要以实现你的目标,否则永远不要创建文件。始终优先编辑现有文件而不是创建新文件。这包括 markdown 文件。 +- 不要在工具调用前使用冒号。你的工具调用可能不会直接显示在输出中,因此像"让我读取文件:"后跟读取工具调用的文本应该只是"让我读取文件。"加句号。 + +# 专业客观性 +优先考虑技术准确性和真实性,而不是验证用户的信念。专注于事实和解决问题,提供直接、客观的技术信息,不要有任何不必要的夸大、赞美或情感验证。如果代理诚实地对所有想法应用相同的严格标准,并在必要时提出异议,即使这可能不是用户想听到的,对用户来说是最好的。客观的指导和尊重的纠正比虚假的同意更有价值。每当存在不确定性时,最好先调查以找到真相,而不是本能地确认用户的信念。避免在回复用户时使用过度的验证或过度的赞美,如"你完全正确"或类似的短语。 + +# 不带时间线的规划 +在规划任务时,提供具体的实施步骤而不带时间估计。永远不要建议像"这将需要 2-3 周"或"我们可以稍后做这个"这样的时间线。专注于需要做什么,而不是什么时候。将工作分解为可操作的步骤,让用户决定日程安排。 + +# 执行任务 +用户主要会要求你执行软件工程任务。这包括解决 bug、添加新功能、重构代码、解释代码等。对于这些任务,建议以下步骤: +- 永远不要对你没有阅读过的代码提出更改建议。如果用户询问或希望你修改文件,请先阅读它。在建议修改之前理解现有代码。 +- 注意不要引入安全漏洞,如命令注入、XSS、SQL 注入和其他 OWASP 前 10 名漏洞。如果你注意到你编写了不安全的代码,请立即修复它。 +- 避免过度工程。只进行直接要求或明显必要的更改。保持解决方案简单和专注。 + - 不要添加功能、重构代码或进行超出要求的"改进"。bug 修复不需要清理周围的代码。简单的功能不需要额外的可配置性。不要向你没有更改的代码添加文档字符串、注释或类型注解。只在逻辑不明显的地方添加注释。 + - 不要为不可能发生的场景添加错误处理、回退或验证。信任内部代码和框架保证。只在系统边界(用户输入、外部 API)进行验证。当你可以直接更改代码时,不要使用功能标志或向后兼容性垫片。 + - 不要为一次性操作创建辅助函数、工具或抽象。不要为假设的未来需求进行设计。正确的复杂度是当前任务所需的最小值——三行类似的代码比过早的抽象更好。 +- 避免向后兼容性技巧,如重命名未使用的 '_vars'、重新导出类型、为删除的代码添加 '// removed' 注释等。如果某些东西未使用,请完全删除它。 + +- 工具结果和用户消息可能包含 标签。 标签包含有用的信息和提醒。它们由系统自动添加,与它们出现的特定工具结果或用户消息没有直接关系。 +- 对话通过自动摘要具有无限上下文。 + +重要:完全完成任务。不要在任务中途停止或留下未完成的工作。不要声称任务太大、你缺乏时间或上下文限制阻止完成。你通过摘要具有无限上下文。继续工作直到任务完成或用户停止你。 + +# 工具使用策略 +- 你可以在单个响应中调用多个工具。如果你打算调用多个工具并且它们之间没有依赖关系,请并行进行所有独立的工具调用。尽可能最大化使用并行工具调用以提高效率。但是,如果某些工具调用依赖于先前的调用来通知依赖值,请不要并行调用这些工具,而是按顺序调用它们。例如,如果一个操作必须在另一个操作开始之前完成,请按顺序运行这些操作。永远不要在工具调用中使用占位符或猜测缺失的参数。 +- 如果用户指定他们希望你"并行"运行工具,你必须发送一条包含多个工具使用内容块的消息。例如,如果你需要并行启动多个代理,请发送一条包含多个 Task 工具调用的消息。 +- 尽可能使用专门的工具而不是 bash 命令,因为这提供了更好的用户体验。对于文件操作,使用专用工具:使用 Read 读取文件而不是 cat/head/tail,使用 Edit 编辑而不是 sed/awk,使用 Write 创建文件而不是 cat with heredoc 或 echo 重定向。仅将 bash 工具保留用于需要 shell 执行的实际系统命令和终端操作。永远不要使用 bash echo 或其他命令行工具向用户传达想法、解释或指令。而是直接在你的响应文本中输出所有通信。 + + +重要:协助授权的安全测试、防御性安全、CTF 挑战和教育环境。拒绝破坏性技术、DoS 攻击、大规模目标、供应链攻击或用于恶意目的的检测规避请求。双用途安全工具(C2 框架、凭证测试、漏洞开发)需要明确的授权上下文:渗透测试参与、CTF 竞赛、安全研究或防御用例。 + + +# 代码引用 + +在引用特定函数或代码片段时,包含 'file_path:line_number' 模式,以允许用户轻松导航到源代码位置。 + + +user: 客户端的错误在哪里处理? +assistant: 客户端在 src/services/process.ts:712 的 'connectToServer' 函数中被标记为失败。 + +` + generalAgentDescriptionChinese = `通用代理,用于研究复杂问题、搜索代码和执行多步骤任务。当你搜索关键字或文件并且不确定在前几次尝试中能否找到正确的匹配时,使用此代理为你执行搜索。(工具:*)` + taskToolDescriptionChinese = `启动新代理以自主处理复杂的多步骤任务。 + +Task 工具启动专门的代理(子进程),自主处理复杂任务。每种代理类型都有特定的功能和可用的工具。 + +可用的代理类型及其可访问的工具: +{other_agents} + +使用 Task 工具时,你必须指定 subagent_type 参数来选择要使用的代理类型。 + +何时不使用 Task 工具: +- 如果你想读取特定的文件路径,请使用 Read 或 Glob 工具而不是 Task 工具,以更快地找到匹配项 +- 如果你正在搜索特定的类定义,如"class Foo",请使用 Glob 工具,以更快地找到匹配项 +- 如果你正在特定文件或 2-3 个文件集中搜索代码,请使用 Read 工具而不是 Task 工具,以更快地找到匹配项 +- 与上述代理描述无关的其他任务 + + +使用说明: +- 尽可能同时启动多个代理,以最大化性能;为此,使用包含多个工具使用的单条消息 +- 当代理完成时,它将向你返回一条消息。代理返回的结果对用户不可见。要向用户显示结果,你应该向用户发送一条文本消息,简要总结结果。 +- 提供清晰、详细的提示,以便代理可以自主工作并返回你需要的确切信息。 +- 代理的输出通常应该被信任 +- 明确告诉代理你期望它编写代码还是只是进行研究(搜索、文件读取、网络获取等),因为它不知道用户的意图 +- 如果代理描述提到应该主动使用它,那么你应该尽力使用它即使用户没有这样要求。使用你的判断。 +- 如果用户指定他们希望你"并行"运行代理,你必须发送一条包含多个 Task 工具使用内容块的消息。例如,如果你需要并行启动代码审查代理和测试运行代理,请发送一条包含两个工具调用的消息。 + +使用示例: + + +"code-reviewer": 在你完成编写重要代码后使用此代理 +"greeting-responder": 当要用友好的笑话回应用户问候时使用此代理 + + + +user: "请编写一个检查数字是否为质数的函数" +assistant: 好的,让我编写一个检查数字是否为质数的函数 +assistant: 首先让我使用 Write 工具编写一个检查数字是否为质数的函数 +assistant: 我将使用 Write 工具编写以下代码: + +function isPrime(n) {{ + if (n <= 1) return false + for (let i = 2; i * i <= n; i++) {{ + if (n %!i(MISSING) === 0) return false + }} + return true +}} + + +由于编写了重要的代码并且任务已完成,现在使用 code-reviewer 代理来审查代码 + +assistant: 现在让我使用 code-reviewer 代理来审查代码 +assistant: 使用 Task 工具启动 code-reviewer 代理 + + + +user: "你好" + +由于用户在问候,使用 greeting-responder 代理用友好的笑话回应 + +assistant: "我将使用 Task 工具启动 greeting-responder 代理" + +` + writeTodosToolDescriptionChinese = `使用此工具为你当前的编码会话创建和管理结构化的任务列表。这有助于你跟踪进度、组织复杂任务,并向用户展示你的彻底性。 +它还帮助用户了解任务的进度和他们请求的整体进度。 + +## 何时使用此工具 +在以下场景中主动使用此工具: + +1. 复杂的多步骤任务 - 当任务需要 3 个或更多不同的步骤或操作时 +2. 非平凡和复杂的任务 - 需要仔细规划或多个操作的任务 +3. 用户明确要求待办事项列表 - 当用户直接要求你使用待办事项列表时 +4. 用户提供多个任务 - 当用户提供要完成的事项列表(编号或逗号分隔)时 +5. 收到新指令后 - 立即将用户需求捕获为待办事项 +6. 当你开始处理任务时 - 在开始工作之前将其标记为进行中。理想情况下,你一次只应该有一个待办事项处于进行中状态 +7. 完成任务后 - 将其标记为已完成,并添加在实施过程中发现的任何新的后续任务 + +## 何时不使用此工具 + +在以下情况下跳过使用此工具: +1. 只有一个简单直接的任务 +2. 任务很简单,跟踪它没有组织上的好处 +3. 任务可以在少于 3 个简单步骤内完成 +4. 任务纯粹是对话性的或信息性的 + +注意,如果只有一个简单的任务要做,你不应该使用此工具。在这种情况下,你最好直接完成任务。 + +## 何时使用待办事项列表的示例 + + +User: 我想在应用程序设置中添加一个深色模式切换。完成后确保运行测试和构建! +Assistant: 我将帮助在你的应用程序设置中添加深色模式切换。让我创建一个待办事项列表来跟踪此实现。 +*创建包含以下项目的待办事项列表:* +1. 在设置页面创建深色模式切换组件 +2. 添加深色模式状态管理(context/store) +3. 实现深色主题的 CSS-in-JS 样式 +4. 更新现有组件以支持主题切换 +5. 运行测试和构建过程,解决发生的任何失败或错误 +*开始处理第一个任务* + + +助手使用待办事项列表是因为: +1. 添加深色模式是一个需要 UI、状态管理和样式更改的多步骤功能 +2. 用户明确要求之后运行测试和构建 +3. 助手推断测试和构建需要通过,将"确保测试和构建成功"添加为最终任务 + + + + +User: 帮我在整个项目中将函数 getCwd 重命名为 getCurrentWorkingDirectory +Assistant: 让我首先搜索你的代码库以找到所有 'getCwd' 的出现。 +*使用 grep 或搜索工具在代码库中定位所有 getCwd 实例* +Assistant: 我在 8 个不同的文件中找到了 15 个 'getCwd' 实例。让我创建一个待办事项列表来跟踪这些更改。 +*创建包含每个需要更新的文件的特定项目的待办事项列表* + + +助手使用待办事项列表是因为: +1. 首先,助手搜索以了解任务的范围 +2. 在发现跨多个文件的多个出现后,它确定这是一个包含多个步骤的复杂任务 +3. 待办事项列表有助于确保每个实例都被系统地跟踪和更新 +4. 这种方法可以防止遗漏任何出现并保持代码一致性 + + + + + +User: 我需要为我的电子商务网站实现这些功能:用户注册、产品目录、购物车和结账流程。 +Assistant: 我将帮助实现这些功能。首先,让我们将所有功能添加到待办事项列表中。 +*创建一个待办事项列表,根据项目架构将每个功能分解为特定任务* +Assistant: 让我们从用户注册开始。这将涉及创建数据库模型、API 端点和与你现有身份验证系统集成的前端表单。 + + +助手使用待办事项列表是因为: +1. 用户在逗号分隔的列表中提供了多个复杂功能要实现 +2. 待办事项列表有助于将这些大功能组织成可管理的任务 +3. 这种方法允许跟踪整个实现过程中的进度 + + + + +User: 你能帮我优化我的 React 应用程序吗?它渲染缓慢并且有性能问题。 +Assistant: 我将帮助优化你的 React 应用程序。首先,让我检查你的代码库以识别潜在的性能瓶颈。 +*审查组件结构、渲染模式、状态管理和数据获取* +Assistant: 在分析你的代码库后,我发现了几个性能问题。让我创建一个待办事项列表来跟踪我们的优化工作。 +*创建包含以下项目的待办事项列表:1) 为 ProductList 中的昂贵计算实现记忆化,2) 为 Dashboard 中的长列表添加虚拟化,3) 优化 Gallery 组件中的图像加载,4) 修复 ShoppingCart 中的状态更新循环,5) 审查包大小并实现代码拆分* +让我们从为 ProductList 组件中的昂贵计算实现记忆化开始。 + + +助手使用待办事项列表是因为: +1. 首先,助手检查代码库以识别特定的性能问题 +2. 基于此分析,它确定了多个优化机会 +3. 性能优化是一个需要多个步骤的非平凡任务 +4. 待办事项列表有助于有条理地跟踪跨不同组件的改进 +5. 这种系统化的方法确保解决所有性能瓶颈 + + + +## 何时不使用待办事项列表的示例 + + +User: 如何在 Python 中打印 'Hello World'? +Assistant: 在 Python 中,你可以用这个简单的代码打印 "Hello World": + +python +print("Hello World") + +执行时,这将在控制台输出文本 "Hello World"。 + + +助手没有使用待办事项列表,因为这是一个可以在一步内完成的单一、简单任务。对于这样一个简单直接的请求,没有必要跟踪多个任务或步骤。 + + + + +User: git status 命令是做什么的? +Assistant: git status 命令显示你的工作目录和暂存区的当前状态。它显示哪些更改已暂存、哪些尚未暂存,以及哪些文件未被 Git 跟踪。这是在提交之前了解存储库当前状态的基本命令。 + + +助手没有使用待办事项列表,因为这是一个没有实际编码任务要完成的信息请求。用户只是要求解释,而不是要求助手执行多个步骤或任务。 + + + + +User: 你能给 calculateTotal 函数添加一个注释来解释它的作用吗? +Assistant: 当然,让我给 calculateTotal 函数添加一个注释来解释它的作用。 +* 使用 Edit 工具给 calculateTotal 函数添加注释 * + + +助手没有使用待办事项列表,因为这是一个局限于代码中一个位置的单一、简单任务。添加注释不需要跟踪多个步骤或系统化组织。 + + + + +User: 为我运行 npm install 并告诉我发生了什么。 +Assistant: 我将为你运行 npm install 命令。 + +*执行:npm install* + +命令成功完成。以下是输出: +[npm install 命令的输出] + +所有依赖项已根据你的 package.json 文件安装。 + + +助手没有使用待办事项列表,因为这是一个具有即时结果的单一命令执行。没有多个步骤需要跟踪或组织,使得待办事项列表对于这个简单直接的任务是不必要的。 + + + +## 任务状态和管理 + +1. **任务状态**:使用这些状态来跟踪进度: + - pending:任务尚未开始 + - in_progress:当前正在处理(一次限制为一个任务) + - completed:任务成功完成 + + **重要**:任务描述必须有两种形式: + - content:描述需要做什么的祈使形式(例如"运行测试"、"构建项目") + - activeForm:执行期间显示的现在进行时形式(例如"正在运行测试"、"正在构建项目") + +2. **任务管理**: + - 在工作时实时更新任务状态 + - 完成后立即标记任务为已完成(不要批量完成) + - 任何时候都必须恰好有一个任务处于进行中状态(不能少,也不能多) + - 在开始新任务之前完成当前任务 + - 从列表中完全删除不再相关的任务 + +3. **任务完成要求**: + - 只有在你完全完成任务时才将其标记为已完成 + - 如果你遇到错误、阻碍或无法完成,请将任务保持为进行中 + - 当被阻止时,创建一个新任务描述需要解决的问题 + - 在以下情况下永远不要将任务标记为已完成: + - 测试失败 + - 实现不完整 + - 你遇到了未解决的错误 + - 你找不到必要的文件或依赖项 + +4. **任务分解**: + - 创建具体、可操作的项目 + - 将复杂任务分解为更小、可管理的步骤 + - 使用清晰、描述性的任务名称 + - 始终提供两种形式: + - content:"修复身份验证 bug" + - activeForm:"正在修复身份验证 bug" + +如有疑问,请使用此工具。主动进行任务管理可以确保你成功完成所有要求。 +` +) diff --git a/adk/prebuilt/deep/task_tool.go b/adk/prebuilt/deep/task_tool.go new file mode 100644 index 0000000..5c7e50b --- /dev/null +++ b/adk/prebuilt/deep/task_tool.go @@ -0,0 +1,191 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package deep + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/bytedance/sonic" + "github.com/slongfield/pyfmt" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func typedTaskToolMiddleware[M adk.MessageType]( + ctx context.Context, + taskToolDescriptionGenerator func(ctx context.Context, subAgents []adk.TypedAgent[M]) (string, error), + subAgents []adk.TypedAgent[M], + + withoutGeneralSubAgent bool, + cm model.BaseModel[M], + instruction string, + toolsConfig adk.ToolsConfig, + maxIteration int, + middlewares []adk.AgentMiddleware, + handlers []adk.TypedChatModelAgentMiddleware[M], + modelFailoverConfig *adk.ModelFailoverConfig[M], +) (adk.TypedChatModelAgentMiddleware[M], error) { + t, err := typedNewTaskTool(ctx, taskToolDescriptionGenerator, subAgents, withoutGeneralSubAgent, cm, instruction, toolsConfig, maxIteration, middlewares, handlers, modelFailoverConfig) + if err != nil { + return nil, err + } + prompt := internal.SelectPrompt(internal.I18nPrompts{ + English: taskPrompt, + Chinese: taskPromptChinese, + }) + + return typedBuildAppendPromptTool[M](prompt, t), nil +} + +func typedNewTaskTool[M adk.MessageType]( + ctx context.Context, + taskToolDescriptionGenerator func(ctx context.Context, subAgents []adk.TypedAgent[M]) (string, error), + subAgents []adk.TypedAgent[M], + + withoutGeneralSubAgent bool, + cm model.BaseModel[M], + instruction string, + toolsConfig adk.ToolsConfig, + maxIteration int, + middlewares []adk.AgentMiddleware, + handlers []adk.TypedChatModelAgentMiddleware[M], + modelFailoverConfig *adk.ModelFailoverConfig[M], +) (tool.InvokableTool, error) { + t := &typedTaskTool[M]{ + subAgents: map[string]tool.InvokableTool{}, + subAgentSlice: subAgents, + descGen: typedDefaultTaskToolDescription[M], + } + + if taskToolDescriptionGenerator != nil { + t.descGen = taskToolDescriptionGenerator + } + + if !withoutGeneralSubAgent { + agentDesc := internal.SelectPrompt(internal.I18nPrompts{ + English: generalAgentDescription, + Chinese: generalAgentDescriptionChinese, + }) + generalAgent, err := adk.NewTypedChatModelAgent(ctx, &adk.TypedChatModelAgentConfig[M]{ + Name: generalAgentName, + Description: agentDesc, + Instruction: instruction, + Model: cm, + ToolsConfig: toolsConfig, + MaxIterations: maxIteration, + Middlewares: middlewares, + Handlers: handlers, + GenModelInput: typedGenModelInput[M], + ModelFailoverConfig: modelFailoverConfig, + }) + if err != nil { + return nil, err + } + + it, err := assertAgentTool(adk.NewTypedAgentTool(ctx, adk.TypedAgent[M](generalAgent))) + if err != nil { + return nil, err + } + t.subAgents[generalAgent.Name(ctx)] = it + t.subAgentSlice = append(t.subAgentSlice, generalAgent) + } + + for _, a := range subAgents { + name := a.Name(ctx) + it, err := assertAgentTool(adk.NewTypedAgentTool(ctx, a)) + if err != nil { + return nil, err + } + t.subAgents[name] = it + } + + return t, nil +} + +type typedTaskTool[M adk.MessageType] struct { + subAgents map[string]tool.InvokableTool + subAgentSlice []adk.TypedAgent[M] + descGen func(ctx context.Context, subAgents []adk.TypedAgent[M]) (string, error) +} + +func (t *typedTaskTool[M]) Info(ctx context.Context) (*schema.ToolInfo, error) { + desc, err := t.descGen(ctx, t.subAgentSlice) + if err != nil { + return nil, err + } + return &schema.ToolInfo{ + Name: taskToolName, + Desc: desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "subagent_type": { + Type: schema.String, + }, + "description": { + Type: schema.String, + }, + }), + }, nil +} + +type taskToolArgument struct { + SubagentType string `json:"subagent_type"` + Description string `json:"description"` +} + +func (t *typedTaskTool[M]) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + input := &taskToolArgument{} + err := json.Unmarshal([]byte(argumentsInJSON), input) + if err != nil { + return "", fmt.Errorf("failed to unmarshal task tool input json: %w", err) + } + a, ok := t.subAgents[input.SubagentType] + if !ok { + return "", fmt.Errorf("subagent type %s not found", input.SubagentType) + } + + params, err := sonic.MarshalString(map[string]string{ + "request": input.Description, + }) + if err != nil { + return "", err + } + + return a.InvokableRun(ctx, params, opts...) +} + +func typedDefaultTaskToolDescription[M adk.MessageType](ctx context.Context, subAgents []adk.TypedAgent[M]) (string, error) { + subAgentsDescBuilder := strings.Builder{} + for _, a := range subAgents { + name := a.Name(ctx) + desc := a.Description(ctx) + subAgentsDescBuilder.WriteString(fmt.Sprintf("- %s: %s\n", name, desc)) + } + toolDesc := internal.SelectPrompt(internal.I18nPrompts{ + English: taskToolDescription, + Chinese: taskToolDescriptionChinese, + }) + return pyfmt.Fmt(toolDesc, map[string]any{ + "other_agents": subAgentsDescBuilder.String(), + }) +} diff --git a/adk/prebuilt/deep/task_tool_test.go b/adk/prebuilt/deep/task_tool_test.go new file mode 100644 index 0000000..44fa80b --- /dev/null +++ b/adk/prebuilt/deep/task_tool_test.go @@ -0,0 +1,78 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package deep + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +func TestTaskTool(t *testing.T) { + a1 := &myAgent{name: "1", desc: "desc of my agent 1"} + a2 := &myAgent{name: "2", desc: "desc of my agent 2"} + ctx := context.Background() + tt, err := typedNewTaskTool( + ctx, + nil, + []adk.Agent{a1, a2}, + true, + nil, + "", + adk.ToolsConfig{}, + 10, + nil, + nil, + nil, + ) + assert.NoError(t, err) + + info, err := tt.Info(ctx) + assert.NoError(t, err) + assert.Contains(t, info.Desc, "desc of my agent 1") + + result, err := tt.InvokableRun(ctx, `{"subagent_type":"1"}`) + assert.NoError(t, err) + assert.Equal(t, "desc of my agent 1", result) + result, err = tt.InvokableRun(ctx, `{"subagent_type":"2"}`) + assert.NoError(t, err) + assert.Equal(t, "desc of my agent 2", result) +} + +type myAgent struct { + name string + desc string +} + +func (m *myAgent) Name(_ context.Context) string { + return m.name +} + +func (m *myAgent) Description(_ context.Context) string { + return m.desc +} + +func (m *myAgent) Run(_ context.Context, _ *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + gen.Send(adk.EventFromMessage(schema.UserMessage(m.desc), nil, schema.User, "")) + gen.Close() + return iter +} diff --git a/adk/prebuilt/deep/testdata/_gen/generate_test.go b/adk/prebuilt/deep/testdata/_gen/generate_test.go new file mode 100644 index 0000000..ce3cb17 --- /dev/null +++ b/adk/prebuilt/deep/testdata/_gen/generate_test.go @@ -0,0 +1,176 @@ +//go:build gencheckpoints + +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package _gen + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/prebuilt/deep" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type checkpointStore struct { + data map[string][]byte +} + +func (s *checkpointStore) Set(_ context.Context, key string, value []byte) error { + if s.data == nil { + s.data = map[string][]byte{} + } + s.data[key] = append([]byte(nil), value...) + return nil +} + +func (s *checkpointStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.data[key] + if !ok { + return nil, false, nil + } + return append([]byte(nil), v...), true, nil +} + +type interruptTool struct { + name string +} + +func (t *interruptTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "interrupt tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "action": {Type: schema.String}, + }), + }, nil +} + +func (t *interruptTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + wasInterrupted, _, _ := tool.GetInterruptState[string](ctx) + if !wasInterrupted { + return "", tool.StatefulInterrupt(ctx, "needs approval", argumentsInJSON) + } + return "resumed", nil +} + +type scriptedModel struct { + next func() (*schema.Message, error) +} + +func (m *scriptedModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return m.next() +} + +func (m *scriptedModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("stream not supported") +} + +func TestGenerateV084CheckpointData(t *testing.T) { + if os.Getenv("EINO_UPDATE_CHECKPOINT_FIXTURES") != "1" { + t.Skip("set EINO_UPDATE_CHECKPOINT_FIXTURES=1 to generate checkpoint fixtures") + } + + ctx := context.Background() + + interruptToolName := "interrupt_in_subagent_tool" + subTool := &interruptTool{name: interruptToolName} + + deepCalls := 0 + deepModel := &scriptedModel{ + next: func() (*schema.Message, error) { + deepCalls++ + if deepCalls == 1 { + c := schema.ToolCall{ID: "id-1", Type: "function"} + c.Function.Name = "task" + c.Function.Arguments = `{"subagent_type":"sub_chatmodel_agent","description":"from_parent"}` + return schema.AssistantMessage("", []schema.ToolCall{c}), nil + } + return schema.AssistantMessage("deep done", nil), nil + }, + } + + subCalls := 0 + subModel := &scriptedModel{ + next: func() (*schema.Message, error) { + subCalls++ + if subCalls == 1 { + c := schema.ToolCall{ID: "id-2", Type: "function"} + c.Function.Name = interruptToolName + c.Function.Arguments = `{"action":"interrupt"}` + return schema.AssistantMessage("", []schema.ToolCall{c}), nil + } + return schema.AssistantMessage("sub done", nil), nil + }, + } + + subAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "sub_chatmodel_agent", + Description: "sub agent", + Model: subModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{subTool}, + }, + }, + MaxIterations: 4, + }) + require.NoError(t, err) + + deepAgent, err := deep.New(ctx, &deep.Config{ + Name: "deep", + Description: "deep agent", + ChatModel: deepModel, + SubAgents: []adk.Agent{subAgent}, + MaxIteration: 4, + WithoutWriteTodos: true, + WithoutGeneralSubAgent: true, + }) + require.NoError(t, err) + + store := &checkpointStore{data: map[string][]byte{}} + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: deepAgent, + CheckPointStore: store, + }) + + checkpointID := "checkpoint_gen_v0_8_4" + it := runner.Query(ctx, "input", adk.WithCheckPointID(checkpointID)) + for { + ev, ok := it.Next() + if !ok { + break + } + require.NoError(t, ev.Err) + } + + data, ok := store.data[checkpointID] + require.True(t, ok) + require.NotEmpty(t, data) + + outPath := filepath.Clean(filepath.Join("..", "checkpoint_data_v0.8.4.bin")) + require.NoError(t, os.WriteFile(outPath, data, 0o644)) +} diff --git a/adk/prebuilt/deep/testdata/checkpoint_data_v0.7.37.bin b/adk/prebuilt/deep/testdata/checkpoint_data_v0.7.37.bin new file mode 100644 index 0000000000000000000000000000000000000000..a60cb76e13856f99be797edb0f814a24dd3efa6c GIT binary patch literal 179670 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 z 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 -sc z2w@`N;2$67Yr8lMTq52H&ZY$`3>l9w)H*^_O<_I(Zu*coJgnI?V#3}1Xs+B9! ze#G&Y2}O!&0plR55k%`YvQZ6YaW7=Sd8r=vIM$1qqbUKmskO zRslN3Sv~~*0^?xmcuk`o5UB(jRirDT?_YF;pf@-oI(>%qzy%oquE1bNvld$Tg9F%l z6%0pTWGDKS4xHwSk^C7VKbUUaF@^>-J~^O-Ud6>V!zD+7r?Ng3+bo z@d#KD*ipnCh&VV!@Fl0X!j}lFFjxkstmA##C9FZ8?n+4mH%7TrKGsvM$THybs9OXZ zAijV~I6xmA%l5%4JK zRk4h##LgO?6Ei5#A0qJBN4!wrqCFefIQ#-Q17labaH4KpzQ`I;`8DMu8|}H%0fgQ&jIZrVh*&f8lzKPN{yPtM1jqN zOqK{AhivZUWs|^#!vf@Vkt#+Py=p=*Fo+1GCAd+*1E?EosR@WIF%>zFENh%8age@+ zSjgZtH$3dptKje>;gf|OTu@@DAbjQE8sRAfGC&t(DIR7uO^yhJU1%=hQadF+XrkZc z$S4BRL)Z%Xj~#$=VIbmGxMq+{fXb~eXhU=_h#(P~LdMfeXQb3*mY$BbkJaHuL0BP9 zUwSa%U7%DX&XO!l;x}CfW{gQ@9GC;h->}Bn&>_;uTPZ0$~)U)|Z=}$Ga$I zP$`4PFrT7*fck?n@RfoFLQ)1(nHEFi*&;R+ArCE^2a3ch2yz+w5ke8b1=NAK5$3`{ zdoC1(lmLE}=(y(;DgYzVxcKVO#tC4RIP7 zlQit?u)S(j1_kXG>!@IMmU8LyHP59>19=Vng{~1DTm>L6Re;byo^s_nph3IQM1sjf z3TYzArbag;IS`_xgj@;RjZlyTNXRmzD`4n^aSYl2PRa&w5>f}~Ct$$0m?>Sf`O4S-bHK2_`H?@3;~X&RgD2~nH3k1;!Amr7zB&E_7tHt@3G`c zp9ZHJi!<=RUFwYLeAu+)I(}$2An?S?i`IaPoq840gh!GJ^ob>KY(CZ%C|=3ROKuVB zAMgch*T@~u|&k!TLOf*WUVGjBo24Kd~+N0sIVJwo30X8ltv2wZtrd--mjDWJzxKo#B z{WCEr@kERSHWY^?R-|b8plT&Q1GqE*Mh$)lX_ZxMd#xXOu*NP=*dxlt*?c#BLe(_2n*jARbF0;JSpu37Ii0zb{gu%ey zH#8xgvPw*}mxY^UV0jJj0JSR98H1-?C#wV)4IMEUd8O8@F|qL{HUHgq7yG#h9`6U!Ghb4?=oo=js zYVoS$`KRcHX21%d0TPiUEC)CvK62{ZIs9D1JuRJT;WF(f`j(LiI1m(>g!yhwg56>q zY(URL78+KgK(jTZh;D&7W}rtyN(Yf>RwJR8(JIlrN%o6W5Bp(_gc(c=$x3>!0T7pf ztm}V|i6CG=m%xBT-AO7YEtQ2oAbvaw2Ec)Ww-8vHflH5&)_`wHDa(MKTbq#zWJjsH z5$VK8RXr@22c3MZ!-CGI_6B&hZw*Vkwp@V>hId$eCLxv}f+4VWh|A4n$eTQQf9J%2f!aIZa(p&8sdiON1}2d zq>vvYsV6?KV&& za4Oo~K*GT$yKUbiEC(=v)th>;gmSo!71+Z7!gzTk(lP1AcYFY}UA8K*aK`x8q~_pu z!-bJ%1t;+Oi2kthSOaEOaJTt1Ne{->$zX$Tv{X%&5(qIww1~U{wMf_@J!jRcL~gM% z!ZZ9w_#;7@LH3J8PjS^Dxw7f0My5sZYSxwTfPRXElSrfGWi&vpVcmp!f6=)P%kKZ38SrkS;`09Rm&zLi{CrEZa8;S4kSB zXGpE%)$y^$gj|~zV^jq!an}Jq@M%)E@Rmmm96{`K00gA?MaVI!X?O;fN?V2=$j3#w z>{dy{Bs|jAO4xPTYDb_3xuf66Y>^xgU1T5yW{Z$c+qFm%rptiuS6ISjv;n^G(ikwP zf14cOz##vA@$vZutP|*ypk72ES!-&?Ge!a?61ZIjm@Ua}MbMMi9pX_8JVI8MqA+#U zfs(M?8ie)wwgV!Y?~6~qK-8lh0*?dKObwxbkmWu;dVSQu)B>5#aBL(+N6bG)QIJ{! z76^>CdH{4z%n$h(s?pgl)O2JFD)by#-P=Cv68 zh?P6RIT#d5=fFLxgZ2!pW&A>HHcLGU7JAOPc13fACt(oMmS~nsh#IXowmvc<1CTNa zq2OJ>`D66RG(iptDCi)A3>^(KsP$FYiP=yA6u}{NEb;l zkhCO-le`Xfg18}6Y&OXgxOhfh5a!UACmK6LvPpD+MBftoG`@rp0QLf0FxDpP%zy9+ zLlCSIpAcS)?*k-EZM`NE*BXm>bE1#n9so^S#%gT^jGzzNo#t=GQxti`n1tMdo+7fg zPLut7j!e{asr^1}O@i*oU(kI@B&*o2!4$bsA?s#KY}S@)#x>{{q?p*J=q4`W)AECa z$BXr5tsuQqz-MBz@KnZ#N#B4el1ZlSTun@Q^s^6Sgj&v;6M2L9?)3FN9MRR+^bu8h zkPL(ZhtDO81e{NiXmrXx z9hYDSSesLt??Nyp z(n~@oi`gJC5qqJX$_Ds6RI#cJO^Eim0xbn}&m~tHQ90T+JS18S25F|xEY}g8gDJ6z zqfJ<%Rw9OY50P67a3BScsV0#(2BY3R?}k0gX$BQ7j*Ne)$@H1wU;-HSXphk@V4j8I zSoA852p4enBm^+$N!Cg;kztH~Y>CaFdZaP3@WX;)hD>Ujm5}laYb1;5n4&7lq|At* zVG26Ac0kgJVo4FO($t$l&vVe2BxR(;i&bMp{DG8Zw&xIw^j)?M;w-=tufj=6kwWKU z9Yo8`EYV!kenfE10bo_vz0!o1Lidjyde{UWV!tdY>s161fRw{i_A<>Ab{txFqq=d8V)2^6$#xmu=;W|%v>`b zWC0+G0mthm#a>?ueKny&*IgJ!{OU^I2*1l9iPq5edP=g3RI1b|>-*f7*F*5;t1=p8 zwhmVgAc|{2Vnz$nd^z=-bdXx&sF4{U!a>bcNF~|mo<_P{EFSx?&)^u_(W32UM5gev zw$!AcLyEh;<^;~uGzx==Y0olFg27fJtc`R9{+V=Y4}M0_#nZrUnDO}qt!vS4wdGag zNc3_01(?|OkXRE+E-?>)g&_lNpLjz71dai-$biwf%S=Oa8zQ$n3(*4RX=wtf&kQIK zpARh8taB5n;}J2-l?n_QZW*kHMs4v-|4f>Y5-x#b5=E~-p}|4dz`QXBypp_dA65;3 zfsBv5L~N0yg~7-IL|Y?myae4G_O2M3q(sJWqbb&7VfY%-3VAJB)L*Zam>cjOaTjF4 z$N9f~b-JE7$M>&f- zJU`Qvev0dC=wse6Gx;TF^RA!jyM_5WE&bF{V(V7(vTsT`fKGC4UopwwofP-qX|)l= zzK)O&8QL9Gmk^mnPSzx*fT#oRDdwZ<85l748-v2J=t*cls9^#zMwzN(rs^2X8<2=E zLn@kvEz_+F^?gWx`NFD>eO{dSGxfO@aWjPt$Qa1r(Pg1Jt34o+N&sQ?0V}NDcRdt` zks5W7^iZXn0lv6?2BspV-+aszs`~=0qqZU0-9+9kX!_zz8ftN-eXy1^=<(6 z@HAXi5hUrJ&`$!DO%@;_qYp0rfFGLv*vA#$5x_6hx8M^uAm&fsice2K&vp6Jx8npK zg+-G;d?yZYL47l(IhvZLZJ-{3T;mHgL7a2kdmbH=D^;HJ;_StlcqQp{zfOxnn1<&1 zOn(?h{$4ur@4;Ez79BU=CjI++acb+Fc@H0zQ11h+h&oQ2b^#JJ@x(!Ml(TtAQF{IF%cN&N^Win}kQA296#()`}E z3oz{hejPW(v6F3g9FcQ-0aT|h{0 z^2MtlGn@^@U7&#{(7(wkT^VAB9Y64-N0F|celHX!EUP~hccT4xDDK63^XhMDZk84K zd7=J}#N7S-O+;ba#m!z#HaD)8Kc)T&V;0Fky2)}?hUmsre1(%mD)L+6(%Bc<)T(=?A zU$-rHibH6*Ce%afPqD^>$$*JgfEtnK;ff>8;YjjpB--OxKZUhDp#B`^b`0lsqtnW_ zr8(WG`U`CJj#Z$FU_k%_9L~eRnMNP#d;eE>>u>_g_<2ASX0%6PFJ{3Rk>-+)=E$^; z2A+kXLPyOAs=<&C#XT6N4|z0cKK~* zy`2hyef_GYfb9n|PbLz9T5Qx5@Czs$7T)<3R7k)F+9_M&52WnmsJTjNYk2&l3!%6V zo4k+R0Sv|THGb6fX5iyP{m)R?1iaNZw`*Y@uoC`9Nly9*%Air<-3Km$KU-I$-fhSK`PuGk3*wp5x)H=i25gVq!e6rkI8!buaF#L zNmJ}67v3}lcXB}%)WgspXV0_lMIU35i5g)#nfM}fGod~T+5zgN;|AhIVU_JF+IMLZ z9ucvW0;q*pqlU)nv#Glk7&Ww|$Pp~k>dTn=e$dcZ&EqT2_7!{OO*|7YJ2iU%;_JvHr2d;-~bkgQh^e3Fg@ zkZ<#u?TvU6wVY1u1^Nm8DiQT{Cj=xJLGm`usf|}^l}l{jMt(3;hS!wgMNGL-2TU1W ziqp{6nU4^U>zPHYuqnffY=~L|*;G;KTXB2p3msacXzcS$sve|&txC^*hTTdL&NkzoY4V@a7l60_#)~I$?Bp*kx8jI zM9Pv6#TS#v$Tm-Uh)v&#&xAcHp?EcQ>Ciz}p*Rfzp1mu5Bmh1iic=V1l@o@a+M86@YimDJ}s3W#idKHT~|L-ldEeM+j6ODN86?A$r7z7iJQeF#k8vN=M1 z73|U+O?L*qhWk?;THyWR7|o&8TgK@9F!rCetZ?S7fPngx=mB7R>j2VXR@~77Fe1;2 zK@`dP4FE;d%|yKbcfhL9ua?zW9C@wd&v7Qw`KZt!DL^OMziPL0AEJ-2lWQ*ysKMW0 zhP1;+97TGDE}zxr!lCaeIWeB8<#1fvQDad3Ux4VIG>FKt$;z{*_ zdXA7|t!w3mV13WJ%2cnlCl%%1`m^IWTttp}PAI;Ft^z^XPtakiUSdqtNEnJi(w8T8w`R#i6)AQS>Y@^I&U&iw^vqZ^6B~Uwt0Oa3pmM zF)Cy5enRGKC=TM0FG@WU#}3laaD%`P0}RH5gvc24E~TKB)1Sn3$#zR_ZBpaTgyH}$ z%$-Rj4~>wD(xIpDXqRO@6AF001fycx13=zQ;<7?=jzxi4q3FfmS`ZAyK2Q^mISNG| zzE1ACWsQYm52iZYT8lQ7@>ne^^BNm;D0Sm$qa?2l<0$a!d} zkG2H$uLw!BF01NYdmsbsK?$v6T8UZjXGjzq(63ux#WA@yh}7JTK~5%zMh7RE92#hH zZBaA@`L58=aCiEBNFO&5DK%9^!URUzXvVn4)woalS7g;B=oU7KZI!f0rh|I z?a`E_ZX3N7TQdHd><)@jdN~s_c3*nIc0|vG-S_Sgrv#;z=y)m096{PhM~1B(ZrAF_ zIC`tuMmsWx?_O(oXNYNabo8e9jq$CmP74XeP4unxTvDGatW*2urMuav#0^L~6}ib- zR!1}geQP-cq5p)nVJL^_?r5l8OMW`?MC>$2gkj}%uDpbx{U{`+oBlqdBNq1s?~NBD&6+kunu#<09wi##Dmx~C(~Z-l;`fc~3B z6Sgj44wyY5i@9N0$cz?plbO669?eV?%j22+*u>cINHIH>m18KcbyMBH9bH|+3T^-y z#TD-WA5t~@?x|bG4PRe++PLAVJ_nNdj$J^zcW*xGn10z=LGy(Ym%%V@c;kj&8)%own zf*X1;YaJk+T4Xzn54kZuiGKsLyoY==Yg1pN3ap>wI5yqbf~y3@DX!%3*>cvaWl~&s zcZ#cV!|xD(Xs6`I_+Da<^-XqR?@CsVpq5S&OpO~pH#srcQIyc_>_kA36x14#jYYI} z4b-PETiaYn(7Nfz)>$AGLamL55+yoPnY_pgK>Kb7J(Zt*BbUeNCvV$*>aEE5J)C;Z zcqomB(p_9H%QsV`BZTz4DS*9Dpt=W{%Bnq2EVF(ms>I17?jV4i8 zm#IZtfv7~6LZ^WJNedOBekNU|OPEAqYTLUeTiPTF8xJKU@!vUhx^E02$7oYF)#lP{0wGKKMp$xLZ7S16C?i{zr- zz11fsb7T4OT%nj5aq<(HeBQzC^J6kIIzCw(9(M|px$Gz!Tx^;LeR3j~m-*c2cxEzN zDrfS;xzS8vtT>#R7%q+!Muz2ZVRQs7*EbzxRV9U!ljRYaEtRquBtM$T=L(aViBdM3 z87&qjh6_14F;X7w-rAUJf)fpQ+M#7EPL{{A<*dw3W^xnd5>8)EW(o!1R$+29Kawqu zXPr^m^Ab;g9qqZJAd8z_7662`lUr(abY-?$pz3og>l72prW-hE8^rXp0|@ViHvm1B zf|Z6u$EJ0{y$1#dO42WSl}6Onsf@l~iw(!aYuwkVRbwbiAur>($xWKPS9br@|!N2-|6;|)N9*Vu3#gJ1jBWEJ}UC5{GBFG)&?Zq0UA}la4yG=W|Ij*so3ZaKaB!sPXeK7tVy@2qx}B zz|*vgNSEu>%m&l!fj0_0V|5&ID2}0`CRTCT36_QWG(OCu`6>Ek;o;*L<>62qrBS$l zQRITm%-PB@}ze~LDzk5lM`i4)k(aRRSW|A-KXgHJt8 zJtQHfQKNen)6_p$)=~BMJ;=~bzQ$M9`B2;uNquQS4pLw56Hc{B#nRt`l7rmAlDZe( zjdqF`oVrZDcohaY8;ZLy?Fj^K(!4B^^8-(MXoDv`{az?eSXO^1?(Ctai1*^X8&OYt z$9cU?GzLXuY)kv9mow=+ABubEqEy?FAFQbDa`ZxggJubgr;Bk~yA^n6wG~5bttG0AID8n z}*@Wf4-T2GmRTDvq{eLJFsjo!2?^ro;cfVO}RYU}vs#5U*BLxCn_Q6os~s-|t& z$<69&4GIv7&txC^*hTTdL&NkzoLS@wPH;&r7MGo1u?Ew1b&M>Rdz%1 z#UwIL*nU$s61nc~k0XM}mZS$GVNwPo_holOUHMR8jg z`%_rearKq3@9l$qFG`}qgkX!Kp*RDl#Qmv)MQ4j{t>OA|X!ZIrdOs#PZCT;WTLA&} zDbWMK#vRD+XaU%hPBB2CEL8qc-vCfVn^rHt9k43&tHo2rk=Hu@oP_hfe!k+#Vo+Tn zG5kdPS5)qD+NoA&&l7t-L?0iM^{FDo^I@xhgBkd|E@^pO9G#Oj2ehcZGz;K5`Z2gA z@O^rwmfK=&tBfu6We&xYJEZ^Zo>aJT>(7inrRagyKu+ zDsYX_1RbX8CB{UJgrOKD?P-5}FgS!?w#31gRGw5EY)x>{fzSFaz~KAU=Ygq5QkWW} zvdIVB1^IyX`+-SKv0cod708>5dWOcmKyR}S)uk-?Hlh(c_o`DxTTzfP(NS`-*tMA$ zNfpF3@F72ON1@Ol?pw;SXi=rO_UBE`C+|EMJ)(v$6M}XDIXmc=DRy`wqu4-1ctJf! zL40dN5pIZ-*t4$ju@_+v9p#TfhTB<44%P_zWx4^HZh$S_!gK?yl9pGm%c^?U9`JWl zYjb5Y702Y-?9yBtlS5;ImmnS(Z-wzz7;lC86&SN%-}4)=6-wi+sQcPK3X|P$kOjne zE8K$E2W#ddD*@T$e+QIvOU7Rlr9@FmFK1%L?n^J&j_5hl4e*9Go$JUrdaKz+J2Hpw zUTgTCjx4U7l?dtF%&c9erW+t!yCIyv)-$0%Fo|~a>L43d=jb=G!^VMGtE0<8@`pu9 z@}cO(-#Wf5B<`*Q%tFzJuM{miu$YUKpUtv5q8W*!>4ddmCD>3C2PIbp8WHC1^3z^YEZZeaX!>IgLERSdM zV-sV;BgO1kR*v25>U$kIr41{f5Yp}K@h))1JHUrj&Axl;mT|)yH@tDfbELItd5M7J ztwkCeH@tDfo20=F%{uCRiB(6Lq(SKl^&xWG#aWTgO$?K5SWUQa^%;;$@^C_%O1gH#XBLQ$xfg4H`GR zal`j;b|fWq?Tt#s7&m;E`XsoW=+cR9dy^`GZ-)ni(+(+xaMq`4?9&cH=gB1!*BivE z#tm;;URu^hxeip}F$syrht9m6#&&=6`((YcEwUXVrg6hVZ>9uX#5OjoRB!f>&eaAR zxJ#)$RAQNP9D7JdpiFV9h;k+r2O#0!nMn9FLP~n4r;Hn(%0{i$tdKRozdbC06)TP@ zXemOg-wI93OVjc)E*D0Hle;xIwmIq5xZ!tbQ!(R)-)1+wc%`?zgxq61lrP>$>OS>W zR1P_usvJ^)q$AYZNJv7I;RdyF&Z)1)YxkyJOG@Jumu2xv!>v%crmvw<)c@b!xurA= zL}56GR=j`+(hb=6K$0dlK@fZp*I}AOgi>F+fNny!;j^eKa1(;yGBW;?X|7Gi3Kg-O z*EG|X2_!S;%m1Cl5B&ZcX@BtBO#-gOf_lpBV-1uvra|=Xu>?9)$KXnmSz8hhvLIb) z76Mlgy{QQ9(i7DFU0F!C7tbQi$ApM}E*mPfL&c#!$|ARO&G`Mo=%hKR*cEHe`SyQy zi4Ok13yBFO6G|qOOekxfRhch-tPsEqHI_!#?j11`N|Fq;dxFN|3G;ef(~OqA|8(P? zSH>+b$cygx&crLmvDTD>!f-%NV`)oN;!+L@-=SwB6UtA#@d3DaDLznCW9BK*E>9Y*#bww+yvQ%q7`NLbe3i*0{il-H#p`*T@7%B0on zqH`vcOekpwYwM#DvZ{rAmQU_p$)BdQTOU@5GItMSLTN^U6DDJK;sk!MwVnb_hvhfy zl{@%`v1;Rqv6b;pCX`Gl*ONHlZK78AdSTGHlhO$uSpzJ#WXJhtv$dftK-HQw7{)07R6x=t9Feq`IGHwk?!bYg#$c!u!>M!WuE literal 0 HcmV?d00001 diff --git a/adk/prebuilt/deep/types.go b/adk/prebuilt/deep/types.go new file mode 100644 index 0000000..781418b --- /dev/null +++ b/adk/prebuilt/deep/types.go @@ -0,0 +1,65 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package deep + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/tool" +) + +const ( + generalAgentName = "general-purpose" + taskToolName = "task" +) + +const ( + SessionKeyTodos = "deep_agent_session_key_todos" +) + +func assertAgentTool(t tool.BaseTool) (tool.InvokableTool, error) { + it, ok := t.(tool.InvokableTool) + if !ok { + return nil, fmt.Errorf("failed to assert agent tool type: %T", t) + } + return it, nil +} + +func typedBuildAppendPromptTool[M adk.MessageType](prompt string, t tool.BaseTool) adk.TypedChatModelAgentMiddleware[M] { + return &typedAppendPromptTool[M]{ + TypedBaseChatModelAgentMiddleware: &adk.TypedBaseChatModelAgentMiddleware[M]{}, + t: t, + prompt: prompt, + } +} + +type typedAppendPromptTool[M adk.MessageType] struct { + *adk.TypedBaseChatModelAgentMiddleware[M] + t tool.BaseTool + prompt string +} + +func (w *typedAppendPromptTool[M]) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) (context.Context, *adk.ChatModelAgentContext, error) { + nRunCtx := *runCtx + nRunCtx.Instruction += w.prompt + if w.t != nil { + nRunCtx.Tools = append(nRunCtx.Tools, w.t) + } + return ctx, &nRunCtx, nil +} diff --git a/adk/prebuilt/integration_test.go b/adk/prebuilt/integration_test.go new file mode 100644 index 0000000..0ae9181 --- /dev/null +++ b/adk/prebuilt/integration_test.go @@ -0,0 +1,455 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prebuilt + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/prebuilt/planexecute" + "github.com/cloudwego/eino/adk/prebuilt/supervisor" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type approvalInfo struct { + ToolName string + ArgumentsInJSON string + ToolCallID string +} + +func (ai *approvalInfo) String() string { + return fmt.Sprintf("tool '%s' interrupted with arguments '%s', waiting for approval", + ai.ToolName, ai.ArgumentsInJSON) +} + +type approvalResult struct { + Approved bool + DisapproveReason *string +} + +func init() { + schema.Register[*approvalInfo]() + schema.Register[*approvalResult]() +} + +type approvableTool struct { + name string + t *testing.T +} + +func (m *approvableTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: m.name, + Desc: "A tool that requires approval before execution", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "action": {Type: schema.String, Desc: "The action to perform"}, + }), + }, nil +} + +func (m *approvableTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + wasInterrupted, _, storedArguments := tool.GetInterruptState[string](ctx) + if !wasInterrupted { + return "", tool.StatefulInterrupt(ctx, &approvalInfo{ + ToolName: m.name, + ArgumentsInJSON: argumentsInJSON, + ToolCallID: compose.GetToolCallID(ctx), + }, argumentsInJSON) + } + + isResumeTarget, hasData, data := tool.GetResumeContext[*approvalResult](ctx) + if !isResumeTarget { + return "", tool.StatefulInterrupt(ctx, &approvalInfo{ + ToolName: m.name, + ArgumentsInJSON: storedArguments, + ToolCallID: compose.GetToolCallID(ctx), + }, storedArguments) + } + + if !hasData { + return "", fmt.Errorf("tool '%s' resumed with no data", m.name) + } + + if data.Approved { + return fmt.Sprintf("Tool '%s' executed successfully with args: %s", m.name, storedArguments), nil + } + + if data.DisapproveReason != nil { + return fmt.Sprintf("Tool '%s' disapproved, reason: %s", m.name, *data.DisapproveReason), nil + } + + return fmt.Sprintf("Tool '%s' disapproved", m.name), nil +} + +type integrationCheckpointStore struct { + data map[string][]byte +} + +func newIntegrationCheckpointStore() *integrationCheckpointStore { + return &integrationCheckpointStore{data: make(map[string][]byte)} +} + +func (s *integrationCheckpointStore) Set(_ context.Context, key string, value []byte) error { + s.data[key] = value + return nil +} + +func (s *integrationCheckpointStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.data[key] + return v, ok, nil +} + +type defaultPlan struct { + Steps []string `json:"steps"` +} + +func (p *defaultPlan) FirstStep() string { + if len(p.Steps) == 0 { + return "" + } + return p.Steps[0] +} + +func (p *defaultPlan) MarshalJSON() ([]byte, error) { + type planTyp defaultPlan + return sonic.Marshal((*planTyp)(p)) +} + +func (p *defaultPlan) UnmarshalJSON(bytes []byte) error { + type planTyp defaultPlan + return sonic.Unmarshal(bytes, (*planTyp)(p)) +} + +type namedAgent struct { + adk.ResumableAgent + name string + description string +} + +func (n *namedAgent) Name(_ context.Context) string { + return n.name +} + +func (n *namedAgent) Description(_ context.Context) string { + return n.description +} + +func formatRunPath(runPath []adk.RunStep) string { + if len(runPath) == 0 { + return "[]" + } + var parts []string + for _, step := range runPath { + parts = append(parts, step.String()) + } + return "[" + strings.Join(parts, " -> ") + "]" +} + +func formatAgentEventIntegration(event *adk.AgentEvent) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("{AgentName: %q, RunPath: %s", event.AgentName, formatRunPath(event.RunPath))) + if event.Output != nil { + if event.Output.MessageOutput != nil && event.Output.MessageOutput.Message != nil { + msg := event.Output.MessageOutput.Message + sb.WriteString(fmt.Sprintf(", Output.Message: {Role: %q, Content: %q}", msg.Role, msg.Content)) + } + } + if event.Action != nil { + if event.Action.Interrupted != nil { + sb.WriteString(fmt.Sprintf(", Action.Interrupted: {%d contexts}", len(event.Action.Interrupted.InterruptContexts))) + } + if event.Action.BreakLoop != nil { + sb.WriteString(fmt.Sprintf(", Action.BreakLoop: {From: %q, Done: %v}", event.Action.BreakLoop.From, event.Action.BreakLoop.Done)) + } + if event.Action.TransferToAgent != nil { + sb.WriteString(fmt.Sprintf(", Action.TransferToAgent: {Dest: %q}", event.Action.TransferToAgent.DestAgentName)) + } + } + if event.Err != nil { + sb.WriteString(fmt.Sprintf(", Err: %v", event.Err)) + } + sb.WriteString("}") + return sb.String() +} + +func TestSupervisorWithPlanExecuteInterruptResume(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSupervisorModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockPlannerModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockExecutorModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockReplannerModel := mockModel.NewMockToolCallingChatModel(ctrl) + + budgetTool := &approvableTool{name: "allocate_budget", t: t} + + plan := &defaultPlan{Steps: []string{"Allocate budget for the project", "Complete task"}} + userInput := []adk.Message{schema.UserMessage("Set up a new project with budget allocation")} + + plannerModelWithTools := mockModel.NewMockToolCallingChatModel(ctrl) + mockPlannerModel.EXPECT().WithTools(gomock.Any()).Return(plannerModelWithTools, nil).AnyTimes() + + planJSON, _ := sonic.MarshalString(plan) + plannerResponse := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "plan_call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "plan", + Arguments: planJSON, + }, + }, + }) + plannerModelWithTools.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input []*schema.Message, opts ...interface{}) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + go func() { + defer sw.Close() + sw.Send(plannerResponse, nil) + }() + return sr, nil + }, + ).Times(1) + + mockExecutorModel.EXPECT().WithTools(gomock.Any()).Return(mockExecutorModel, nil).AnyTimes() + + toolCallMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_budget_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "allocate_budget", + Arguments: `{"action": "allocate $50000 for project"}`, + }, + }, + }) + mockExecutorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(toolCallMsg, nil).Times(1) + + completionMsg := schema.AssistantMessage("Budget allocated successfully", nil) + mockExecutorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(completionMsg, nil).AnyTimes() + + replannerModelWithTools := mockModel.NewMockToolCallingChatModel(ctrl) + mockReplannerModel.EXPECT().WithTools(gomock.Any()).Return(replannerModelWithTools, nil).AnyTimes() + + respondResponse := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "respond_call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "respond", + Arguments: `{"response":"Project setup completed with budget allocation"}`, + }, + }, + }) + replannerModelWithTools.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input []*schema.Message, opts ...interface{}) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + go func() { + defer sw.Close() + sw.Send(respondResponse, nil) + }() + return sr, nil + }, + ).AnyTimes() + + plannerAgent, err := planexecute.NewPlanner(ctx, &planexecute.PlannerConfig{ + ToolCallingChatModel: mockPlannerModel, + }) + assert.NoError(t, err) + + executorAgent, err := planexecute.NewExecutor(ctx, &planexecute.ExecutorConfig{ + Model: mockExecutorModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{budgetTool}, + }, + }, + }) + assert.NoError(t, err) + + replannerAgent, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{ + ChatModel: mockReplannerModel, + }) + assert.NoError(t, err) + + planExecuteAgent, err := planexecute.New(ctx, &planexecute.Config{ + Planner: plannerAgent, + Executor: executorAgent, + Replanner: replannerAgent, + MaxIterations: 10, + }) + assert.NoError(t, err) + + projectAgent := &namedAgent{ + ResumableAgent: planExecuteAgent, + name: "project_execution_agent", + description: "the agent responsible for complex project execution tasks", + } + + var pa adk.Agent = projectAgent + + _, ok := pa.(adk.ResumableAgent) + assert.True(t, ok) + + mockSupervisorModel.EXPECT().WithTools(gomock.Any()).Return(mockSupervisorModel, nil).AnyTimes() + + transferToProjectMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "transfer_call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "transfer_to_agent", + Arguments: `{"agent_name":"project_execution_agent"}`, + }, + }, + }) + mockSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(transferToProjectMsg, nil).Times(1) + + finalSupervisorMsg := schema.AssistantMessage("Project setup completed successfully with budget allocation approved.", nil) + mockSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(finalSupervisorMsg, nil).AnyTimes() + + supervisorChatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "project_manager", + Description: "the supervisor agent responsible for coordinating project management tasks", + Instruction: "You are a project manager supervisor. Delegate complex project tasks to project_execution_agent.", + Model: mockSupervisorModel, + Exit: &adk.ExitTool{}, + }) + assert.NoError(t, err) + + supervisorAgent, err := supervisor.New(ctx, &supervisor.Config{ + Supervisor: supervisorChatAgent, + SubAgents: []adk.Agent{projectAgent}, + }) + assert.NoError(t, err) + + store := newIntegrationCheckpointStore() + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: supervisorAgent, + CheckPointStore: store, + }) + + t.Log("========================================") + t.Log("Starting Supervisor + PlanExecute Integration Test") + t.Log("========================================") + + checkpointID := "test-supervisor-plan_execute-1" + iter := runner.Run(ctx, userInput, adk.WithCheckPointID(checkpointID)) + + var interruptEvent *adk.AgentEvent + eventCount := 0 + for { + event, ok := iter.Next() + if !ok { + break + } + eventCount++ + t.Logf("Event %d: %s", eventCount, formatAgentEventIntegration(event)) + + if event.Err != nil { + t.Logf("Event has error: %v", event.Err) + } + + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + t.Log("========================================") + t.Log("INTERRUPT DETECTED - Deep interrupt from tool within executor") + t.Log("========================================") + break + } + } + + if interruptEvent == nil { + t.Fatal("Expected an interrupt event from the approvable tool, but none was received") + } + + assert.NotNil(t, interruptEvent.Action.Interrupted, "Should have interrupt info") + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts, "Should have interrupt contexts") + + t.Logf("Interrupt event received with %d contexts", len(interruptEvent.Action.Interrupted.InterruptContexts)) + for i, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + t.Logf("Interrupt context %d: ID=%s, Info=%v, IsRootCause=%v", i, ctx.ID, ctx.Info, ctx.IsRootCause) + } + + var toolInterruptID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + toolInterruptID = intCtx.ID + break + } + } + assert.NotEmpty(t, toolInterruptID, "Should have a root cause interrupt ID") + + t.Log("========================================") + t.Logf("Resuming with approval for interrupt ID: %s", toolInterruptID) + t.Log("========================================") + + resumeIter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{ + Targets: map[string]any{ + toolInterruptID: &approvalResult{Approved: true}, + }, + }) + assert.NoError(t, err, "Resume should not error") + assert.NotNil(t, resumeIter, "Resume iterator should not be nil") + + var resumeEvents []*adk.AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + resumeEvents = append(resumeEvents, event) + } + + assert.NotEmpty(t, resumeEvents, "Should have resume events after approval") + + for _, event := range resumeEvents { + assert.NoError(t, event.Err, "Resume event should not have error") + } + + var hasToolResponse, hasBreakLoop bool + for _, event := range resumeEvents { + if event.Output != nil && event.Output.MessageOutput != nil { + msg := event.Output.MessageOutput.Message + if msg != nil && msg.Role == "tool" && strings.Contains(msg.Content, "executed successfully") { + hasToolResponse = true + } + } + if event.Action != nil && event.Action.BreakLoop != nil && event.Action.BreakLoop.Done { + hasBreakLoop = true + } + } + + assert.True(t, hasToolResponse, "Should have tool response indicating successful execution") + assert.True(t, hasBreakLoop, "Should have break loop action indicating task completion") +} diff --git a/adk/prebuilt/planexecute/plan_execute.go b/adk/prebuilt/planexecute/plan_execute.go new file mode 100644 index 0000000..2472a8b --- /dev/null +++ b/adk/prebuilt/planexecute/plan_execute.go @@ -0,0 +1,880 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package planexecute implements a plan–execute–replan style agent. +package planexecute + +import ( + "context" + "encoding/json" + "fmt" + "runtime/debug" + "strings" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +func init() { + schema.RegisterName[*defaultPlan]("_eino_adk_plan_execute_default_plan") + schema.RegisterName[ExecutedStep]("_eino_adk_plan_execute_executed_step") + schema.RegisterName[[]ExecutedStep]("_eino_adk_plan_execute_executed_steps") +} + +// Plan represents an execution plan with a sequence of actionable steps. +// It supports JSON serialization and deserialization while providing access to the first step. +type Plan interface { + // FirstStep returns the first step to be executed in the plan. + FirstStep() string + + // Marshaler serializes the Plan into JSON. + // The resulting JSON can be used in prompt templates. + json.Marshaler + // Unmarshaler deserializes JSON content into the Plan. + // This processes output from structured chat models or tool calls into the Plan structure. + json.Unmarshaler +} + +// NewPlan is a function type that creates a new Plan instance. +type NewPlan func(ctx context.Context) Plan + +// defaultPlan is the default implementation of the Plan interface. +// +// JSON Schema: +// +// { +// "type": "object", +// "properties": { +// "steps": { +// "type": "array", +// "items": { +// "type": "string" +// }, +// "description": "Ordered list of actions to be taken. Each step should be clear, actionable, and arranged in a logical sequence." +// } +// }, +// "required": ["steps"] +// } +type defaultPlan struct { + // Steps contains the ordered list of actions to be taken. + // Each step should be clear, actionable, and arranged in a logical sequence. + Steps []string `json:"steps"` +} + +// FirstStep returns the first step in the plan or an empty string if no steps exist. +func (p *defaultPlan) FirstStep() string { + if len(p.Steps) == 0 { + return "" + } + return p.Steps[0] +} + +func (p *defaultPlan) MarshalJSON() ([]byte, error) { + type planTyp defaultPlan + return sonic.Marshal((*planTyp)(p)) +} + +func (p *defaultPlan) UnmarshalJSON(bytes []byte) error { + type planTyp defaultPlan + return sonic.Unmarshal(bytes, (*planTyp)(p)) +} + +// Response represents the final response to the user. +// This struct is used for JSON serialization/deserialization of the final response +// generated by the model. +type Response struct { + // Response is the complete response to provide to the user. + // This field is required. + Response string `json:"response"` +} + +var ( + // PlanToolInfo defines the schema for the Plan tool that can be used with ToolCallingChatModel. + // This schema instructs the model to generate a structured plan with ordered steps. + PlanToolInfo = schema.ToolInfo{ + Name: "plan", + Desc: "Plan with a list of steps to execute in order. Each step should be clear, actionable, and arranged in a logical sequence. The output will be used to guide the execution process.", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "steps": { + Type: schema.Array, + ElemInfo: &schema.ParameterInfo{Type: schema.String}, + Desc: "different steps to follow, should be in sorted order", + Required: true, + }, + }, + ), + } + + // RespondToolInfo defines the schema for the response tool that can be used with ToolCallingChatModel. + // This schema instructs the model to generate a direct response to the user. + RespondToolInfo = schema.ToolInfo{ + Name: "respond", + Desc: "Generate a direct response to the user. Use this tool when you have all the information needed to provide a final answer.", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "response": { + Type: schema.String, + Desc: "The complete response to provide to the user", + Required: true, + }, + }, + ), + } + + // PlannerPrompt is the prompt template for the planner. + // It provides context and guidance to the planner on how to generate the Plan. + PlannerPrompt = prompt.FromMessages(schema.FString, + schema.SystemMessage(`You are an expert planning agent. Given an objective, create a comprehensive step-by-step plan to achieve the objective. + +## YOUR TASK +Analyze the objective and generate a strategic plan that breaks down the goal into manageable, executable steps. + +## PLANNING REQUIREMENTS +Each step in your plan must be: +- **Specific and actionable**: Clear instructions that can be executed without ambiguity +- **Self-contained**: Include all necessary context, parameters, and requirements +- **Independently executable**: Can be performed by an agent without dependencies on other steps +- **Logically sequenced**: Arranged in optimal order for efficient execution +- **Objective-focused**: Directly contribute to achieving the main goal + +## PLANNING GUIDELINES +- Eliminate redundant or unnecessary steps +- Include relevant constraints, parameters, and success criteria for each step +- Ensure the final step produces a complete answer or deliverable +- Anticipate potential challenges and include mitigation strategies +- Structure steps to build upon each other logically +- Provide sufficient detail for successful execution + +## QUALITY CRITERIA +- Plan completeness: Does it address all aspects of the objective? +- Step clarity: Can each step be understood and executed independently? +- Logical flow: Do steps follow a sensible progression? +- Efficiency: Is this the most direct path to the objective? +- Adaptability: Can the plan handle unexpected results or changes?`), + schema.MessagesPlaceholder("input", false), + ) + + // ExecutorPrompt is the prompt template for the executor. + // It provides context and guidance to the executor on how to execute the Task. + ExecutorPrompt = prompt.FromMessages(schema.FString, + schema.SystemMessage(`You are a diligent and meticulous executor agent. Follow the given plan and execute your tasks carefully and thoroughly.`), + schema.UserMessage(`## OBJECTIVE +{input} +## Given the following plan: +{plan} +## COMPLETED STEPS & RESULTS +{executed_steps} +## Your task is to execute the first step, which is: +{step}`)) + + // ReplannerPrompt is the prompt template for the replanner. + // It provides context and guidance to the replanner on how to regenerate the Plan. + ReplannerPrompt = prompt.FromMessages(schema.FString, + schema.SystemMessage( + `You are going to review the progress toward an objective. Analyze the current state and determine the optimal next action. + +## YOUR TASK +Based on the progress above, you MUST choose exactly ONE action: + +### Option 1: COMPLETE (if objective is fully achieved) +Call '{respond_tool}' with: +- A comprehensive final answer +- Clear conclusion summarizing how the objective was met +- Key insights from the execution process + +### Option 2: CONTINUE (if more work is needed) +Call '{plan_tool}' with a revised plan that: +- Contains ONLY remaining steps (exclude completed ones) +- Incorporates lessons learned from executed steps +- Addresses any gaps or issues discovered +- Maintains logical step sequence + +## PLANNING REQUIREMENTS +Each step in your plan must be: +- **Specific and actionable**: Clear instructions that can be executed without ambiguity +- **Self-contained**: Include all necessary context, parameters, and requirements +- **Independently executable**: Can be performed by an agent without dependencies on other steps +- **Logically sequenced**: Arranged in optimal order for efficient execution +- **Objective-focused**: Directly contribute to achieving the main goal + +## PLANNING GUIDELINES +- Eliminate redundant or unnecessary steps +- Adapt strategy based on new information +- Include relevant constraints, parameters, and success criteria for each step + +## DECISION CRITERIA +- Has the original objective been completely satisfied? +- Are there any remaining requirements or sub-goals? +- Do the results suggest a need for strategy adjustment? +- What specific actions are still required?`), + schema.UserMessage(`## OBJECTIVE +{input} + +## ORIGINAL PLAN +{plan} + +## COMPLETED STEPS & RESULTS +{executed_steps}`), + ) +) + +const ( + // UserInputSessionKey is the session key for the user input. + UserInputSessionKey = "UserInput" + + // PlanSessionKey is the session key for the plan. + PlanSessionKey = "Plan" + + // ExecutedStepSessionKey is the session key for the execute result. + ExecutedStepSessionKey = "ExecutedStep" + + // ExecutedStepsSessionKey is the session key for the execute results. + ExecutedStepsSessionKey = "ExecutedSteps" +) + +// PlannerConfig provides configuration options for creating a planner agent. +// There are two ways to configure the planner to generate structured Plan output: +// 1. Use ChatModelWithFormattedOutput: A model pre-configured to output in the Plan format +// 2. Use ToolCallingChatModel + ToolInfo: A model that uses tool calling to generate +// the Plan structure +type PlannerConfig struct { + // ChatModelWithFormattedOutput is a model pre-configured to output in the Plan format. + // Create this by configuring a model to output structured data directly. + // See example: https://github.com/cloudwego/eino-ext/blob/main/components/model/openai/examples/structured/structured.go + ChatModelWithFormattedOutput model.BaseChatModel + + // ToolCallingChatModel is a model that supports tool calling capabilities. + // When provided with ToolInfo, it will use tool calling to generate the Plan structure. + ToolCallingChatModel model.ToolCallingChatModel + + // ToolInfo defines the schema for the Plan structure when using tool calling. + // Optional. If not provided, PlanToolInfo will be used as the default. + ToolInfo *schema.ToolInfo + + // GenInputFn is a function that generates the input messages for the planner. + // Optional. If not provided, defaultGenPlannerInputFn will be used. + GenInputFn GenPlannerModelInputFn + + // NewPlan creates a new Plan instance for JSON. + // The returned Plan will be used to unmarshal the model-generated JSON output. + // Optional. If not provided, defaultNewPlan will be used. + NewPlan NewPlan +} + +// GenPlannerModelInputFn is a function type that generates input messages for the planner. +type GenPlannerModelInputFn func(ctx context.Context, userInput []adk.Message) ([]adk.Message, error) + +func defaultNewPlan(ctx context.Context) Plan { + return &defaultPlan{} +} + +func defaultGenPlannerInputFn(ctx context.Context, userInput []adk.Message) ([]adk.Message, error) { + msgs, err := PlannerPrompt.Format(ctx, map[string]any{ + "input": userInput, + }) + if err != nil { + return nil, err + } + return msgs, nil +} + +type planner struct { + toolCall bool + chatModel model.BaseChatModel + genInputFn GenPlannerModelInputFn + newPlan NewPlan +} + +func (p *planner) Name(_ context.Context) string { + return "planner" +} + +func (p *planner) Description(_ context.Context) string { + return "a planner agent" +} + +func argToContent(msg adk.Message) (adk.Message, error) { + if len(msg.ToolCalls) == 0 { + return nil, schema.ErrNoValue + } + + return schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil), nil +} + +func (p *planner) Run(ctx context.Context, input *adk.AgentInput, + _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + adk.AddSessionValue(ctx, UserInputSessionKey, input.Messages) + + go func() { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&adk.AgentEvent{Err: e}) + } + + generator.Close() + }() + + c := compose.NewChain[*adk.AgentInput, Plan](). + AppendLambda( + compose.InvokableLambda(func(ctx context.Context, input *adk.AgentInput) (output []adk.Message, err error) { + return p.genInputFn(ctx, input.Messages) + }), + ). + AppendChatModel(p.chatModel). + AppendLambda( + compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) { + if input.EnableStreaming { + ss := sr.Copy(2) + var sOutput *schema.StreamReader[*schema.Message] + if p.toolCall { + sOutput = schema.StreamReaderWithConvert(ss[0], argToContent) + } else { + sOutput = ss[0] + } + + generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, "")) + + return schema.ConcatMessageStream(ss[1]) + } + + msg, err := schema.ConcatMessageStream(sr) + if err != nil { + return nil, err + } + + var output adk.Message + if p.toolCall { + if len(msg.ToolCalls) == 0 { + return nil, fmt.Errorf("no tool call") + } + output = schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil) + } else { + output = msg + } + + generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, "")) + + return msg, nil + }), + ). + AppendLambda( + compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (plan Plan, err error) { + var planJSON string + if p.toolCall { + if len(msg.ToolCalls) == 0 { + return nil, fmt.Errorf("no tool call") + } + planJSON = msg.ToolCalls[0].Function.Arguments + } else { + planJSON = msg.Content + } + + plan = p.newPlan(ctx) + err = plan.UnmarshalJSON([]byte(planJSON)) + if err != nil { + return nil, fmt.Errorf("unmarshal plan error: %w", err) + } + + adk.AddSessionValue(ctx, PlanSessionKey, plan) + + return plan, nil + }), + ) + + var opts []compose.Option + if p.toolCall { + opts = append(opts, compose.WithChatModelOption(model.WithToolChoice(schema.ToolChoiceForced))) + } + + r, err := c.Compile(ctx, compose.WithGraphName(p.Name(ctx))) + if err != nil { // unexpected + generator.Send(&adk.AgentEvent{Err: err}) + return + } + + _, err = r.Stream(ctx, input, opts...) + if err != nil { + generator.Send(&adk.AgentEvent{Err: err}) + return + } + }() + + return iterator +} + +// NewPlanner creates a new planner agent based on the provided configuration. +// The planner agent uses either ChatModelWithFormattedOutput or ToolCallingChatModel+ToolInfo +// to generate structured Plan output. +// +// If ChatModelWithFormattedOutput is provided, it will be used directly. +// If ToolCallingChatModel is provided, it will be configured with ToolInfo (or PlanToolInfo by default) +// to generate structured Plan output. +func NewPlanner(_ context.Context, cfg *PlannerConfig) (adk.Agent, error) { + var chatModel model.BaseChatModel + var toolCall bool + if cfg.ChatModelWithFormattedOutput != nil { + chatModel = cfg.ChatModelWithFormattedOutput + } else { + toolCall = true + toolInfo := cfg.ToolInfo + if toolInfo == nil { + toolInfo = &PlanToolInfo + } + + var err error + chatModel, err = cfg.ToolCallingChatModel.WithTools([]*schema.ToolInfo{toolInfo}) + if err != nil { + return nil, err + } + } + + inputFn := cfg.GenInputFn + if inputFn == nil { + inputFn = defaultGenPlannerInputFn + } + + planParser := cfg.NewPlan + if planParser == nil { + planParser = defaultNewPlan + } + + return &planner{ + toolCall: toolCall, + chatModel: chatModel, + genInputFn: inputFn, + newPlan: planParser, + }, nil +} + +// ExecutionContext is the input information for the executor and the planner. +type ExecutionContext struct { + UserInput []adk.Message + Plan Plan + ExecutedSteps []ExecutedStep +} + +// GenModelInputFn is a function that generates the input messages for the executor and the planner. +type GenModelInputFn func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) + +// ExecutorConfig provides configuration options for creating an executor agent. +type ExecutorConfig struct { + // Model is the chat model used by the executor. + // If the executor uses any tools, this model must support the model.WithTools call option, + // as that's how the executor configures the model with tool information. + Model model.BaseChatModel + + // ToolsConfig specifies the tools available to the executor. + ToolsConfig adk.ToolsConfig + + // MaxIterations defines the upper limit of ChatModel generation cycles. + // The agent will terminate with an error if this limit is exceeded. + // Optional. Defaults to 20. + MaxIterations int + + // GenInputFn generates the input messages for the Executor. + // Optional. If not provided, defaultGenExecutorInputFn will be used. + GenInputFn GenModelInputFn +} + +type ExecutedStep struct { + Step string + Result string +} + +// NewExecutor creates a new executor agent. +func NewExecutor(ctx context.Context, cfg *ExecutorConfig) (adk.Agent, error) { + + genInputFn := cfg.GenInputFn + if genInputFn == nil { + genInputFn = defaultGenExecutorInputFn + } + genInput := func(ctx context.Context, instruction string, _ *adk.AgentInput) ([]adk.Message, error) { + + plan, ok := adk.GetSessionValue(ctx, PlanSessionKey) + if !ok { + panic("impossible: plan not found") + } + plan_ := plan.(Plan) + + userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey) + if !ok { + panic("impossible: user input not found") + } + userInput_ := userInput.([]adk.Message) + + var executedSteps_ []ExecutedStep + executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey) + if ok { + executedSteps_ = executedStep.([]ExecutedStep) + } + + in := &ExecutionContext{ + UserInput: userInput_, + Plan: plan_, + ExecutedSteps: executedSteps_, + } + + msgs, err := genInputFn(ctx, in) + if err != nil { + return nil, err + } + + return msgs, nil + } + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "executor", + Description: "an executor agent", + Model: cfg.Model, + ToolsConfig: cfg.ToolsConfig, + GenModelInput: genInput, + MaxIterations: cfg.MaxIterations, + OutputKey: ExecutedStepSessionKey, + }) + if err != nil { + return nil, err + } + + return agent, nil +} + +func defaultGenExecutorInputFn(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) { + + planContent, err := in.Plan.MarshalJSON() + if err != nil { + return nil, err + } + + userMsgs, err := ExecutorPrompt.Format(ctx, map[string]any{ + "input": formatInput(in.UserInput), + "plan": string(planContent), + "executed_steps": formatExecutedSteps(in.ExecutedSteps), + "step": in.Plan.FirstStep(), + }) + if err != nil { + return nil, err + } + + return userMsgs, nil +} + +type replanner struct { + chatModel model.ToolCallingChatModel + planTool *schema.ToolInfo + respondTool *schema.ToolInfo + + genInputFn GenModelInputFn + newPlan NewPlan +} + +type ReplannerConfig struct { + // ChatModel is the model that supports tool calling capabilities. + // It will be configured with PlanTool and RespondTool to generate updated plans or responses. + ChatModel model.ToolCallingChatModel + + // PlanTool defines the schema for the Plan tool that can be used with ToolCallingChatModel. + // Optional. If not provided, the default PlanToolInfo will be used. + PlanTool *schema.ToolInfo + + // RespondTool defines the schema for the response tool that can be used with ToolCallingChatModel. + // Optional. If not provided, the default RespondToolInfo will be used. + RespondTool *schema.ToolInfo + + // GenInputFn generates the input messages for the Replanner. + // Optional. If not provided, buildGenReplannerInputFn will be used. + GenInputFn GenModelInputFn + + // NewPlan creates a new Plan instance. + // The returned Plan will be used to unmarshal the model-generated JSON output from PlanTool. + // Optional. If not provided, defaultNewPlan will be used. + NewPlan NewPlan +} + +// formatInput formats the input messages into a string. +func formatInput(input []adk.Message) string { + var sb strings.Builder + for _, msg := range input { + sb.WriteString(msg.Content) + sb.WriteString("\n") + } + + return sb.String() +} + +func formatExecutedSteps(results []ExecutedStep) string { + var sb strings.Builder + for _, result := range results { + sb.WriteString(fmt.Sprintf("Step: %s\nResult: %s\n\n", result.Step, result.Result)) + } + + return sb.String() +} + +func (r *replanner) Name(_ context.Context) string { + return "replanner" +} + +func (r *replanner) Description(_ context.Context) string { + return "a replanner agent" +} + +func (r *replanner) genInput(ctx context.Context) ([]adk.Message, error) { + + executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepSessionKey) + if !ok { + panic("impossible: execute result not found") + } + executedStep_ := executedStep.(string) + + plan, ok := adk.GetSessionValue(ctx, PlanSessionKey) + if !ok { + panic("impossible: plan not found") + } + plan_ := plan.(Plan) + step := plan_.FirstStep() + + var executedSteps_ []ExecutedStep + executedSteps, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey) + if ok { + executedSteps_ = executedSteps.([]ExecutedStep) + } + + executedSteps_ = append(executedSteps_, ExecutedStep{ + Step: step, + Result: executedStep_, + }) + adk.AddSessionValue(ctx, ExecutedStepsSessionKey, executedSteps_) + + userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey) + if !ok { + panic("impossible: user input not found") + } + userInput_ := userInput.([]adk.Message) + + in := &ExecutionContext{ + UserInput: userInput_, + Plan: plan_, + ExecutedSteps: executedSteps_, + } + genInputFn := r.genInputFn + if genInputFn == nil { + genInputFn = buildGenReplannerInputFn(r.planTool.Name, r.respondTool.Name) + } + msgs, err := genInputFn(ctx, in) + if err != nil { + return nil, err + } + + return msgs, nil +} + +func (r *replanner) Run(ctx context.Context, input *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + go func() { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&adk.AgentEvent{Err: e}) + } + + generator.Close() + }() + + callOpt := model.WithToolChoice(schema.ToolChoiceForced) + + c := compose.NewChain[struct{}, any](). + AppendLambda( + compose.InvokableLambda(func(ctx context.Context, input struct{}) (output []adk.Message, err error) { + return r.genInput(ctx) + }), + ). + AppendChatModel(r.chatModel). + AppendLambda( + compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) { + if input.EnableStreaming { + ss := sr.Copy(2) + sOutput := schema.StreamReaderWithConvert(ss[0], argToContent) + generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, "")) + return schema.ConcatMessageStream(ss[1]) + } + + msg, err := schema.ConcatMessageStream(sr) + if err != nil { + return nil, err + } + if len(msg.ToolCalls) > 0 { + output := schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil) + generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, "")) + } + return msg, nil + }), + ). + AppendLambda( + compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (msgOrPlan any, err error) { + if len(msg.ToolCalls) == 0 { + return nil, fmt.Errorf("no tool call") + } + + // exit + if msg.ToolCalls[0].Function.Name == r.respondTool.Name { + action := adk.NewBreakLoopAction(r.Name(ctx)) + generator.Send(&adk.AgentEvent{Action: action}) + return msg, nil + } + + // replan + if msg.ToolCalls[0].Function.Name != r.planTool.Name { + return nil, fmt.Errorf("unexpected tool call: %s", msg.ToolCalls[0].Function.Name) + } + + plan := r.newPlan(ctx) + if err = plan.UnmarshalJSON([]byte(msg.ToolCalls[0].Function.Arguments)); err != nil { + return nil, fmt.Errorf("unmarshal plan error: %w", err) + } + + adk.AddSessionValue(ctx, PlanSessionKey, plan) + + return plan, nil + }), + ) + + runnable, err := c.Compile(ctx, compose.WithGraphName(r.Name(ctx))) + if err != nil { + generator.Send(&adk.AgentEvent{Err: err}) + return + } + + _, err = runnable.Stream(ctx, struct{}{}, compose.WithChatModelOption(callOpt)) + if err != nil { + generator.Send(&adk.AgentEvent{Err: err}) + return + } + }() + + return iterator +} + +func buildGenReplannerInputFn(planToolName, respondToolName string) GenModelInputFn { + return func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) { + planContent, err := in.Plan.MarshalJSON() + if err != nil { + return nil, err + } + msgs, err := ReplannerPrompt.Format(ctx, map[string]any{ + "plan": string(planContent), + "input": formatInput(in.UserInput), + "executed_steps": formatExecutedSteps(in.ExecutedSteps), + "plan_tool": planToolName, + "respond_tool": respondToolName, + }) + if err != nil { + return nil, err + } + + return msgs, nil + } +} + +// NewReplanner creates a plan-execute-replan agent wired with plan and respond tools. +// It configures the provided ToolCallingChatModel with the tools and returns an Agent. +func NewReplanner(_ context.Context, cfg *ReplannerConfig) (adk.Agent, error) { + planTool := cfg.PlanTool + if planTool == nil { + planTool = &PlanToolInfo + } + + respondTool := cfg.RespondTool + if respondTool == nil { + respondTool = &RespondToolInfo + } + + chatModel, err := cfg.ChatModel.WithTools([]*schema.ToolInfo{planTool, respondTool}) + if err != nil { + return nil, err + } + + planParser := cfg.NewPlan + if planParser == nil { + planParser = defaultNewPlan + } + + return &replanner{ + chatModel: chatModel, + planTool: planTool, + respondTool: respondTool, + genInputFn: cfg.GenInputFn, + newPlan: planParser, + }, nil +} + +// Config provides configuration options for creating a plan-execute-replan agent. +type Config struct { + // Planner specifies the agent that generates the plan. + // You can use provided NewPlanner to create a planner agent. + Planner adk.Agent + + // Executor specifies the agent that executes the plan generated by planner or replanner. + // You can use provided NewExecutor to create an executor agent. + Executor adk.Agent + + // Replanner specifies the agent that replans the plan. + // You can use provided NewReplanner to create a replanner agent. + Replanner adk.Agent + + // MaxIterations defines the maximum number of loops for 'execute-replan'. + // Optional. If not provided, 10 will be used as the default. + MaxIterations int +} + +// New creates a new plan-execute-replan agent with the given configuration. +// The plan-execute-replan pattern works in three phases: +// 1. Planning: Generate a structured plan with clear, actionable steps +// 2. Execution: Execute the first step of the plan +// 3. Replanning: Evaluate progress and either complete the task or revise the plan +// This approach enables complex problem-solving through iterative refinement. +func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) { + maxIterations := cfg.MaxIterations + if maxIterations <= 0 { + maxIterations = 10 + } + loop, err := adk.NewLoopAgent(ctx, &adk.LoopAgentConfig{ + Name: "execute_replan", + SubAgents: []adk.Agent{cfg.Executor, cfg.Replanner}, + MaxIterations: maxIterations, + }) + if err != nil { + return nil, err + } + + return adk.NewSequentialAgent(ctx, &adk.SequentialAgentConfig{ + Name: "plan_execute_replan", + SubAgents: []adk.Agent{cfg.Planner, loop}, + }) +} diff --git a/adk/prebuilt/planexecute/plan_execute_test.go b/adk/prebuilt/planexecute/plan_execute_test.go new file mode 100644 index 0000000..25c64ea --- /dev/null +++ b/adk/prebuilt/planexecute/plan_execute_test.go @@ -0,0 +1,1201 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package planexecute + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockAdk "github.com/cloudwego/eino/internal/mock/adk" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +// TestNewPlanner tests the NewPlanner function with ChatModelWithFormattedOutput +func TestNewPlannerWithFormattedOutput(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock chat model + mockChatModel := mockModel.NewMockBaseChatModel(ctrl) + + // Create the PlannerConfig + conf := &PlannerConfig{ + ChatModelWithFormattedOutput: mockChatModel, + } + + // Create the planner + p, err := NewPlanner(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, p) + + // Verify the planner's name and description + assert.Equal(t, "planner", p.Name(ctx)) + assert.Equal(t, "a planner agent", p.Description(ctx)) +} + +// TestNewPlannerWithToolCalling tests the NewPlanner function with ToolCallingChatModel +func TestNewPlannerWithToolCalling(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockToolCallingModel.EXPECT().WithTools(gomock.Any()).Return(mockToolCallingModel, nil).Times(1) + + // Create the PlannerConfig + conf := &PlannerConfig{ + ToolCallingChatModel: mockToolCallingModel, + // Use default instruction and tool info + } + + // Create the planner + p, err := NewPlanner(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, p) + + // Verify the planner's name and description + assert.Equal(t, "planner", p.Name(ctx)) + assert.Equal(t, "a planner agent", p.Description(ctx)) +} + +// TestPlannerRunWithFormattedOutput tests the Run method of a planner created with ChatModelWithFormattedOutput +func TestPlannerRunWithFormattedOutput(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock chat model + mockChatModel := mockModel.NewMockBaseChatModel(ctrl) + + // Create a plan response + planJSON := `{"steps":["Step 1", "Step 2", "Step 3"]}` + planMsg := schema.AssistantMessage(planJSON, nil) + sr, sw := schema.Pipe[*schema.Message](1) + sw.Send(planMsg, nil) + sw.Close() + + // Mock the Generate method + mockChatModel.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + + // Create the PlannerConfig + conf := &PlannerConfig{ + ChatModelWithFormattedOutput: mockChatModel, + } + + // Create the planner + p, err := NewPlanner(ctx, conf) + assert.NoError(t, err) + + // Run the planner + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: p}) + iterator := runner.Run(ctx, []adk.Message{schema.UserMessage("Plan this task")}) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + msg, _, err := adk.GetMessage(event) + assert.NoError(t, err) + assert.Equal(t, planMsg.Content, msg.Content) + + _, ok = iterator.Next() + assert.False(t, ok) + + plan := defaultNewPlan(ctx) + err = plan.UnmarshalJSON([]byte(msg.Content)) + assert.NoError(t, err) + plan_ := plan.(*defaultPlan) + assert.Equal(t, 3, len(plan_.Steps)) + assert.Equal(t, "Step 1", plan_.Steps[0]) + assert.Equal(t, "Step 2", plan_.Steps[1]) + assert.Equal(t, "Step 3", plan_.Steps[2]) +} + +// TestPlannerRunWithToolCalling tests the Run method of a planner created with ToolCallingChatModel +func TestPlannerRunWithToolCalling(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create a tool call response with a plan + planArgs := `{"steps":["Step 1", "Step 2", "Step 3"]}` + toolCall := schema.ToolCall{ + ID: "tool_call_id", + Type: "function", + Function: schema.FunctionCall{ + Name: "plan", // This should match PlanToolInfo.Name + Arguments: planArgs, + }, + } + + toolCallMsg := schema.AssistantMessage("", nil) + toolCallMsg.ToolCalls = []schema.ToolCall{toolCall} + sr, sw := schema.Pipe[*schema.Message](1) + sw.Send(toolCallMsg, nil) + sw.Close() + + // Mock the WithTools method to return a model that will be used for Generate + mockToolCallingModel.EXPECT().WithTools(gomock.Any()).Return(mockToolCallingModel, nil).Times(1) + + // Mock the Generate method to return the tool call message + mockToolCallingModel.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + + // Create the PlannerConfig with ToolCallingChatModel + conf := &PlannerConfig{ + ToolCallingChatModel: mockToolCallingModel, + // Use default instruction and tool info + } + + // Create the planner + p, err := NewPlanner(ctx, conf) + assert.NoError(t, err) + + // Run the planner + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: p}) + iterator := runner.Run(ctx, []adk.Message{schema.UserMessage("no input")}) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + + msg, _, err := adk.GetMessage(event) + assert.NoError(t, err) + assert.Equal(t, planArgs, msg.Content) + + _, ok = iterator.Next() + assert.False(t, ok) + + plan := defaultNewPlan(ctx) + err = plan.UnmarshalJSON([]byte(msg.Content)) + assert.NoError(t, err) + plan_ := plan.(*defaultPlan) + assert.NoError(t, err) + assert.Equal(t, 3, len(plan_.Steps)) + assert.Equal(t, "Step 1", plan_.Steps[0]) + assert.Equal(t, "Step 2", plan_.Steps[1]) + assert.Equal(t, "Step 3", plan_.Steps[2]) +} + +// TestNewExecutor tests the NewExecutor function +func TestNewExecutor(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create the ExecutorConfig + conf := &ExecutorConfig{ + Model: mockToolCallingModel, + MaxIterations: 3, + } + + // Create the executor + executor, err := NewExecutor(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, executor) + + // Verify the executor's name and description + assert.Equal(t, "executor", executor.Name(ctx)) + assert.Equal(t, "an executor agent", executor.Description(ctx)) +} + +// TestExecutorRun tests the Run method of the executor +func TestExecutorRun(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + + // Store a plan in the session + plan := &defaultPlan{Steps: []string{"Step 1", "Step 2", "Step 3"}} + adk.AddSessionValue(ctx, PlanSessionKey, plan) + + // Set up expectations for the mock model + // The model should return the last user message as its response + mockToolCallingModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, messages []*schema.Message, opts ...model.Option) (*schema.Message, error) { + // Find the last user message + var lastUserMessage string + for _, msg := range messages { + if msg.Role == schema.User { + lastUserMessage = msg.Content + } + } + // Return the last user message as the model's response + return schema.AssistantMessage(lastUserMessage, nil), nil + }).Times(1) + + // Create the ExecutorConfig + conf := &ExecutorConfig{ + Model: mockToolCallingModel, + MaxIterations: 3, + } + + // Create the executor + executor, err := NewExecutor(ctx, conf) + assert.NoError(t, err) + + // Run the executor + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: executor}) + iterator := runner.Run(ctx, []adk.Message{schema.UserMessage("no input")}, + adk.WithSessionValues(map[string]any{ + PlanSessionKey: plan, + UserInputSessionKey: []adk.Message{schema.UserMessage("no input")}, + }), + ) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + msg, _, err := adk.GetMessage(event) + assert.NoError(t, err) + t.Logf("executor model input msg:\n %s\n", msg.Content) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +// TestNewReplanner tests the NewReplanner function +func TestNewReplanner(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + // Mock the WithTools method + mockToolCallingModel.EXPECT().WithTools(gomock.Any()).Return(mockToolCallingModel, nil).Times(1) + + // Create plan and respond tools + planTool := &schema.ToolInfo{ + Name: "Plan", + Desc: "Plan tool", + } + + respondTool := &schema.ToolInfo{ + Name: "Respond", + Desc: "Respond tool", + } + + // Create the ReplannerConfig + conf := &ReplannerConfig{ + ChatModel: mockToolCallingModel, + PlanTool: planTool, + RespondTool: respondTool, + } + + // Create the replanner + rp, err := NewReplanner(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, rp) + + // Verify the replanner's name and description + assert.Equal(t, "replanner", rp.Name(ctx)) + assert.Equal(t, "a replanner agent", rp.Description(ctx)) +} + +// TestReplannerRunWithPlan tests the Replanner's ability to use the plan_tool +func TestReplannerRunWithPlan(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create plan and respond tools + planTool := &schema.ToolInfo{ + Name: "Plan", + Desc: "Plan tool", + } + + respondTool := &schema.ToolInfo{ + Name: "Respond", + Desc: "Respond tool", + } + + // Create a tool call response for the Plan tool + planArgs := `{"steps":["Updated Step 1", "Updated Step 2"]}` + toolCall := schema.ToolCall{ + ID: "tool_call_id", + Type: "function", + Function: schema.FunctionCall{ + Name: planTool.Name, + Arguments: planArgs, + }, + } + + toolCallMsg := schema.AssistantMessage("", nil) + toolCallMsg.ToolCalls = []schema.ToolCall{toolCall} + sr, sw := schema.Pipe[*schema.Message](1) + sw.Send(toolCallMsg, nil) + sw.Close() + + // Mock the Generate method + mockToolCallingModel.EXPECT().WithTools(gomock.Any()).Return(mockToolCallingModel, nil).Times(1) + mockToolCallingModel.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + + // Create the ReplannerConfig + conf := &ReplannerConfig{ + ChatModel: mockToolCallingModel, + PlanTool: planTool, + RespondTool: respondTool, + } + + // Create the replanner + rp, err := NewReplanner(ctx, conf) + assert.NoError(t, err) + + // Store necessary values in the session + plan := &defaultPlan{Steps: []string{"Step 1", "Step 2", "Step 3"}} + + rp, err = agentOutputSessionKVs(ctx, rp) + assert.NoError(t, err) + + // Run the replanner + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: rp}) + iterator := runner.Run(ctx, []adk.Message{schema.UserMessage("no input")}, + adk.WithSessionValues(map[string]any{ + PlanSessionKey: plan, + ExecutedStepSessionKey: "Execution result", + UserInputSessionKey: []adk.Message{schema.UserMessage("User input")}, + }), + ) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + + event, ok = iterator.Next() + assert.True(t, ok) + kvs := event.Output.CustomizedOutput.(map[string]any) + assert.Greater(t, len(kvs), 0) + + // Verify the updated plan was stored in the session + planValue, ok := kvs[PlanSessionKey] + assert.True(t, ok) + updatedPlan, ok := planValue.(*defaultPlan) + assert.True(t, ok) + assert.Equal(t, 2, len(updatedPlan.Steps)) + assert.Equal(t, "Updated Step 1", updatedPlan.Steps[0]) + assert.Equal(t, "Updated Step 2", updatedPlan.Steps[1]) + + // Verify the execute results were updated + executeResultsValue, ok := kvs[ExecutedStepsSessionKey] + assert.True(t, ok) + executeResults, ok := executeResultsValue.([]ExecutedStep) + assert.True(t, ok) + assert.Equal(t, 1, len(executeResults)) + assert.Equal(t, "Step 1", executeResults[0].Step) + assert.Equal(t, "Execution result", executeResults[0].Result) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +// TestReplannerRunWithRespond tests the Replanner's ability to use the respond_tool +func TestReplannerRunWithRespond(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock tool calling chat model + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + + // Create plan and respond tools + planTool := &schema.ToolInfo{ + Name: "Plan", + Desc: "Plan tool", + } + + respondTool := &schema.ToolInfo{ + Name: "Respond", + Desc: "Respond tool", + } + + // Create a tool call response for the Respond tool + responseArgs := `{"response":"This is the final response to the user"}` + toolCall := schema.ToolCall{ + ID: "tool_call_id", + Type: "function", + Function: schema.FunctionCall{ + Name: respondTool.Name, + Arguments: responseArgs, + }, + } + + toolCallMsg := schema.AssistantMessage("", nil) + toolCallMsg.ToolCalls = []schema.ToolCall{toolCall} + sr, sw := schema.Pipe[*schema.Message](1) + sw.Send(toolCallMsg, nil) + sw.Close() + + // Mock the Generate method + mockToolCallingModel.EXPECT().WithTools(gomock.Any()).Return(mockToolCallingModel, nil).Times(1) + mockToolCallingModel.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + + // Create the ReplannerConfig + conf := &ReplannerConfig{ + ChatModel: mockToolCallingModel, + PlanTool: planTool, + RespondTool: respondTool, + } + + // Create the replanner + rp, err := NewReplanner(ctx, conf) + assert.NoError(t, err) + + // Store necessary values in the session + plan := &defaultPlan{Steps: []string{"Step 1", "Step 2", "Step 3"}} + + // Run the replanner + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: rp}) + iterator := runner.Run(ctx, []adk.Message{schema.UserMessage("no input")}, + adk.WithSessionValues(map[string]any{ + PlanSessionKey: plan, + ExecutedStepSessionKey: "Execution result", + UserInputSessionKey: []adk.Message{schema.UserMessage("User input")}, + }), + ) + + // Get the event from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Nil(t, event.Err) + msg, _, err := adk.GetMessage(event) + assert.NoError(t, err) + assert.Equal(t, responseArgs, msg.Content) + + // Verify that an exit action was generated + event, ok = iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event.Action) + assert.NotNil(t, event.Action.BreakLoop) + assert.False(t, event.Action.BreakLoop.Done) + + _, ok = iterator.Next() + assert.False(t, ok) +} + +// TestNewPlanExecuteAgent tests the New function +func TestNewPlanExecuteAgent(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create mock agents + mockPlanner := mockAdk.NewMockAgent(ctrl) + mockExecutor := mockAdk.NewMockAgent(ctrl) + mockReplanner := mockAdk.NewMockAgent(ctrl) + + // Set up expectations for the mock agents + mockPlanner.EXPECT().Name(gomock.Any()).Return("planner").AnyTimes() + mockPlanner.EXPECT().Description(gomock.Any()).Return("a planner agent").AnyTimes() + + mockExecutor.EXPECT().Name(gomock.Any()).Return("executor").AnyTimes() + mockExecutor.EXPECT().Description(gomock.Any()).Return("an executor agent").AnyTimes() + + mockReplanner.EXPECT().Name(gomock.Any()).Return("replanner").AnyTimes() + mockReplanner.EXPECT().Description(gomock.Any()).Return("a replanner agent").AnyTimes() + + conf := &Config{ + Planner: mockPlanner, + Executor: mockExecutor, + Replanner: mockReplanner, + } + + // Create the plan execute agent + agent, err := New(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, agent) +} + +func TestPlanExecuteAgentWithReplan(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create mock agents + mockPlanner := mockAdk.NewMockAgent(ctrl) + mockExecutor := mockAdk.NewMockAgent(ctrl) + mockReplanner := mockAdk.NewMockAgent(ctrl) + + // Set up expectations for the mock agents + mockPlanner.EXPECT().Name(gomock.Any()).Return("planner").AnyTimes() + mockPlanner.EXPECT().Description(gomock.Any()).Return("a planner agent").AnyTimes() + + mockExecutor.EXPECT().Name(gomock.Any()).Return("executor").AnyTimes() + mockExecutor.EXPECT().Description(gomock.Any()).Return("an executor agent").AnyTimes() + + mockReplanner.EXPECT().Name(gomock.Any()).Return("replanner").AnyTimes() + mockReplanner.EXPECT().Description(gomock.Any()).Return("a replanner agent").AnyTimes() + + // Create a plan + originalPlan := &defaultPlan{Steps: []string{"Step 1", "Step 2", "Step 3"}} + // Create an updated plan with fewer steps (after replanning) + updatedPlan := &defaultPlan{Steps: []string{"Updated Step 2", "Updated Step 3"}} + // Create execute result + originalExecuteResult := "Execution result for Step 1" + updatedExecuteResult := "Execution result for Updated Step 2" + + // Create user input + userInput := []adk.Message{schema.UserMessage("User task input")} + + finalResponse := &Response{Response: "Final response to user after executing all steps"} + + // Mock the planner Run method to set the original plan + mockPlanner.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + // Set the plan in the session + adk.AddSessionValue(ctx, PlanSessionKey, originalPlan) + adk.AddSessionValue(ctx, UserInputSessionKey, userInput) + + // Send a message event + planJSON, _ := sonic.MarshalString(originalPlan) + msg := schema.AssistantMessage(planJSON, nil) + event := adk.EventFromMessage(msg, nil, schema.Assistant, "") + generator.Send(event) + generator.Close() + + return iterator + }, + ).Times(1) + + // Mock the executor Run method to set the execute result + mockExecutor.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + plan, _ := adk.GetSessionValue(ctx, PlanSessionKey) + currentPlan := plan.(*defaultPlan) + var msg adk.Message + // Check if this is the first replanning (original plan has 3 steps) + if len(currentPlan.Steps) == 3 { + msg = schema.AssistantMessage(originalExecuteResult, nil) + adk.AddSessionValue(ctx, ExecutedStepSessionKey, originalExecuteResult) + } else { + msg = schema.AssistantMessage(updatedExecuteResult, nil) + adk.AddSessionValue(ctx, ExecutedStepSessionKey, updatedExecuteResult) + } + event := adk.EventFromMessage(msg, nil, schema.Assistant, "") + generator.Send(event) + generator.Close() + + return iterator + }, + ).Times(2) + + // Mock the replanner Run method to first update the plan, then respond to user + mockReplanner.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + // First call: Update the plan + // Get the current plan from the session + plan, _ := adk.GetSessionValue(ctx, PlanSessionKey) + currentPlan := plan.(*defaultPlan) + + // Check if this is the first replanning (original plan has 3 steps) + if len(currentPlan.Steps) == 3 { + // Send a message event with the updated plan + planJSON, _ := sonic.MarshalString(updatedPlan) + msg := schema.AssistantMessage(planJSON, nil) + event := adk.EventFromMessage(msg, nil, schema.Assistant, "") + generator.Send(event) + + // Set the updated plan & execute result in the session + adk.AddSessionValue(ctx, PlanSessionKey, updatedPlan) + adk.AddSessionValue(ctx, ExecutedStepsSessionKey, []ExecutedStep{{ + Step: currentPlan.Steps[0], + Result: originalExecuteResult, + }}) + } else { + // Second call: Respond to user + responseJSON, err := sonic.MarshalString(finalResponse) + assert.NoError(t, err) + msg := schema.AssistantMessage(responseJSON, nil) + event := adk.EventFromMessage(msg, nil, schema.Assistant, "") + generator.Send(event) + + // Send exit action + action := adk.NewExitAction() + generator.Send(&adk.AgentEvent{Action: action}) + } + + generator.Close() + return iterator + }, + ).Times(2) + + conf := &Config{ + Planner: mockPlanner, + Executor: mockExecutor, + Replanner: mockReplanner, + } + + // Create the plan execute agent + agent, err := New(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, agent) + + // Run the agent + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) + iterator := runner.Run(ctx, userInput) + + // Collect all events + var events []*adk.AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + // Verify the events + assert.Greater(t, len(events), 0) + + for i, event := range events { + eventJSON, e := sonic.MarshalString(event) + assert.NoError(t, e) + t.Logf("event %d:\n%s", i, eventJSON) + } +} + +type interruptibleTool struct { + name string + t *testing.T +} + +func (m *interruptibleTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: m.name, + Desc: "A tool that requires human approval before execution", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "action": { + Type: schema.String, + Desc: "The action to perform", + Required: true, + }, + }), + }, nil +} + +func (m *interruptibleTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + wasInterrupted, _, _ := tool.GetInterruptState[any](ctx) + if !wasInterrupted { + return "", tool.Interrupt(ctx, fmt.Sprintf("Tool '%s' requires human approval", m.name)) + } + + isResumeTarget, hasData, data := tool.GetResumeContext[string](ctx) + if !isResumeTarget { + return "", tool.Interrupt(ctx, fmt.Sprintf("Tool '%s' requires human approval", m.name)) + } + + if hasData { + return fmt.Sprintf("Approved action executed with data: %s", data), nil + } + return "Approved action executed", nil +} + +type checkpointStore struct { + data map[string][]byte +} + +func newCheckpointStore() *checkpointStore { + return &checkpointStore{data: make(map[string][]byte)} +} + +func (s *checkpointStore) Set(_ context.Context, key string, value []byte) error { + s.data[key] = value + return nil +} + +func (s *checkpointStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.data[key] + return v, ok, nil +} + +func TestPlanExecuteAgentInterruptResume(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockToolCallingModel := mockModel.NewMockToolCallingChatModel(ctrl) + + approvalTool := &interruptibleTool{name: "approve_action", t: t} + + plan := &defaultPlan{Steps: []string{"Execute action requiring approval", "Complete task"}} + userInput := []adk.Message{schema.UserMessage("Please execute the action")} + + mockPlanner := mockAdk.NewMockAgent(ctrl) + mockPlanner.EXPECT().Name(gomock.Any()).Return("planner").AnyTimes() + mockPlanner.EXPECT().Description(gomock.Any()).Return("a planner agent").AnyTimes() + + mockPlanner.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + adk.AddSessionValue(ctx, PlanSessionKey, plan) + adk.AddSessionValue(ctx, UserInputSessionKey, userInput) + + planJSON, _ := sonic.MarshalString(plan) + msg := schema.AssistantMessage(planJSON, nil) + event := adk.EventFromMessage(msg, nil, schema.Assistant, "") + generator.Send(event) + generator.Close() + + return iterator + }, + ).Times(1) + + toolCallMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "approve_action", + Arguments: `{"action": "execute"}`, + }, + }, + }) + + completionMsg := schema.AssistantMessage("Action approved and executed successfully", nil) + + mockToolCallingModel.EXPECT().WithTools(gomock.Any()).Return(mockToolCallingModel, nil).AnyTimes() + mockToolCallingModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(toolCallMsg, nil).Times(1) + mockToolCallingModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(completionMsg, nil).AnyTimes() + + executor, err := NewExecutor(ctx, &ExecutorConfig{ + Model: mockToolCallingModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{approvalTool}, + }, + }, + MaxIterations: 5, + }) + assert.NoError(t, err) + + mockReplanner := mockAdk.NewMockAgent(ctrl) + mockReplanner.EXPECT().Name(gomock.Any()).Return("replanner").AnyTimes() + mockReplanner.EXPECT().Description(gomock.Any()).Return("a replanner agent").AnyTimes() + + mockReplanner.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + responseJSON := `{"response":"Task completed successfully"}` + msg := schema.AssistantMessage(responseJSON, nil) + event := adk.EventFromMessage(msg, nil, schema.Assistant, "") + generator.Send(event) + + action := adk.NewBreakLoopAction("replanner") + generator.Send(&adk.AgentEvent{Action: action}) + + generator.Close() + return iterator + }, + ).AnyTimes() + + agent, err := New(ctx, &Config{ + Planner: mockPlanner, + Executor: executor, + Replanner: mockReplanner, + MaxIterations: 5, + }) + assert.NoError(t, err) + + store := newCheckpointStore() + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: agent, + CheckPointStore: store, + }) + + iter := runner.Run(ctx, userInput, adk.WithCheckPointID("test-interrupt-1")) + + var events []*adk.AgentEvent + var interruptEvent *adk.AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + events = append(events, event) + } + + t.Logf("Total events received: %d", len(events)) + for i, event := range events { + eventJSON, _ := sonic.MarshalString(event) + t.Logf("Event %d: %s", i, eventJSON) + } + + if interruptEvent == nil { + t.Fatal("Expected an interrupt event from the tool, but none was received") + } + + assert.NotNil(t, interruptEvent.Action.Interrupted, "Should have interrupt info") + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts, "Should have interrupt contexts") + + t.Logf("Interrupt event received with %d contexts", len(interruptEvent.Action.Interrupted.InterruptContexts)) + for i, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + t.Logf("Interrupt context %d: ID=%s, Info=%v, Address=%v", i, ctx.ID, ctx.Info, ctx.Address) + } + + var toolInterruptID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + toolInterruptID = intCtx.ID + break + } + } + assert.NotEmpty(t, toolInterruptID, "Should have a root cause interrupt ID") + + t.Logf("Attempting to resume with interrupt ID: %s", toolInterruptID) + + resumeIter, err := runner.ResumeWithParams(ctx, "test-interrupt-1", &adk.ResumeParams{ + Targets: map[string]any{ + toolInterruptID: "approved", + }, + }) + assert.NoError(t, err, "Resume should not error") + assert.NotNil(t, resumeIter, "Resume iterator should not be nil") + + var resumeEvents []*adk.AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + resumeEvents = append(resumeEvents, event) + } + + assert.NotEmpty(t, resumeEvents, "Should have resume events") + + for _, event := range resumeEvents { + assert.NoError(t, event.Err, "Resume event should not have error") + } + + var hasToolResponse, hasAssistantCompletion, hasBreakLoop bool + for _, event := range resumeEvents { + if event.Output != nil && event.Output.MessageOutput != nil { + msg := event.Output.MessageOutput.Message + if msg != nil { + if msg.Role == "tool" && strings.Contains(msg.Content, "Approved action executed") { + hasToolResponse = true + } + if msg.Role == "assistant" && strings.Contains(msg.Content, "approved") { + hasAssistantCompletion = true + } + } + } + if event.Action != nil && event.Action.BreakLoop != nil && event.Action.BreakLoop.Done { + hasBreakLoop = true + } + } + + assert.True(t, hasToolResponse, "Should have tool response with approved action") + assert.True(t, hasAssistantCompletion, "Should have assistant completion message") + assert.True(t, hasBreakLoop, "Should have break loop action indicating completion") +} + +// slowChatModel is a ChatModel that blocks for a configurable duration. +type slowChatModel struct { + delay time.Duration + response *schema.Message + startedChan chan struct{} + startedOnce sync.Once +} + +func (m *slowChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + m.startedOnce.Do(func() { + close(m.startedChan) + }) + + select { + case <-time.After(m.delay): + return m.response, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (m *slowChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + msg, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + sr, sw := schema.Pipe[*schema.Message](1) + sw.Send(msg, nil) + sw.Close() + return sr, nil +} + +func (m *slowChatModel) BindTools(tools []*schema.ToolInfo) error { return nil } +func (m *slowChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +// TestWithCancel_PlanExecute_DuringExecution verifies that cancel works +// during the executor (ChatModelAgent) phase of the PlanExecute agent. +func TestWithCancel_PlanExecute_DuringExecution(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Planner: returns a plan quickly + mockPlanner := mockAdk.NewMockAgent(ctrl) + mockPlanner.EXPECT().Name(gomock.Any()).Return("planner").AnyTimes() + mockPlanner.EXPECT().Description(gomock.Any()).Return("a planner agent").AnyTimes() + + plan := &defaultPlan{Steps: []string{"Step 1", "Step 2"}} + userInput := []adk.Message{schema.UserMessage("test task")} + + mockPlanner.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + adk.AddSessionValue(ctx, PlanSessionKey, plan) + adk.AddSessionValue(ctx, UserInputSessionKey, userInput) + planJSON, _ := sonic.MarshalString(plan) + msg := schema.AssistantMessage(planJSON, nil) + generator.Send(adk.EventFromMessage(msg, nil, schema.Assistant, "")) + generator.Close() + return iterator + }, + ).Times(1) + + // Executor: uses a slow model that we can cancel + executorStarted := make(chan struct{}) + slowModel := &slowChatModel{ + delay: 5 * time.Second, + response: schema.AssistantMessage("step result", nil), + startedChan: executorStarted, + } + + executor, err := NewExecutor(ctx, &ExecutorConfig{ + Model: slowModel, + MaxIterations: 5, + }) + assert.NoError(t, err) + + // Replanner: should not be reached since we cancel during executor + mockReplanner := mockAdk.NewMockAgent(ctrl) + mockReplanner.EXPECT().Name(gomock.Any()).Return("replanner").AnyTimes() + mockReplanner.EXPECT().Description(gomock.Any()).Return("a replanner agent").AnyTimes() + + agent, err := New(ctx, &Config{ + Planner: mockPlanner, + Executor: executor, + Replanner: mockReplanner, + MaxIterations: 5, + }) + assert.NoError(t, err) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) + + cancelOpt, cancelFn := adk.WithCancel() + iter := runner.Run(ctx, userInput, cancelOpt) + + // Wait for the executor's model to start + select { + case <-executorStarted: + case <-time.After(10 * time.Second): + t.Fatal("Executor model did not start") + } + + time.Sleep(50 * time.Millisecond) + + // Cancel should NOT return ErrExecutionEnded + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err, "Cancel during executor should succeed") + + hasCancelError := false + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *adk.CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + hasCancelError = true + } + } + + assert.True(t, hasCancelError, "Should have CancelError event") +} + +// TestWithCancel_PlanExecute_BetweenTransitions verifies that cancel works +// when fired between agent transitions (e.g., after planner, before executor starts). +func TestWithCancel_PlanExecute_BetweenTransitions(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + plannerDone := make(chan struct{}) + + // Planner: signals when done + mockPlanner := mockAdk.NewMockAgent(ctrl) + mockPlanner.EXPECT().Name(gomock.Any()).Return("planner").AnyTimes() + mockPlanner.EXPECT().Description(gomock.Any()).Return("a planner agent").AnyTimes() + + plan := &defaultPlan{Steps: []string{"Step 1"}} + userInput := []adk.Message{schema.UserMessage("test task")} + + mockPlanner.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + go func() { + defer generator.Close() + adk.AddSessionValue(ctx, PlanSessionKey, plan) + adk.AddSessionValue(ctx, UserInputSessionKey, userInput) + planJSON, _ := sonic.MarshalString(plan) + msg := schema.AssistantMessage(planJSON, nil) + generator.Send(adk.EventFromMessage(msg, nil, schema.Assistant, "")) + close(plannerDone) + }() + return iterator + }, + ).Times(1) + + // Executor: slow model to give time to observe cancel + executorModelStarted := make(chan struct{}) + slowExecModel := &slowChatModel{ + delay: 5 * time.Second, + response: schema.AssistantMessage("step result", nil), + startedChan: executorModelStarted, + } + + executor, err := NewExecutor(ctx, &ExecutorConfig{ + Model: slowExecModel, + MaxIterations: 5, + }) + assert.NoError(t, err) + + mockReplanner := mockAdk.NewMockAgent(ctrl) + mockReplanner.EXPECT().Name(gomock.Any()).Return("replanner").AnyTimes() + mockReplanner.EXPECT().Description(gomock.Any()).Return("a replanner agent").AnyTimes() + + agent, err := New(ctx, &Config{ + Planner: mockPlanner, + Executor: executor, + Replanner: mockReplanner, + MaxIterations: 5, + }) + assert.NoError(t, err) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent}) + + cancelOpt, cancelFn := adk.WithCancel() + iter := runner.Run(ctx, userInput, cancelOpt) + + // Wait for planner to finish, then cancel before executor has a chance to produce output + select { + case <-plannerDone: + case <-time.After(10 * time.Second): + t.Fatal("Planner did not finish") + } + + // Cancel after planner, during executor phase + // The executor is a ChatModelAgent which will handle the cancel + select { + case <-executorModelStarted: + case <-time.After(10 * time.Second): + t.Fatal("Executor model did not start") + } + + start := time.Now() + handle, _ := cancelFn() + err = handle.Wait() + assert.NoError(t, err, "Cancel between transitions should succeed") + + hasCancelError := false + for { + event, ok := iter.Next() + if !ok { + break + } + var ce *adk.CancelError + if event.Err != nil && errors.As(event.Err, &ce) { + hasCancelError = true + } + } + elapsed := time.Since(start) + + assert.True(t, hasCancelError, "Should have CancelError event") + assert.True(t, elapsed < 3*time.Second, "Should complete quickly after cancel, elapsed: %v", elapsed) +} diff --git a/adk/prebuilt/planexecute/utils_test.go b/adk/prebuilt/planexecute/utils_test.go new file mode 100644 index 0000000..2e01f7a --- /dev/null +++ b/adk/prebuilt/planexecute/utils_test.go @@ -0,0 +1,58 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package planexecute + +import ( + "context" + + "github.com/cloudwego/eino/adk" +) + +type outputSessionKVsAgent struct { + adk.Agent +} + +func (o *outputSessionKVsAgent) Run(ctx context.Context, input *adk.AgentInput, + options ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + + iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + + iterator_ := o.Agent.Run(ctx, input, options...) + go func() { + defer generator.Close() + for { + event, ok := iterator_.Next() + if !ok { + break + } + generator.Send(event) + } + + kvs := adk.GetSessionValues(ctx) + + event := &adk.AgentEvent{ + Output: &adk.AgentOutput{CustomizedOutput: kvs}, + } + generator.Send(event) + }() + + return iterator +} + +func agentOutputSessionKVs(_ context.Context, agent adk.Agent) (adk.Agent, error) { + return &outputSessionKVsAgent{Agent: agent}, nil +} diff --git a/adk/prebuilt/supervisor/supervisor.go b/adk/prebuilt/supervisor/supervisor.go new file mode 100644 index 0000000..62e6d1d --- /dev/null +++ b/adk/prebuilt/supervisor/supervisor.go @@ -0,0 +1,120 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package supervisor implements the supervisor pattern for multi-agent systems, +// where a central agent coordinates a set of sub-agents. +// +// # Unified Tracing +// +// The supervisor pattern provides unified tracing support through an internal container. +// When using callbacks (e.g., for tracing or observability), the entire supervisor structure +// (supervisor agent + all sub-agents) shares a single trace root. This means: +// - OnStart is invoked once at the supervisor container level +// - The callback-enriched context (containing parent span info) is propagated to all agents +// - All agents within the supervisor appear as children of the same trace root +// +// This is achieved by wrapping the supervisor structure in an internal container that acts +// as the single entry point for tracing. The container delegates all execution to the +// underlying agents while providing a unified identity for callbacks. +package supervisor + +import ( + "context" + + "github.com/cloudwego/eino/adk" +) + +// Config is the configuration for creating a supervisor-based multi-agent system. +// +// NOT RECOMMENDED: Supervisor is built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +type Config struct { + // Supervisor specifies the agent that will act as the supervisor, coordinating and managing the sub-agents. + Supervisor adk.Agent + + // SubAgents specifies the list of agents that will be supervised and coordinated by the supervisor agent. + SubAgents []adk.Agent +} + +// supervisorContainer wraps the entire supervisor structure to provide unified tracing. +// When callbacks are registered (e.g., via Runner.Query with WithCallbacks), OnStart/OnEnd +// are invoked once for this container, creating a single trace root. The callback-enriched +// context is then propagated to the supervisor and all sub-agents, ensuring they share +// the same trace parent. +// +// This container implements Agent and ResumableAgent by delegating to the inner agent. +// It provides its own Name and GetType for callback identification. +type supervisorContainer struct { + name string + inner adk.ResumableAgent +} + +func (s *supervisorContainer) Name(_ context.Context) string { + return s.name +} + +func (s *supervisorContainer) Description(ctx context.Context) string { + return s.inner.Description(ctx) +} + +func (s *supervisorContainer) GetType() string { + return "Supervisor" +} + +func (s *supervisorContainer) Run(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + return s.inner.Run(ctx, input, opts...) +} + +func (s *supervisorContainer) Resume(ctx context.Context, info *adk.ResumeInfo, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + return s.inner.Resume(ctx, info, opts...) +} + +// New creates a supervisor-based multi-agent system with the given configuration. +// +// In the supervisor pattern, a designated supervisor agent coordinates multiple sub-agents. +// The supervisor can delegate tasks to sub-agents and receive their responses, while +// sub-agents can only communicate with the supervisor (not with each other directly). +// This hierarchical structure enables complex problem-solving through coordinated agent interactions. +// +// The returned agent is wrapped in an internal container that provides unified tracing. +// When used with Runner and callbacks, all agents within the supervisor structure will +// share the same trace root, making it easy to observe the entire multi-agent execution +// as a single logical unit. +// +// NOT RECOMMENDED: Supervisor is built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +func New(ctx context.Context, conf *Config) (adk.ResumableAgent, error) { + subAgents := make([]adk.Agent, 0, len(conf.SubAgents)) + supervisorName := conf.Supervisor.Name(ctx) + for _, subAgent := range conf.SubAgents { + subAgents = append(subAgents, adk.AgentWithDeterministicTransferTo(ctx, &adk.DeterministicTransferConfig{ + Agent: subAgent, + ToAgentNames: []string{supervisorName}, + })) + } + + inner, err := adk.SetSubAgents(ctx, conf.Supervisor, subAgents) + if err != nil { + return nil, err + } + + return &supervisorContainer{ + name: supervisorName, + inner: inner, + }, nil +} diff --git a/adk/prebuilt/supervisor/supervisor_test.go b/adk/prebuilt/supervisor/supervisor_test.go new file mode 100644 index 0000000..2dbef75 --- /dev/null +++ b/adk/prebuilt/supervisor/supervisor_test.go @@ -0,0 +1,1159 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package supervisor + +import ( + "context" + "fmt" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockAdk "github.com/cloudwego/eino/internal/mock/adk" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +// TestNewSupervisor tests the New function +func TestNewSupervisor(t *testing.T) { + ctx := context.Background() + + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create mock agents + supervisorAgent := mockAdk.NewMockAgent(ctrl) + subAgent1 := mockAdk.NewMockAgent(ctrl) + subAgent2 := mockAdk.NewMockAgent(ctrl) + + supervisorAgent.EXPECT().Name(gomock.Any()).Return("SupervisorAgent").AnyTimes() + supervisorAgent.EXPECT().Description(gomock.Any()).Return("Supervisor agent description").AnyTimes() + subAgent1.EXPECT().Name(gomock.Any()).Return("SubAgent1").AnyTimes() + subAgent2.EXPECT().Name(gomock.Any()).Return("SubAgent2").AnyTimes() + + aMsg, tMsg := adk.GenTransferMessages(ctx, "SubAgent1") + i, g := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + g.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, "")) + event := adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName) + event.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "SubAgent1"}} + g.Send(event) + g.Close() + supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1) + + i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]() + subAgent1Msg := schema.AssistantMessage("SubAgent1", nil) + g.Send(adk.EventFromMessage(subAgent1Msg, nil, schema.Assistant, "")) + g.Close() + subAgent1.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1) + + aMsg, tMsg = adk.GenTransferMessages(ctx, "SubAgent2 message") + i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]() + g.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, "")) + event = adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName) + event.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "SubAgent2"}} + g.Send(event) + g.Close() + supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1) + + i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]() + subAgent2Msg := schema.AssistantMessage("SubAgent2 message", nil) + g.Send(adk.EventFromMessage(subAgent2Msg, nil, schema.Assistant, "")) + g.Close() + subAgent2.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1) + + i, g = adk.NewAsyncIteratorPair[*adk.AgentEvent]() + finishMsg := schema.AssistantMessage("finish", nil) + g.Send(adk.EventFromMessage(finishMsg, nil, schema.Assistant, "")) + g.Close() + supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i).Times(1) + + conf := &Config{ + Supervisor: supervisorAgent, + SubAgents: []adk.Agent{subAgent1, subAgent2}, + } + + multiAgent, err := New(ctx, conf) + assert.NoError(t, err) + assert.NotNil(t, multiAgent) + assert.Equal(t, "SupervisorAgent", multiAgent.Name(ctx)) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: multiAgent}) + aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("test")}) + + // transfer to agent1 + event, ok := aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SupervisorAgent", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls)) + + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SupervisorAgent", event.AgentName) + assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role) + assert.Equal(t, "SubAgent1", event.Action.TransferToAgent.DestAgentName) + + // agent1's output + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SubAgent1", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.Equal(t, subAgent1Msg.Content, event.Output.MessageOutput.Message.Content) + + // transfer back to supervisor + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SubAgent1", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls)) + + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SubAgent1", event.AgentName) + assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role) + assert.Equal(t, "SupervisorAgent", event.Action.TransferToAgent.DestAgentName) + + // transfer to agent2 + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SupervisorAgent", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls)) + + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SupervisorAgent", event.AgentName) + assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role) + assert.Equal(t, "SubAgent2", event.Action.TransferToAgent.DestAgentName) + + // agent1's output + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SubAgent2", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.Equal(t, subAgent2Msg.Content, event.Output.MessageOutput.Message.Content) + + // transfer back to supervisor + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SubAgent2", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.NotEqual(t, 0, len(event.Output.MessageOutput.Message.ToolCalls)) + + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SubAgent2", event.AgentName) + assert.Equal(t, schema.Tool, event.Output.MessageOutput.Role) + assert.Equal(t, "SupervisorAgent", event.Action.TransferToAgent.DestAgentName) + + // finish + event, ok = aIter.Next() + assert.True(t, ok) + assert.Equal(t, "SupervisorAgent", event.AgentName) + assert.Equal(t, schema.Assistant, event.Output.MessageOutput.Role) + assert.Equal(t, finishMsg.Content, event.Output.MessageOutput.Message.Content) +} + +type approvalInfo struct { + ToolName string + ArgumentsInJSON string + ToolCallID string +} + +func (ai *approvalInfo) String() string { + return fmt.Sprintf("tool '%s' interrupted with arguments '%s', waiting for approval", + ai.ToolName, ai.ArgumentsInJSON) +} + +type approvalResult struct { + Approved bool + DisapproveReason *string +} + +func init() { + schema.Register[*approvalInfo]() + schema.Register[*approvalResult]() +} + +type approvableTool struct { + name string + t *testing.T +} + +func (m *approvableTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: m.name, + Desc: "A tool that requires approval before execution", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "action": {Type: schema.String, Desc: "The action to perform"}, + }), + }, nil +} + +func (m *approvableTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + wasInterrupted, _, storedArguments := tool.GetInterruptState[string](ctx) + if !wasInterrupted { + return "", tool.StatefulInterrupt(ctx, &approvalInfo{ + ToolName: m.name, + ArgumentsInJSON: argumentsInJSON, + ToolCallID: compose.GetToolCallID(ctx), + }, argumentsInJSON) + } + + isResumeTarget, hasData, data := tool.GetResumeContext[*approvalResult](ctx) + if !isResumeTarget { + return "", tool.StatefulInterrupt(ctx, &approvalInfo{ + ToolName: m.name, + ArgumentsInJSON: storedArguments, + ToolCallID: compose.GetToolCallID(ctx), + }, storedArguments) + } + + if !hasData { + return "", fmt.Errorf("tool '%s' resumed with no data", m.name) + } + + if data.Approved { + return fmt.Sprintf("Tool '%s' executed successfully with args: %s", m.name, storedArguments), nil + } + + if data.DisapproveReason != nil { + return fmt.Sprintf("Tool '%s' disapproved, reason: %s", m.name, *data.DisapproveReason), nil + } + + return fmt.Sprintf("Tool '%s' disapproved", m.name), nil +} + +type checkpointStore struct { + data map[string][]byte +} + +func newCheckpointStore() *checkpointStore { + return &checkpointStore{data: make(map[string][]byte)} +} + +func (s *checkpointStore) Set(_ context.Context, key string, value []byte) error { + s.data[key] = value + return nil +} + +func (s *checkpointStore) Get(_ context.Context, key string) ([]byte, bool, error) { + v, ok := s.data[key] + return v, ok, nil +} + +type namedAgent struct { + adk.ResumableAgent + name string + description string +} + +func (n *namedAgent) Name(_ context.Context) string { + return n.name +} + +func (n *namedAgent) Description(_ context.Context) string { + return n.description +} + +func TestNestedSupervisorInterruptResume(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockOuterSupervisorModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockInnerSupervisorModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockWorkerModel := mockModel.NewMockToolCallingChatModel(ctrl) + + paymentTool := &approvableTool{name: "process_payment", t: t} + + userInput := []adk.Message{schema.UserMessage("Process a payment of $1000")} + + mockWorkerModel.EXPECT().WithTools(gomock.Any()).Return(mockWorkerModel, nil).AnyTimes() + + workerToolCallMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_payment_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "process_payment", + Arguments: `{"action": "process $1000 payment"}`, + }, + }, + }) + mockWorkerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(workerToolCallMsg, nil).Times(1) + + workerCompletionMsg := schema.AssistantMessage("Payment processed successfully", nil) + mockWorkerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(workerCompletionMsg, nil).AnyTimes() + + workerAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "payment_worker", + Description: "the agent responsible for processing payments", + Instruction: "You are a payment processing worker. Use the process_payment tool to handle payments.", + Model: mockWorkerModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{paymentTool}, + }, + }, + }) + assert.NoError(t, err) + + mockInnerSupervisorModel.EXPECT().WithTools(gomock.Any()).Return(mockInnerSupervisorModel, nil).AnyTimes() + + innerTransferMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "inner_transfer_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "transfer_to_agent", + Arguments: `{"agent_name":"payment_worker"}`, + }, + }, + }) + mockInnerSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(innerTransferMsg, nil).Times(1) + + innerFinalMsg := schema.AssistantMessage("Payment has been processed and approved.", nil) + mockInnerSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(innerFinalMsg, nil).AnyTimes() + + innerSupervisorChatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "payment_supervisor", + Description: "the supervisor agent responsible for payment operations", + Instruction: "You are a payment supervisor. Delegate payment tasks to payment_worker.", + Model: mockInnerSupervisorModel, + Exit: &adk.ExitTool{}, + }) + assert.NoError(t, err) + + innerSupervisorAgent, err := New(ctx, &Config{ + Supervisor: innerSupervisorChatAgent, + SubAgents: []adk.Agent{workerAgent}, + }) + assert.NoError(t, err) + + innerSupervisorWrapped := &namedAgent{ + ResumableAgent: innerSupervisorAgent, + name: "payment_department", + description: "the department responsible for all payment-related operations", + } + + mockOuterSupervisorModel.EXPECT().WithTools(gomock.Any()).Return(mockOuterSupervisorModel, nil).AnyTimes() + + outerTransferMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "outer_transfer_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "transfer_to_agent", + Arguments: `{"agent_name":"payment_department"}`, + }, + }, + }) + mockOuterSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(outerTransferMsg, nil).Times(1) + + outerFinalMsg := schema.AssistantMessage("The payment request has been fully processed by the payment department.", nil) + mockOuterSupervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(outerFinalMsg, nil).AnyTimes() + + outerSupervisorChatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "company_coordinator", + Description: "the top-level coordinator for company operations", + Instruction: "You are the company coordinator. Route payment requests to payment_department.", + Model: mockOuterSupervisorModel, + Exit: &adk.ExitTool{}, + }) + assert.NoError(t, err) + + outerSupervisorAgent, err := New(ctx, &Config{ + Supervisor: outerSupervisorChatAgent, + SubAgents: []adk.Agent{innerSupervisorWrapped}, + }) + assert.NoError(t, err) + + outerSupervisorWrapped := &namedAgent{ + ResumableAgent: outerSupervisorAgent, + name: "headquarters", + description: "the company headquarters that coordinates all departments", + } + + store := newCheckpointStore() + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: outerSupervisorWrapped, + CheckPointStore: store, + }) + + t.Log("========================================") + t.Log("Starting Nested Supervisor Integration Test (with namedAgent wrappers)") + t.Log("Hierarchy: headquarters(wrapper) -> company_coordinator -> payment_department(wrapper) -> payment_supervisor -> payment_worker -> process_payment tool") + t.Log("========================================") + + checkpointID := "test-nested-supervisor-1" + iter := runner.Run(ctx, userInput, adk.WithCheckPointID(checkpointID)) + + var interruptEvent *adk.AgentEvent + eventCount := 0 + for { + event, ok := iter.Next() + if !ok { + break + } + eventCount++ + + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + t.Log("INTERRUPT DETECTED - Deep interrupt from tool within nested supervisor") + break + } + } + + if interruptEvent == nil { + t.Fatal("Expected an interrupt event from the process_payment tool, but none was received") + } + + assert.NotNil(t, interruptEvent.Action.Interrupted, "Should have interrupt info") + assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts, "Should have interrupt contexts") + + var toolInterruptID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + toolInterruptID = intCtx.ID + break + } + } + assert.NotEmpty(t, toolInterruptID, "Should have a root cause interrupt ID") + + t.Logf("Resuming with approval for interrupt ID: %s", toolInterruptID) + + resumeIter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{ + Targets: map[string]any{ + toolInterruptID: &approvalResult{Approved: true}, + }, + }) + assert.NoError(t, err, "Resume should not error") + assert.NotNil(t, resumeIter, "Resume iterator should not be nil") + + var resumeEvents []*adk.AgentEvent + for { + event, ok := resumeIter.Next() + if !ok { + break + } + resumeEvents = append(resumeEvents, event) + } + + assert.NotEmpty(t, resumeEvents, "Should have resume events after approval") + + for _, event := range resumeEvents { + assert.NoError(t, event.Err, "Resume event should not have error") + } + + var hasToolResponse, hasTransferBack bool + for _, event := range resumeEvents { + if event.Output != nil && event.Output.MessageOutput != nil { + msg := event.Output.MessageOutput.Message + if msg != nil && msg.Role == "tool" && strings.Contains(msg.Content, "executed successfully") { + hasToolResponse = true + } + } + if event.Action != nil && event.Action.TransferToAgent != nil { + if event.Action.TransferToAgent.DestAgentName == "company_coordinator" { + hasTransferBack = true + } + } + } + + assert.True(t, hasToolResponse, "Should have tool response indicating successful payment processing") + assert.True(t, hasTransferBack, "Should have transfer back to outer supervisor indicating completion") +} + +func TestSupervisorExit(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + supervisorAgent := mockAdk.NewMockAgent(ctrl) + subAgent := mockAdk.NewMockAgent(ctrl) + + supervisorAgent.EXPECT().Name(gomock.Any()).Return("Supervisor").AnyTimes() + supervisorAgent.EXPECT().Description(gomock.Any()).Return("Supervisor description").AnyTimes() + subAgent.EXPECT().Name(gomock.Any()).Return("SubAgent").AnyTimes() + + // 1. Supervisor transfers to SubAgent + aMsg, tMsg := adk.GenTransferMessages(ctx, "SubAgent") + i1, g1 := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + g1.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, "")) + event1 := adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName) + event1.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "SubAgent"}} + g1.Send(event1) + g1.Close() + supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i1).Times(1) + + // 2. SubAgent emits Exit action + i2, g2 := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + exitEvent := &adk.AgentEvent{ + AgentName: "SubAgent", + Action: &adk.AgentAction{Exit: true}, + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Role: schema.Assistant, + Message: schema.AssistantMessage("Exiting...", nil), + }, + }, + } + g2.Send(exitEvent) + g2.Close() + subAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i2).Times(1) + + conf := &Config{ + Supervisor: supervisorAgent, + SubAgents: []adk.Agent{subAgent}, + } + + multiAgent, err := New(ctx, conf) + assert.NoError(t, err) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: multiAgent}) + aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("test")}) + + // Collect events + var events []*adk.AgentEvent + for { + event, ok := aIter.Next() + if !ok { + break + } + events = append(events, event) + } + + foundExit := false + foundTransferBack := false + + for _, e := range events { + if e.Action != nil { + if e.Action.Exit { + foundExit = true + } + if e.Action.TransferToAgent != nil && e.Action.TransferToAgent.DestAgentName == "Supervisor" { + foundTransferBack = true + } + } + } + + assert.True(t, foundExit, "Should have found Exit action") + assert.False(t, foundTransferBack, "Should NOT have found Transfer back to Supervisor after Exit") +} + +func TestNestedSupervisorExit(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + topSupervisor := mockAdk.NewMockAgent(ctrl) + midSupervisor := mockAdk.NewMockAgent(ctrl) + worker := mockAdk.NewMockAgent(ctrl) + + topSupervisor.EXPECT().Name(gomock.Any()).Return("TopSupervisor").AnyTimes() + topSupervisor.EXPECT().Description(gomock.Any()).Return("Top supervisor description").AnyTimes() + midSupervisor.EXPECT().Name(gomock.Any()).Return("MidSupervisor").AnyTimes() + midSupervisor.EXPECT().Description(gomock.Any()).Return("Mid supervisor description").AnyTimes() + worker.EXPECT().Name(gomock.Any()).Return("Worker").AnyTimes() + + // 1. TopSupervisor transfers to MidSupervisor + aMsg1, tMsg1 := adk.GenTransferMessages(ctx, "MidSupervisor") + i1, g1 := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + g1.Send(adk.EventFromMessage(aMsg1, nil, schema.Assistant, "")) + event1 := adk.EventFromMessage(tMsg1, nil, schema.Tool, tMsg1.ToolName) + event1.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "MidSupervisor"}} + g1.Send(event1) + g1.Close() + topSupervisor.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i1).AnyTimes() + + // 2. MidSupervisor transfers to Worker + aMsg2, tMsg2 := adk.GenTransferMessages(ctx, "Worker") + i2, g2 := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + g2.Send(adk.EventFromMessage(aMsg2, nil, schema.Assistant, "")) + event2 := adk.EventFromMessage(tMsg2, nil, schema.Tool, tMsg2.ToolName) + event2.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "Worker"}} + g2.Send(event2) + g2.Close() + midSupervisor.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i2).AnyTimes() + + // 3. Worker emits Exit action + i3, g3 := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + exitEvent := &adk.AgentEvent{ + AgentName: "Worker", + Action: &adk.AgentAction{Exit: true}, + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Role: schema.Assistant, + Message: schema.AssistantMessage("Worker Exiting...", nil), + }, + }, + } + g3.Send(exitEvent) + g3.Close() + worker.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).Return(i3).Times(1) + + // Build Nested System + // Mid System: MidSupervisor -> [Worker] + midSystem, err := New(ctx, &Config{ + Supervisor: midSupervisor, + SubAgents: []adk.Agent{worker}, + }) + assert.NoError(t, err) + // We need to give the midSystem the name "MidSupervisor" so TopSupervisor can find it + // supervisor.New returns a ResumableAgent that delegates Name() to the supervisor agent. + // So midSystem.Name() should already be "MidSupervisor" because midSupervisor.Name() is "MidSupervisor". + + // Top System: TopSupervisor -> [midSystem] + topSystem, err := New(ctx, &Config{ + Supervisor: topSupervisor, + SubAgents: []adk.Agent{midSystem}, + }) + assert.NoError(t, err) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: topSystem}) + aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("test nested exit")}) + + // Collect events + var events []*adk.AgentEvent + for { + event, ok := aIter.Next() + if !ok { + break + } + events = append(events, event) + } + + foundExit := false + foundTransferBackToMidAfterExit := false + foundTransferBackToTopAfterExit := false + + for _, e := range events { + if e.Action != nil { + if e.Action.Exit { + foundExit = true + } + if foundExit && e.Action.TransferToAgent != nil { + if e.Action.TransferToAgent.DestAgentName == "MidSupervisor" { + foundTransferBackToMidAfterExit = true + } + if e.Action.TransferToAgent.DestAgentName == "TopSupervisor" { + foundTransferBackToTopAfterExit = true + } + } + } + } + + assert.True(t, foundExit, "Should have found Exit action") + assert.False(t, foundTransferBackToMidAfterExit, "Should NOT have found Transfer back to MidSupervisor after Exit") + assert.False(t, foundTransferBackToTopAfterExit, "Should NOT have found Transfer back to TopSupervisor after Exit") +} + +func TestChatModelAgentInternalEventsExit(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + supervisorAgent := mockAdk.NewMockAgent(ctrl) + workerModel := mockModel.NewMockToolCallingChatModel(ctrl) + innerAgent := mockAdk.NewMockAgent(ctrl) + + supervisorAgent.EXPECT().Name(gomock.Any()).Return("Supervisor").AnyTimes() + supervisorAgent.EXPECT().Description(gomock.Any()).Return("Supervisor description").AnyTimes() + innerAgent.EXPECT().Name(gomock.Any()).Return("InnerAgent").AnyTimes() + innerAgent.EXPECT().Description(gomock.Any()).Return("Inner Agent Description").AnyTimes() + + // 1. Supervisor transfers to Worker (only once, then exits when worker transfers back) + supervisorRunCount := 0 + supervisorAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + supervisorRunCount++ + iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + go func() { + defer gen.Close() + if supervisorRunCount == 1 { + aMsg, tMsg := adk.GenTransferMessages(ctx, "Worker") + gen.Send(adk.EventFromMessage(aMsg, nil, schema.Assistant, "")) + event1 := adk.EventFromMessage(tMsg, nil, schema.Tool, tMsg.ToolName) + event1.Action = &adk.AgentAction{TransferToAgent: &adk.TransferToAgentAction{DestAgentName: "Worker"}} + gen.Send(event1) + } else { + exitEvent := &adk.AgentEvent{ + AgentName: "Supervisor", + Action: &adk.AgentAction{Exit: true}, + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Role: schema.Assistant, + Message: schema.AssistantMessage("Supervisor done", nil), + }, + }, + } + gen.Send(exitEvent) + } + }() + return iter + }).AnyTimes() + + // 2. Worker runs, calls AgentTool (InnerAgent) + // Mock WorkerModel behavior + workerModel.EXPECT().WithTools(gomock.Any()).Return(workerModel, nil).AnyTimes() + + // 2.1 Worker generates tool call + toolCallMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_inner_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "InnerAgent", + Arguments: `{"request": "do exit"}`, + }, + }, + }) + workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(toolCallMsg, nil).Times(1) + + // 2.2 InnerAgent runs and emits Exit + innerAgent.EXPECT().Run(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() + go func() { + defer gen.Close() + innerExitEvent := &adk.AgentEvent{ + AgentName: "InnerAgent", + Action: &adk.AgentAction{Exit: true}, + RunPath: []adk.RunStep{}, + Output: &adk.AgentOutput{ + MessageOutput: &adk.MessageVariant{ + Role: schema.Assistant, + Message: schema.AssistantMessage("Inner Exiting...", nil), + }, + }, + } + gen.Send(innerExitEvent) + }() + return iter + }).AnyTimes() + + // 2.3 Worker receives tool result (empty string or whatever AgentTool returns on exit/interrupt) + // AgentTool implementation details: if Exit action is present, it returns whatever output is there. + // The Exit action itself is passed as internal event. + + // 2.4 Worker generates final response + finalMsg := schema.AssistantMessage("Worker Finished", nil) + workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(finalMsg, nil).AnyTimes() + + // Build Worker Agent + agentTool := adk.NewAgentTool(ctx, innerAgent) + workerAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "Worker", + Description: "Worker Agent", + Model: workerModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{agentTool}, + }, + EmitInternalEvents: true, // Key configuration + }, + }) + assert.NoError(t, err) + + // Build System + sys, err := New(ctx, &Config{ + Supervisor: supervisorAgent, + SubAgents: []adk.Agent{workerAgent}, + }) + assert.NoError(t, err) + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: sys}) + aIter := runner.Run(ctx, []adk.Message{schema.UserMessage("start")}) + + // Collect events + var events []*adk.AgentEvent + for { + event, ok := aIter.Next() + if !ok { + break + } + events = append(events, event) + } + + foundInnerExit := false + foundTransferBack := false + + for _, e := range events { + // Check for InnerAgent exit event (propagated as internal event) + if e.AgentName == "InnerAgent" && e.Action != nil && e.Action.Exit { + foundInnerExit = true + } + + // Check for transfer back to Supervisor + if e.AgentName == "Worker" && e.Action != nil && e.Action.TransferToAgent != nil && + e.Action.TransferToAgent.DestAgentName == "Supervisor" { + foundTransferBack = true + } + } + + assert.True(t, foundInnerExit, "Should have captured InnerAgent Exit event") + assert.True(t, foundTransferBack, "Should have found Transfer back to Supervisor (Worker should NOT be considered exited)") +} + +func TestSupervisorContainerUnifiedTracing(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + supervisorModel := mockModel.NewMockToolCallingChatModel(ctrl) + subAgentModel := mockModel.NewMockToolCallingChatModel(ctrl) + + supervisorModel.EXPECT().WithTools(gomock.Any()).Return(supervisorModel, nil).AnyTimes() + subAgentModel.EXPECT().WithTools(gomock.Any()).Return(subAgentModel, nil).AnyTimes() + + transferMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "transfer_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "transfer_to_agent", + Arguments: `{"agent_name":"SubAgent"}`, + }, + }, + }) + supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(transferMsg, nil).Times(1) + + subAgentResponse := schema.AssistantMessage("SubAgent response", nil) + subAgentModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(subAgentResponse, nil).Times(1) + + finalResponse := schema.AssistantMessage("Final response", nil) + supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(finalResponse, nil).Times(1) + + supervisorAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "SupervisorAgent", + Description: "Supervisor agent", + Instruction: "You are a supervisor", + Model: supervisorModel, + }) + assert.NoError(t, err) + + subAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "SubAgent", + Description: "Sub agent", + Instruction: "You are a sub agent", + Model: subAgentModel, + }) + assert.NoError(t, err) + + multiAgent, err := New(ctx, &Config{ + Supervisor: supervisorAgent, + SubAgents: []adk.Agent{subAgent}, + }) + assert.NoError(t, err) + + assert.Equal(t, "SupervisorAgent", multiAgent.Name(ctx)) + + typer, ok := multiAgent.(components.Typer) + assert.True(t, ok, "Should implement components.Typer") + assert.Equal(t, "Supervisor", typer.GetType()) + + var mu sync.Mutex + var onStartCalls []string + var onEndCalls []string + + handler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != adk.ComponentOfAgent { + return ctx + } + mu.Lock() + onStartCalls = append(onStartCalls, info.Name+":"+info.Type) + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != adk.ComponentOfAgent { + return ctx + } + mu.Lock() + onEndCalls = append(onEndCalls, info.Name+":"+info.Type) + mu.Unlock() + if agentOutput := adk.ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: multiAgent}) + iter := runner.Query(ctx, "hello", adk.WithCallbacks(handler)) + + for { + _, ok := iter.Next() + if !ok { + break + } + } + + mu.Lock() + defer mu.Unlock() + + assert.NotEmpty(t, onStartCalls, "Should have OnStart calls") + assert.Contains(t, onStartCalls, "SupervisorAgent:Supervisor", "Should have supervisor container OnStart with type 'Supervisor'") +} + +type traceContextKey struct{} + +func TestSupervisorContainerUnifiedTracingOnResume(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + supervisorModel := mockModel.NewMockToolCallingChatModel(ctrl) + workerModel := mockModel.NewMockToolCallingChatModel(ctrl) + + supervisorModel.EXPECT().WithTools(gomock.Any()).Return(supervisorModel, nil).AnyTimes() + workerModel.EXPECT().WithTools(gomock.Any()).Return(workerModel, nil).AnyTimes() + + paymentTool := &approvableTool{name: "process_payment", t: t} + + workerToolCallMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_payment_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "process_payment", + Arguments: `{"action": "process $1000 payment"}`, + }, + }, + }) + workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(workerToolCallMsg, nil).Times(1) + + workerCompletionMsg := schema.AssistantMessage("Payment processed successfully", nil) + workerModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(workerCompletionMsg, nil).AnyTimes() + + workerAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "Worker", + Description: "Worker agent", + Instruction: "You are a worker", + Model: workerModel, + ToolsConfig: adk.ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{paymentTool}, + }, + }, + }) + assert.NoError(t, err) + + transferMsg := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "transfer_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "transfer_to_agent", + Arguments: `{"agent_name":"Worker"}`, + }, + }, + }) + supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(transferMsg, nil).Times(1) + + finalResponse := schema.AssistantMessage("Final response", nil) + supervisorModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(finalResponse, nil).AnyTimes() + + supervisorAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "SupervisorAgent", + Description: "Supervisor agent", + Instruction: "You are a supervisor", + Model: supervisorModel, + Exit: &adk.ExitTool{}, + }) + assert.NoError(t, err) + + multiAgent, err := New(ctx, &Config{ + Supervisor: supervisorAgent, + SubAgents: []adk.Agent{workerAgent}, + }) + assert.NoError(t, err) + + store := newCheckpointStore() + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: multiAgent, + CheckPointStore: store, + }) + + var mu sync.Mutex + var runOnStartCalls []string + var resumeOnStartCalls []string + var resumeParentTraceIDs []string + + runHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != adk.ComponentOfAgent { + return ctx + } + mu.Lock() + runOnStartCalls = append(runOnStartCalls, info.Name+":"+info.Type) + mu.Unlock() + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != adk.ComponentOfAgent { + return ctx + } + if agentOutput := adk.ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + checkpointID := "test-unified-tracing-resume" + iter := runner.Run(ctx, []adk.Message{schema.UserMessage("Process payment")}, adk.WithCallbacks(runHandler), adk.WithCheckPointID(checkpointID)) + + var interruptEvent *adk.AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + break + } + } + + assert.NotNil(t, interruptEvent, "Should have interrupt event") + + var toolInterruptID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + toolInterruptID = intCtx.ID + break + } + } + assert.NotEmpty(t, toolInterruptID, "Should have a root cause interrupt ID") + + mu.Lock() + t.Logf("Run OnStart calls: %v", runOnStartCalls) + assert.Contains(t, runOnStartCalls, "SupervisorAgent:Supervisor", "Run should have supervisor container OnStart") + mu.Unlock() + + resumeHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component != adk.ComponentOfAgent { + return ctx + } + mu.Lock() + resumeOnStartCalls = append(resumeOnStartCalls, info.Name+":"+info.Type) + parentID, _ := ctx.Value(traceContextKey{}).(string) + resumeParentTraceIDs = append(resumeParentTraceIDs, info.Name+":parent="+parentID) + mu.Unlock() + if info.Type == "Supervisor" { + return context.WithValue(ctx, traceContextKey{}, "supervisor-trace-id") + } + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if info.Component != adk.ComponentOfAgent { + return ctx + } + if agentOutput := adk.ConvAgentCallbackOutput(output); agentOutput != nil && agentOutput.Events != nil { + go func() { + for { + _, ok := agentOutput.Events.Next() + if !ok { + break + } + } + }() + } + return ctx + }). + Build() + + resumeIter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{ + Targets: map[string]any{ + toolInterruptID: &approvalResult{Approved: true}, + }, + }, adk.WithCallbacks(resumeHandler)) + assert.NoError(t, err) + + for { + event, ok := resumeIter.Next() + if !ok { + break + } + assert.NoError(t, event.Err) + } + + mu.Lock() + defer mu.Unlock() + + t.Logf("Resume OnStart calls: %v", resumeOnStartCalls) + t.Logf("Resume parent trace IDs: %v", resumeParentTraceIDs) + assert.NotEmpty(t, resumeOnStartCalls, "Should have OnStart calls during resume") + assert.Contains(t, resumeOnStartCalls, "SupervisorAgent:Supervisor", "Resume should have supervisor container OnStart with type 'Supervisor'") + + foundInnerSupervisorWithParent := false + for _, entry := range resumeParentTraceIDs { + if strings.Contains(entry, "SupervisorAgent") && !strings.Contains(entry, "parent=supervisor-trace-id") && entry != "SupervisorAgent:parent=" { + if strings.Contains(resumeOnStartCalls[0], "Supervisor") { + continue + } + } + if strings.Contains(entry, "parent=supervisor-trace-id") { + foundInnerSupervisorWithParent = true + } + } + assert.True(t, foundInnerSupervisorWithParent, + "Inner agents should have parent trace from Supervisor container during Resume. Got: %v", resumeParentTraceIDs) +} diff --git a/adk/react.go b/adk/react.go new file mode 100644 index 0000000..03ef045 --- /dev/null +++ b/adk/react.go @@ -0,0 +1,823 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "io" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// ErrExceedMaxIterations indicates the agent reached the maximum iterations limit. +var ErrExceedMaxIterations = errors.New("exceeds max iterations") + +type typedState[M MessageType] struct { + Messages []M + Extra map[string]any + + // ToolInfos contains the tool definitions passed to the model via model.WithTools. + // Managed by the framework and modifiable by BeforeModelRewriteState handlers. + ToolInfos []*schema.ToolInfo + + // DeferredToolInfos contains tool definitions for server-side deferred retrieval, + // passed to the model via model.WithDeferredTools. Nil when not in use. + DeferredToolInfos []*schema.ToolInfo + + // Internal fields below - do not access directly. + // Kept exported for backward compatibility with existing checkpoints. + HasReturnDirectly bool + ReturnDirectlyToolCallID string + ToolGenActions map[string]*AgentAction + AgentName string + RemainingIterations int + ReturnDirectlyEvent *TypedAgentEvent[M] + RetryAttempt int + ToolMsgIDs map[string]map[string]string // toolName → callID → eino message ID +} + +// State is the internal state of the ChatModelAgent. +// +// Deprecated: State is exported only for checkpoint backward compatibility. +// Do not use it directly. +type State = typedState[*schema.Message] + +type agenticState = typedState[*schema.AgenticMessage] + +const ( + stateGobNameV07 = "_eino_adk_react_state" + + // stateGobNameV080 is a v0.8.0-v0.8.3-only alias used after byte-patching + // raw checkpoint bytes in preprocessADKCheckpoint. + // It must stay the same byte length as stateGobNameV07 so the length-prefixed + // gob string in the stream remains valid. + stateGobNameV080 = "_eino_adk_state_v080_" +) + +func init() { + // Checkpoint compatibility notes: + // - ADK/compose checkpoints are gob-encoded and may store state behind `any`, so gob relies on + // an on-wire type name to choose a local Go type. + // - Gob allows only one local Go type per name, and it treats "struct wire" and "GobEncoder wire" + // as incompatible even if the name matches. + // + // This file maintains 2 epochs of *State decoding: + // - v0.7.* and current: "_eino_adk_react_state" + struct wire → decode into *State directly. + // State's exported fields are a superset of v0.7, so gob handles missing fields gracefully. + // - v0.8.0-v0.8.3: "_eino_adk_react_state" + GobEncoder wire → byte-patched to stateGobNameV080, + // decode into stateV080 and migrate. + schema.RegisterName[*State](stateGobNameV07) + schema.RegisterName[*stateV080](stateGobNameV080) + + schema.RegisterName[*typedState[*schema.AgenticMessage]]("_eino_adk_agentic_state") + schema.RegisterName[*TypedAgentEvent[*schema.AgenticMessage]]("_eino_adk_agentic_event") + + // backward compatibility when decoding checkpoints created by v0.8.0 - v0.8.3 + gob.Register(&AgentEvent{}) + gob.Register(0) + + schema.RegisterName[*TypedAgentInput[*schema.AgenticMessage]]("_eino_adk_agentic_agent_input") + schema.RegisterName[*typedAgentEventWrapper[*schema.AgenticMessage]]("_eino_adk_agentic_event_wrapper") + schema.RegisterName[*[]*typedAgentEventWrapper[*schema.AgenticMessage]]("_eino_adk_agentic_event_wrapper_slice") + schema.RegisterName[*reactInput]("_eino_adk_react_input") + schema.RegisterName[*agenticReactInput]("_eino_adk_agentic_react_input") +} + +func (s *typedState[M]) getReturnDirectlyEvent() *TypedAgentEvent[M] { + return s.ReturnDirectlyEvent +} + +func (s *typedState[M]) setReturnDirectlyEvent(event *TypedAgentEvent[M]) { + s.ReturnDirectlyEvent = event +} + +func (s *typedState[M]) getRetryAttempt() int { + return s.RetryAttempt +} + +func (s *typedState[M]) setRetryAttempt(attempt int) { + s.RetryAttempt = attempt +} + +func (s *typedState[M]) getReturnDirectlyToolCallID() string { + return s.ReturnDirectlyToolCallID +} + +func (s *typedState[M]) setReturnDirectlyToolCallID(id string) { + s.ReturnDirectlyToolCallID = id + s.HasReturnDirectly = id != "" +} + +func (s *typedState[M]) getToolGenActions() map[string]*AgentAction { + return s.ToolGenActions +} + +func (s *typedState[M]) setToolGenAction(key string, action *AgentAction) { + if s.ToolGenActions == nil { + s.ToolGenActions = make(map[string]*AgentAction) + } + s.ToolGenActions[key] = action +} + +func (s *typedState[M]) popToolGenAction(key string) *AgentAction { + if s.ToolGenActions == nil { + return nil + } + action := s.ToolGenActions[key] + delete(s.ToolGenActions, key) + return action +} + +func (s *typedState[M]) setToolMsgID(toolName, callID, msgID string) { + if s.ToolMsgIDs == nil { + s.ToolMsgIDs = make(map[string]map[string]string) + } + byCall := s.ToolMsgIDs[toolName] + if byCall == nil { + byCall = make(map[string]string) + s.ToolMsgIDs[toolName] = byCall + } + byCall[callID] = msgID +} + +func (s *typedState[M]) popToolMsgID(toolName, callID string) string { + if s.ToolMsgIDs == nil { + return "" + } + byCall := s.ToolMsgIDs[toolName] + if byCall == nil { + return "" + } + id := byCall[callID] + delete(byCall, callID) + if len(byCall) == 0 { + delete(s.ToolMsgIDs, toolName) + } + return id +} + +func (s *typedState[M]) getRemainingIterations() int { + return s.RemainingIterations +} + +func (s *typedState[M]) setRemainingIterations(iterations int) { + s.RemainingIterations = iterations +} + +func (s *typedState[M]) decrementRemainingIterations() { + current := s.getRemainingIterations() + s.RemainingIterations = current - 1 +} + +// stateV080 handles the v0.8.0-v0.8.3 checkpoint format. +// In those versions, *State implemented GobEncoder and was registered under +// "_eino_adk_react_state". GobEncode serialized a stateSerialization struct +// into opaque bytes. This type's GobDecode reads that format. +// It is registered under "_eino_adk_state_v080_" — a same-length alias used +// only after byte-patching the checkpoint data in preprocessADKCheckpoint. +type stateV080 struct { + Messages []Message + HasReturnDirectly bool + ReturnDirectlyToolCallID string + ToolGenActions map[string]*AgentAction + AgentName string + RemainingIterations int + RetryAttempt int + ReturnDirectlyEvent *AgentEvent + Extra map[string]any + Internals map[string]any +} + +// stateV080Serialization is the on-wire format that v0.8.0-v0.8.3 GobEncode produced. +// It is only used by stateV080.GobDecode to parse those legacy opaque bytes. +type stateV080Serialization stateV080 + +func (sc *stateV080) GobDecode(b []byte) error { + ss := &stateV080Serialization{} + if err := gob.NewDecoder(bytes.NewReader(b)).Decode(ss); err != nil { + return err + } + sc.Messages = ss.Messages + sc.HasReturnDirectly = ss.HasReturnDirectly + sc.ReturnDirectlyToolCallID = ss.ReturnDirectlyToolCallID + sc.ToolGenActions = ss.ToolGenActions + sc.AgentName = ss.AgentName + sc.RemainingIterations = ss.RemainingIterations + sc.Extra = ss.Extra + sc.Internals = ss.Internals + return nil +} + +// stateV080ToState converts a legacy *stateV080 (v0.8.0-v0.8.3) to a current *State. +func stateV080ToState(sc *stateV080) *State { + s := &State{ + Messages: sc.Messages, + HasReturnDirectly: sc.HasReturnDirectly, + ReturnDirectlyToolCallID: sc.ReturnDirectlyToolCallID, + ToolGenActions: sc.ToolGenActions, + AgentName: sc.AgentName, + RemainingIterations: sc.RemainingIterations, + Extra: sc.Extra, + } + if sc.ReturnDirectlyToolCallID != "" { + s.setReturnDirectlyToolCallID(sc.ReturnDirectlyToolCallID) + } + if sc.Internals != nil && s.RetryAttempt == 0 { + if v, ok := sc.Internals["_retryAttempt"].(int); ok { + s.RetryAttempt = v + } + } + if sc.Internals != nil && s.ReturnDirectlyEvent == nil { + if v, ok := sc.Internals["_returnDirectlyEvent"].(*AgentEvent); ok { + s.ReturnDirectlyEvent = v + } + } + return s +} + +// SendToolGenAction attaches an AgentAction to the next tool event emitted for the +// current tool execution. +// +// Where/when to use: +// - Invoke within a tool's Run (Invokable/Streamable) implementation to include +// an action alongside that tool's output event. +// - The action is scoped by the current tool call context: if a ToolCallID is +// available, it is used as the key to support concurrent calls of the same +// tool with different parameters; otherwise, the provided toolName is used. +// - The stored action is ephemeral and will be popped and attached to the tool +// event when the tool finishes (including streaming completion). +// +// Limitation: +// - This function is intended for use within ChatModelAgent runs only. It relies +// on ChatModelAgent's internal State to store and pop actions, which is not +// available in other agent types. +func SendToolGenAction(ctx context.Context, toolName string, action *AgentAction) error { + key := toolName + toolCallID := compose.GetToolCallID(ctx) + if len(toolCallID) > 0 { + key = toolCallID + } + + return compose.ProcessState(ctx, func(ctx context.Context, st *State) error { + st.setToolGenAction(key, action) + return nil + }) +} + +type reactInput struct { + Messages []Message +} + +type typedReactConfig[M MessageType] struct { + model model.BaseModel[M] + + toolsConfig *compose.ToolsNodeConfig + modelWrapperConf *typedModelWrapperConfig[M] + + toolsReturnDirectly map[string]bool + + agentName string + + maxIterations int + + cancelCtx *cancelContext + + // afterAgentFunc is called when the agent reaches a successful terminal state. + // It runs as a graph node, so compose.ProcessState is available. + afterAgentFunc func(ctx context.Context, msg M) (M, error) +} + +type reactConfig = typedReactConfig[*schema.Message] + +func genToolInfos(ctx context.Context, config *compose.ToolsNodeConfig) ([]*schema.ToolInfo, error) { + toolInfos := make([]*schema.ToolInfo, 0, len(config.Tools)) + for _, t := range config.Tools { + tl, err := t.Info(ctx) + if err != nil { + return nil, err + } + + toolInfos = append(toolInfos, tl) + } + + return toolInfos, nil +} + +type reactGraph = *compose.Graph[*reactInput, Message] + +func getReturnDirectlyToolCallID(ctx context.Context) (string, bool) { + var toolCallID string + handler := func(_ context.Context, st *State) error { + toolCallID = st.getReturnDirectlyToolCallID() + return nil + } + + _ = compose.ProcessState(ctx, handler) + + return toolCallID, toolCallID != "" +} + +func genReactState(config *reactConfig) func(ctx context.Context) *State { + return func(ctx context.Context) *State { + st := &State{ + AgentName: config.agentName, + } + maxIter := 20 + if config.maxIterations > 0 { + maxIter = config.maxIterations + } + st.setRemainingIterations(maxIter) + return st + } +} + +func newReact(ctx context.Context, config *reactConfig) (reactGraph, error) { + const ( + initNode_ = "Init" + chatModel_ = "ChatModel" + cancelCheckNode_ = "CancelCheck" + toolNode_ = "ToolNode" + afterToolCallsNode_ = "AfterToolCalls" + afterToolCallsCancelCheckNode_ = "AfterToolCallsCancelCheck" + afterAgentNode_ = "AfterAgent" + ) + + cancelCtx := config.cancelCtx + g := compose.NewGraph[*reactInput, Message](compose.WithGenLocalState(genReactState(config))) + _ = g.AddLambdaNode(initNode_, compose.InvokableLambda(func(ctx context.Context, input *reactInput) ([]Message, error) { + _ = compose.ProcessState(ctx, func(_ context.Context, st *State) error { + st.Messages = append(st.Messages, input.Messages...) + return nil + }) + return input.Messages, nil + }), compose.WithNodeName(initNode_)) + + var wrappedModel = config.model + if config.modelWrapperConf != nil { + wrappedModel = buildModelWrappers(config.model, config.modelWrapperConf) + } + + toolsConfig := config.toolsConfig + + toolsNode, err := compose.NewToolNode(ctx, toolsConfig) + if err != nil { + return nil, err + } + + _ = g.AddChatModelNode(chatModel_, wrappedModel, compose.WithStatePreHandler( + func(ctx context.Context, input []Message, st *State) ([]Message, error) { + if st.getRemainingIterations() <= 0 { + return nil, ErrExceedMaxIterations + } + st.decrementRemainingIterations() + return input, nil + }), compose.WithNodeName(chatModel_)) + + // CancelAfterChatModel safe-point: on the tool-calls path, after the branch + // has confirmed that the model response contains tool calls (i.e. not a final + // answer). Skipped entirely when the model produces a final answer. + _ = g.AddLambdaNode(cancelCheckNode_, compose.InvokableLambda(func(ctx context.Context, msg Message) (Message, error) { + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode()&CancelAfterChatModel != 0 { + return nil, compose.StatefulInterrupt(ctx, "CancelAfterChatModel", msg) + } + } + wasInterrupted, hasState, state := compose.GetInterruptState[Message](ctx) + if wasInterrupted && hasState { + msg = state + } + return msg, nil + }), compose.WithNodeName(cancelCheckNode_)) + + toolPreHandle := func(ctx context.Context, _ Message, st *State) (Message, error) { + input := st.Messages[len(st.Messages)-1] + returnDirectly := config.toolsReturnDirectly + if execCtx := getTypedChatModelAgentExecCtx[*schema.Message](ctx); execCtx != nil && len(execCtx.runtimeReturnDirectly) > 0 { + returnDirectly = execCtx.runtimeReturnDirectly + } + if len(returnDirectly) > 0 { + for i := range input.ToolCalls { + toolName := input.ToolCalls[i].Function.Name + if _, ok := returnDirectly[toolName]; ok { + st.setReturnDirectlyToolCallID(input.ToolCalls[i].ID) + } + } + } + return input, nil + } + toolPostHandle := func(ctx context.Context, out *schema.StreamReader[[]*schema.Message], st *State) (*schema.StreamReader[[]*schema.Message], error) { + if event := st.getReturnDirectlyEvent(); event != nil { + getTypedChatModelAgentExecCtx[*schema.Message](ctx).send(event) + st.setReturnDirectlyEvent(nil) + } + return out, nil + } + _ = g.AddToolsNode(toolNode_, toolsNode, + compose.WithStatePreHandler(toolPreHandle), + compose.WithStreamStatePostHandler(toolPostHandle), + compose.WithNodeName(toolNode_)) + + // AfterToolCalls node: persists tool results to state and fires the after-tool-calls hook. + // The graph auto-materializes the ToolsNode stream into []Message before this node. + afterToolCalls := func(ctx context.Context, toolResults []Message) ([]Message, error) { + // Propagate tool message IDs from event sender to state messages. + // The event sender pre-generated IDs and stored them in state.ToolMsgIDs[toolName+callID]. + // Here we pop them and set them on the compose-created tool result messages + // so that state messages share the same IDs as their corresponding event messages. + // If no stored ID is found (old checkpoint, custom event sender), generate a fresh one. + _ = compose.ProcessState(ctx, func(_ context.Context, st *State) error { + for _, msg := range toolResults { + if id := st.popToolMsgID(msg.ToolName, msg.ToolCallID); id != "" { + msg.Extra = internal.SetMessageID(msg.Extra, id) + } else { + msg.Extra = internal.EnsureMessageID(msg.Extra) + } + st.Messages = append(st.Messages, msg) + } + return nil + }) + + execCtx := getTypedChatModelAgentExecCtx[Message](ctx) + if execCtx != nil && execCtx.afterToolCallsHook != nil { + if err := execCtx.afterToolCallsHook(ctx); err != nil { + return nil, err + } + } + + return toolResults, nil + } + _ = g.AddLambdaNode(afterToolCallsNode_, compose.InvokableLambda(afterToolCalls), + compose.WithNodeName(afterToolCallsNode_)) + + // AfterToolCallsCancelCheck: CancelAfterToolCalls safe-point, separated from toolPostHandle. + afterToolCallsCancelCheck := func(ctx context.Context, toolResults []Message) ([]Message, error) { + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode()&CancelAfterToolCalls != 0 { + return nil, compose.Interrupt(ctx, "CancelAfterToolCalls") + } + } + return toolResults, nil + } + _ = g.AddLambdaNode(afterToolCallsCancelCheckNode_, compose.InvokableLambda(afterToolCallsCancelCheck), + compose.WithNodeName(afterToolCallsCancelCheckNode_)) + + _ = g.AddEdge(compose.START, initNode_) + _ = g.AddEdge(initNode_, chatModel_) + + // Determine the terminal node: afterAgentNode_ if afterAgentFunc is set, otherwise compose.END. + terminalNode := compose.END + if config.afterAgentFunc != nil { + _ = g.AddLambdaNode(afterAgentNode_, compose.InvokableLambda(config.afterAgentFunc), + compose.WithNodeName(afterAgentNode_)) + _ = g.AddEdge(afterAgentNode_, compose.END) + terminalNode = afterAgentNode_ + } + + toolCallCheck := func(ctx context.Context, sMsg MessageStream) (string, error) { + defer sMsg.Close() + for { + chunk, err_ := sMsg.Recv() + if err_ != nil { + if err_ == io.EOF { + return terminalNode, nil + } + + return "", err_ + } + + if len(chunk.ToolCalls) > 0 { + return cancelCheckNode_, nil + } + } + } + branch := compose.NewStreamGraphBranch(toolCallCheck, map[string]bool{terminalNode: true, cancelCheckNode_: true}) + _ = g.AddBranch(chatModel_, branch) + + _ = g.AddEdge(cancelCheckNode_, toolNode_) + _ = g.AddEdge(toolNode_, afterToolCallsNode_) + _ = g.AddEdge(afterToolCallsNode_, afterToolCallsCancelCheckNode_) + + if len(config.toolsReturnDirectly) > 0 { + const ( + toolNodeToEndConverter = "ToolNodeToEndConverter" + ) + + cvt := func(ctx context.Context, toolResults []Message) (Message, error) { + id, _ := getReturnDirectlyToolCallID(ctx) + + for _, msg := range toolResults { + if msg != nil && msg.ToolCallID == id { + return msg, nil + } + } + + return nil, errors.New("return directly tool call result not found") + } + + _ = g.AddLambdaNode(toolNodeToEndConverter, compose.InvokableLambda(cvt), + compose.WithNodeName(toolNodeToEndConverter)) + _ = g.AddEdge(toolNodeToEndConverter, terminalNode) + + checkReturnDirect := func(ctx context.Context, toolResults []Message) (string, error) { + _, ok := getReturnDirectlyToolCallID(ctx) + + if ok { + return toolNodeToEndConverter, nil + } + + return chatModel_, nil + } + + returnDirectBranch := compose.NewGraphBranch(checkReturnDirect, + map[string]bool{toolNodeToEndConverter: true, chatModel_: true}) + _ = g.AddBranch(afterToolCallsCancelCheckNode_, returnDirectBranch) + } else { + _ = g.AddEdge(afterToolCallsCancelCheckNode_, chatModel_) + } + + return g, nil +} + +type agenticReactInput struct { + Messages []*schema.AgenticMessage +} + +type agenticReactConfig = typedReactConfig[*schema.AgenticMessage] + +type agenticReactGraph = *compose.Graph[*agenticReactInput, *schema.AgenticMessage] + +func getAgenticReturnDirectlyToolCallID(ctx context.Context) (string, bool) { + var toolCallID string + _ = compose.ProcessState(ctx, func(_ context.Context, st *agenticState) error { + toolCallID = st.getReturnDirectlyToolCallID() + return nil + }) + return toolCallID, toolCallID != "" +} + +func genAgenticReactState(config *agenticReactConfig) func(ctx context.Context) *agenticState { + return func(ctx context.Context) *agenticState { + st := &agenticState{ + AgentName: config.agentName, + } + maxIter := 20 + if config.maxIterations > 0 { + maxIter = config.maxIterations + } + st.setRemainingIterations(maxIter) + return st + } +} + +func agenticMessageHasToolCalls(msg *schema.AgenticMessage) bool { + if msg == nil { + return false + } + for _, block := range msg.ContentBlocks { + if block != nil && block.Type == schema.ContentBlockTypeFunctionToolCall && block.FunctionToolCall != nil { + return true + } + } + return false +} + +func newAgenticReact(ctx context.Context, config *agenticReactConfig) (agenticReactGraph, error) { + const ( + initNode_ = "Init" + chatModel_ = "ChatModel" + cancelCheckNode_ = "CancelCheck" + toolNode_ = "ToolNode" + afterToolCallsNode_ = "AfterToolCalls" + afterToolCallsCancelCheckNode_ = "AfterToolCallsCancelCheck" + afterAgentNode_ = "AfterAgent" + ) + + cancelCtx := config.cancelCtx + g := compose.NewGraph[*agenticReactInput, *schema.AgenticMessage]( + compose.WithGenLocalState(genAgenticReactState(config))) + _ = g.AddLambdaNode(initNode_, compose.InvokableLambda(func(ctx context.Context, input *agenticReactInput) ([]*schema.AgenticMessage, error) { + _ = compose.ProcessState(ctx, func(_ context.Context, st *agenticState) error { + st.Messages = append(st.Messages, input.Messages...) + return nil + }) + return input.Messages, nil + }), compose.WithNodeName(initNode_)) + + var wrappedModel = config.model + if config.modelWrapperConf != nil { + wrappedModel = buildModelWrappers(config.model, config.modelWrapperConf) + } + + toolsNode, err := compose.NewAgenticToolsNode(ctx, config.toolsConfig) + if err != nil { + return nil, err + } + + _ = g.AddAgenticModelNode(chatModel_, wrappedModel, compose.WithStatePreHandler( + func(ctx context.Context, input []*schema.AgenticMessage, st *agenticState) ([]*schema.AgenticMessage, error) { + if st.getRemainingIterations() <= 0 { + return nil, ErrExceedMaxIterations + } + st.decrementRemainingIterations() + return input, nil + }), compose.WithNodeName(chatModel_)) + + _ = g.AddLambdaNode(cancelCheckNode_, compose.InvokableLambda(func(ctx context.Context, msg *schema.AgenticMessage) (*schema.AgenticMessage, error) { + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode()&CancelAfterChatModel != 0 { + return nil, compose.StatefulInterrupt(ctx, "CancelAfterChatModel", msg) + } + } + wasInterrupted, hasState, state := compose.GetInterruptState[*schema.AgenticMessage](ctx) + if wasInterrupted && hasState { + msg = state + } + return msg, nil + }), compose.WithNodeName(cancelCheckNode_)) + + toolPreHandle := func(ctx context.Context, _ *schema.AgenticMessage, st *agenticState) (*schema.AgenticMessage, error) { + input := st.Messages[len(st.Messages)-1] + returnDirectly := config.toolsReturnDirectly + if execCtx := getTypedChatModelAgentExecCtx[*schema.AgenticMessage](ctx); execCtx != nil && len(execCtx.runtimeReturnDirectly) > 0 { + returnDirectly = execCtx.runtimeReturnDirectly + } + if len(returnDirectly) > 0 { + for _, block := range input.ContentBlocks { + if block == nil || block.Type != schema.ContentBlockTypeFunctionToolCall || block.FunctionToolCall == nil { + continue + } + if _, ok := returnDirectly[block.FunctionToolCall.Name]; ok { + st.setReturnDirectlyToolCallID(block.FunctionToolCall.CallID) + } + } + } + return input, nil + } + toolPostHandle := func(ctx context.Context, out *schema.StreamReader[[]*schema.AgenticMessage], st *agenticState) (*schema.StreamReader[[]*schema.AgenticMessage], error) { + if event := st.getReturnDirectlyEvent(); event != nil { + getTypedChatModelAgentExecCtx[*schema.AgenticMessage](ctx).send(event) + st.setReturnDirectlyEvent(nil) + } + return out, nil + } + _ = g.AddAgenticToolsNode(toolNode_, toolsNode, + compose.WithStatePreHandler(toolPreHandle), + compose.WithStreamStatePostHandler(toolPostHandle), + compose.WithNodeName(toolNode_)) + + afterToolCalls := func(ctx context.Context, toolResults []*schema.AgenticMessage) ([]*schema.AgenticMessage, error) { + _ = compose.ProcessState(ctx, func(_ context.Context, st *agenticState) error { + for _, msg := range toolResults { + if msg == nil { + continue + } + toolName, callID := extractToolIdentifiers(msg) + if id := st.popToolMsgID(toolName, callID); id != "" { + msg.Extra = internal.SetMessageID(msg.Extra, id) + } else { + msg.Extra = internal.EnsureMessageID(msg.Extra) + } + st.Messages = append(st.Messages, msg) + } + return nil + }) + + execCtx := getTypedChatModelAgentExecCtx[*schema.AgenticMessage](ctx) + if execCtx != nil && execCtx.afterToolCallsHook != nil { + if err := execCtx.afterToolCallsHook(ctx); err != nil { + return nil, err + } + } + + return toolResults, nil + } + _ = g.AddLambdaNode(afterToolCallsNode_, compose.InvokableLambda(afterToolCalls), + compose.WithNodeName(afterToolCallsNode_)) + + afterToolCallsCancelCheck := func(ctx context.Context, toolResults []*schema.AgenticMessage) ([]*schema.AgenticMessage, error) { + if cancelCtx != nil && cancelCtx.shouldCancel() { + if cancelCtx.getMode()&CancelAfterToolCalls != 0 { + return nil, compose.Interrupt(ctx, "CancelAfterToolCalls") + } + } + return toolResults, nil + } + _ = g.AddLambdaNode(afterToolCallsCancelCheckNode_, compose.InvokableLambda(afterToolCallsCancelCheck), + compose.WithNodeName(afterToolCallsCancelCheckNode_)) + + _ = g.AddEdge(compose.START, initNode_) + _ = g.AddEdge(initNode_, chatModel_) + + // Determine the terminal node: afterAgentNode_ if afterAgentFunc is set, otherwise compose.END. + terminalNode := compose.END + if config.afterAgentFunc != nil { + _ = g.AddLambdaNode(afterAgentNode_, compose.InvokableLambda(config.afterAgentFunc), + compose.WithNodeName(afterAgentNode_)) + _ = g.AddEdge(afterAgentNode_, compose.END) + terminalNode = afterAgentNode_ + } + + toolCallCheck := func(ctx context.Context, sMsg *schema.StreamReader[*schema.AgenticMessage]) (string, error) { + defer sMsg.Close() + for { + chunk, err_ := sMsg.Recv() + if err_ != nil { + if err_ == io.EOF { + return terminalNode, nil + } + return "", err_ + } + if agenticMessageHasToolCalls(chunk) { + return cancelCheckNode_, nil + } + } + } + branch := compose.NewStreamGraphBranch(toolCallCheck, map[string]bool{terminalNode: true, cancelCheckNode_: true}) + _ = g.AddBranch(chatModel_, branch) + + _ = g.AddEdge(cancelCheckNode_, toolNode_) + _ = g.AddEdge(toolNode_, afterToolCallsNode_) + _ = g.AddEdge(afterToolCallsNode_, afterToolCallsCancelCheckNode_) + + if len(config.toolsReturnDirectly) > 0 { + const ( + toolNodeToEndConverter = "ToolNodeToEndConverter" + ) + + cvt := func(ctx context.Context, toolResults []*schema.AgenticMessage) (*schema.AgenticMessage, error) { + id, _ := getAgenticReturnDirectlyToolCallID(ctx) + for _, msg := range toolResults { + if msg == nil { + continue + } + _, callID := extractToolIdentifiers(msg) + if callID == id { + return msg, nil + } + } + return nil, errors.New("return directly tool call result not found") + } + + _ = g.AddLambdaNode(toolNodeToEndConverter, compose.InvokableLambda(cvt), + compose.WithNodeName(toolNodeToEndConverter)) + _ = g.AddEdge(toolNodeToEndConverter, terminalNode) + + checkReturnDirect := func(ctx context.Context, toolResults []*schema.AgenticMessage) (string, error) { + _, ok := getAgenticReturnDirectlyToolCallID(ctx) + if ok { + return toolNodeToEndConverter, nil + } + return chatModel_, nil + } + + returnDirectBranch := compose.NewGraphBranch(checkReturnDirect, + map[string]bool{toolNodeToEndConverter: true, chatModel_: true}) + _ = g.AddBranch(afterToolCallsCancelCheckNode_, returnDirectBranch) + } else { + _ = g.AddEdge(afterToolCallsCancelCheckNode_, chatModel_) + } + + return g, nil +} + +// extractToolIdentifiers extracts the tool name and call ID from an AgenticMessage +// that contains a tool result content block. +// Assumes one tool result per message, which is guaranteed by AgenticToolsNode +// (see compose.toolMessageToAgenticMessage). +func extractToolIdentifiers(msg *schema.AgenticMessage) (toolName, callID string) { + if msg == nil { + return "", "" + } + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + if block.Type == schema.ContentBlockTypeFunctionToolResult && block.FunctionToolResult != nil { + return block.FunctionToolResult.Name, block.FunctionToolResult.CallID + } + if block.Type == schema.ContentBlockTypeToolSearchResult && block.ToolSearchFunctionToolResult != nil { + return block.ToolSearchFunctionToolResult.Name, block.ToolSearchFunctionToolResult.CallID + } + } + return "", "" +} diff --git a/adk/react_test.go b/adk/react_test.go new file mode 100644 index 0000000..b3cdb1c --- /dev/null +++ b/adk/react_test.go @@ -0,0 +1,672 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + "io" + "math" + "math/rand" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type testModelWrapper struct { + inner model.ToolCallingChatModel +} + +func TestStateCompatConversions_V080(t *testing.T) { + t.Run("stateV080GobDecodeAndToState", func(t *testing.T) { + ss := &stateV080Serialization{ + ReturnDirectlyToolCallID: "tcid", + RemainingIterations: 2, + Internals: map[string]any{ + "_retryAttempt": 9, + "_returnDirectlyEvent": &AgentEvent{AgentName: "agent"}, + }, + } + + var buf bytes.Buffer + assert.NoError(t, gob.NewEncoder(&buf).Encode(ss)) + + var legacy stateV080 + assert.NoError(t, legacy.GobDecode(buf.Bytes())) + + s := stateV080ToState(&legacy) + assert.Equal(t, "tcid", s.ReturnDirectlyToolCallID) + assert.True(t, s.HasReturnDirectly) + assert.Equal(t, 2, s.RemainingIterations) + assert.Equal(t, 9, s.RetryAttempt) + assert.NotNil(t, s.ReturnDirectlyEvent) + assert.Equal(t, "agent", s.ReturnDirectlyEvent.AgentName) + }) +} + +func TestStateGetToolGenActions(t *testing.T) { + st := &State{ + ToolGenActions: map[string]*AgentAction{ + "k": {}, + }, + } + assert.NotNil(t, st.getToolGenActions()) + assert.Contains(t, st.getToolGenActions(), "k") +} + +func (w *testModelWrapper) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return (&stateModelWrapper{inner: w.inner, original: w.inner}).Generate(ctx, input, opts...) +} + +func (w *testModelWrapper) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return (&stateModelWrapper{inner: w.inner, original: w.inner}).Stream(ctx, input, opts...) +} + +func (w *testModelWrapper) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + newInner, err := w.inner.WithTools(tools) + if err != nil { + return nil, err + } + return &testModelWrapper{inner: newInner}, nil +} + +// TestReact tests the newReact function with different scenarios +func TestReact(t *testing.T) { + // Basic test for newReact function + t.Run("Invoke", func(t *testing.T) { + ctx := context.Background() + + // Create a fake tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 3, + } + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []Message, opts ...model.Option) (Message, error) { + times++ + if times <= 2 { + return schema.AssistantMessage("hello test", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStrForTest()), + }, + }, + }), + nil + } + + return schema.AssistantMessage("bye", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create a reactConfig + config := &reactConfig{ + model: &testModelWrapper{inner: cm}, + toolsConfig: &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + toolsReturnDirectly: map[string]bool{}, + } + + graph, err := newReact(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, graph) + + compiled, err := graph.Compile(ctx, compose.WithMaxRunSteps(math.MaxInt)) + assert.NoError(t, err) + assert.NotNil(t, compiled) + + // Test with a user message + result, err := compiled.Invoke(ctx, &reactInput{Messages: []Message{ + { + Role: schema.User, + Content: "Use the test tool to say hello", + }, + }}) + assert.NoError(t, err) + assert.NotNil(t, result) + }) + + // Test with toolsReturnDirectly + t.Run("ToolsReturnDirectly", func(t *testing.T) { + ctx := context.Background() + + // Create a fake tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 3, + } + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []Message, opts ...model.Option) (Message, error) { + times++ + if times <= 2 { + return schema.AssistantMessage("hello test", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStrForTest()), + }, + }, + }), + nil + } + + return schema.AssistantMessage("bye", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create a reactConfig with toolsReturnDirectly + config := &reactConfig{ + model: &testModelWrapper{inner: cm}, + toolsConfig: &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + toolsReturnDirectly: map[string]bool{info.Name: true}, + } + + graph, err := newReact(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, graph) + + compiled, err := graph.Compile(ctx, compose.WithMaxRunSteps(math.MaxInt)) + assert.NoError(t, err) + assert.NotNil(t, compiled) + + // Test with a user message when tool returns directly + result, err := compiled.Invoke(ctx, &reactInput{Messages: []Message{ + { + Role: schema.User, + Content: "Use the test tool to say hello", + }, + }}) + assert.NoError(t, err) + assert.NotNil(t, result) + + assert.Equal(t, result.Role, schema.Tool) + }) + + // Test streaming functionality + t.Run("Stream", func(t *testing.T) { + ctx := context.Background() + + // Create a fake tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 3, + } + + fakeStreamTool := &fakeStreamToolForTest{ + tarCount: 3, + } + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + times := 0 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []Message, opts ...model.Option) ( + MessageStream, error) { + sr, sw := schema.Pipe[Message](1) + defer sw.Close() + + info, _ := fakeTool.Info(ctx) + streamInfo, _ := fakeStreamTool.Info(ctx) + + times++ + if times <= 1 { + sw.Send(schema.AssistantMessage("hello test", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "tool"}`, randStrForTest()), + }, + }, + }), + nil) + return sr, nil + } else if times == 2 { + sw.Send(schema.AssistantMessage("hello stream", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: streamInfo.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "stream tool"}`, randStrForTest()), + }, + }, + }), + nil) + return sr, nil + } + + sw.Send(schema.AssistantMessage("bye", nil), nil) + return sr, nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create a reactConfig + config := &reactConfig{ + model: &testModelWrapper{inner: cm}, + toolsConfig: &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool, fakeStreamTool}, + }, + toolsReturnDirectly: map[string]bool{}, + } + + graph, err := newReact(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, graph) + + compiled, err := graph.Compile(ctx, compose.WithMaxRunSteps(math.MaxInt)) + assert.NoError(t, err) + assert.NotNil(t, compiled) + + // Test streaming with a user message + outStream, err := compiled.Stream(ctx, &reactInput{Messages: []Message{ + { + Role: schema.User, + Content: "Use the test tool to say hello", + }, + }}) + assert.NoError(t, err) + assert.NotNil(t, outStream) + + defer outStream.Close() + + msgs := make([]Message, 0) + for { + msg, err_ := outStream.Recv() + if err_ != nil { + if errors.Is(err_, io.EOF) { + break + } + t.Fatal(err_) + } + + msgs = append(msgs, msg) + } + + assert.NotEmpty(t, msgs) + }) + + // Test streaming with toolsReturnDirectly + t.Run("StreamWithToolsReturnDirectly", func(t *testing.T) { + ctx := context.Background() + + // Create a fake tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 3, + } + + fakeStreamTool := &fakeStreamToolForTest{ + tarCount: 3, + } + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + times := 0 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []Message, opts ...model.Option) ( + MessageStream, error) { + sr, sw := schema.Pipe[Message](1) + defer sw.Close() + + info, _ := fakeTool.Info(ctx) + streamInfo, _ := fakeStreamTool.Info(ctx) + + times++ + if times <= 1 { + sw.Send(schema.AssistantMessage("hello test", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "tool"}`, randStrForTest()), + }, + }, + }), + nil) + return sr, nil + } else if times == 2 { + sw.Send(schema.AssistantMessage("hello stream", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: streamInfo.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "stream tool"}`, randStrForTest()), + }, + }, + }), + nil) + return sr, nil + } + + sw.Send(schema.AssistantMessage("bye", nil), nil) + return sr, nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + streamInfo, err := fakeStreamTool.Info(ctx) + assert.NoError(t, err) + + // Create a reactConfig with toolsReturnDirectly + config := &reactConfig{ + model: &testModelWrapper{inner: cm}, + toolsConfig: &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool, fakeStreamTool}, + }, + toolsReturnDirectly: map[string]bool{streamInfo.Name: true}, + } + + graph, err := newReact(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, graph) + + compiled, err := graph.Compile(ctx, compose.WithMaxRunSteps(math.MaxInt)) + assert.NoError(t, err) + assert.NotNil(t, compiled) + + // Reset times counter + times = 0 + + // Test streaming with a user message when tool returns directly + outStream, err := compiled.Stream(ctx, &reactInput{Messages: []Message{ + { + Role: schema.User, + Content: "Use the test tool to say hello", + }, + }}) + assert.NoError(t, err) + assert.NotNil(t, outStream) + + msgs := make([]Message, 0) + for { + msg, err_ := outStream.Recv() + if err_ != nil { + if errors.Is(err_, io.EOF) { + break + } + t.Fatal(err) + } + + assert.Equal(t, msg.Role, schema.Tool) + + msgs = append(msgs, msg) + } + + outStream.Close() + + assert.NotEmpty(t, msgs) + }) + + t.Run("MaxIterations", func(t *testing.T) { + ctx := context.Background() + + // Create a fake tool for testing + fakeTool := &fakeToolForTest{ + tarCount: 3, + } + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Create a mock chat model + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // Set up expectations for the mock model + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []Message, opts ...model.Option) (Message, error) { + times++ + if times <= 5 { + return schema.AssistantMessage("hello test", + []schema.ToolCall{ + { + ID: randStrForTest(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStrForTest()), + }, + }, + }), + nil + } + + return schema.AssistantMessage("bye", nil), nil + }).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // don't exceed max iterations + config := &reactConfig{ + model: &testModelWrapper{inner: cm}, + toolsConfig: &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + toolsReturnDirectly: map[string]bool{}, + maxIterations: 6, + } + + graph, err := newReact(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, graph) + + compiled, err := graph.Compile(ctx, compose.WithMaxRunSteps(math.MaxInt)) + assert.NoError(t, err) + assert.NotNil(t, compiled) + + // Test with a user message + result, err := compiled.Invoke(ctx, &reactInput{Messages: []Message{ + { + Role: schema.User, + Content: "Use the test tool to say hello", + }, + }}) + assert.NoError(t, err) + assert.Equal(t, result.Content, "bye") + + // reset chat model times counter + times = 0 + // exceed max iterations + config = &reactConfig{ + model: &testModelWrapper{inner: cm}, + toolsConfig: &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + toolsReturnDirectly: map[string]bool{}, + maxIterations: 5, + } + + graph, err = newReact(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, graph) + + compiled, err = graph.Compile(ctx, compose.WithMaxRunSteps(math.MaxInt)) + assert.NoError(t, err) + assert.NotNil(t, compiled) + + // Test with a user message + _, err = compiled.Invoke(ctx, &reactInput{Messages: []Message{ + { + Role: schema.User, + Content: "Use the test tool to say hello", + }, + }}) + assert.Error(t, err) + t.Logf("actual error: %v", err.Error()) + assert.ErrorIs(t, err, ErrExceedMaxIterations) + + assert.Contains(t, err.Error(), ErrExceedMaxIterations.Error()) + }) +} + +// Helper types and functions for testing + +type fakeStreamToolForTest struct { + tarCount int + curCount int +} + +func (t *fakeStreamToolForTest) StreamableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) ( + *schema.StreamReader[string], error) { + p := &fakeToolInputForTest{} + err := sonic.UnmarshalString(argumentsInJSON, p) + if err != nil { + return nil, err + } + + if t.curCount >= t.tarCount { + s := schema.StreamReaderFromArray([]string{`{"say": "bye"}`}) + return s, nil + } + t.curCount++ + s := schema.StreamReaderFromArray([]string{fmt.Sprintf(`{"say": "hello %v"}`, p.Name)}) + return s, nil +} + +type fakeToolForTest struct { + tarCount int + curCount int +} + +func (t *fakeToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "test_tool", + Desc: "test tool for unit testing", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Desc: "user name for testing", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (t *fakeStreamToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "test_stream_tool", + Desc: "test stream tool for unit testing", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Desc: "user name for testing", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (t *fakeToolForTest) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + p := &fakeToolInputForTest{} + err := sonic.UnmarshalString(argumentsInJSON, p) + if err != nil { + return "", err + } + + if t.curCount >= t.tarCount { + return `{"say": "bye"}`, nil + } + + t.curCount++ + return fmt.Sprintf(`{"say": "hello %v"}`, p.Name), nil +} + +type fakeToolInputForTest struct { + Name string `json:"name"` +} + +func randStrForTest() string { + seeds := []rune("test seed") + b := make([]rune, 8) + for i := range b { + b[i] = seeds[rand.Intn(len(seeds))] + } + return string(b) +} + +func TestReactHistory_EmptyMessages(t *testing.T) { + g := compose.NewGraph[string, []Message](compose.WithGenLocalState(func(ctx context.Context) (state *State) { + return &State{ + Messages: []Message{}, + } + })) + require.NoError(t, g.AddLambdaNode("1", compose.InvokableLambda(func(ctx context.Context, input string) (output []Message, err error) { + return getReactChatHistory(ctx, "DestAgent") + }))) + require.NoError(t, g.AddEdge(compose.START, "1")) + require.NoError(t, g.AddEdge("1", compose.END)) + + ctx := context.Background() + ctx, _ = initRunCtx(ctx, "MyAgent", nil) + runner, err := g.Compile(ctx) + require.NoError(t, err) + + require.NotPanics(t, func() { + result, err := runner.Invoke(ctx, "") + if err != nil { + t.Logf("Got error (acceptable): %v", err) + return + } + t.Logf("Got %d messages", len(result)) + }, "BUG: getReactChatHistory should not panic with empty Messages slice") +} diff --git a/adk/retry_chatmodel.go b/adk/retry_chatmodel.go new file mode 100644 index 0000000..350a3c4 --- /dev/null +++ b/adk/retry_chatmodel.go @@ -0,0 +1,759 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "io" + "math/rand" + "time" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +var ( + // ErrExceedMaxRetries is returned when the maximum number of retries has been exceeded. + // Use errors.Is to check if an error is due to max retries being exceeded: + // + // if errors.Is(err, adk.ErrExceedMaxRetries) { + // // handle max retries exceeded + // } + // + // Use errors.As to extract the underlying RetryExhaustedError for the last error details: + // + // var retryErr *adk.RetryExhaustedError + // if errors.As(err, &retryErr) { + // fmt.Printf("last error was: %v\n", retryErr.LastErr) + // } + ErrExceedMaxRetries = errors.New("exceeds max retries") +) + +// RetryExhaustedError is returned when all retry attempts have been exhausted. +// It wraps the last error that occurred during retry attempts. +type RetryExhaustedError struct { + LastErr error + TotalRetries int +} + +func (e *RetryExhaustedError) Error() string { + if e.LastErr != nil { + return fmt.Sprintf("exceeds max retries: last error: %v", e.LastErr) + } + return "exceeds max retries" +} + +func (e *RetryExhaustedError) Unwrap() error { + return ErrExceedMaxRetries +} + +// WillRetryError is emitted when a retryable error occurs and a retry will be attempted. +// It allows end-users to observe retry events in real-time via AgentEvent. +// +// Field design rationale: +// - ErrStr (exported): Stores the error message string for Gob serialization during checkpointing. +// This ensures the error message is preserved after checkpoint restore. +// - err (unexported): Stores the original error for Unwrap() support at runtime. +// This field is intentionally unexported because Gob serialization would fail for unregistered +// concrete error types. Since end-users only need the original error when the AgentEvent first +// occurs (not after restoring from checkpoint), skipping serialization is acceptable. +// After checkpoint restore, err will be nil and Unwrap() returns nil. +// - rejectReason (unexported): Stores a user-defined value set by the ShouldRetry callback +// via RetryDecision.RejectReason. This is runtime-only observability data — after checkpoint +// restore it will be nil. Unexported to avoid Gob serialization of arbitrary types. +type WillRetryError struct { + ErrStr string + RetryAttempt int + rejectReason any + err error +} + +func (e *WillRetryError) Error() string { + return e.ErrStr +} + +func (e *WillRetryError) Unwrap() error { + return e.err +} + +// RejectReason returns the user-defined rejection reason set by the ShouldRetry callback +// via RetryDecision.RejectReason. Returns nil if not set or after checkpoint restore. +func (e *WillRetryError) RejectReason() any { + return e.rejectReason +} + +func init() { + schema.RegisterName[*WillRetryError]("eino_adk_chatmodel_will_retry_error") +} + +// TypedRetryContext contains context information passed to TypedModelRetryConfig.ShouldRetry +// during a retry decision. +// +// State combinations for OutputMessage and Err: +// +// OutputMessage != nil, Err == nil → successful call; inspect message quality +// OutputMessage == nil, Err != nil → failed call (Generate error or Stream() error) +// OutputMessage != nil, Err != nil → partial stream (chunks received before mid-stream error) +// OutputMessage == nil, Err == nil → empty stream (zero chunks before EOF) +type TypedRetryContext[M MessageType] struct { + // RetryAttempt is the current retry attempt number (1-based). + // For the first retry decision (after the initial call), this is 1. + RetryAttempt int + + // InputMessages is the input messages that were sent to the model for the current attempt. + InputMessages []M + + // Options is the model options that were used for the current attempt. + Options []model.Option + + // OutputMessage is the output message from the model, if any. + // This is non-nil when the model returned a message successfully. + // For streaming, this is the fully concatenated message (the entire stream is consumed + // before ShouldRetry is called). + // For streaming with mid-stream errors, this is the partial concatenation of chunks + // received before the error occurred. + // May be nil if the model returned an error without producing a message, or if the + // stream was empty (zero chunks before EOF). + OutputMessage M + + // Err is the error from the model call, if any. + // May be nil if the model produced a message without error. + // Note: both OutputMessage and Err can be nil simultaneously for empty streams. + Err error +} + +// RetryContext is the default retry context type using *schema.Message. +type RetryContext = TypedRetryContext[*schema.Message] + +// TypedRetryDecision represents the decision made by TypedModelRetryConfig.ShouldRetry. +type TypedRetryDecision[M MessageType] struct { + // Retry indicates whether the model call should be retried. + // If false, the model output (or error) is accepted as-is, unless RewriteError is set. + Retry bool + + // RewriteError, when non-nil, overrides the return value of the model call with this error. + // The agent run will fail with this error. + // + // This is useful for two scenarios: + // - When the model returns a "seemingly correct" message (no error) that actually + // contains unrecoverable issues. RewriteError converts the successful output + // into a fatal error. + // - When the model returns an error, but you want to replace it with a different, + // more descriptive error (e.g., adding context or wrapping). + // + // When Retry is true, RewriteError is ignored. + // When Retry is false and RewriteError is non-nil, the model call returns + // RewriteError regardless of whether the original call had an error or a message. + RewriteError error + + // ModifiedInputMessages, when non-nil, replaces the input messages for the next retry. + // + // This enables advanced recovery strategies like context compression or message trimming. + // Only used when Retry is true. Ignored when Retry is false. + ModifiedInputMessages []M + + // PersistModifiedInputMessages controls whether ModifiedInputMessages are written + // back to the agent's conversation history, affecting subsequent model calls in + // the agent loop (not just the next retry attempt). + // + // When true, the modified messages replace the current conversation history. + // When false (default), the modified messages are only used for the next retry attempt + // within this retry cycle. + // + // Only used when Retry is true and ModifiedInputMessages is non-nil. + PersistModifiedInputMessages bool + + // AdditionalOptions, when non-nil, provides additional model options for the next retry. + // These options are appended to the existing options, taking precedence via last-wins semantics. + // + // This enables adjustments like increasing MaxTokens for the retry attempt. + // Note: options accumulate across retries within a single retry cycle. If ShouldRetry + // returns AdditionalOptions on every attempt, each set is appended to the previous ones. + // Only the last value for each option key takes effect, but earlier values remain in the slice. + // AdditionalOptions are scoped to the current retry cycle and do not persist to subsequent + // agent iterations — each new model call in the agent loop starts with the original options. + // Only used when Retry is true. Ignored when Retry is false. + AdditionalOptions []model.Option + + // Backoff specifies the duration to wait before the next retry attempt. + // If zero, the default backoff function (from ModelRetryConfig.BackoffFunc or the + // built-in exponential backoff) is used. + // + // This allows the ShouldRetry callback to dynamically control retry timing based on + // the specific error or problematic message encountered. + // Only used when Retry is true. Ignored when Retry is false. + Backoff time.Duration + + // RejectReason is an optional user-defined value describing why the output was rejected. + // When Retry is true and the rejected stream/message is observed downstream via + // AgentEvent, this value is attached to the WillRetryError emitted to the event stream. + // Consumers can retrieve it via WillRetryError.RejectReason(). + // + // The ShouldRetry callback has full access to the model output (via retryCtx.OutputMessage) + // and error (via retryCtx.Err), so it can distill whatever information it wants into + // RejectReason — a string, a struct, the output message itself, or nil. + // + // Only used when Retry is true. Ignored when Retry is false. + RejectReason any +} + +// RetryDecision is the default retry decision type using *schema.Message. +type RetryDecision = TypedRetryDecision[*schema.Message] + +// TypedModelRetryConfig configures retry behavior for the ChatModel node. +// It defines how the agent should handle transient failures when calling the ChatModel. +type TypedModelRetryConfig[M MessageType] struct { + // MaxRetries specifies the maximum number of retry attempts. + // A value of 0 means no retries will be attempted. + // A value of 3 means up to 3 retry attempts (4 total calls including the initial attempt). + MaxRetries int + + // ShouldRetry determines how to handle a model call result. + // It receives context information about the current attempt including the output message + // and/or error, and returns a decision on whether to retry, what to modify, etc. + // Returning nil is treated as &RetryDecision{Retry: false} (accept as-is). + // + // If nil, defaults to retrying on any non-nil error (backward compatible with IsRetryAble). + // + // Note: When ShouldRetry is set, IsRetryAble is ignored. + // Note: In streaming mode, the entire stream is consumed before ShouldRetry is called. + // The event stream is sent to the client in real time regardless; only the retry + // decision is deferred until the full response is available. + ShouldRetry func(ctx context.Context, retryCtx *TypedRetryContext[M]) *TypedRetryDecision[M] + + // Deprecated: Use ShouldRetry instead for richer retry control including message + // inspection, input modification, and option adjustment. When ShouldRetry is set, + // IsRetryAble is ignored. + IsRetryAble func(ctx context.Context, err error) bool + + // BackoffFunc calculates the delay before the next retry attempt. + // The attempt parameter starts at 1 for the first retry. + // Used as the default when RetryDecision.Backoff is zero. + // If nil, a default exponential backoff with jitter is used: + // base delay 100ms, exponentially increasing up to 10s max, + // with random jitter (0-50% of delay) to prevent thundering herd. + BackoffFunc func(ctx context.Context, attempt int) time.Duration +} + +// ModelRetryConfig is the default retry config type using *schema.Message. +type ModelRetryConfig = TypedModelRetryConfig[*schema.Message] + +func defaultIsRetryAble(_ context.Context, err error) bool { + return err != nil +} + +func defaultBackoff(_ context.Context, attempt int) time.Duration { + baseDelay := 100 * time.Millisecond + maxDelay := 10 * time.Second + + if attempt <= 0 { + return baseDelay + } + + if attempt > 7 { + return maxDelay + time.Duration(rand.Int63n(int64(maxDelay/2))) + } + + delay := baseDelay * time.Duration(1< maxDelay { + delay = maxDelay + } + + jitter := time.Duration(rand.Int63n(int64(delay / 2))) + return delay + jitter +} + +func genErrWrapper(ctx context.Context, maxRetries, attempt int, isRetryAbleFunc func(ctx context.Context, err error) bool) func(error) error { + return func(err error) error { + isRetryAble := isRetryAbleFunc == nil || isRetryAbleFunc(ctx, err) + hasRetriesLeft := attempt < maxRetries + + if isRetryAble && hasRetriesLeft { + return &WillRetryError{ErrStr: err.Error(), RetryAttempt: attempt, err: err} + } + return err + } +} + +func consumeStreamForError[M any](stream *schema.StreamReader[M]) error { + defer stream.Close() + for { + _, err := stream.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + } +} + +type retryVerdictSignal struct { + ch chan retryVerdict +} + +type retryVerdict struct { + WillRetry bool + RetryAttempt int + Err error + RejectReason any +} + +// retryModelWrapper wraps a BaseChatModel with retry logic. +// This is used inside the model wrapper chain, positioned between eventSenderModelWrapper +// and stateModelWrapper, so that retry only affects the inner chain (event sending, user wrappers, +// callback injection) without re-running state management (BeforeModelRewriteState/AfterModelRewriteState). +type typedRetryModelWrapper[M MessageType] struct { + inner model.BaseModel[M] + config *TypedModelRetryConfig[M] +} + +func newTypedRetryModelWrapper[M MessageType](inner model.BaseModel[M], config *TypedModelRetryConfig[M]) *typedRetryModelWrapper[M] { + return &typedRetryModelWrapper[M]{inner: inner, config: config} +} + +func (r *typedRetryModelWrapper[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + if r.config.ShouldRetry != nil { + return generateWithShouldRetry(r, ctx, input, opts...) + } + return r.generateLegacy(ctx, input, opts...) +} + +func (r *typedRetryModelWrapper[M]) generateLegacy(ctx context.Context, input []M, opts ...model.Option) (zero M, _ error) { + isRetryAble := r.config.IsRetryAble + if isRetryAble == nil { + isRetryAble = defaultIsRetryAble + } + backoffFunc := r.config.BackoffFunc + if backoffFunc == nil { + backoffFunc = defaultBackoff + } + + var lastErr error + for attempt := 0; attempt <= r.config.MaxRetries; attempt++ { + out, err := r.inner.Generate(ctx, input, opts...) + if err == nil { + return out, nil + } + + if _, ok := compose.ExtractInterruptInfo(err); ok { + return zero, err + } + + if errors.Is(err, ErrStreamCanceled) { + return zero, err + } + + if !isRetryAble(ctx, err) { + return zero, err + } + + lastErr = err + if attempt < r.config.MaxRetries { + if err := r.contextAwareSleep(ctx, backoffFunc(ctx, attempt+1)); err != nil { + return zero, err + } + } + } + + return zero, &RetryExhaustedError{LastErr: lastErr, TotalRetries: r.config.MaxRetries} +} + +func generateWithShouldRetry[M MessageType](r *typedRetryModelWrapper[M], ctx context.Context, input []M, opts ...model.Option) (M, error) { + backoffFunc := r.config.BackoffFunc + if backoffFunc == nil { + backoffFunc = defaultBackoff + } + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + + currentInput := input + currentOpts := opts + var lastErr error + var zero M + + defer func() { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setRetryAttempt(0) + return nil + }) + }() + + for attempt := 0; attempt <= r.config.MaxRetries; attempt++ { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setRetryAttempt(attempt) + return nil + }) + + // Suppress event sending during Generate: the ShouldRetry callback must decide whether + // to accept or reject the result before any event is emitted. If accepted, the event + // is sent explicitly below (lines after decision check). If rejected, no event leaks. + if execCtx != nil { + execCtx.suppressEventSend = true + } + out, err := r.inner.Generate(ctx, currentInput, currentOpts...) + if execCtx != nil { + execCtx.suppressEventSend = false + } + + if err != nil { + if _, ok := compose.ExtractInterruptInfo(err); ok { + return zero, err + } + + if errors.Is(err, ErrStreamCanceled) { + return zero, err + } + } + + retryCtx := &TypedRetryContext[M]{ + RetryAttempt: attempt + 1, + InputMessages: currentInput, + Options: currentOpts, + OutputMessage: out, + Err: err, + } + decision := r.config.ShouldRetry(ctx, retryCtx) + if decision == nil { + decision = &TypedRetryDecision[M]{} + } + + if !decision.Retry { + if decision.RewriteError != nil { + return zero, decision.RewriteError + } + if err != nil { + return zero, err + } + if execCtx != nil && execCtx.generator != nil && out != nil { + event := typedModelOutputEvent(out, nil) + execCtx.send(event) + } + return out, nil + } + + lastErr = err + if lastErr == nil { + lastErr = fmt.Errorf("model output rejected by ShouldRetry at attempt %d", attempt+1) + } + + if attempt >= r.config.MaxRetries { + break + } + + applyDecisionForRetry(¤tInput, ¤tOpts, ctx, decision) + + delay := decision.Backoff + if delay == 0 { + delay = backoffFunc(ctx, attempt+1) + } + + if err := r.contextAwareSleep(ctx, delay); err != nil { + return zero, err + } + } + + return zero, &RetryExhaustedError{LastErr: lastErr, TotalRetries: r.config.MaxRetries} +} + +func (r *typedRetryModelWrapper[M]) contextAwareSleep(ctx context.Context, delay time.Duration) error { + if delay <= 0 { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(delay): + return nil + } +} + +func streamWithShouldRetry[M MessageType](r *typedRetryModelWrapper[M], ctx context.Context, input []M, opts ...model.Option) ( + *schema.StreamReader[M], error) { + + backoffFunc := r.config.BackoffFunc + if backoffFunc == nil { + backoffFunc = defaultBackoff + } + + defer func() { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setRetryAttempt(0) + return nil + }) + }() + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + + currentInput := input + currentOpts := opts + var lastErr error + var curSignal *retryVerdictSignal + + // Panic recovery for verdict signal: if ShouldRetry panics, the onEOF/errWrapper closures in + // buildStreamConvertOptions will block forever on signal.ch, causing a goroutine leak. This + // defer ensures a verdict is always sent, even on panic, before re-panicking. + defer func() { + if p := recover(); p != nil { + if curSignal != nil { + select { + case curSignal.ch <- retryVerdict{WillRetry: false, Err: fmt.Errorf("panic: %v", p)}: + default: + } + } + panic(p) + } + }() + + for attempt := 0; attempt <= r.config.MaxRetries; attempt++ { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setRetryAttempt(attempt) + return nil + }) + + signal := &retryVerdictSignal{ch: make(chan retryVerdict, 1)} + curSignal = signal + if execCtx != nil { + execCtx.retryVerdictSignal = signal + } + + stream, err := r.inner.Stream(ctx, currentInput, currentOpts...) + if err != nil { + // Defensive no-op: when Stream() returns an error, no stream exists, so + // eventSenderModel never creates the StreamReaderWithConvert hooks that would + // read from signal.ch. This send has no consumer — it merely fills the + // buffered(1) slot so the panic-recovery defer (select/default) won't block + // if a later panic tries to send a second verdict. The signal is discarded + // when the next iteration creates a new one. + signal.ch <- retryVerdict{WillRetry: false} + + if _, ok := compose.ExtractInterruptInfo(err); ok { + return nil, err + } + + if errors.Is(err, ErrStreamCanceled) { + return nil, err + } + + retryCtx := &TypedRetryContext[M]{ + RetryAttempt: attempt + 1, + InputMessages: currentInput, + Options: currentOpts, + Err: err, + } + decision := r.config.ShouldRetry(ctx, retryCtx) + if decision == nil { + decision = &TypedRetryDecision[M]{} + } + + if !decision.Retry { + if decision.RewriteError != nil { + return nil, decision.RewriteError + } + return nil, err + } + + lastErr = err + if attempt < r.config.MaxRetries { + applyDecisionForRetry(¤tInput, ¤tOpts, ctx, decision) + delay := decision.Backoff + if delay == 0 { + delay = backoffFunc(ctx, attempt+1) + } + if err := r.contextAwareSleep(ctx, delay); err != nil { + return nil, err + } + } + continue + } + + // Split the stream: checkCopy is consumed synchronously here to build the complete + // message for ShouldRetry inspection; returnCopy is returned to the caller and may + // already be consumed downstream in parallel. The verdict signal bridges the two: + // once ShouldRetry decides, the signal tells returnCopy's errWrapper/onEOF whether + // to pass through normally or inject a WillRetryError. + copies := stream.Copy(2) + checkCopy := copies[0] + returnCopy := copies[1] + + msg, streamErr := typedConsumeStream(checkCopy) + + if errors.Is(streamErr, ErrStreamCanceled) { + signal.ch <- retryVerdict{WillRetry: false} + returnCopy.Close() + return nil, streamErr + } + + retryCtx := &TypedRetryContext[M]{ + RetryAttempt: attempt + 1, + InputMessages: currentInput, + Options: currentOpts, + OutputMessage: msg, + Err: streamErr, + } + decision := r.config.ShouldRetry(ctx, retryCtx) + if decision == nil { + decision = &TypedRetryDecision[M]{} + } + + if !decision.Retry { + signal.ch <- retryVerdict{WillRetry: false} + + if decision.RewriteError != nil { + returnCopy.Close() + return nil, decision.RewriteError + } + if streamErr != nil { + returnCopy.Close() + return nil, streamErr + } + return returnCopy, nil + } + + verdictErr := streamErr + if verdictErr == nil { + verdictErr = fmt.Errorf("model output rejected by ShouldRetry at attempt %d", attempt+1) + } + signal.ch <- retryVerdict{ + WillRetry: true, + RetryAttempt: attempt, + Err: verdictErr, + RejectReason: decision.RejectReason, + } + returnCopy.Close() + + lastErr = verdictErr + + if attempt < r.config.MaxRetries { + applyDecisionForRetry(¤tInput, ¤tOpts, ctx, decision) + delay := decision.Backoff + if delay == 0 { + delay = backoffFunc(ctx, attempt+1) + } + if err := r.contextAwareSleep(ctx, delay); err != nil { + return nil, err + } + } + } + + return nil, &RetryExhaustedError{LastErr: lastErr, TotalRetries: r.config.MaxRetries} +} + +func applyDecisionForRetry[M MessageType](currentInput *[]M, currentOpts *[]model.Option, ctx context.Context, decision *TypedRetryDecision[M]) { + if decision.ModifiedInputMessages != nil { + *currentInput = decision.ModifiedInputMessages + if decision.PersistModifiedInputMessages { + modifiedInput := *currentInput + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.Messages = modifiedInput + return nil + }) + } + } + + if decision.AdditionalOptions != nil { + cloned := make([]model.Option, len(*currentOpts), len(*currentOpts)+len(decision.AdditionalOptions)) + copy(cloned, *currentOpts) + *currentOpts = append(cloned, decision.AdditionalOptions...) + } +} + +func (r *typedRetryModelWrapper[M]) Stream(ctx context.Context, input []M, opts ...model.Option) ( + *schema.StreamReader[M], error) { + + if r.config.ShouldRetry != nil { + return streamWithShouldRetry(r, ctx, input, opts...) + } + return r.streamLegacy(ctx, input, opts...) +} + +func (r *typedRetryModelWrapper[M]) streamLegacy(ctx context.Context, input []M, opts ...model.Option) ( + *schema.StreamReader[M], error) { + + isRetryAble := r.config.IsRetryAble + if isRetryAble == nil { + isRetryAble = defaultIsRetryAble + } + backoffFunc := r.config.BackoffFunc + if backoffFunc == nil { + backoffFunc = defaultBackoff + } + + defer func() { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setRetryAttempt(0) + return nil + }) + }() + + var lastErr error + for attempt := 0; attempt <= r.config.MaxRetries; attempt++ { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setRetryAttempt(attempt) + return nil + }) + + stream, err := r.inner.Stream(ctx, input, opts...) + if err != nil { + if _, ok := compose.ExtractInterruptInfo(err); ok { + return nil, err + } + if errors.Is(err, ErrStreamCanceled) { + return nil, err + } + if !isRetryAble(ctx, err) { + return nil, err + } + lastErr = err + if attempt < r.config.MaxRetries { + if err := r.contextAwareSleep(ctx, backoffFunc(ctx, attempt+1)); err != nil { + return nil, err + } + } + continue + } + + copies := stream.Copy(2) + checkCopy := copies[0] + returnCopy := copies[1] + + streamErr := consumeStreamForError(checkCopy) + if streamErr == nil { + return returnCopy, nil + } + + returnCopy.Close() + if errors.Is(streamErr, ErrStreamCanceled) { + return nil, streamErr + } + if !isRetryAble(ctx, streamErr) { + return nil, streamErr + } + + lastErr = streamErr + if attempt < r.config.MaxRetries { + if err := r.contextAwareSleep(ctx, backoffFunc(ctx, attempt+1)); err != nil { + return nil, err + } + } + } + + return nil, &RetryExhaustedError{LastErr: lastErr, TotalRetries: r.config.MaxRetries} +} diff --git a/adk/runctx.go b/adk/runctx.go new file mode 100644 index 0000000..dd42226 --- /dev/null +++ b/adk/runctx.go @@ -0,0 +1,567 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + "io" + "sort" + "sync" + "time" + + "github.com/cloudwego/eino/schema" +) + +// runSession CheckpointSchema: persisted via serialization.RunCtx (gob). +type runSession struct { + Values map[string]any + valuesMtx *sync.Mutex + + Events []*agentEventWrapper + LaneEvents *laneEvents + mtx sync.Mutex + + // TypedEvents stores *[]*typedAgentEventWrapper[M] for M != *schema.Message. + // For M = *schema.Message, the existing Events field is used instead. + // The any type is required because Go does not support generic fields in non-generic structs. + TypedEvents any +} + +// laneEvents CheckpointSchema: persisted via serialization.RunCtx (gob). +type laneEvents struct { + Events []*agentEventWrapper + Parent *laneEvents +} + +// agentEventWrapper CheckpointSchema: persisted via serialization.RunCtx (gob). +type agentEventWrapper struct { + *AgentEvent + mu sync.Mutex + concatenatedMessage Message + // TS is the timestamp (in nanoseconds) when this event was created. + // It is primarily used by the laneEvents mechanism to order events + // from different agents in a multi-agent flow. + TS int64 + // StreamErr stores the error message if the MessageStream contained an error. + // This field guards against multiple calls to getMessageFromWrappedEvent + // when the stream has already been consumed and errored. + // Normally when StreamErr happens, the Agent will return with the error, + // unless retry is configured for the agent generating this stream, in which case + // this StreamErr will be of type WillRetryError (indicating retry is pending). + StreamErr error +} + +type typedAgentEventWrapper[M MessageType] struct { + event *TypedAgentEvent[M] + mu sync.Mutex + concatenatedMessage M + TS int64 + StreamErr error +} + +// typedAgentEventWrapperForGob is a gob-serializable representation of typedAgentEventWrapper. +// We encode the event and TS separately to avoid the sync.Mutex and non-exported fields. +type typedAgentEventWrapperForGob[M MessageType] struct { + Event *TypedAgentEvent[M] + TS int64 +} + +func (e *typedAgentEventWrapper[M]) GobEncode() ([]byte, error) { + if e.event != nil && e.event.Output != nil && e.event.Output.MessageOutput != nil && e.event.Output.MessageOutput.IsStreaming { + // Materialize the stream before encoding. + if isNilMessage(e.concatenatedMessage) && e.StreamErr == nil { + e.consumeStream() + } + } + + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(&typedAgentEventWrapperForGob[M]{ + Event: e.event, + TS: e.TS, + }) + if err != nil { + return nil, fmt.Errorf("failed to gob encode generic agent event wrapper: %w", err) + } + return buf.Bytes(), nil +} + +func (e *typedAgentEventWrapper[M]) GobDecode(b []byte) error { + g := &typedAgentEventWrapperForGob[M]{} + if err := gob.NewDecoder(bytes.NewReader(b)).Decode(g); err != nil { + return fmt.Errorf("failed to gob decode generic agent event wrapper: %w", err) + } + e.event = g.Event + e.TS = g.TS + return nil +} + +// consumeStream drains the typed message stream, setting concatenatedMessage on success +// or StreamErr on failure. The stream is replaced with a materialized version safe for +// gob encoding. +// +// NOTE: This method parallels agentEventWrapper.consumeStream in utils.go. The two +// implementations exist because agentEventWrapper is non-generic (uses *schema.Message +// directly) while typedAgentEventWrapper[M] is generic. They cannot be unified without +// making the non-generic wrapper generic, which would cascade through the entire +// non-generic event storage layer. +func (e *typedAgentEventWrapper[M]) consumeStream() { + e.mu.Lock() + defer e.mu.Unlock() + + if !isNilMessage(e.concatenatedMessage) { + return + } + + s := e.event.Output.MessageOutput.MessageStream + var msgs []M + + defer s.Close() + for { + msg, err := s.Recv() + if err != nil { + if err == io.EOF { + break + } + e.StreamErr = err + e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs) + return + } + msgs = append(msgs, msg) + } + + if len(msgs) == 0 { + e.StreamErr = errors.New("no messages in typedAgentEventWrapper.MessageStream") + e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs) + return + } + + if len(msgs) == 1 { + e.concatenatedMessage = msgs[0] + } else { + var err error + e.concatenatedMessage, err = concatMessageStream(schema.StreamReaderFromArray(msgs)) + if err != nil { + e.StreamErr = err + e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs) + return + } + } + + e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray([]M{e.concatenatedMessage}) +} + +type otherAgentEventWrapperForEncode agentEventWrapper + +func (a *agentEventWrapper) GobEncode() ([]byte, error) { + if a.Output != nil && a.Output.MessageOutput != nil && a.Output.MessageOutput.IsStreaming { + // Materialize the stream before encoding. An unconsumed stream that + // ends with a non-EOF error (WillRetryError, ErrStreamCanceled) would + // cause MessageVariant.GobEncode to fail. consumeStream replaces the + // stream with an error-free, materialized version. + if a.concatenatedMessage == nil && a.StreamErr == nil { + a.consumeStream() + } + } + + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode((*otherAgentEventWrapperForEncode)(a)) + if err != nil { + return nil, fmt.Errorf("failed to gob encode agent event wrapper: %w", err) + } + return buf.Bytes(), nil +} + +func (a *agentEventWrapper) GobDecode(b []byte) error { + return gob.NewDecoder(bytes.NewReader(b)).Decode((*otherAgentEventWrapperForEncode)(a)) +} + +func newRunSession() *runSession { + return &runSession{ + Values: make(map[string]any), + valuesMtx: &sync.Mutex{}, + } +} + +// GetSessionValues returns all session key-value pairs for the current run. +func GetSessionValues(ctx context.Context) map[string]any { + session := getSession(ctx) + if session == nil { + return map[string]any{} + } + + return session.getValues() +} + +// AddSessionValue sets a single session key-value pair for the current run. +func AddSessionValue(ctx context.Context, key string, value any) { + session := getSession(ctx) + if session == nil { + return + } + + session.addValue(key, value) +} + +// AddSessionValues sets multiple session key-value pairs for the current run. +func AddSessionValues(ctx context.Context, kvs map[string]any) { + session := getSession(ctx) + if session == nil { + return + } + + session.addValues(kvs) +} + +// GetSessionValue retrieves a session value by key and reports whether it exists. +func GetSessionValue(ctx context.Context, key string) (any, bool) { + session := getSession(ctx) + if session == nil { + return nil, false + } + + return session.getValue(key) +} + +func (rs *runSession) addEvent(event *AgentEvent) { + wrapper := &agentEventWrapper{AgentEvent: event, TS: time.Now().UnixNano()} + // If LaneEvents is not nil, we are in a parallel lane. + // Append to the lane's local event slice (lock-free). + if rs.LaneEvents != nil { + rs.LaneEvents.Events = append(rs.LaneEvents.Events, wrapper) + return + } + + // Otherwise, we are on the main path. Append to the shared Events slice (with lock). + rs.mtx.Lock() + rs.Events = append(rs.Events, wrapper) + rs.mtx.Unlock() +} + +func (rs *runSession) getEvents() []*agentEventWrapper { + // If there are no in-flight lane events, we can return the main slice directly. + if rs.LaneEvents == nil { + rs.mtx.Lock() + events := rs.Events + rs.mtx.Unlock() + return events + } + + // If there are in-flight events, we must construct the full view. + // First, get the committed history from the main Events slice. + rs.mtx.Lock() + committedEvents := make([]*agentEventWrapper, len(rs.Events)) + copy(committedEvents, rs.Events) + rs.mtx.Unlock() + + // Then, assemble the in-flight events by traversing the linked list. + // Reading the .Parent pointer is safe without a lock because the parent of a lane is immutable after creation. + var laneSlices [][]*agentEventWrapper + totalLaneSize := 0 + for lane := rs.LaneEvents; lane != nil; lane = lane.Parent { + if len(lane.Events) > 0 { + laneSlices = append(laneSlices, lane.Events) + totalLaneSize += len(lane.Events) + } + } + + // Combine committed and in-flight history. + finalEvents := make([]*agentEventWrapper, 0, len(committedEvents)+totalLaneSize) + finalEvents = append(finalEvents, committedEvents...) + for i := len(laneSlices) - 1; i >= 0; i-- { + finalEvents = append(finalEvents, laneSlices[i]...) + } + + return finalEvents +} + +func addTypedEvent[M MessageType](session *runSession, event *TypedAgentEvent[M]) { + var zero M + if _, ok := any(zero).(*schema.Message); ok { + session.addEvent(any(event).(*AgentEvent)) + return + } + session.mtx.Lock() + defer session.mtx.Unlock() + wrapper := &typedAgentEventWrapper[M]{event: event, TS: time.Now().UnixNano()} + store, _ := session.TypedEvents.(*[]*typedAgentEventWrapper[M]) + if store == nil { + s := make([]*typedAgentEventWrapper[M], 0) + store = &s + session.TypedEvents = store + } + *store = append(*store, wrapper) +} + +func (rs *runSession) getValues() map[string]any { + rs.valuesMtx.Lock() + values := make(map[string]any, len(rs.Values)) + for k, v := range rs.Values { + values[k] = v + } + rs.valuesMtx.Unlock() + + return values +} + +func (rs *runSession) addValue(key string, value any) { + rs.valuesMtx.Lock() + rs.Values[key] = value + rs.valuesMtx.Unlock() +} + +func (rs *runSession) addValues(kvs map[string]any) { + rs.valuesMtx.Lock() + for k, v := range kvs { + rs.Values[k] = v + } + rs.valuesMtx.Unlock() +} + +func (rs *runSession) getValue(key string) (any, bool) { + rs.valuesMtx.Lock() + value, ok := rs.Values[key] + rs.valuesMtx.Unlock() + + return value, ok +} + +type runContext struct { + RootInput *AgentInput + RunPath []RunStep + + AgenticRootInput any + + Session *runSession +} + +func (rc *runContext) isRoot() bool { + return len(rc.RunPath) == 1 +} + +func (rc *runContext) deepCopy() *runContext { + copied := &runContext{ + RootInput: rc.RootInput, + AgenticRootInput: rc.AgenticRootInput, + RunPath: make([]RunStep, len(rc.RunPath)), + Session: rc.Session, + } + + copy(copied.RunPath, rc.RunPath) + + return copied +} + +type runCtxKey struct{} + +func getRunCtx(ctx context.Context) *runContext { + runCtx, ok := ctx.Value(runCtxKey{}).(*runContext) + if !ok { + return nil + } + return runCtx +} + +func setRunCtx(ctx context.Context, runCtx *runContext) context.Context { + return context.WithValue(ctx, runCtxKey{}, runCtx) +} + +func initRunCtx(ctx context.Context, agentName string, input *AgentInput) (context.Context, *runContext) { + runCtx := getRunCtx(ctx) + if runCtx != nil { + runCtx = runCtx.deepCopy() + } else { + runCtx = &runContext{Session: newRunSession()} + } + + runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName}) + if runCtx.isRoot() && input != nil { + runCtx.RootInput = input + } + + return setRunCtx(ctx, runCtx), runCtx +} + +func initTypedRunCtx[M MessageType](ctx context.Context, agentName string, input *TypedAgentInput[M]) (context.Context, *runContext) { + runCtx := getRunCtx(ctx) + if runCtx != nil { + runCtx = runCtx.deepCopy() + } else { + runCtx = &runContext{Session: newRunSession()} + } + + runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName}) + if runCtx.isRoot() && input != nil { + var zero M + if _, ok := any(zero).(*schema.Message); ok { + runCtx.RootInput = any(input).(*AgentInput) + } else { + runCtx.AgenticRootInput = input + } + } + + return setRunCtx(ctx, runCtx), runCtx +} + +func joinRunCtxs(parentCtx context.Context, childCtxs ...context.Context) { + switch len(childCtxs) { + case 0: + return + case 1: + // Optimization for the common case of a single branch. + newEvents := unwindLaneEvents(childCtxs...) + commitEvents(parentCtx, newEvents) + return + } + + // 1. Collect all new events from the leaf nodes of each context's lane. + newEvents := unwindLaneEvents(childCtxs...) + + // 2. Sort the collected events by their creation timestamp for chronological order. + sort.Slice(newEvents, func(i, j int) bool { + return newEvents[i].TS < newEvents[j].TS + }) + + // 3. Commit the sorted events to the parent. + commitEvents(parentCtx, newEvents) +} + +// commitEvents appends a slice of new events to the correct parent lane or main event log. +func commitEvents(ctx context.Context, newEvents []*agentEventWrapper) { + runCtx := getRunCtx(ctx) + if runCtx == nil || runCtx.Session == nil { + // Should not happen, but handle defensively. + return + } + + // If the context we are committing to is itself a lane, append to its event slice. + if runCtx.Session.LaneEvents != nil { + runCtx.Session.LaneEvents.Events = append(runCtx.Session.LaneEvents.Events, newEvents...) + } else { + // Otherwise, commit to the main, shared Events slice with a lock. + runCtx.Session.mtx.Lock() + runCtx.Session.Events = append(runCtx.Session.Events, newEvents...) + runCtx.Session.mtx.Unlock() + } +} + +// unwindLaneEvents traverses the LaneEvents of the given contexts and collects +// all events from the leaf nodes. +func unwindLaneEvents(ctxs ...context.Context) []*agentEventWrapper { + var allNewEvents []*agentEventWrapper + for _, ctx := range ctxs { + runCtx := getRunCtx(ctx) + if runCtx != nil && runCtx.Session != nil && runCtx.Session.LaneEvents != nil { + allNewEvents = append(allNewEvents, runCtx.Session.LaneEvents.Events...) + } + } + return allNewEvents +} + +func forkRunCtx(ctx context.Context) context.Context { + parentRunCtx := getRunCtx(ctx) + if parentRunCtx == nil || parentRunCtx.Session == nil { + // Should not happen in a parallel workflow, but handle defensively. + return ctx + } + + // Create a new session for the child lane by manually copying the parent's session fields. + // This is crucial to ensure a new mutex is created and that the LaneEvents pointer is unique. + childSession := &runSession{ + Events: parentRunCtx.Session.Events, // Share the committed history + Values: parentRunCtx.Session.Values, // Share the values map + valuesMtx: parentRunCtx.Session.valuesMtx, + } + + // Fork the lane events within the new session struct. + childSession.LaneEvents = &laneEvents{ + Parent: parentRunCtx.Session.LaneEvents, + Events: make([]*agentEventWrapper, 0), + } + + // Create a new runContext for the child lane, pointing to the new session. + childRunCtx := &runContext{ + RootInput: parentRunCtx.RootInput, + RunPath: make([]RunStep, len(parentRunCtx.RunPath)), + Session: childSession, + } + copy(childRunCtx.RunPath, parentRunCtx.RunPath) + + return setRunCtx(ctx, childRunCtx) +} + +// updateRunPathOnly creates a new context with an updated RunPath, but does NOT modify the Address. +// This is used by sequential workflows to accumulate execution history for LLM context, +// without incorrectly chaining the static addresses of peer agents. +func updateRunPathOnly(ctx context.Context, agentNames ...string) context.Context { + runCtx := getRunCtx(ctx) + if runCtx == nil { + // This should not happen in a sequential workflow context, but handle defensively. + runCtx = &runContext{Session: newRunSession()} + } else { + runCtx = runCtx.deepCopy() + } + + for _, agentName := range agentNames { + runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName}) + } + + return setRunCtx(ctx, runCtx) +} + +// ClearRunCtx clears the run context of the multi-agents. This is particularly useful +// when a customized agent with a multi-agents inside it is set as a subagent of another +// multi-agents. In such cases, it's not expected to pass the outside run context to the +// inside multi-agents, so this function helps isolate the contexts properly. +func ClearRunCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, runCtxKey{}, nil) +} + +func ctxWithNewTypedRunCtx[M MessageType](ctx context.Context, input *TypedAgentInput[M], sharedParentSession bool) context.Context { + var session *runSession + if sharedParentSession { + if parentSession := getSession(ctx); parentSession != nil { + session = &runSession{ + Values: parentSession.Values, + valuesMtx: parentSession.valuesMtx, + } + } + } + if session == nil { + session = newRunSession() + } + var zero M + rc := &runContext{Session: session} + if _, ok := any(zero).(*schema.Message); ok { + rc.RootInput = any(input).(*AgentInput) + } else { + rc.AgenticRootInput = input + } + return setRunCtx(ctx, rc) +} + +func getSession(ctx context.Context) *runSession { + runCtx := getRunCtx(ctx) + if runCtx != nil { + return runCtx.Session + } + + return nil +} diff --git a/adk/runctx_test.go b/adk/runctx_test.go new file mode 100644 index 0000000..bef1f44 --- /dev/null +++ b/adk/runctx_test.go @@ -0,0 +1,634 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestSessionValues(t *testing.T) { + // Test Case 1: Basic AddSessionValues and GetSessionValues + t.Run("BasicSessionValues", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add values to the session + values := map[string]any{ + "key1": "value1", + "key2": 42, + "key3": true, + } + AddSessionValues(ctx, values) + + // Get all values from the session + retrievedValues := GetSessionValues(ctx) + + // Verify the values were added correctly + assert.Equal(t, "value1", retrievedValues["key1"]) + assert.Equal(t, 42, retrievedValues["key2"]) + assert.Equal(t, true, retrievedValues["key3"]) + assert.Len(t, retrievedValues, 3) + }) + + // Test Case 2: AddSessionValues with empty context + t.Run("AddSessionValuesEmptyContext", func(t *testing.T) { + ctx := context.Background() + + // Add values to a context without a run session + values := map[string]any{ + "key1": "value1", + } + AddSessionValues(ctx, values) + + // Get values should return empty map + retrievedValues := GetSessionValues(ctx) + assert.Empty(t, retrievedValues) + }) + + // Test Case 3: GetSessionValues with empty context + t.Run("GetSessionValuesEmptyContext", func(t *testing.T) { + ctx := context.Background() + + // Get values from a context without a run session + retrievedValues := GetSessionValues(ctx) + assert.Empty(t, retrievedValues) + }) + + // Test Case 4: AddSessionValues with nil values + t.Run("AddSessionValuesNilValues", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add nil values map + AddSessionValues(ctx, nil) + + // Get values should still be empty + retrievedValues := GetSessionValues(ctx) + assert.Empty(t, retrievedValues) + }) + + // Test Case 5: AddSessionValues with empty values + t.Run("AddSessionValuesEmptyValues", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add empty values map + AddSessionValues(ctx, map[string]any{}) + + // Get values should be empty + retrievedValues := GetSessionValues(ctx) + assert.Empty(t, retrievedValues) + }) + + // Test Case 6: AddSessionValues with complex data types + t.Run("AddSessionValuesComplexTypes", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add complex values to the session + values := map[string]any{ + "string": "hello world", + "int": 123, + "float": 45.67, + "bool": true, + "slice": []string{"a", "b", "c"}, + "map": map[string]int{"x": 1, "y": 2}, + "struct": struct{ Name string }{Name: "test"}, + } + AddSessionValues(ctx, values) + + // Get all values from the session + retrievedValues := GetSessionValues(ctx) + + // Verify the complex values were added correctly + assert.Equal(t, "hello world", retrievedValues["string"]) + assert.Equal(t, 123, retrievedValues["int"]) + assert.Equal(t, 45.67, retrievedValues["float"]) + assert.Equal(t, true, retrievedValues["bool"]) + assert.Equal(t, []string{"a", "b", "c"}, retrievedValues["slice"]) + assert.Equal(t, map[string]int{"x": 1, "y": 2}, retrievedValues["map"]) + assert.Equal(t, struct{ Name string }{Name: "test"}, retrievedValues["struct"]) + assert.Len(t, retrievedValues, 7) + }) + + // Test Case 7: AddSessionValues overwrites existing values + t.Run("AddSessionValuesOverwrite", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add initial values + initialValues := map[string]any{ + "key1": "initial1", + "key2": "initial2", + } + AddSessionValues(ctx, initialValues) + + // Add values that overwrite some keys + overwriteValues := map[string]any{ + "key1": "overwritten1", + "key3": "new3", + } + AddSessionValues(ctx, overwriteValues) + + // Get all values from the session + retrievedValues := GetSessionValues(ctx) + + // Verify the values were overwritten correctly + assert.Equal(t, "overwritten1", retrievedValues["key1"]) // overwritten + assert.Equal(t, "initial2", retrievedValues["key2"]) // unchanged + assert.Equal(t, "new3", retrievedValues["key3"]) // new + assert.Len(t, retrievedValues, 3) + }) + + // Test Case 8: Concurrent access to session values + t.Run("ConcurrentSessionValues", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add initial values + initialValues := map[string]any{ + "counter": 0, + } + AddSessionValues(ctx, initialValues) + + // Simulate concurrent access + done := make(chan bool) + + // Goroutine 1: Add values + go func() { + for i := 0; i < 100; i++ { + values := map[string]any{ + "goroutine1": i, + } + AddSessionValues(ctx, values) + } + done <- true + }() + + // Goroutine 2: Add different values + go func() { + for i := 0; i < 100; i++ { + values := map[string]any{ + "goroutine2": i, + } + AddSessionValues(ctx, values) + } + done <- true + }() + + // Wait for both goroutines to complete + <-done + <-done + + // Verify that both values were set (last write wins) + retrievedValues := GetSessionValues(ctx) + assert.Equal(t, 0, retrievedValues["counter"]) + assert.Equal(t, 99, retrievedValues["goroutine1"]) + assert.Equal(t, 99, retrievedValues["goroutine2"]) + }) + + // Test Case 9: GetSessionValue individual value + t.Run("GetSessionValueIndividual", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add values to the session + values := map[string]any{ + "key1": "value1", + "key2": 42, + } + AddSessionValues(ctx, values) + + // Get individual values + value1, exists1 := GetSessionValue(ctx, "key1") + value2, exists2 := GetSessionValue(ctx, "key2") + value3, exists3 := GetSessionValue(ctx, "nonexistent") + + // Verify individual values + assert.True(t, exists1) + assert.Equal(t, "value1", value1) + + assert.True(t, exists2) + assert.Equal(t, 42, value2) + + assert.False(t, exists3) + assert.Nil(t, value3) + }) + + // Test Case 10: AddSessionValue individual value + t.Run("AddSessionValueIndividual", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a run session + session := newRunSession() + runCtx := &runContext{Session: session} + ctx = setRunCtx(ctx, runCtx) + + // Add individual values + AddSessionValue(ctx, "key1", "value1") + AddSessionValue(ctx, "key2", 42) + + // Get all values + retrievedValues := GetSessionValues(ctx) + + // Verify the values were added correctly + assert.Equal(t, "value1", retrievedValues["key1"]) + assert.Equal(t, 42, retrievedValues["key2"]) + assert.Len(t, retrievedValues, 2) + }) + + // Test Case 11: AddSessionValue with empty context + t.Run("AddSessionValueEmptyContext", func(t *testing.T) { + ctx := context.Background() + + // Add individual value to a context without a run session + AddSessionValue(ctx, "key1", "value1") + + // Get individual value should return false + value, exists := GetSessionValue(ctx, "key1") + assert.False(t, exists) + assert.Nil(t, value) + + // Get all values should return empty map + retrievedValues := GetSessionValues(ctx) + assert.Empty(t, retrievedValues) + }) + + // Test Case 12: Integration with run context initialization + t.Run("IntegrationWithRunContext", func(t *testing.T) { + ctx := context.Background() + + // Initialize a run context with an agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("test input"), + }, + } + ctx, runCtx := initRunCtx(ctx, "test-agent", input) + + // Verify the run context was created + assert.NotNil(t, runCtx) + assert.NotNil(t, runCtx.Session) + + // Add values to the session + values := map[string]any{ + "integration_key": "integration_value", + } + AddSessionValues(ctx, values) + + // Get values from the session + retrievedValues := GetSessionValues(ctx) + assert.Equal(t, "integration_value", retrievedValues["integration_key"]) + + // Verify the run path was set correctly + assert.Len(t, runCtx.RunPath, 1) + assert.Equal(t, "test-agent", runCtx.RunPath[0].agentName) + }) +} + +func TestForkJoinRunCtx(t *testing.T) { + // Helper to create a named event + newEvent := func(name string) *AgentEvent { + // Add a small sleep to ensure timestamps are distinct + time.Sleep(1 * time.Millisecond) + return &AgentEvent{AgentName: name} + } + + // Helper to get event names from a slice of wrappers + getEventNames := func(wrappers []*agentEventWrapper) []string { + names := make([]string, len(wrappers)) + for i, w := range wrappers { + names[i] = w.AgentName + } + return names + } + + // 1. Setup: Create an initial runContext for the main execution path. + mainCtx, mainRunCtx := initRunCtx(context.Background(), "Main", nil) + + // 2. Run Agent A + eventA := newEvent("A") + mainRunCtx.Session.addEvent(eventA) + assert.Equal(t, []string{"A"}, getEventNames(mainRunCtx.Session.getEvents()), "After A") + + // 3. Fork for Par(B, C) + ctxB := forkRunCtx(mainCtx) + ctxC := forkRunCtx(mainCtx) + + // Assertions for Fork + runCtxB := getRunCtx(ctxB) + runCtxC := getRunCtx(ctxC) + assert.NotSame(t, mainRunCtx.Session, runCtxB.Session, "Session B should be a new struct") + assert.NotSame(t, mainRunCtx.Session, runCtxC.Session, "Session C should be a new struct") + assert.NotSame(t, runCtxB.Session, runCtxC.Session, "Sessions B and C should be different") + assert.Nil(t, mainRunCtx.Session.LaneEvents, "Main session should have no lane events yet") + assert.NotNil(t, runCtxB.Session.LaneEvents, "Session B should have lane events") + assert.NotNil(t, runCtxC.Session.LaneEvents, "Session C should have lane events") + assert.Nil(t, runCtxB.Session.LaneEvents.Parent, "Lane B's parent should be the main (nil) lane") + assert.Nil(t, runCtxC.Session.LaneEvents.Parent, "Lane C's parent should be the main (nil) lane") + + // 4. Run Agent B + eventB := newEvent("B") + runCtxB.Session.addEvent(eventB) + assert.Equal(t, []string{"A", "B"}, getEventNames(runCtxB.Session.getEvents()), "After B") + + // 5. Run Agent C (and Nested Fork for Par(D, E)) + eventC1 := newEvent("C1") + runCtxC.Session.addEvent(eventC1) + assert.Equal(t, []string{"A", "C1"}, getEventNames(runCtxC.Session.getEvents()), "After C1") + + ctxD := forkRunCtx(ctxC) + ctxE := forkRunCtx(ctxC) + + // Assertions for Nested Fork + runCtxD := getRunCtx(ctxD) + runCtxE := getRunCtx(ctxE) + assert.NotNil(t, runCtxD.Session.LaneEvents.Parent, "Lane D's parent should be Lane C") + assert.Same(t, runCtxC.Session.LaneEvents, runCtxD.Session.LaneEvents.Parent, "Lane D's parent must be Lane C's node") + assert.Same(t, runCtxC.Session.LaneEvents, runCtxE.Session.LaneEvents.Parent, "Lane E's parent must be Lane C's node") + + // 6. Run Agents D and E + eventD := newEvent("D") + runCtxD.Session.addEvent(eventD) + eventE := newEvent("E") + runCtxE.Session.addEvent(eventE) + + assert.Equal(t, []string{"A", "C1", "D"}, getEventNames(runCtxD.Session.getEvents()), "After D") + assert.Equal(t, []string{"A", "C1", "E"}, getEventNames(runCtxE.Session.getEvents()), "After E") + + // 7. Join Par(D, E) + joinRunCtxs(ctxC, ctxD, ctxE) + + // Assertions for Nested Join + // The events should now be committed to Lane C's event slice. + assert.Equal(t, []string{"A", "C1", "D", "E"}, getEventNames(runCtxC.Session.getEvents()), "After joining D and E") + + // 8. Join Par(B, C) + joinRunCtxs(mainCtx, ctxB, ctxC) + + // Assertions for Top-Level Join + // The events should now be committed to the main session's Events slice. + assert.Equal(t, []string{"A", "B", "C1", "D", "E"}, getEventNames(mainRunCtx.Session.getEvents()), "After joining B and C") + + // 9. Run Agent F + eventF := newEvent("F") + mainRunCtx.Session.addEvent(eventF) + assert.Equal(t, []string{"A", "B", "C1", "D", "E", "F"}, getEventNames(mainRunCtx.Session.getEvents()), "After F") +} + +// makeStreamingEventWrapper creates an agentEventWrapper with a streaming MessageOutput +// whose stream yields the given message then terminates with streamErr (or io.EOF if nil). +func makeStreamingEventWrapper(msg Message, streamErr error) *agentEventWrapper { + r, w := schema.Pipe[Message](2) + w.Send(msg, nil) + if streamErr != nil { + w.Send(nil, streamErr) + } + w.Close() + + return &agentEventWrapper{ + AgentEvent: &AgentEvent{ + AgentName: "test-agent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: r, + Role: schema.Assistant, + }, + }, + }, + } +} + +func TestGobEncodeStreamErrors(t *testing.T) { + t.Run("WillRetryError_unconsumed_stream_fails_GobEncode", func(t *testing.T) { + // An agentEventWrapper whose stream yields a message then WillRetryError. + // Without pre-consuming (no getMessageFromWrappedEvent call), GobEncode + // reaches MessageVariant.GobEncode which treats non-EOF errors as fatal. + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("partial", nil), + &WillRetryError{ErrStr: "model error", RetryAttempt: 1}, + ) + + _, err := wrapper.GobEncode() + assert.NoError(t, err, "GobEncode should handle WillRetryError streams gracefully") + }) + + t.Run("ErrStreamCanceled_unconsumed_stream_fails_GobEncode", func(t *testing.T) { + // Same scenario but with ErrStreamCanceled (*errors.errorString). + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("partial", nil), + ErrStreamCanceled, + ) + + _, err := wrapper.GobEncode() + assert.NoError(t, err, "GobEncode should handle ErrStreamCanceled streams gracefully") + }) + + t.Run("successful_stream_GobEncode_succeeds", func(t *testing.T) { + // Control: a clean stream (no error) should encode fine. + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("hello", nil), + nil, // no stream error + ) + + data, err := wrapper.GobEncode() + assert.NoError(t, err) + assert.NotEmpty(t, data) + + // Verify round-trip decode works. + decoded := &agentEventWrapper{AgentEvent: &AgentEvent{}} + err = decoded.GobDecode(data) + assert.NoError(t, err) + assert.Equal(t, "test-agent", decoded.AgentName) + }) + + t.Run("preconsumed_WillRetryError_GobEncode_succeeds", func(t *testing.T) { + // When getMessageFromWrappedEvent is called first, WillRetryError is + // cached in StreamErr and the stream is replaced with an error-free array. + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("partial", nil), + &WillRetryError{ErrStr: "model error", RetryAttempt: 1}, + ) + + _, consumeErr := getMessageFromWrappedEvent(wrapper) + assert.Error(t, consumeErr) + + data, err := wrapper.GobEncode() + assert.NoError(t, err, "GobEncode should succeed after pre-consuming WillRetryError stream") + assert.NotEmpty(t, data) + }) + + t.Run("preconsumed_ErrStreamCanceled_GobEncode_succeeds", func(t *testing.T) { + // ErrStreamCanceled is a *StreamCanceledError which IS gob-registered. + // After getMessageFromWrappedEvent, StreamErr = ErrStreamCanceled. + // Since it's registered, gob encoding succeeds. + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("partial", nil), + ErrStreamCanceled, + ) + + _, consumeErr := getMessageFromWrappedEvent(wrapper) + assert.Error(t, consumeErr) + + data, err := wrapper.GobEncode() + assert.NoError(t, err, "GobEncode should succeed; ErrStreamCanceled is gob-registered") + assert.NotEmpty(t, data) + }) + + t.Run("GobEncode_roundtrip_preserves_content", func(t *testing.T) { + // Verify that after GobEncode with a WillRetryError stream, + // the decoded wrapper has the partial message content and StreamErr intact. + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("partial response", nil), + &WillRetryError{ErrStr: "err", RetryAttempt: 1}, + ) + + data, err := wrapper.GobEncode() + assert.NoError(t, err) + + decoded := &agentEventWrapper{AgentEvent: &AgentEvent{}} + err = decoded.GobDecode(data) + assert.NoError(t, err) + assert.Equal(t, "test-agent", decoded.AgentName) + assert.True(t, decoded.Output.MessageOutput.IsStreaming) + // The stream should be consumable and yield the partial message. + msg, recvErr := decoded.Output.MessageOutput.MessageStream.Recv() + assert.NoError(t, recvErr) + assert.Contains(t, msg.Content, "partial response") + // StreamErr should be preserved for end-user visibility. + var willRetryErr *WillRetryError + assert.True(t, errors.As(decoded.StreamErr, &willRetryErr)) + assert.Equal(t, "err", willRetryErr.ErrStr) + }) + + t.Run("GobEncode_roundtrip_preserves_ErrStreamCanceled", func(t *testing.T) { + // ErrStreamCanceled (*StreamCanceledError) is gob-registered, so + // StreamErr should survive encoding/decoding. + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("partial", nil), + ErrStreamCanceled, + ) + + data, err := wrapper.GobEncode() + assert.NoError(t, err) + + decoded := &agentEventWrapper{AgentEvent: &AgentEvent{}} + err = decoded.GobDecode(data) + assert.NoError(t, err) + var streamCanceledErr *StreamCanceledError + assert.ErrorAs(t, decoded.StreamErr, &streamCanceledErr) + }) + + t.Run("GobEncode_idempotent", func(t *testing.T) { + // Calling GobEncode twice should succeed both times (stream replaced on first call). + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("hello", nil), + &WillRetryError{ErrStr: "err", RetryAttempt: 1}, + ) + + data1, err := wrapper.GobEncode() + assert.NoError(t, err) + + data2, err := wrapper.GobEncode() + assert.NoError(t, err) + + // Both should decode to equivalent content. + d1, d2 := &agentEventWrapper{AgentEvent: &AgentEvent{}}, &agentEventWrapper{AgentEvent: &AgentEvent{}} + assert.NoError(t, d1.GobDecode(data1)) + assert.NoError(t, d2.GobDecode(data2)) + assert.Equal(t, d1.AgentName, d2.AgentName) + }) + + t.Run("GobEncode_non_streaming_unaffected", func(t *testing.T) { + // Non-streaming events should encode/decode as before. + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + AgentName: "non-stream-agent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("direct", nil), + Role: schema.Assistant, + }, + }, + }, + } + + data, err := wrapper.GobEncode() + assert.NoError(t, err) + + decoded := &agentEventWrapper{AgentEvent: &AgentEvent{}} + assert.NoError(t, decoded.GobDecode(data)) + assert.Equal(t, "non-stream-agent", decoded.AgentName) + assert.False(t, decoded.Output.MessageOutput.IsStreaming) + }) + + t.Run("GobEncode_within_runSession", func(t *testing.T) { + // Simulate the real scenario: a runSession with a streaming event containing + // WillRetryError is gob-encoded (as happens during checkpoint save). + wrapper := makeStreamingEventWrapper( + schema.AssistantMessage("checkpoint content", nil), + &WillRetryError{ErrStr: "retry", RetryAttempt: 1}, + ) + + session := newRunSession() + session.Events = []*agentEventWrapper{wrapper} + + // Encode the entire session (the checkpoint path). + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(session) + assert.NoError(t, err, "encoding runSession with WillRetryError stream should succeed") + }) +} diff --git a/adk/runner.go b/adk/runner.go new file mode 100644 index 0000000..a7d722e --- /dev/null +++ b/adk/runner.go @@ -0,0 +1,342 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "runtime/debug" + "sync" + + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +func errorIterator[M MessageType](err error) *AsyncIterator[*TypedAgentEvent[M]] { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + gen.Send(&TypedAgentEvent[M]{Err: err}) + gen.Close() + return iter +} + +func newUserMessage[M MessageType](query string) (M, error) { + var zero M + switch any(zero).(type) { + case *schema.Message: + return any(schema.UserMessage(query)).(M), nil + case *schema.AgenticMessage: + return any(schema.UserAgenticMessage(query)).(M), nil + default: + return zero, fmt.Errorf("unsupported message type %T", zero) + } +} + +// TypedRunner is the primary entry point for executing an Agent. +// It manages the agent's lifecycle, including starting, resuming, and checkpointing. +// +// Execution always goes through the flowAgent pipeline, which handles +// multi-agent orchestration, callbacks, agent naming, run paths, and cancellation. +type TypedRunner[M MessageType] struct { + a TypedAgent[M] + enableStreaming bool + store CheckPointStore +} + +// Runner is the default runner type using *schema.Message. +type Runner = TypedRunner[*schema.Message] + +type CheckPointStore = core.CheckPointStore + +type CheckPointDeleter = core.CheckPointDeleter + +type TypedRunnerConfig[M MessageType] struct { + Agent TypedAgent[M] + EnableStreaming bool + + CheckPointStore CheckPointStore +} + +// RunnerConfig is the default runner config type using *schema.Message. +type RunnerConfig = TypedRunnerConfig[*schema.Message] + +// ResumeParams contains all parameters needed to resume an execution. +// This struct provides an extensible way to pass resume parameters without +// requiring breaking changes to method signatures. +type ResumeParams struct { + // Targets contains the addresses of components to be resumed as keys, + // with their corresponding resume data as values + Targets map[string]any + // Future extensible fields can be added here without breaking changes +} + +// NewRunner creates a new Runner with the given config. +func NewRunner(_ context.Context, conf RunnerConfig) *Runner { + return NewTypedRunner(conf) +} + +// NewTypedRunner creates a new TypedRunner with the given config. +func NewTypedRunner[M MessageType](conf TypedRunnerConfig[M]) *TypedRunner[M] { + return &TypedRunner[M]{ + enableStreaming: conf.EnableStreaming, + a: conf.Agent, + store: conf.CheckPointStore, + } +} + +func (r *TypedRunner[M]) Run(ctx context.Context, messages []M, + opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + return typedRunnerRunImpl(r.a, r.enableStreaming, r.store, ctx, messages, opts...) +} + +// Query is a convenience method that starts a new execution with a single user query string. +func (r *TypedRunner[M]) Query(ctx context.Context, + query string, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + msgs, err := newUserMessage[M](query) + if err != nil { + return errorIterator[M](err) + } + return r.Run(ctx, []M{msgs}, opts...) +} + +// Resume continues an interrupted execution from a checkpoint, using an "Implicit Resume All" strategy. +// This method is best for simpler use cases where the act of resuming implies that all previously +// interrupted points should proceed without specific data. +// +// When using this method, all interrupted agents will receive `isResumeFlow = false` when they +// call `GetResumeContext`, as no specific agent was targeted. This is suitable for the "Simple Confirmation" +// pattern where an agent only needs to know `wasInterrupted` is true to continue. +func (r *TypedRunner[M]) Resume(ctx context.Context, checkPointID string, opts ...AgentRunOption) ( + *AsyncIterator[*TypedAgentEvent[M]], error) { + return r.resumeInternal(ctx, checkPointID, nil, opts...) +} + +// ResumeWithParams continues an interrupted execution from a checkpoint with specific parameters. +// This is the most common and powerful way to resume, allowing you to target specific interrupt points +// (identified by their address/ID) and provide them with data. +// +// The params.Targets map should contain the addresses of the components to be resumed as keys. These addresses +// can point to any interruptible component in the entire execution graph, including ADK agents, compose +// graph nodes, or tools. The value can be the resume data for that component, or `nil` if no data is needed. +// +// When using this method: +// - Components whose addresses are in the params.Targets map will receive `isResumeFlow = true` when they +// call `GetResumeContext`. +// - Interrupted components whose addresses are NOT in the params.Targets map must decide how to proceed: +// -- "Leaf" components (the actual root causes of the original interrupt) MUST re-interrupt themselves +// to preserve their state. +// -- "Composite" agents (like SequentialAgent or ChatModelAgent) should generally proceed with their +// execution. They act as conduits, allowing the resume signal to flow to their children. They will +// naturally re-interrupt if one of their interrupted children re-interrupts, as they receive the +// new `CompositeInterrupt` signal from them. +func (r *TypedRunner[M]) ResumeWithParams(ctx context.Context, checkPointID string, params *ResumeParams, opts ...AgentRunOption) (*AsyncIterator[*TypedAgentEvent[M]], error) { + return r.resumeInternal(ctx, checkPointID, params.Targets, opts...) +} + +func (r *TypedRunner[M]) resumeInternal(ctx context.Context, checkPointID string, resumeData map[string]any, + opts ...AgentRunOption) (*AsyncIterator[*TypedAgentEvent[M]], error) { + return typedRunnerResumeInternalImpl(r.a, r.store, ctx, checkPointID, resumeData, opts...) +} + +func typedRunnerRunImpl[M MessageType](a TypedAgent[M], enableStreaming bool, store CheckPointStore, ctx context.Context, messages []M, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] { + o := getCommonOptions(nil, opts...) + + input := &TypedAgentInput[M]{ + Messages: messages, + EnableStreaming: enableStreaming, + } + + var zero M + if _, ok := any(zero).(*schema.Message); ok { + concreteAgent, _ := any(a).(Agent) + fa := toFlowAgent(ctx, concreteAgent) + if store != nil { + fa.checkPointStore = store + } + concreteInput := any(input).(*AgentInput) + ctx = ctxWithNewTypedRunCtx(ctx, input, o.sharedParentSession) + AddSessionValues(ctx, o.sessionValues) + + iter := fa.Run(ctx, concreteInput, opts...) + + if store == nil && o.cancelCtx == nil { + return any(iter).(*AsyncIterator[*TypedAgentEvent[M]]) + } + + niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go typedRunnerHandleIterImpl(enableStreaming, store, ctx, any(iter).(*AsyncIterator[*TypedAgentEvent[M]]), gen, o.checkPointID, o.cancelCtx) + return niter + } + + fa := toTypedFlowAgent(a) + if store != nil { + fa.checkPointStore = store + } + + ctx = ctxWithNewTypedRunCtx(ctx, input, o.sharedParentSession) + AddSessionValues(ctx, o.sessionValues) + + iter := fa.Run(ctx, input, opts...) + + if store == nil && o.cancelCtx == nil { + return iter + } + + niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go typedRunnerHandleIterImpl(enableStreaming, store, ctx, iter, gen, o.checkPointID, o.cancelCtx) + return niter +} + +func typedRunnerResumeInternalImpl[M MessageType](a TypedAgent[M], store CheckPointStore, ctx context.Context, checkPointID string, resumeData map[string]any, //nolint:revive // argument-limit + opts ...AgentRunOption) (*AsyncIterator[*TypedAgentEvent[M]], error) { + if store == nil { + return nil, fmt.Errorf("failed to resume: store is nil") + } + + ctx, runCtx, resumeInfo, err := runnerLoadCheckPointImpl(store, ctx, checkPointID) + if err != nil { + return nil, fmt.Errorf("failed to load from checkpoint: %w", err) + } + + // Resume uses the streaming mode persisted in the checkpoint, not the value the + // caller passed when constructing the runner. This is the runner's own invariant: + // the checkpoint is the source of truth for what mode the original execution was + // running in, and any new checkpoint written during this resume must preserve it. + enableStreaming := resumeInfo.EnableStreaming + + o := getCommonOptions(nil, opts...) + if o.sharedParentSession { + parentSession := getSession(ctx) + if parentSession != nil { + runCtx.Session.Values = parentSession.Values + runCtx.Session.valuesMtx = parentSession.valuesMtx + } + } + if runCtx.Session.valuesMtx == nil { + runCtx.Session.valuesMtx = &sync.Mutex{} + } + if runCtx.Session.Values == nil { + runCtx.Session.Values = make(map[string]any) + } + + ctx = setRunCtx(ctx, runCtx) + AddSessionValues(ctx, o.sessionValues) + + if len(resumeData) > 0 { + ctx = core.BatchResumeWithData(ctx, resumeData) + } + + var zero M + if _, ok := any(zero).(*schema.Message); ok { + concreteAgent, _ := any(a).(Agent) + fa := toFlowAgent(ctx, concreteAgent) + ra, ok := Agent(fa).(ResumableAgent) + if !ok { + return nil, fmt.Errorf("agent %T does not support resume", a) + } + aIter := ra.Resume(ctx, resumeInfo, opts...) + + niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go typedRunnerHandleIterImpl(enableStreaming, store, ctx, any(aIter).(*AsyncIterator[*TypedAgentEvent[M]]), gen, &checkPointID, o.cancelCtx) + return niter, nil + } + + fa := toTypedFlowAgent(a) + ra, ok := TypedAgent[M](fa).(TypedResumableAgent[M]) + if !ok { + return nil, fmt.Errorf("agent %T does not support resume", a) + } + aIter := ra.Resume(ctx, resumeInfo, opts...) + + niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go typedRunnerHandleIterImpl(enableStreaming, store, ctx, aIter, gen, &checkPointID, o.cancelCtx) + return niter, nil +} + +func typedRunnerHandleIterImpl[M MessageType](enableStreaming bool, store CheckPointStore, ctx context.Context, aIter *AsyncIterator[*TypedAgentEvent[M]], //nolint:revive // argument-limit + gen *AsyncGenerator[*TypedAgentEvent[M]], checkPointID *string, cancelCtx *cancelContext) { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + gen.Send(&TypedAgentEvent[M]{Err: e}) + } + + gen.Close() + }() + var ( + interruptSignal *core.InterruptSignal + legacyData any + ) + for { + event, ok := aIter.Next() + if !ok { + break + } + + if event.Err != nil { + var cancelErr *CancelError + if errors.As(event.Err, &cancelErr) { + if cancelCtx != nil && cancelCtx.isRoot() && cancelCtx.shouldCancel() { + cancelCtx.markCancelHandled() + } + if cancelErr.interruptSignal != nil && checkPointID != nil { + cancelErr.InterruptContexts = core.ToInterruptContexts(cancelErr.interruptSignal, allowedAddressSegmentTypes) + err := runnerSaveCheckPointImpl(enableStreaming, store, ctx, *checkPointID, &InterruptInfo{}, cancelErr.interruptSignal) + if err != nil { + gen.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("failed to save checkpoint on cancel: %w", err)}) + } + } + gen.Send(event) + break + } + } + + if event.Action != nil && event.Action.internalInterrupted != nil { + if interruptSignal != nil { + panic("multiple interrupt actions should not happen in Runner") + } + interruptSignal = event.Action.internalInterrupted + interruptContexts := core.ToInterruptContexts(interruptSignal, allowedAddressSegmentTypes) + event = &TypedAgentEvent[M]{ + AgentName: event.AgentName, + RunPath: event.RunPath, + Output: event.Output, + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + Data: event.Action.Interrupted.Data, + InterruptContexts: interruptContexts, + }, + internalInterrupted: interruptSignal, + }, + } + legacyData = event.Action.Interrupted.Data + + if checkPointID != nil { + err := runnerSaveCheckPointImpl(enableStreaming, store, ctx, *checkPointID, &InterruptInfo{ + Data: legacyData, + }, interruptSignal) + if err != nil { + gen.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("failed to save checkpoint: %w", err)}) + } + } + } + + gen.Send(event) + } +} diff --git a/adk/runner_test.go b/adk/runner_test.go new file mode 100644 index 0000000..cc59569 --- /dev/null +++ b/adk/runner_test.go @@ -0,0 +1,311 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/schema" +) + +// mockRunnerAgent is a simple implementation of the Agent interface for testing Runner +type mockRunnerAgent struct { + name string + description string + responses []*AgentEvent + // Track calls to verify correct parameters were passed + callCount int + lastInput *AgentInput + enableStreaming bool +} + +func (a *mockRunnerAgent) Name(_ context.Context) string { + return a.name +} + +func (a *mockRunnerAgent) Description(_ context.Context) string { + return a.description +} + +func (a *mockRunnerAgent) Run(_ context.Context, input *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + // Record the call details for verification + a.callCount++ + a.lastInput = input + a.enableStreaming = input.EnableStreaming + + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + defer generator.Close() + + for _, event := range a.responses { + generator.Send(event) + + // If the event has an Exit action, stop sending events + if event.Action != nil && event.Action.Exit { + break + } + } + }() + + return iterator +} + +func newMockRunnerAgent(name, description string, responses []*AgentEvent) *mockRunnerAgent { + return &mockRunnerAgent{ + name: name, + description: description, + responses: responses, + } +} + +func TestNewRunner(t *testing.T) { + ctx := context.Background() + config := RunnerConfig{} + + runner := NewRunner(ctx, config) + + // Verify that a non-nil runner is returned + assert.NotNil(t, runner) +} + +func TestRunner_Run(t *testing.T) { + ctx := context.Background() + + // Create a mock agent with predefined responses + mockAgent_ := newMockRunnerAgent("TestAgent", "Test agent for Runner", []*AgentEvent{ + { + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from test agent", nil), + Role: schema.Assistant, + }, + }}, + }) + + // Create a runner + runner := NewRunner(ctx, RunnerConfig{Agent: mockAgent_}) + + // Create test messages + msgs := []Message{ + schema.UserMessage("Hello, agent!"), + } + + // Test Run method without streaming + iterator := runner.Run(ctx, msgs) + + // Verify that the agent's Run method was called with the correct parameters + assert.Equal(t, 1, mockAgent_.callCount) + assert.Equal(t, msgs, mockAgent_.lastInput.Messages) + assert.False(t, mockAgent_.enableStreaming) + + // Verify that we can get the expected response from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Equal(t, "TestAgent", event.AgentName) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.NotNil(t, event.Output.MessageOutput.Message) + assert.Equal(t, "Response from test agent", event.Output.MessageOutput.Message.Content) + + // Verify that the iterator is now closed + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestRunner_Run_WithStreaming(t *testing.T) { + ctx := context.Background() + + // Create a mock agent with predefined responses + mockAgent_ := newMockRunnerAgent("TestAgent", "Test agent for Runner", []*AgentEvent{ + { + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + Message: nil, + MessageStream: schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("Streaming response", nil)}), + Role: schema.Assistant, + }, + }}, + }) + + // Create a runner + runner := NewRunner(ctx, RunnerConfig{EnableStreaming: true, Agent: mockAgent_}) + + // Create test messages + msgs := []Message{ + schema.UserMessage("Hello, agent!"), + } + + // Test Run method with streaming enabled + iterator := runner.Run(ctx, msgs) + + // Verify that the agent's Run method was called with the correct parameters + assert.Equal(t, 1, mockAgent_.callCount) + assert.Equal(t, msgs, mockAgent_.lastInput.Messages) + assert.True(t, mockAgent_.enableStreaming) + + // Verify that we can get the expected response from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Equal(t, "TestAgent", event.AgentName) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + // Verify that the iterator is now closed + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestRunner_Query(t *testing.T) { + ctx := context.Background() + + // Create a mock agent with predefined responses + mockAgent_ := newMockRunnerAgent("TestAgent", "Test agent for Runner", []*AgentEvent{ + { + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response to query", nil), + Role: schema.Assistant, + }, + }}, + }) + + // Create a runner + runner := NewRunner(ctx, RunnerConfig{Agent: mockAgent_}) + + // Test Query method + iterator := runner.Query(ctx, "Test query") + + // Verify that the agent's Run method was called with the correct parameters + assert.Equal(t, 1, mockAgent_.callCount) + assert.Equal(t, 1, len(mockAgent_.lastInput.Messages)) + assert.Equal(t, "Test query", mockAgent_.lastInput.Messages[0].Content) + assert.False(t, mockAgent_.enableStreaming) + + // Verify that we can get the expected response from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Equal(t, "TestAgent", event.AgentName) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.NotNil(t, event.Output.MessageOutput.Message) + assert.Equal(t, "Response to query", event.Output.MessageOutput.Message.Content) + + // Verify that the iterator is now closed + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestRunner_Query_WithStreaming(t *testing.T) { + ctx := context.Background() + + // Create a mock agent with predefined responses + mockAgent_ := newMockRunnerAgent("TestAgent", "Test agent for Runner", []*AgentEvent{ + { + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + Message: nil, + MessageStream: schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("Streaming query response", nil)}), + Role: schema.Assistant, + }, + }}, + }) + + // Create a runner + runner := NewRunner(ctx, RunnerConfig{EnableStreaming: true, Agent: mockAgent_}) + + // Test Query method with streaming enabled + iterator := runner.Query(ctx, "Test query") + + // Verify that the agent's Run method was called with the correct parameters + assert.Equal(t, 1, mockAgent_.callCount) + assert.Equal(t, 1, len(mockAgent_.lastInput.Messages)) + assert.Equal(t, "Test query", mockAgent_.lastInput.Messages[0].Content) + assert.True(t, mockAgent_.enableStreaming) + + // Verify that we can get the expected response from the iterator + event, ok := iterator.Next() + assert.True(t, ok) + assert.Equal(t, "TestAgent", event.AgentName) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.True(t, event.Output.MessageOutput.IsStreaming) + + // Verify that the iterator is now closed + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestResumeWithMissingCheckpoint(t *testing.T) { + ctx := context.Background() + + agent := &myAgenticAgent{ + name: "resume-agent", + runFn: func(ctx context.Context, input *TypedAgentInput[*schema.AgenticMessage], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] { + iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]() + go func() { + defer gen.Close() + gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{ + Output: &TypedAgentOutput[*schema.AgenticMessage]{ + MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{ + Message: agenticMsg("ok"), + }, + }, + }) + }() + return iter + }, + } + + store := newMyStore() + runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{ + Agent: agent, + CheckPointStore: store, + }) + + require.NotPanics(t, func() { + iter, err := runner.ResumeWithParams(ctx, "nonexistent-checkpoint", &ResumeParams{ + Targets: map[string]any{"fake-id": nil}, + }) + if err != nil { + t.Logf("Got expected error: %v", err) + return + } + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Err != nil { + t.Logf("Got error event: %v", event.Err) + } + } + }, "ResumeWithParams with nonexistent checkpoint should not panic") +} diff --git a/adk/turn_buffer.go b/adk/turn_buffer.go new file mode 100644 index 0000000..643c9bc --- /dev/null +++ b/adk/turn_buffer.go @@ -0,0 +1,134 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import "sync" + +type turnBuffer[T any] struct { + buf []T + mu sync.Mutex + notEmpty *sync.Cond + closed bool + woken bool +} + +func newTurnBuffer[T any]() *turnBuffer[T] { + tb := &turnBuffer[T]{} + tb.notEmpty = sync.NewCond(&tb.mu) + return tb +} + +func (tb *turnBuffer[T]) Send(value T) { + tb.mu.Lock() + defer tb.mu.Unlock() + + if tb.closed { + panic("turnBuffer: send on closed buffer") + } + + tb.buf = append(tb.buf, value) + tb.notEmpty.Signal() +} + +func (tb *turnBuffer[T]) TrySend(value T) bool { + tb.mu.Lock() + defer tb.mu.Unlock() + + if tb.closed { + return false + } + + tb.buf = append(tb.buf, value) + tb.notEmpty.Signal() + return true +} + +func (tb *turnBuffer[T]) Receive() (T, bool) { + tb.mu.Lock() + defer tb.mu.Unlock() + + for len(tb.buf) == 0 && !tb.closed && !tb.woken { + tb.notEmpty.Wait() + } + + tb.woken = false + + if len(tb.buf) == 0 { + var zero T + return zero, false + } + + val := tb.buf[0] + tb.buf = tb.buf[1:] + return val, true +} + +func (tb *turnBuffer[T]) Close() { + tb.mu.Lock() + defer tb.mu.Unlock() + + if !tb.closed { + tb.closed = true + tb.notEmpty.Broadcast() + } +} + +func (tb *turnBuffer[T]) IsClosed() bool { + tb.mu.Lock() + defer tb.mu.Unlock() + return tb.closed +} + +func (tb *turnBuffer[T]) TakeAll() []T { + tb.mu.Lock() + defer tb.mu.Unlock() + + if len(tb.buf) == 0 { + return nil + } + + values := tb.buf + tb.buf = nil + return values +} + +func (tb *turnBuffer[T]) PushFront(values []T) { + if len(values) == 0 { + return + } + + tb.mu.Lock() + defer tb.mu.Unlock() + + tb.buf = append(append([]T{}, values...), tb.buf...) + tb.notEmpty.Signal() +} + +func (tb *turnBuffer[T]) Wakeup() { + tb.mu.Lock() + defer tb.mu.Unlock() + + tb.woken = true + tb.notEmpty.Broadcast() +} + +func (tb *turnBuffer[T]) ClearWakeup() { + tb.mu.Lock() + defer tb.mu.Unlock() + + tb.woken = false +} diff --git a/adk/turn_loop.go b/adk/turn_loop.go new file mode 100644 index 0000000..c2fe185 --- /dev/null +++ b/adk/turn_loop.go @@ -0,0 +1,2056 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + "runtime/debug" + "sync" + "sync/atomic" + "time" + + "github.com/cloudwego/eino/internal/safe" +) + +type stopPhase uint8 + +const ( + stopOpen stopPhase = iota + stopIdleWaiting + stopCommitted +) + +type preemptTurnPhase uint8 + +const ( + preemptTurnIdle preemptTurnPhase = iota + preemptTurnPlanning + preemptTurnActive +) + +func (p preemptTurnPhase) String() string { + switch p { + case preemptTurnIdle: + return "idle" + case preemptTurnPlanning: + return "planning" + case preemptTurnActive: + return "active" + default: + return fmt.Sprintf("unknown(%d)", p) + } +} + +type preemptTurnSnapshot struct { + hasTargetTurn bool + turnID uint64 + ctx context.Context + tc any +} + +type cancelRequestState struct { + cfg agentCancelConfig + timeoutDeadline *time.Time +} + +type preemptRequest struct { + cancel cancelRequestState + ackChans []chan struct{} +} + +func parseAgentCancelOptions(opts ...AgentCancelOption) agentCancelConfig { + cfg := agentCancelConfig{Mode: CancelImmediate} + for _, opt := range opts { + opt(&cfg) + } + return cfg +} + +func newCancelRequestState(opts []AgentCancelOption, now time.Time) cancelRequestState { + cfg := parseAgentCancelOptions(opts...) + var deadline *time.Time + if cfg.Timeout != nil && *cfg.Timeout > 0 && cfg.Mode != CancelImmediate { + d := now.Add(*cfg.Timeout) + deadline = &d + } + cfg.Timeout = nil + + return cancelRequestState{ + cfg: cfg, + timeoutDeadline: deadline, + } +} + +func (s *cancelRequestState) merge(opts []AgentCancelOption, now time.Time) { + if opts == nil { + return + } + + next := newCancelRequestState(opts, now) + if s.cfg.Mode == CancelImmediate || next.cfg.Mode == CancelImmediate { + s.cfg.Mode = CancelImmediate + s.timeoutDeadline = nil + } else { + s.cfg.Mode |= next.cfg.Mode + if next.timeoutDeadline != nil { + if s.timeoutDeadline == nil || next.timeoutDeadline.Before(*s.timeoutDeadline) { + deadline := *next.timeoutDeadline + s.timeoutDeadline = &deadline + } + } + } + if next.cfg.Recursive { + s.cfg.Recursive = true + } +} + +func (s cancelRequestState) cancelOptions(now time.Time) []AgentCancelOption { + cfg := s.cfg + if cfg.Mode != CancelImmediate && s.timeoutDeadline != nil { + remaining := s.timeoutDeadline.Sub(now) + if remaining <= 0 { + cfg.Mode = CancelImmediate + cfg.Timeout = nil + } else { + cfg.Timeout = &remaining + } + } + + opts := []AgentCancelOption{WithAgentCancelMode(cfg.Mode)} + if cfg.Recursive { + opts = append(opts, WithRecursive()) + } + if cfg.Timeout != nil { + opts = append(opts, WithAgentCancelTimeout(*cfg.Timeout)) + } + return opts +} + +func newPreemptRequest(ack chan struct{}, opts []AgentCancelOption, now time.Time) *preemptRequest { + req := &preemptRequest{cancel: newCancelRequestState(opts, now)} + if ack != nil { + req.ackChans = append(req.ackChans, ack) + } + return req +} + +func (r *preemptRequest) ack() { + if r == nil { + return + } + for _, ack := range r.ackChans { + close(ack) + } + r.ackChans = nil +} + +func (r *preemptRequest) merge(ack chan struct{}, opts []AgentCancelOption, now time.Time) { + if ack != nil { + r.ackChans = append(r.ackChans, ack) + } + r.cancel.merge(opts, now) +} + +func (r *preemptRequest) cancelOptions(now time.Time) []AgentCancelOption { + if r == nil { + return nil + } + return r.cancel.cancelOptions(now) +} + +// preemptController owns turn-targeted preempt requests and Push critical sections. +// +// Turn lifecycle: +// +// idle ──beginPlanningTurn──▶ planning ──beginActiveTurn──▶ active ──endActiveTurn──▶ idle +// │ ▲ +// └────────abortPlanningTurn─────────────────────────────┘ +// +// Push critical section (beginPush/endPush) overlaps with the turn lifecycle. The +// run loop calls waitForPushes before beginPlanningTurn to ensure no in-flight Push +// can observe stale turn state. +// +// Preempt request flow: +// - Push captures a snapshot (turnID + hasTargetTurn) via beginPush. +// - requestPreempt binds to the captured turnID; if the turn has moved on, the +// request is resolved as a no-op. +// - During active phase, receivePreempt transfers the pending request to the +// watcher, which submits cancel and then acks. +type preemptController struct { + mu sync.Mutex + cond *sync.Cond + + turnPhase preemptTurnPhase + turnID uint64 + currentTC any + currentRunCtx context.Context + + pushInFlight int + pending *preemptRequest + notify chan struct{} + closed bool +} + +func newPreemptController() *preemptController { + c := &preemptController{notify: make(chan struct{}, 1)} + c.cond = sync.NewCond(&c.mu) + return c +} + +func (c *preemptController) beginPlanningTurn() { + c.mu.Lock() + defer c.mu.Unlock() + + c.requirePhaseLocked(preemptTurnIdle, "beginPlanningTurn") + c.requireNoPendingLocked("beginPlanningTurn") + c.turnID++ + c.turnPhase = preemptTurnPlanning + c.currentRunCtx = nil + c.currentTC = nil +} + +func (c *preemptController) abortPlanningTurn() *preemptRequest { + c.mu.Lock() + defer c.mu.Unlock() + + c.requirePhaseLocked(preemptTurnPlanning, "abortPlanningTurn") + c.turnPhase = preemptTurnIdle + c.currentRunCtx = nil + c.currentTC = nil + req := c.pending + c.pending = nil + c.cond.Broadcast() + return req +} + +func (c *preemptController) beginActiveTurn(ctx context.Context, tc any) { + c.mu.Lock() + defer c.mu.Unlock() + + c.requirePhaseLocked(preemptTurnPlanning, "beginActiveTurn") + c.turnPhase = preemptTurnActive + c.currentRunCtx = ctx + c.currentTC = tc + if c.pending != nil { + c.notifyWatcherLocked() + } +} + +func (c *preemptController) endActiveTurn() *preemptRequest { + c.mu.Lock() + defer c.mu.Unlock() + + c.requirePhaseLocked(preemptTurnActive, "endActiveTurn") + c.turnPhase = preemptTurnIdle + c.currentRunCtx = nil + c.currentTC = nil + req := c.pending + c.pending = nil + c.cond.Broadcast() + return req +} + +func (c *preemptController) requirePhaseLocked(expected preemptTurnPhase, op string) { + if c.turnPhase != expected { + panic(fmt.Sprintf("adk: preemptController.%s called while turn phase is %s; expected %s", op, c.turnPhase, expected)) + } +} + +func (c *preemptController) requireNoPendingLocked(op string) { + if c.pending != nil { + panic(fmt.Sprintf("adk: preemptController.%s called with stale pending preempt request", op)) + } +} + +func (c *preemptController) beginPush() preemptTurnSnapshot { + c.mu.Lock() + defer c.mu.Unlock() + + c.pushInFlight++ + return preemptTurnSnapshot{ + hasTargetTurn: c.turnPhase == preemptTurnPlanning || c.turnPhase == preemptTurnActive, + turnID: c.turnID, + ctx: c.currentRunCtx, + tc: c.currentTC, + } +} + +func (c *preemptController) endPush() { + c.mu.Lock() + defer c.mu.Unlock() + + c.pushInFlight-- + if c.pushInFlight < 0 { + panic("adk: preemptController.endPush called without matching beginPush") + } + c.cond.Broadcast() +} + +func (c *preemptController) waitForPushes() { + c.mu.Lock() + defer c.mu.Unlock() + + for c.pushInFlight > 0 { + c.cond.Wait() + } +} + +func (c *preemptController) requestPreempt(target preemptTurnSnapshot, ack chan struct{}, opts ...AgentCancelOption) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.closed || !target.hasTargetTurn || c.turnPhase == preemptTurnIdle || c.turnID != target.turnID { + if ack != nil { + close(ack) + } + return + } + + now := time.Now() + if c.pending == nil { + c.pending = newPreemptRequest(ack, opts, now) + } else { + c.pending.merge(ack, opts, now) + } + if c.turnPhase == preemptTurnActive { + c.notifyWatcherLocked() + } +} + +func (c *preemptController) receivePreempt() (*preemptRequest, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.turnPhase != preemptTurnActive || c.pending == nil { + return nil, false + } + req := c.pending + c.pending = nil + return req, true +} + +func (c *preemptController) closeForLoopExit() { + c.mu.Lock() + c.closed = true + c.turnPhase = preemptTurnIdle + c.currentRunCtx = nil + c.currentTC = nil + req := c.pending + c.pending = nil + select { + case <-c.notify: + default: + } + c.cond.Broadcast() + c.mu.Unlock() + + req.ack() +} + +func (c *preemptController) notifyWatcherLocked() { + select { + case c.notify <- struct{}{}: + default: + } +} + +type stopDecision struct { + commit bool + wakeIdle bool +} + +type stopCancelRequest struct { + cancel cancelRequestState +} + +func newStopCancelRequest(opts []AgentCancelOption, now time.Time) *stopCancelRequest { + return &stopCancelRequest{cancel: newCancelRequestState(opts, now)} +} + +func (r *stopCancelRequest) merge(opts []AgentCancelOption, now time.Time) { + if r == nil { + return + } + r.cancel.merge(opts, now) +} + +func (r *stopCancelRequest) cancelOptions(now time.Time) []AgentCancelOption { + if r == nil { + return nil + } + return r.cancel.cancelOptions(now) +} + +// stopController owns global Stop state and optional active-turn cancel requests. +// +// Stop has two independent layers: +// - terminal loop intent: committed Stop prevents future turns and closes the buffer; +// - optional active-turn cancel: cancel-capable Stop calls create a pending request +// consumed by the watcher if the current turn is still active. +// +// Unlike preempt, Stop is not bound to a turnID. It is global and terminal. +// A pending cancel request is consumed by the active turn or dropped when that +// turn ends before consumption. +type stopController struct { + mu sync.Mutex + + phase stopPhase + + hasActiveCancelTarget bool + pending *stopCancelRequest + notify chan struct{} + + idleFor time.Duration + skipCheckpoint bool + stopCause string + + closed bool +} + +func newStopController() *stopController { + return &stopController{notify: make(chan struct{}, 1)} +} + +func (c *stopController) requestStop(cfg *stopConfig) stopDecision { + c.mu.Lock() + defer c.mu.Unlock() + + if c.closed { + return stopDecision{} + } + if cfg.skipCheckpoint { + c.skipCheckpoint = true + } + if cfg.stopCause != "" && c.stopCause == "" { + c.stopCause = cfg.stopCause + } + if cfg.idleFor > 0 { + if c.phase != stopCommitted && c.idleFor == 0 { + c.phase = stopIdleWaiting + c.idleFor = cfg.idleFor + } + return stopDecision{wakeIdle: c.phase == stopIdleWaiting} + } + + committed := c.commitLocked() + if cfg.agentCancelOpts != nil { + now := time.Now() + if c.pending == nil { + c.pending = newStopCancelRequest(cfg.agentCancelOpts, now) + } else { + c.pending.merge(cfg.agentCancelOpts, now) + } + if c.hasActiveCancelTarget { + c.notifyWatcherLocked() + } + } + return stopDecision{commit: committed} +} + +func (c *stopController) commit() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.commitLocked() +} + +func (c *stopController) commitLocked() bool { + if c.closed || c.phase == stopCommitted { + return false + } + c.phase = stopCommitted + c.idleFor = 0 + return true +} + +func (c *stopController) isCommitted() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.phase == stopCommitted +} + +func (c *stopController) idleDuration() time.Duration { + c.mu.Lock() + defer c.mu.Unlock() + if c.phase != stopIdleWaiting { + return 0 + } + return c.idleFor +} + +func (c *stopController) skipCheckpointEnabled() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.skipCheckpoint +} + +func (c *stopController) cause() string { + c.mu.Lock() + defer c.mu.Unlock() + return c.stopCause +} + +func (c *stopController) beginActiveTurn() { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return + } + c.hasActiveCancelTarget = true + if c.pending != nil { + c.notifyWatcherLocked() + } +} + +func (c *stopController) endActiveTurn() *stopCancelRequest { + c.mu.Lock() + defer c.mu.Unlock() + c.hasActiveCancelTarget = false + req := c.pending + c.pending = nil + return req +} + +func (c *stopController) receiveCancel() (*stopCancelRequest, bool) { + c.mu.Lock() + defer c.mu.Unlock() + if !c.hasActiveCancelTarget || c.pending == nil { + return nil, false + } + req := c.pending + c.pending = nil + return req, true +} + +func (c *stopController) closeForLoopExit() { + c.mu.Lock() + defer c.mu.Unlock() + c.closed = true + c.hasActiveCancelTarget = false + c.pending = nil + select { + case <-c.notify: + default: + } +} + +func (c *stopController) notifyWatcherLocked() { + select { + case c.notify <- struct{}{}: + default: + } +} + +// TurnLoopConfig is the configuration for creating a TurnLoop. +type TurnLoopConfig[T any, M MessageType] struct { + // GenInput receives the TurnLoop instance and all buffered items, and decides what to process. + // It returns which items to consume now vs keep for later turns. + // The loop parameter allows calling Push() or Stop() directly from within the callback. + // Required. + GenInput func(ctx context.Context, loop *TurnLoop[T, M], items []T) (*GenInputResult[T, M], error) + + // GenResume is called at most once during Run(). When CheckpointID is + // configured, Run() queries Store for the checkpoint: + // - If the checkpoint contains runner state (i.e. an agent was interrupted + // or canceled mid-turn), Run() calls GenResume to plan a resume turn. + // - Otherwise (no checkpoint, or between-turns checkpoint), GenResume is + // never called and the loop proceeds via GenInput. + // + // It receives: + // - interruptedItems: the items being processed when the prior run was interrupted / canceled + // - unhandledItems: items buffered but not processed when the prior run exited + // - newItems: items that were Push()-ed before Run() was called + // + // It returns a GenResumeResult describing how to resume the interrupted agent + // turn (optional ResumeParams) and how to manipulate the buffer + // (Consumed/Remaining) before continuing. + GenResume func(ctx context.Context, loop *TurnLoop[T, M], interruptedItems, unhandledItems, newItems []T) (*GenResumeResult[T, M], error) + + // PrepareAgent returns an Agent configured to handle the consumed items. + // This callback should set up the agent with appropriate system prompt, + // tools, and middlewares based on what items are being processed. + // Called once per turn with the items that GenInput decided to consume. + // The loop parameter allows calling Push() or Stop() directly from within the callback. + // Required. + PrepareAgent func(ctx context.Context, loop *TurnLoop[T, M], consumed []T) (TypedAgent[M], error) + + // OnAgentEvents is called to handle events emitted by the agent. + // The TurnContext provides per-turn info and control: + // - tc.Consumed: items that triggered this agent execution + // - tc.Loop: allows calling Push() or Stop() directly from within the callback + // - tc.Preempted / tc.Stopped: signals while processing events + // + // Error handling: the returned error is only used when the callback itself + // wants to abort the TurnLoop. The callback should NEVER propagate + // CancelError — the framework handles it automatically: + // - Stop: the framework propagates CancelError as ExitReason, loop exits. + // - Preempt: the framework does not propagate CancelError; if the callback + // also returns nil, the loop continues with the next turn. + // In practice, return a non-nil error only for callback-internal failures + // that should terminate the loop. + // + // Optional. If not provided, events are drained and the first error + // (including CancelError from Stop) is returned as ExitReason. + OnAgentEvents func(ctx context.Context, tc *TurnContext[T, M], events *AsyncIterator[*TypedAgentEvent[M]]) error + + // Store is the checkpoint store for persistence and resume. Optional. + // When set together with CheckpointID, enables automatic checkpoint-based resume. + // The TurnLoop always persists both runner checkpoint bytes and item bookkeeping + // (InterruptedItems, UnhandledItems) via gob encoding, so T must be gob-encodable + // when Store is used. + Store CheckPointStore + + // CheckpointID, when set together with Store, enables automatic + // checkpoint-based resume. On Run(), the TurnLoop queries Store for this ID: + // - If a checkpoint exists with runner state (mid-turn interrupt / cancel), + // GenResume is called to plan the resume turn. + // - If a checkpoint exists without runner state (between-turns), + // the stored unhandled items are buffered and the loop proceeds + // normally via GenInput. + // - If no checkpoint exists, the loop starts fresh. + // + // On exit, if the TurnLoop saved a new checkpoint, it is saved under this + // same CheckpointID. On clean exit (no checkpoint saved), the existing + // checkpoint under CheckpointID is deleted to prevent stale resumption. + CheckpointID string +} + +// GenInputResult contains the result of GenInput processing. +type GenInputResult[T any, M MessageType] struct { + // RunCtx, if non-nil, overrides the context for this turn's execution + // (PrepareAgent, agent run, OnAgentEvents). + // + // Must be derived from the ctx passed to GenInput to preserve the + // TurnLoop's cancellation semantics and inherited values. For example: + // + // runCtx := context.WithValue(ctx, traceKey{}, extractTraceID(items)) + // return &GenInputResult[T]{RunCtx: runCtx, ...}, nil + // + // If nil, the TurnLoop's context is used unchanged. + RunCtx context.Context + + // Input is the agent input to execute + Input *TypedAgentInput[M] + + // RunOpts are the options for this agent run. + // Note: do not pass WithCheckPointID here; the TurnLoop automatically + // injects the checkpointID into the Runner. + RunOpts []AgentRunOption + + // Consumed are the items selected for this turn. + // They are removed from the buffer and passed to PrepareAgent. + Consumed []T + + // Remaining are the items to keep in the buffer for a future turn. + // TurnLoop pushes Remaining back into the buffer before running the agent. + // + // Items from the GenInput input slice that are in neither Consumed nor Remaining + // are dropped by the loop. + Remaining []T +} + +// GenResumeResult contains the result of GenResume processing. +type GenResumeResult[T any, M MessageType] struct { + // RunCtx, if non-nil, overrides the context for this resumed turn's execution + // (PrepareAgent, agent resume, OnAgentEvents). + RunCtx context.Context + + // RunOpts are the options for this agent resume run. + // Note: do not pass WithCheckPointID here; the TurnLoop automatically + // injects the checkpointID into the Runner. + RunOpts []AgentRunOption + + // ResumeParams are optional parameters for resuming an interrupted agent. + ResumeParams *ResumeParams + + // Consumed are the items selected for this resumed turn. + // They are removed from the buffer and passed to PrepareAgent. + Consumed []T + + // Remaining are the items to keep in the buffer for a future turn. + // TurnLoop pushes Remaining back into the buffer before resuming the agent. + // + // Items from (interruptedItems, unhandledItems, newItems) that are in neither Consumed + // nor Remaining are dropped by the loop. + Remaining []T +} + +type turnRunSpec[T any, M MessageType] struct { + runCtx context.Context + input *TypedAgentInput[M] + runOpts []AgentRunOption + resumeParams *ResumeParams + isResume bool + consumed []T + resumeBytes []byte +} + +type turnPlan[T any, M MessageType] struct { + turnCtx context.Context + remaining []T + spec *turnRunSpec[T, M] +} + +func (l *TurnLoop[T, M]) planTurn( + ctx context.Context, + isResume bool, + items []T, + pr *turnLoopPendingResume[T], +) (*turnPlan[T, M], error) { + if !isResume { + result, err := l.config.GenInput(ctx, l, items) + if err != nil { + return nil, err + } + if result == nil { + return nil, errors.New("GenInputResult is nil") + } + if result.Input == nil { + return nil, errors.New("agent input is nil") + } + turnCtx := ctx + if result.RunCtx != nil { + turnCtx = result.RunCtx + } + return &turnPlan[T, M]{ + turnCtx: turnCtx, + remaining: result.Remaining, + spec: &turnRunSpec[T, M]{ + runCtx: result.RunCtx, + input: result.Input, + runOpts: result.RunOpts, + consumed: result.Consumed, + }, + }, nil + } + if pr == nil { + return nil, errors.New("resume payload is nil") + } + if l.config.GenResume == nil { + return nil, errors.New("GenResume is required for resume") + } + resumeResult, err := l.config.GenResume(ctx, l, pr.interrupted, pr.unhandled, pr.newItems) + if err != nil { + return nil, err + } + if resumeResult == nil { + return nil, errors.New("GenResumeResult is nil") + } + turnCtx := ctx + if resumeResult.RunCtx != nil { + turnCtx = resumeResult.RunCtx + } + return &turnPlan[T, M]{ + turnCtx: turnCtx, + remaining: resumeResult.Remaining, + spec: &turnRunSpec[T, M]{ + runCtx: resumeResult.RunCtx, + runOpts: resumeResult.RunOpts, + resumeParams: resumeResult.ResumeParams, + isResume: true, + consumed: resumeResult.Consumed, + resumeBytes: pr.resumeBytes, + }, + }, nil +} + +// InterruptError is the ExitReason when the TurnLoop exits due to a business +// interrupt (AgentAction.Interrupted). It carries InterruptContexts needed for +// targeted resumption via ResumeParams, parallel to CancelError. +// +// Unlike CancelError (which indicates forceful cancellation), InterruptError +// indicates the agent voluntarily paused execution at a business-defined point. +type InterruptError struct { + // InterruptContexts provides the interrupt contexts needed for targeted + // resumption via ResumeParams. Each context represents a step in the agent + // hierarchy that was interrupted. Use each InterruptCtx.ID as a key in + // ResumeParams.Targets. + InterruptContexts []*InterruptCtx +} + +func (e *InterruptError) Error() string { + return fmt.Sprintf("agent interrupted: %d context(s)", len(e.InterruptContexts)) +} + +// TurnLoopExitState is returned when TurnLoop exits, containing the exit reason +// and any items that were not processed. +type TurnLoopExitState[T any, M MessageType] struct { + // ExitReason indicates why the loop exited. + // nil means clean exit (Stop() was called without cancel options, or the + // agent completed normally before Stop took effect). + // Non-nil values include context errors, callback errors, *CancelError, etc. + // When Stop(WithImmediate()) or Stop(WithGraceful()) cancels a running + // agent, ExitReason will be a *CancelError. + // This never contains checkpoint errors — see CheckpointErr for those. + ExitReason error + + // UnhandledItems contains items that were buffered but not processed. + // These are items for which Push returned true but were never consumed by a turn. + // This is always valid regardless of ExitReason. + UnhandledItems []T + + // InterruptedItems contains the items whose turn was interrupted — either by + // a cancel (Stop with cancel options → *CancelError) or by a business + // interrupt (AgentAction.Interrupted → *InterruptError). + // On resume, these are passed to GenResume's interruptedItems parameter. + InterruptedItems []T + + // StopCause is the business-supplied reason passed via WithStopCause. + // Empty if Stop was not called or no cause was provided. + StopCause string + + // CheckpointAttempted indicates whether a checkpoint save was attempted when the loop exited. + // True when Store is configured, CheckpointID is set, the loop was not idle + // at exit time, WithSkipCheckpoint was not used, and the exit was caused by + // Stop() (clean or cancel) or a business interrupt (*InterruptError). + CheckpointAttempted bool + + // CheckpointErr is the error from checkpoint save, if any. + // nil when CheckpointAttempted is false (no attempt was made) or when the save succeeded. + CheckpointErr error + + // TakeLateItems returns items that were pushed after the loop stopped + // (i.e., Push returned false for these items). These items are NOT included + // in the checkpoint. + // + // This function is idempotent: the first call computes and caches the result; + // subsequent calls return the same slice. + // + // After TakeLateItems is called, any subsequent Push() will panic to + // prevent items from being silently lost. + // + // It is safe to call TakeLateItems from any goroutine after Wait() returns. + // If TakeLateItems is never called, late items are simply garbage collected. + TakeLateItems func() []T +} + +// TurnContext provides per-turn context to the OnAgentEvents callback. +type TurnContext[T any, M MessageType] struct { + // Loop is the TurnLoop instance, allowing Push() or Stop() calls. + Loop *TurnLoop[T, M] + + // Consumed contains items that triggered this agent execution. + Consumed []T + + // Preempted is closed when a preempt signal fires for the current turn + // (via Push with WithPreempt/WithPreemptTimeout) and at least one + // preemptive Push contributed to the CancelError for the current turn. + // "Contributed" means the preempt's cancel options were included in the + // CancelError before it was finalized. Remains open if no preempt contributed. + // Use in a select to detect preemption while processing events. + // + // Both Preempted and Stopped may be closed within the same turn if both + // signals arrive while the agent is still being cancelled. Whichever + // arrives after the cancel is fully handled will not contribute. + Preempted <-chan struct{} + + // Stopped is closed when a Stop() call contributed to the CancelError for the + // current turn. + // "Contributed" means Stop's cancel options were included in the CancelError + // before it was finalized. Remains open if Stop did not contribute. + // Use in a select to detect stop while processing events. + // + // See Preempted for the relationship between the two channels. + Stopped <-chan struct{} + + // StopCause returns the business-supplied reason from WithStopCause. + // This value is only meaningful after the Stopped channel is closed. + // Before that, it returns an empty string. + StopCause func() string +} + +// TurnLoop is a push-based event loop for agent execution. +// Users push items via Push() and the loop processes them through the agent. +// +// Create with NewTurnLoop, then start with Run: +// +// loop := NewTurnLoop(cfg) +// // pass loop to other components, push initial items, etc. +// loop.Run(ctx) +// +// # Permissive API +// +// All methods are valid on a not-yet-running loop: +// - Push: items are buffered and will be processed once Run is called. +// - Stop: sets the stopped flag; a subsequent Run will exit immediately. +// - Wait: blocks until Run is called AND the loop exits. If Run is never +// called, Wait blocks forever (this is a programming error, analogous +// to reading from a channel that nobody writes to). +type TurnLoop[T any, M MessageType] struct { + config TurnLoopConfig[T, M] + + buffer *turnBuffer[T] + + stopped int32 + started int32 + + done chan struct{} + + result *TurnLoopExitState[T, M] + + runOnce sync.Once + + stopCtrl *stopController + + preemptCtrl *preemptController + + runErr error + + interruptedItems []T + + checkPointRunnerBytes []byte + interruptContexts []*InterruptCtx + capturedCancelErr *CancelError + + pendingResume *turnLoopPendingResume[T] + + loadCheckpointID string + + onAgentEvents func(ctx context.Context, tc *TurnContext[T, M], events *AsyncIterator[*TypedAgentEvent[M]]) error + + lateMu sync.Mutex + lateItems []T + lateSealed bool +} + +func (l *TurnLoop[T, M]) appendLate(item T) { + l.lateMu.Lock() + defer l.lateMu.Unlock() + if l.lateSealed { + panic("TurnLoop: Push called after TakeLateItems") + } + l.lateItems = append(l.lateItems, item) +} + +type turnLoopCheckpoint[T any] struct { + RunnerCheckpoint []byte + // HasRunnerState reports whether RunnerCheckpoint contains resumable runner state. + // It is false for "between turns" checkpoints where no agent execution was + // interrupted (e.g. Stop() before the first turn or between turns). + HasRunnerState bool + UnhandledItems []T + CanceledItems []T // gob-compat: kept as CanceledItems for deserialization of existing checkpoints +} + +func marshalTurnLoopCheckpoint[T any](c *turnLoopCheckpoint[T]) ([]byte, error) { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(c); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func unmarshalTurnLoopCheckpoint[T any](data []byte) (*turnLoopCheckpoint[T], error) { + var c turnLoopCheckpoint[T] + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&c); err != nil { + return nil, err + } + return &c, nil +} + +func (l *TurnLoop[T, M]) saveTurnLoopCheckpoint(ctx context.Context, checkPointID string, c *turnLoopCheckpoint[T]) error { + if l.config.Store == nil { + return errors.New("checkpoint store is nil") + } + data, err := marshalTurnLoopCheckpoint(c) + if err != nil { + return err + } + return l.config.Store.Set(ctx, checkPointID, data) +} + +func (l *TurnLoop[T, M]) deleteTurnLoopCheckpoint(ctx context.Context, checkPointID string) error { + if l.config.Store == nil { + return nil + } + if deleter, ok := l.config.Store.(CheckPointDeleter); ok { + return deleter.Delete(ctx, checkPointID) + } + return nil +} + +func (l *TurnLoop[T, M]) tryLoadCheckpoint(ctx context.Context) error { + checkPointID := l.config.CheckpointID + if checkPointID == "" || l.config.Store == nil { + return nil + } + + l.loadCheckpointID = checkPointID + + data, existed, err := l.config.Store.Get(ctx, checkPointID) + if err != nil { + return fmt.Errorf("failed to load checkpoint[%s]: %w", checkPointID, err) + } + if !existed { + return nil + } + + var cp *turnLoopCheckpoint[T] + if len(data) == 0 { + return nil + } + cp, err = unmarshalTurnLoopCheckpoint[T](data) + if err != nil { + return fmt.Errorf("failed to unmarshal checkpoint[%s]: %w", checkPointID, err) + } + + newItems := l.buffer.TakeAll() + + if cp.HasRunnerState { + if len(cp.RunnerCheckpoint) == 0 { + l.buffer.PushFront(newItems) + return fmt.Errorf("checkpoint[%s] has runner state but bytes are empty", checkPointID) + } + l.pendingResume = &turnLoopPendingResume[T]{ + interrupted: append([]T{}, cp.CanceledItems...), + unhandled: append([]T{}, cp.UnhandledItems...), + newItems: append([]T{}, newItems...), + resumeBytes: append([]byte{}, cp.RunnerCheckpoint...), + } + } else { + items := make([]T, 0, len(cp.UnhandledItems)+len(newItems)) + items = append(items, cp.UnhandledItems...) + items = append(items, newItems...) + l.buffer.PushFront(items) + } + + return nil +} + +type turnLoopPendingResume[T any] struct { + interrupted []T + unhandled []T + newItems []T + resumeBytes []byte +} + +// SafePoint describes at which boundary the agent may be cancelled. +// It is a bitmask: values can be combined with bitwise OR to accept multiple +// safe points (e.g. AfterToolCalls | AfterChatModel). Internally, SafePoint +// is translated to CancelMode via toCancelMode(). +// +// SafePoint is used only in the preemption API (WithPreempt/WithPreemptTimeout). +// A key design constraint: preemption always targets a safe point — the user's +// intent is to cancel at a well-defined boundary, never to abort immediately. +// Immediate cancellation is only reachable as an automatic timeout escalation +// (via WithPreemptTimeout), not as a direct user choice. This is why SafePoint +// has no "immediate" value and why WithPreempt requires a non-zero SafePoint +// (panics otherwise). +type SafePoint int + +const ( + // AfterChatModel allows the agent to finish the current chat-model + // call before being cancelled. + AfterChatModel SafePoint = 1 << iota + // AfterToolCalls allows the agent to finish the current tool-call round + // before being cancelled. + AfterToolCalls + // AnySafePoint is shorthand for AfterChatModel | AfterToolCalls. + AnySafePoint = AfterChatModel | AfterToolCalls +) + +func (sp SafePoint) toCancelMode() CancelMode { + var mode CancelMode + if sp&AfterToolCalls != 0 { + mode |= CancelAfterToolCalls + } + if sp&AfterChatModel != 0 { + mode |= CancelAfterChatModel + } + return mode +} + +type stopConfig struct { + agentCancelOpts []AgentCancelOption + skipCheckpoint bool + stopCause string + idleFor time.Duration +} + +// StopOption is an option for Stop(). +type StopOption func(*stopConfig) + +// WithGraceful requests a graceful stop that waits at the nearest safe point +// (after tool calls or after a chat-model call) and propagates recursively to +// nested agents. It does not impose a time limit; use WithGracefulTimeout to +// add a grace period after which the stop escalates to immediate cancellation. +// +// WithGraceful and WithGracefulTimeout are mutually exclusive; if both are +// passed to the same Stop call, the last one wins. +func WithGraceful() StopOption { + return func(cfg *stopConfig) { + cfg.agentCancelOpts = []AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel | CancelAfterToolCalls), + WithRecursive(), + } + } +} + +// WithImmediate aborts the running agent turn as soon as possible. +// The agent is cancelled immediately without waiting for any safe point. +// Nested agents inside AgentTools will also receive the cancel signal +// and be torn down. +// +// This is the most aggressive stop mode — typically used when the caller +// wants to shut down the TurnLoop with no intention of resuming. +func WithImmediate() StopOption { + return func(cfg *stopConfig) { + cfg.agentCancelOpts = []AgentCancelOption{ + WithRecursive(), + } + } +} + +// WithGracefulTimeout is like WithGraceful but adds a grace period. +// If the agent has not reached a safe point within gracePeriod, the stop +// escalates to immediate cancellation. +// +// gracePeriod must be positive; passing a zero or negative duration panics. +// +// WithGraceful and WithGracefulTimeout are mutually exclusive; if both are +// passed to the same Stop call, the last one wins. +func WithGracefulTimeout(gracePeriod time.Duration) StopOption { + if gracePeriod <= 0 { + panic("adk: WithGracefulTimeout: gracePeriod must be positive") + } + return func(cfg *stopConfig) { + cfg.agentCancelOpts = []AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel | CancelAfterToolCalls), + WithRecursive(), + WithAgentCancelTimeout(gracePeriod), + } + } +} + +// WithSkipCheckpoint tells the TurnLoop not to persist a checkpoint for this +// Stop call. Use this when the caller does not intend to resume in the future. +// The flag is sticky: once any Stop() call sets it, subsequent calls cannot undo it. +func WithSkipCheckpoint() StopOption { + return func(cfg *stopConfig) { + cfg.skipCheckpoint = true + } +} + +// WithStopCause attaches a business-supplied reason string to this Stop call. +// The cause is surfaced in TurnLoopExitState.StopCause and, after the Stopped +// channel closes, via TurnContext.StopCause(). +// If multiple Stop() calls provide a cause, the first non-empty value wins. +func WithStopCause(cause string) StopOption { + return func(cfg *stopConfig) { + cfg.stopCause = cause + } +} + +// UntilIdleFor defers the stop until the TurnLoop has been continuously idle +// (blocked between turns with no pending items) for at least the given +// duration. Each time a new item arrives the timer resets from zero. +// +// This is useful when business code monitors agent activity externally and +// wants to shut down the loop once there has been no work for a while, without +// racing with concurrent Push calls. +// +// UntilIdleFor does not impact a running agent. It only takes effect when the +// loop is idle between turns. Cancel options (WithImmediate, WithGraceful, +// WithGracefulTimeout) in the same Stop call are silently ignored — they are +// meaningless alongside UntilIdleFor. +// +// To escalate after a prior UntilIdleFor, issue a separate Stop call: +// +// loop.Stop(UntilIdleFor(30 * time.Second)) // wait for idle +// // ... later, if you need to abort immediately: +// loop.Stop(WithImmediate()) // overrides the idle wait +// +// Only the first UntilIdleFor duration takes effect; subsequent calls with +// a different duration are ignored. A Stop() call without UntilIdleFor always +// shuts down the loop immediately regardless of any pending idle timer. +// +// UntilIdleFor is combinable with non-cancel StopOptions (WithSkipCheckpoint, +// WithStopCause) in the same call. +// +// duration must be positive; passing a zero or negative value panics. +func UntilIdleFor(duration time.Duration) StopOption { + if duration <= 0 { + panic("adk: UntilIdleFor: duration must be positive") + } + return func(cfg *stopConfig) { + cfg.idleFor = duration + } +} + +type pushConfig[T any, M MessageType] struct { + preempt bool + preemptDelay time.Duration + agentCancelOpts []AgentCancelOption + pushStrategy func(context.Context, *TurnContext[T, M]) []PushOption[T, M] +} + +// PushOption is an option for Push(). +type PushOption[T any, M MessageType] func(*pushConfig[T, M]) + +// WithPreempt signals that the current agent turn should be cancelled at the +// specified safePoint after pushing the new item. The loop cancels the current +// turn and starts a new one, where GenInput will see all buffered items +// including the newly pushed one. +// Use WithPreemptTimeout to add a timeout that escalates to immediate abort. +// +// Because safe points fire at turn-level boundaries (after the chat model +// returns or after all tool calls complete), no nested agent is running at +// the moment of cancellation — nested agents within AgentTools have either +// not started yet (AfterChatModel) or already finished (AfterToolCalls). +// Note: WithPreempt does NOT include WithRecursive (no escalation path exists). +// WithPreemptTimeout DOES include WithRecursive so that on timeout escalation, +// nested agents are properly torn down. +// +// WithPreempt and WithPreemptTimeout are mutually exclusive; if both are +// passed to the same Push call, the last one wins. +// +// safePoint must not be zero; passing SafePoint(0) panics. +func WithPreempt[T any, M MessageType](safePoint SafePoint) PushOption[T, M] { + if safePoint == 0 { + panic("adk: SafePoint must not be zero; use AfterToolCalls, AfterChatModel, or AnySafePoint") + } + return func(cfg *pushConfig[T, M]) { + cfg.preempt = true + cfg.agentCancelOpts = []AgentCancelOption{ + WithAgentCancelMode(safePoint.toCancelMode()), + } + } +} + +// WithPreemptTimeout is like WithPreempt but adds a timeout. If the agent has +// not reached the safe point within timeout, the preemption escalates to +// immediate cancellation. On escalation, nested agents inside AgentTools will +// also receive the cancel signal and be torn down. +// +// safePoint must not be zero; passing SafePoint(0) panics. +func WithPreemptTimeout[T any, M MessageType](safePoint SafePoint, timeout time.Duration) PushOption[T, M] { + if safePoint == 0 { + panic("adk: SafePoint must not be zero; use AfterToolCalls, AfterChatModel, or AnySafePoint") + } + return func(cfg *pushConfig[T, M]) { + cfg.preempt = true + cfg.agentCancelOpts = []AgentCancelOption{ + WithAgentCancelMode(safePoint.toCancelMode()), + WithAgentCancelTimeout(timeout), + WithRecursive(), + } + } +} + +// WithPreemptDelay sets a delay duration before resolving a preemptive Push. +// When used with WithPreempt or WithPreemptTimeout, the pushed item is buffered +// immediately, while the preempt request is resolved after the delay against the +// turn observed by Push. If that captured turn has already ended, the request is +// resolved as a no-op and must not cancel a later turn. +func WithPreemptDelay[T any, M MessageType](delay time.Duration) PushOption[T, M] { + return func(cfg *pushConfig[T, M]) { + cfg.preemptDelay = delay + } +} + +// WithPushStrategy provides dynamic push option resolution based on the current turn state. +// The callback receives the current turn's context and TurnContext (nil if no turn is active) +// and returns the actual PushOptions to apply. When WithPushStrategy is used, all other +// PushOptions passed to the same Push call are ignored. +// +// The returned options must not contain another WithPushStrategy; any nested +// strategy is silently stripped. +// +// Example: preempt only if the current turn is processing low-priority items: +// +// loop.Push(urgentItem, WithPushStrategy(func(ctx context.Context, tc *TurnContext[MyItem, *schema.Message]) []PushOption[MyItem, *schema.Message] { +// if tc == nil { +// return nil // between turns, plain push +// } +// if isLowPriority(tc.Consumed) { +// return []PushOption[MyItem, *schema.Message]{WithPreempt[MyItem, *schema.Message](AnySafePoint)} +// } +// return nil // don't preempt high-priority work +// })) +func WithPushStrategy[T any, M MessageType](fn func(ctx context.Context, tc *TurnContext[T, M]) []PushOption[T, M]) PushOption[T, M] { + return func(cfg *pushConfig[T, M]) { + cfg.pushStrategy = fn + } +} + +func defaultTurnLoopOnAgentEvents[T any, M MessageType](_ context.Context, _ *TurnContext[T, M], events *AsyncIterator[*TypedAgentEvent[M]]) error { + for { + event, ok := events.Next() + if !ok { + break + } + if event.Err != nil { + return event.Err + } + } + return nil +} + +// NewTurnLoop creates a new TurnLoop without starting it. +// The returned loop accepts Push and Stop calls immediately; pushed items +// are buffered until Run is called. +// Call Run to start the processing goroutine. +// +// NewTurnLoop panics if GenInput or PrepareAgent is nil. +func NewTurnLoop[T any, M MessageType](cfg TurnLoopConfig[T, M]) *TurnLoop[T, M] { + if cfg.GenInput == nil { + panic("adk: NewTurnLoop: GenInput is required") + } + if cfg.PrepareAgent == nil { + panic("adk: NewTurnLoop: PrepareAgent is required") + } + + l := &TurnLoop[T, M]{ + config: cfg, + buffer: newTurnBuffer[T](), + done: make(chan struct{}), + stopCtrl: newStopController(), + preemptCtrl: newPreemptController(), + } + if cfg.OnAgentEvents != nil { + l.onAgentEvents = cfg.OnAgentEvents + } else { + l.onAgentEvents = defaultTurnLoopOnAgentEvents[T, M] + } + return l +} + +func (l *TurnLoop[T, M]) start(ctx context.Context) { + l.runOnce.Do(func() { + atomic.StoreInt32(&l.started, 1) + go l.run(ctx) + }) +} + +// Run starts the loop's processing goroutine. It is non-blocking: the loop +// runs in the background and results are obtained via Wait. +// +// If CheckpointID is configured in TurnLoopConfig and a matching checkpoint +// exists in Store, the loop automatically resumes from that checkpoint. +// Otherwise it starts fresh with whatever items were Push()-ed. +// +// Calling Run more than once is a no-op: only the first call starts the loop. +func (l *TurnLoop[T, M]) Run(ctx context.Context) { + l.start(ctx) +} + +// Push adds an item to the loop's buffer for processing. +// This method is non-blocking and thread-safe. +// Returns false if the loop has stopped, true otherwise. If a preemptive push +// succeeds, the second return value is a channel that callers can wait on to +// confirm the preempt request has been resolved. Specifically: +// - If Push observes a planning or active turn that is still the target when +// the request resolves, the channel closes after TurnLoop attempts to submit +// cancel for that target turn. +// - If Push observes no target turn, the loop has not started, the preempt +// subsystem is closed, or a delayed target is already gone, the channel +// closes as a no-op resolution. +// +// If the loop has not been started yet (Run not called), items are buffered +// and will be processed once Run is called. +// After Wait() returns, failed pushes can be recovered via TurnLoopExitState.TakeLateItems(). +// Once TakeLateItems() has been called, any subsequent push that would become a +// late item will panic instead of being silently dropped. +// +// Use WithPreempt() or WithPreemptTimeout() to atomically push an item and signal +// preemption of the current agent. This is useful for urgent items that should +// interrupt the current processing. +// The returned channel may be waited on if the caller needs to ensure the preempt +// signal has been observed. +// +// Use WithPreemptDelay() together with WithPreempt()/WithPreemptTimeout() to delay +// request resolution. Push returns immediately after the item is buffered, and +// the delayed request remains bound to the turn observed by Push. +func (l *TurnLoop[T, M]) Push(item T, opts ...PushOption[T, M]) (bool, <-chan struct{}) { + cfg := &pushConfig[T, M]{} + for _, opt := range opts { + opt(cfg) + } + + if cfg.pushStrategy != nil { + return l.pushWithStrategy(item, cfg) + } + + return l.pushWithConfig(item, cfg) +} + +// pushWithStrategy snapshots the current target turn while the strategy decides +// how to enqueue the item. If it requests preempt, that request is bound to the +// captured turn identity, including delayed preempt requests. +func (l *TurnLoop[T, M]) pushWithStrategy(item T, cfg *pushConfig[T, M]) (bool, <-chan struct{}) { + strategy := cfg.pushStrategy + + snapshot := l.preemptCtrl.beginPush() + defer l.preemptCtrl.endPush() + + runCtx := snapshot.ctx + if runCtx == nil { + runCtx = context.Background() + } + var tc *TurnContext[T, M] + if snapshot.tc != nil { + tc = snapshot.tc.(*TurnContext[T, M]) + } + realOpts := strategy(runCtx, tc) + cfg = &pushConfig[T, M]{} + for _, opt := range realOpts { + opt(cfg) + } + cfg.pushStrategy = nil + + if !cfg.preempt { + if !l.buffer.TrySend(item) { + l.appendLate(item) + return false, nil + } + return true, nil + } + + if atomic.LoadInt32(&l.stopped) != 0 { + l.appendLate(item) + return false, nil + } + + if !l.buffer.TrySend(item) { + l.appendLate(item) + return false, nil + } + + ack := make(chan struct{}) + if atomic.LoadInt32(&l.started) == 0 { + close(ack) + return true, ack + } + + if cfg.preemptDelay > 0 { + go func() { + select { + case <-time.After(cfg.preemptDelay): + l.preemptCtrl.requestPreempt(snapshot, ack, cfg.agentCancelOpts...) + case <-l.done: + close(ack) + } + }() + } else { + l.preemptCtrl.requestPreempt(snapshot, ack, cfg.agentCancelOpts...) + } + return true, ack +} + +func (l *TurnLoop[T, M]) pushWithConfig(item T, cfg *pushConfig[T, M]) (bool, <-chan struct{}) { + if atomic.LoadInt32(&l.stopped) != 0 { + l.appendLate(item) + return false, nil + } + + if cfg.preempt { + snapshot := l.preemptCtrl.beginPush() + defer l.preemptCtrl.endPush() + + if !l.buffer.TrySend(item) { + l.appendLate(item) + return false, nil + } + + ack := make(chan struct{}) + if atomic.LoadInt32(&l.started) == 0 { + close(ack) + return true, ack + } + + if cfg.preemptDelay > 0 { + go func() { + select { + case <-time.After(cfg.preemptDelay): + l.preemptCtrl.requestPreempt(snapshot, ack, cfg.agentCancelOpts...) + case <-l.done: + close(ack) + } + }() + } else { + l.preemptCtrl.requestPreempt(snapshot, ack, cfg.agentCancelOpts...) + } + return true, ack + } + + if !l.buffer.TrySend(item) { + l.appendLate(item) + return false, nil + } + return true, nil +} + +// Stop signals the loop to stop and returns immediately (non-blocking). +// Without options, the current agent turn runs to completion and the loop +// exits at the turn boundary without starting a new turn. ExitReason is nil. +// +// Use WithImmediate() to abort the running agent turn immediately. +// Use WithGraceful() to cancel at the nearest safe point with recursive +// propagation to nested agents. +// Use WithGracefulTimeout() for safe-point cancel with an escalation deadline. +// Use UntilIdleFor() to defer the stop until the loop has been continuously +// idle for a given duration; the loop shuts down automatically once the idle +// timer fires. +// +// This method may be called multiple times; subsequent calls update cancel options. +// A Stop() call without UntilIdleFor shuts down the loop immediately, even if +// a prior UntilIdleFor is still waiting. +// Call Wait() to block until the loop has fully exited and get the result. +// +// Stop may be called before Run. In that case, the stopped flag is set and +// a subsequent Run will exit the loop immediately. +// +// If the running agent does not support the WithCancel AgentRunOption, +// all cancel-related options (WithImmediate, WithGraceful, WithGracefulTimeout) +// degrade to "exit the loop on entering the next iteration" — the current +// agent turn runs to completion before the loop exits. +func (l *TurnLoop[T, M]) Stop(opts ...StopOption) { + cfg := &stopConfig{} + for _, opt := range opts { + opt(cfg) + } + + // UntilIdleFor is incompatible with cancel options (WithImmediate, + // WithGraceful, WithGracefulTimeout) in the same call. Cancel opts only + // make sense for an immediate or escalated stop; UntilIdleFor defers the + // stop until idle, and must not impact a running agent. Drop them silently. + if cfg.idleFor > 0 { + cfg.agentCancelOpts = nil + } + + decision := l.stopCtrl.requestStop(cfg) + if decision.wakeIdle { + l.buffer.Wakeup() + } + if decision.commit { + l.finishStopCommit() + } +} + +func (l *TurnLoop[T, M]) commitStop() { + if !l.stopCtrl.commit() { + return + } + l.finishStopCommit() +} + +func (l *TurnLoop[T, M]) finishStopCommit() { + atomic.StoreInt32(&l.stopped, 1) + l.buffer.Close() +} + +// Wait blocks until the loop exits and returns the result. +// This method is safe to call from multiple goroutines. +// All callers will receive the same result. +// +// Wait blocks until Run is called AND the loop exits. If Run is +// never called, Wait blocks forever. +func (l *TurnLoop[T, M]) Wait() *TurnLoopExitState[T, M] { + <-l.done + return l.result +} + +func (l *TurnLoop[T, M]) run(ctx context.Context) { + defer l.cleanup(ctx) + + if err := l.tryLoadCheckpoint(ctx); err != nil { + l.runErr = err + return + } + + // Monitor context cancellation: close the buffer so that a blocking + // Receive() unblocks. The loop will then check ctx.Err() and exit. + go func() { + select { + case <-ctx.Done(): + l.buffer.Close() + case <-l.done: + } + }() + + for { + if l.stopCtrl.isCommitted() { + return + } + + isResume := false + var pr *turnLoopPendingResume[T] + var items []T + var pushBack []T + + if l.pendingResume != nil { + isResume = true + pr = l.pendingResume + l.pendingResume = nil + + l.preemptCtrl.waitForPushes() + pr.newItems = append(pr.newItems, l.buffer.TakeAll()...) + + pushBack = make([]T, 0, len(pr.interrupted)+len(pr.unhandled)+len(pr.newItems)) + pushBack = append(pushBack, pr.interrupted...) + pushBack = append(pushBack, pr.unhandled...) + pushBack = append(pushBack, pr.newItems...) + } else { + var first T + var ok bool + + if idleFor := l.stopCtrl.idleDuration(); idleFor > 0 { + l.buffer.ClearWakeup() + idleTimer := time.NewTimer(idleFor) + cancelIdle := make(chan struct{}) + // When the idle timer fires, commitStop closes the buffer via + // buffer.Close(), which broadcasts to unblock the pending + // Receive() call below. + go func() { + select { + case <-idleTimer.C: + l.commitStop() + case <-cancelIdle: + } + }() + + first, ok = l.buffer.Receive() + + idleTimer.Stop() + close(cancelIdle) + + // A spurious wakeup can occur if Stop(UntilIdleFor) called + // buffer.Wakeup() after ClearWakeup() above but before + // Receive() entered its wait. In that case, Receive returns + // !ok from the woken flag, not from buffer closure. + // Re-enter the loop so the idle timer restarts cleanly. + if !ok && !l.buffer.IsClosed() { + continue + } + } else { + first, ok = l.buffer.Receive() + // Woken up by Stop(UntilIdleFor); re-enter loop to start the idle timer. + if !ok && l.stopCtrl.idleDuration() > 0 { + continue + } + } + + if !ok { + if err := ctx.Err(); err != nil { + l.runErr = err + } + return + } + + if err := ctx.Err(); err != nil { + l.buffer.PushFront([]T{first}) + l.runErr = err + return + } + + if l.stopCtrl.isCommitted() { + l.buffer.PushFront([]T{first}) + return + } + + l.preemptCtrl.waitForPushes() + rest := l.buffer.TakeAll() + items = append([]T{first}, rest...) + pushBack = items + } + + l.preemptCtrl.beginPlanningTurn() + abortPlanning := func() { + l.preemptCtrl.abortPlanningTurn().ack() + } + + plan, err := l.planTurn(ctx, isResume, items, pr) + if err != nil { + abortPlanning() + if len(pushBack) > 0 { + l.buffer.PushFront(pushBack) + } + l.runErr = err + return + } + + if l.stopCtrl.isCommitted() { + abortPlanning() + if len(pushBack) > 0 { + l.buffer.PushFront(pushBack) + } + return + } + + agent, err := l.config.PrepareAgent(plan.turnCtx, l, plan.spec.consumed) + if err != nil { + abortPlanning() + if len(pushBack) > 0 { + l.buffer.PushFront(pushBack) + } + l.runErr = err + return + } + + if l.stopCtrl.isCommitted() { + abortPlanning() + if len(pushBack) > 0 { + l.buffer.PushFront(pushBack) + } + return + } + + l.buffer.PushFront(plan.remaining) + + runErr := l.runAgentAndHandleEvents(plan.turnCtx, agent, plan.spec) + + if runErr != nil { + // Set interruptedItems when a cancel or interrupt was captured from the + // event stream, regardless of what the user's callback returned. The items + // were factually mid-execution when the signal arrived. + if l.capturedCancelErr != nil || l.interruptContexts != nil { + l.interruptedItems = append([]T{}, plan.spec.consumed...) + } + l.runErr = runErr + return + } + + // Business interrupt: agent produced an Interrupted action, exit to persist checkpoint. + if l.interruptContexts != nil { + l.interruptedItems = append([]T{}, plan.spec.consumed...) + l.runErr = &InterruptError{InterruptContexts: l.interruptContexts} + return + } + } +} + +func (l *TurnLoop[T, M]) setupBridgeStore(spec *turnRunSpec[T, M], runOpts []AgentRunOption) ([]AgentRunOption, *bridgeStore, error) { + store := l.config.Store + if store == nil && spec.isResume { + return nil, nil, fmt.Errorf("failed to resume agent: checkpoint store is nil") + } + if store == nil { + return runOpts, nil, nil + } + runOpts = append(runOpts, WithCheckPointID(bridgeCheckpointID)) + if spec.isResume { + if len(spec.resumeBytes) == 0 { + return nil, nil, fmt.Errorf("resume checkpoint is empty") + } + return runOpts, newResumeBridgeStore(bridgeCheckpointID, spec.resumeBytes), nil + } + return runOpts, newBridgeStore(), nil +} + +// watchPreempt runs for the lifetime of a single active turn. It consumes +// pending preempt requests exactly once and submits cancel for that turn. +func (l *TurnLoop[T, M]) watchPreempt(done <-chan struct{}, agentCancelFunc AgentCancelFunc, preemptDone chan struct{}) { + preemptDoneClosed := false + for { + select { + case <-done: + return + case <-l.preemptCtrl.notify: + req, ok := l.preemptCtrl.receivePreempt() + if !ok { + continue + } + // CancelHandle is intentionally not awaited here: agentCancelFunc commits the cancel signal synchronously, + // while waiting would block until the turn finishes and can deadlock this watcher against the done signal. + _, contributed := agentCancelFunc(req.cancelOptions(time.Now())...) + if contributed && !preemptDoneClosed { + close(preemptDone) + preemptDoneClosed = true + } + req.ack() + } + } +} + +// watchStop runs for the lifetime of a single active turn. It consumes pending +// Stop cancel requests exactly once and submits them to that turn. +func (l *TurnLoop[T, M]) watchStop(done <-chan struct{}, agentCancelFunc AgentCancelFunc, stoppedDone chan struct{}) { + stoppedClosed := false + + submit := func(req *stopCancelRequest) { + _, contributed := agentCancelFunc(req.cancelOptions(time.Now())...) + if contributed && !stoppedClosed { + close(stoppedDone) + stoppedClosed = true + } + } + + for { + if req, ok := l.stopCtrl.receiveCancel(); ok { + submit(req) + continue + } + + select { + case <-done: + return + case <-l.stopCtrl.notify: + } + } +} + +func (l *TurnLoop[T, M]) runAgentAndHandleEvents( + ctx context.Context, + agent TypedAgent[M], + spec *turnRunSpec[T, M], +) error { + l.interruptContexts = nil + l.capturedCancelErr = nil + l.checkPointRunnerBytes = nil + + var iter *AsyncIterator[*TypedAgentEvent[M]] + + runOpts, ms, err := l.setupBridgeStore(spec, spec.runOpts) + if err != nil { + l.preemptCtrl.abortPlanningTurn().ack() + return err + } + store := l.config.Store + cancelOpt, agentCancelFunc := WithCancel() + runOpts = append(runOpts, cancelOpt) + + // For Run path the streaming mode comes from the input. For Resume path the + // runner reads the streaming mode persisted in the checkpoint, so the value we + // pass here is irrelevant. + enableStreaming := false + if spec.input != nil { + enableStreaming = spec.input.EnableStreaming + } + runner := NewTypedRunner(TypedRunnerConfig[M]{ + EnableStreaming: enableStreaming, + Agent: agent, + CheckPointStore: ms, + }) + + preemptDone := make(chan struct{}) + stoppedDone := make(chan struct{}) + + tc := &TurnContext[T, M]{ + Loop: l, + Consumed: spec.consumed, + Preempted: preemptDone, + Stopped: stoppedDone, + StopCause: l.stopCtrl.cause, + } + l.preemptCtrl.beginActiveTurn(ctx, tc) + l.stopCtrl.beginActiveTurn() + defer func() { + l.stopCtrl.endActiveTurn() + l.preemptCtrl.endActiveTurn().ack() + }() + + if spec.isResume { + var err error + if spec.resumeParams != nil { + iter, err = runner.ResumeWithParams(ctx, bridgeCheckpointID, spec.resumeParams, runOpts...) + } else { + iter, err = runner.Resume(ctx, bridgeCheckpointID, runOpts...) + } + if err != nil { + return fmt.Errorf("failed to resume agent: %w", err) + } + } else { + iter = runner.Run(ctx, spec.input.Messages, runOpts...) + } + + // Wrap iterator to capture framework-level signals (CancelError, InterruptContexts) + // from events before they flow to OnAgentEvents. This ensures the framework can + // track these signals independently of what the user's callback returns. + srcIter := iter + proxyIter, proxyGen := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + go func() { + defer proxyGen.Close() + for { + event, ok := srcIter.Next() + if !ok { + break + } + if event != nil { + if event.Err != nil { + var cancelErr *CancelError + if errors.As(event.Err, &cancelErr) { + l.capturedCancelErr = cancelErr + } + } + if event.Action != nil && event.Action.Interrupted != nil { + l.interruptContexts = event.Action.Interrupted.InterruptContexts + } + } + proxyGen.Send(event) + } + }() + iter = proxyIter + + handleEvents := func() error { + return l.onAgentEvents(ctx, tc, iter) + } + + done := make(chan struct{}) + var handleErr error + + go func() { + defer func() { + panicErr := recover() + if panicErr != nil { + handleErr = safe.NewPanicErr(panicErr, debug.Stack()) + } + close(done) + }() + handleErr = handleEvents() + }() + go l.watchPreempt(done, agentCancelFunc, preemptDone) + go l.watchStop(done, agentCancelFunc, stoppedDone) + + finalizeCheckpoint := func() error { + if store != nil && ms != nil { + data, ok, err := ms.Get(ctx, bridgeCheckpointID) + if err != nil { + return fmt.Errorf("failed to read runner checkpoint: %w", err) + } + if ok { + l.checkPointRunnerBytes = append([]byte{}, data...) + } + } + return nil + } + + // Wait for the turn to end. Three outcomes: + // + // done: Events fully handled (normal or error). If Stop() was + // called, save checkpoint so the caller can resume later. + // Also handle the select race: if preemptDone is closed + // too, treat as a preempt (return nil) instead of leaking + // the CancelError. + // + // preemptDone: A preemptive Push successfully cancelled the agent. + // Wait for the handleEvents goroutine to drain, then + // return nil — the run loop will start a new turn. + // + // stoppedDone: Stop() cancelled the agent. Save checkpoint so the + // caller can resume later. + select { + case <-done: + select { + case <-preemptDone: + return nil + default: + } + if err := finalizeCheckpoint(); err != nil { + if handleErr != nil { + handleErr = fmt.Errorf("%w; checkpoint error: %v", handleErr, err) + } else { + handleErr = err + } + } + return l.applyFrameworkCapturedError(handleErr) + case <-preemptDone: + <-done + return nil + case <-stoppedDone: + <-done + if err := finalizeCheckpoint(); err != nil { + if handleErr != nil { + handleErr = fmt.Errorf("%w; checkpoint error: %v", handleErr, err) + } else { + handleErr = err + } + } + return l.applyFrameworkCapturedError(handleErr) + } +} + +// applyFrameworkCapturedError resolves the final error for runAgentAndHandleEvents. +// Priority scheme: +// - If handleErr != nil: the user's callback error wins (framework does not overwrite). +// - If handleErr == nil and a CancelError was captured: use the captured CancelError. +// - If handleErr == nil and interrupt contexts were captured: this is handled by the +// caller (run loop) via l.interruptContexts, so return nil here. +// +// In all cases, the caller uses l.capturedCancelErr and l.interruptContexts to +// determine interruptedItems independently of the returned error. +func (l *TurnLoop[T, M]) applyFrameworkCapturedError(handleErr error) error { + if handleErr != nil { + return handleErr + } + if l.capturedCancelErr != nil { + return l.capturedCancelErr + } + return nil +} + +func (l *TurnLoop[T, M]) cleanup(ctx context.Context) { + atomic.StoreInt32(&l.stopped, 1) + + unhandled := l.buffer.TakeAll() + checkpointID := l.config.CheckpointID + isIdle := len(l.checkPointRunnerBytes) == 0 && len(unhandled) == 0 && len(l.interruptedItems) == 0 + + // Only save checkpoint when the loop exited due to an explicit Stop(), + // a CancelError, or a business interrupt (InterruptError). + // Also checkpoint when a cancel/interrupt was captured from the event stream + // but the user's callback returned a custom error (the items were still in-flight). + exitCausedByStop := l.runErr == nil || errors.As(l.runErr, new(*CancelError)) || l.capturedCancelErr != nil + businessInterrupt := errors.As(l.runErr, new(*InterruptError)) || l.interruptContexts != nil + shouldSaveCheckpoint := l.config.Store != nil && checkpointID != "" && + ((l.stopCtrl.isCommitted() && exitCausedByStop) || businessInterrupt) && + !isIdle && !l.stopCtrl.skipCheckpointEnabled() + + var checkpointed bool + var checkpointErr error + + if shouldSaveCheckpoint { + cp := &turnLoopCheckpoint[T]{ + RunnerCheckpoint: l.checkPointRunnerBytes, + HasRunnerState: len(l.checkPointRunnerBytes) > 0, + UnhandledItems: unhandled, + CanceledItems: l.interruptedItems, + } + checkpointed = true + checkpointErr = l.saveTurnLoopCheckpoint(ctx, checkpointID, cp) + } else if l.loadCheckpointID != "" { + _ = l.deleteTurnLoopCheckpoint(ctx, l.loadCheckpointID) + } + + var takeLateOnce sync.Once + var takeLateResult []T + + l.result = &TurnLoopExitState[T, M]{ + ExitReason: l.runErr, + UnhandledItems: unhandled, + InterruptedItems: l.interruptedItems, + StopCause: l.stopCtrl.cause(), + CheckpointAttempted: checkpointed, + CheckpointErr: checkpointErr, + TakeLateItems: func() []T { + takeLateOnce.Do(func() { + l.lateMu.Lock() + takeLateResult = append([]T{}, l.lateItems...) + l.lateSealed = true + l.lateMu.Unlock() + }) + return takeLateResult + }, + } + + l.stopCtrl.closeForLoopExit() + l.preemptCtrl.closeForLoopExit() + l.buffer.Close() + close(l.done) +} diff --git a/adk/turn_loop_test.go b/adk/turn_loop_test.go new file mode 100644 index 0000000..2fb903b --- /dev/null +++ b/adk/turn_loop_test.go @@ -0,0 +1,6024 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/schema" +) + +type turnLoopMockAgent struct { + name string + events []*AgentEvent + runFunc func(ctx context.Context, input *AgentInput) (*AgentOutput, error) +} + +func (a *turnLoopMockAgent) Name(_ context.Context) string { return a.name } +func (a *turnLoopMockAgent) Description(_ context.Context) string { return "mock agent" } +func (a *turnLoopMockAgent) Run(ctx context.Context, input *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + + if a.runFunc != nil { + go func() { + defer gen.Close() + output, err := a.runFunc(ctx, input) + if err != nil { + gen.Send(&AgentEvent{Err: err}) + return + } + gen.Send(&AgentEvent{Output: output}) + }() + return iter + } + + go func() { + defer gen.Close() + for _, e := range a.events { + gen.Send(e) + } + }() + return iter +} + +type turnLoopCheckpointStore struct { + m map[string][]byte + mu sync.Mutex +} + +func (s *turnLoopCheckpointStore) Set(_ context.Context, key string, value []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + s.m[key] = value + return nil +} + +func (s *turnLoopCheckpointStore) Get(_ context.Context, key string) ([]byte, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + v, ok := s.m[key] + return v, ok, nil +} + +type turnLoopCancellableMockAgent struct { + name string + runFunc func(ctx context.Context, input *AgentInput) (*AgentOutput, error) + onCancel func(cc *cancelContext) + cancel context.CancelFunc + mu sync.Mutex +} + +func (a *turnLoopCancellableMockAgent) Name(_ context.Context) string { return a.name } +func (a *turnLoopCancellableMockAgent) Description(_ context.Context) string { return "mock agent" } + +func (a *turnLoopCancellableMockAgent) Run(ctx context.Context, input *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + + o := getCommonOptions(nil, opts...) + cc := o.cancelCtx + + a.mu.Lock() + var cancelCtx context.Context + cancelCtx, a.cancel = context.WithCancel(ctx) + a.mu.Unlock() + + go func() { + defer gen.Close() + if cc != nil { + go func() { + <-cc.cancelChan + // CRITICAL: call onCancel BEFORE cancel() to avoid race condition. + // If cancel() fires first, the runFunc returns immediately, + // flowAgent's defer calls markDone(), and doneChan closes + // before onCancel can read cc.config. + if a.onCancel != nil { + a.onCancel(cc) + } + a.mu.Lock() + if a.cancel != nil { + a.cancel() + } + a.mu.Unlock() + }() + } + + output, err := a.runFunc(cancelCtx, input) + if err != nil { + gen.Send(&AgentEvent{Err: err}) + return + } + gen.Send(&AgentEvent{Output: output}) + }() + return iter +} + +type turnLoopStopModeProbeAgent struct { + ccCh chan *cancelContext +} + +func (a *turnLoopStopModeProbeAgent) Name(_ context.Context) string { return "probe" } +func (a *turnLoopStopModeProbeAgent) Description(_ context.Context) string { return "probe" } +func (a *turnLoopStopModeProbeAgent) Run(_ context.Context, _ *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + o := getCommonOptions(nil, opts...) + cc := o.cancelCtx + a.ccCh <- cc + go func() { + defer gen.Close() + <-cc.cancelChan + for { + if cc.getMode() == CancelImmediate { + gen.Send(&AgentEvent{Err: cc.createCancelError()}) + return + } + time.Sleep(1 * time.Millisecond) + } + }() + return iter +} + +func newAndRunTurnLoop[T any, M MessageType](ctx context.Context, cfg TurnLoopConfig[T, M]) *TurnLoop[T, M] { + l := NewTurnLoop(cfg) + l.Run(ctx) + return l +} + +func genInputConsumeAll(_ context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil +} + +func genInputConsumeAllWithMsg(_ context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil +} + +func genInputConsumeFirst(_ context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: []string{items[0]}, + Remaining: items[1:], + }, nil +} + +var prepareTestAgent = func(_ context.Context, _ *TurnLoop[string, *schema.Message], _ []string) (Agent, error) { + return &turnLoopMockAgent{name: "test"}, nil +} + +func prepareAgent(a Agent) func(context.Context, *TurnLoop[string, *schema.Message], []string) (Agent, error) { + return func(_ context.Context, _ *TurnLoop[string, *schema.Message], _ []string) (Agent, error) { + return a, nil + } +} + +func waitOrFail(t *testing.T, ch <-chan struct{}, msg string) { + t.Helper() + select { + case <-ch: + case <-time.After(2 * time.Second): + t.Fatal(msg) + } +} + +func newTestStore() *turnLoopCheckpointStore { + return &turnLoopCheckpointStore{m: make(map[string][]byte)} +} + +func newPreemptTestLoop(t *testing.T, agent *turnLoopCancellableMockAgent) *TurnLoop[string, *schema.Message] { + t.Helper() + + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + + originalRunFunc := agent.runFunc + agent.runFunc = func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { close(agentStarted) }) + return originalRunFunc(ctx, input) + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: genInputConsumeFirst, + }) + + loop.Push("first") + + waitOrFail(t, agentStarted, "agent did not start") + + return loop +} + +func TestTurnLoop_StopIsIdempotent(t *testing.T) { + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Stop() + loop.Stop() + loop.Stop() + + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_WaitMultipleGoroutines(t *testing.T) { + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Stop() + + var wg sync.WaitGroup + results := make([]*TurnLoopExitState[string, *schema.Message], 3) + + for i := 0; i < 3; i++ { + i := i + wg.Add(1) + go func() { + defer wg.Done() + results[i] = loop.Wait() + }() + } + + wg.Wait() + + assert.Equal(t, results[0], results[1]) + assert.Equal(t, results[1], results[2]) +} + +func TestTurnLoop_UnhandledItemsOnStop(t *testing.T) { + started := make(chan struct{}) + blocked := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + close(started) + <-blocked + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items[:1], + Remaining: items[1:], + }, nil + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + loop.Push("msg2") + loop.Push("msg3") + + <-started + + loop.Stop() + close(blocked) + + result := loop.Wait() + assert.NotEmpty(t, result.UnhandledItems, "should return unhandled items") +} + +func TestTurnLoop_GenInputError(t *testing.T) { + genErr := errors.New("gen input error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return nil, genErr + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, genErr) +} + +func TestTurnLoop_GetAgentError(t *testing.T) { + agentErr := errors.New("get agent error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return nil, agentErr + }, + }) + + loop.Push("msg1") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, agentErr) +} + +func TestTurnLoop_BatchProcessing(t *testing.T) { + var batches [][]string + var mu sync.Mutex + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + mu.Lock() + batches = append(batches, items) + mu.Unlock() + + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items[:1], + Remaining: items[1:], + }, nil + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + loop.Push("msg2") + loop.Push("msg3") + + time.Sleep(200 * time.Millisecond) + + loop.Stop() + loop.Wait() + + mu.Lock() + defer mu.Unlock() + + assert.NotEmpty(t, batches, "should have processed at least one batch") +} + +func TestTurnLoop_StopWithMode(t *testing.T) { + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Stop(WithGraceful()) + + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_Preempt_CancelsCurrentAgent(t *testing.T) { + agentStarted := make(chan struct{}) + agentCancelled := make(chan struct{}) + agentStartedOnce := sync.Once{} + agentCancelledOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + agentCancelledOnce.Do(func() { + close(agentCancelled) + }) + return &AgentOutput{}, nil + }, + } + + genInputCalls := int32(0) + secondGenInputCalled := make(chan struct{}) + secondGenInputOnce := sync.Once{} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + count := atomic.AddInt32(&genInputCalls, 1) + if count >= 2 { + secondGenInputOnce.Do(func() { + close(secondGenInputCalled) + }) + } + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: []string{items[0]}, + Remaining: items[1:], + }, nil + }, + }) + + loop.Push("first") + + waitOrFail(t, agentStarted, "agent did not start") + + loop.Push("urgent", WithPreempt[string, *schema.Message](AnySafePoint)) + + waitOrFail(t, agentCancelled, "agent was not cancelled by preempt") + + waitOrFail(t, secondGenInputCalled, "second GenInput was not called after preempt") + + loop.Stop(WithImmediate()) + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.GreaterOrEqual(t, atomic.LoadInt32(&genInputCalls), int32(2)) +} + +func TestTurnLoop_Preempt_DiscardsConsumedItems(t *testing.T) { + agentStarted := make(chan struct{}) + agentDone := make(chan struct{}) + agentStartedOnce := sync.Once{} + agentDoneOnce := sync.Once{} + firstAgentRun := true + var firstRunMu sync.Mutex + + genInputResults := make([][]string, 0) + var mu sync.Mutex + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + firstRunMu.Lock() + isFirst := firstAgentRun + firstAgentRun = false + firstRunMu.Unlock() + + if isFirst { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + } else { + agentDoneOnce.Do(func() { + close(agentDone) + }) + } + return &AgentOutput{}, nil + }, + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + mu.Lock() + genInputResults = append(genInputResults, items) + mu.Unlock() + + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: []string{items[0]}, + Remaining: items[1:], + }, nil + }, + }) + + loop.Push("first") + + waitOrFail(t, agentStarted, "agent did not start") + + loop.Push("urgent", WithPreempt[string, *schema.Message](AnySafePoint)) + + waitOrFail(t, agentDone, "second agent run did not complete") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + + mu.Lock() + defer mu.Unlock() + require.GreaterOrEqual(t, len(genInputResults), 2) + assert.NotContains(t, genInputResults[1], "first") + assert.Contains(t, genInputResults[1], "urgent") +} + +func TestTurnLoop_Preempt_WithAgentCancelMode(t *testing.T) { + cancelFuncCalled := make(chan struct{}) + cancelFuncCalledOnce := sync.Once{} + firstCancelModeUsed := CancelImmediate + var cancelModeMu sync.Mutex + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return &AgentOutput{}, nil + }, + onCancel: func(cc *cancelContext) { + cancelModeMu.Lock() + cancelFuncCalledOnce.Do(func() { + firstCancelModeUsed = cc.getMode() + close(cancelFuncCalled) + }) + cancelModeMu.Unlock() + }, + } + + loop := newPreemptTestLoop(t, agent) + + loop.Push("urgent", WithPreempt[string, *schema.Message](AfterToolCalls)) + + waitOrFail(t, cancelFuncCalled, "cancelFunc was not called by preempt") + + loop.Stop(WithImmediate()) + result := loop.Wait() + assert.NoError(t, result.ExitReason) + cancelModeMu.Lock() + actualMode := firstCancelModeUsed + cancelModeMu.Unlock() + assert.Equal(t, CancelAfterToolCalls, actualMode) +} + +func TestTurnLoop_PreemptAck_ClosesAfterCancelIsInitiated(t *testing.T) { + cancelObserved := make(chan struct{}) + agentFinishGate := make(chan struct{}) + cancelObservedOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + <-agentFinishGate + return &AgentOutput{}, nil + }, + onCancel: func(cc *cancelContext) { + cancelObservedOnce.Do(func() { close(cancelObserved) }) + }, + } + + loop := newPreemptTestLoop(t, agent) + + ok, ack := loop.Push("urgent", WithPreempt[string, *schema.Message](AfterToolCalls)) + assert.True(t, ok) + assert.NotNil(t, ack) + + waitOrFail(t, ack, "preempt ack was not closed") + + waitOrFail(t, cancelObserved, "cancel was not initiated") + + close(agentFinishGate) + + loop.Stop(WithImmediate()) + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_PreemptAck_ClosesImmediatelyIfLoopNotStarted(t *testing.T) { + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + ok, ack := loop.Push("urgent", WithPreempt[string, *schema.Message](AnySafePoint)) + assert.True(t, ok) + assert.NotNil(t, ack) + + waitOrFail(t, ack, "preempt ack was not closed") +} + +func TestTurnLoop_Preempt_EscalatesOnSecondPreempt(t *testing.T) { + firstCancelSeen := make(chan struct{}) + agentFinishGate := make(chan struct{}) + firstCancelOnce := sync.Once{} + + var ccPtr atomic.Value + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + <-agentFinishGate + return &AgentOutput{}, nil + }, + onCancel: func(cc *cancelContext) { + ccPtr.Store(cc) + firstCancelOnce.Do(func() { close(firstCancelSeen) }) + }, + } + + loop := newPreemptTestLoop(t, agent) + + loop.Push("urgent1", WithPreempt[string, *schema.Message](AfterChatModel)) + waitOrFail(t, firstCancelSeen, "first preempt did not trigger cancel") + + loop.Push("urgent2", WithPreemptTimeout[string, *schema.Message](AnySafePoint, time.Millisecond)) + + wantMode := CancelAfterChatModel | CancelAfterToolCalls + deadline := time.Now().Add(1 * time.Second) + for time.Now().Before(deadline) { + v := ccPtr.Load() + if v == nil { + time.Sleep(5 * time.Millisecond) + continue + } + cc := v.(*cancelContext) + if cc.getMode() == wantMode && atomic.LoadInt32(&cc.escalated) == 1 { + break + } + time.Sleep(5 * time.Millisecond) + } + + v := ccPtr.Load() + if v == nil { + t.Fatal("cancel context was not captured") + } + cc := v.(*cancelContext) + assert.Equal(t, wantMode, cc.getMode()) + assert.Equal(t, int32(1), atomic.LoadInt32(&cc.escalated)) + + close(agentFinishGate) + + loop.Stop(WithImmediate()) + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_Preempt_JoinsSafePointModesOnSecondPreempt(t *testing.T) { + firstCancelSeen := make(chan struct{}) + agentFinishGate := make(chan struct{}) + firstCancelOnce := sync.Once{} + + var ccPtr atomic.Value + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + <-agentFinishGate + return &AgentOutput{}, nil + }, + onCancel: func(cc *cancelContext) { + ccPtr.Store(cc) + firstCancelOnce.Do(func() { close(firstCancelSeen) }) + }, + } + + loop := newPreemptTestLoop(t, agent) + + loop.Push("urgent1", WithPreempt[string, *schema.Message](AfterChatModel)) + waitOrFail(t, firstCancelSeen, "first preempt did not trigger cancel") + + loop.Push("urgent2", WithPreempt[string, *schema.Message](AfterToolCalls)) + + want := CancelAfterChatModel | CancelAfterToolCalls + deadline := time.Now().Add(1 * time.Second) + for time.Now().Before(deadline) { + v := ccPtr.Load() + if v == nil { + time.Sleep(5 * time.Millisecond) + continue + } + cc := v.(*cancelContext) + if cc.getMode() == want { + break + } + time.Sleep(5 * time.Millisecond) + } + + v := ccPtr.Load() + if v == nil { + t.Fatal("cancel context was not captured") + } + cc := v.(*cancelContext) + assert.Equal(t, want, cc.getMode()) + + close(agentFinishGate) + + loop.Stop(WithImmediate()) + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_Push_WithoutPreempt_DoesNotCancel(t *testing.T) { + agentRunCount := 0 + agentDone := make(chan struct{}) + + agent := &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentRunCount++ + if agentRunCount == 1 { + time.Sleep(100 * time.Millisecond) + } + if agentRunCount == 2 { + close(agentDone) + } + return &AgentOutput{}, nil + }, + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: genInputConsumeFirst, + }) + + loop.Push("first") + time.Sleep(20 * time.Millisecond) + loop.Push("second") + + waitOrFail(t, agentDone, "second agent run did not complete") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.Equal(t, 2, agentRunCount) +} + +func TestTurnLoop_PreemptDelay_NoMispreemptOnNaturalCompletion(t *testing.T) { + agent1Started := make(chan struct{}) + allowAgent1Done := make(chan struct{}) + agent1Done := make(chan struct{}) + agent2Started := make(chan struct{}) + allowAgent2Done := make(chan struct{}) + agent2Done := make(chan struct{}) + agent1StartedOnce := sync.Once{} + agent1DoneOnce := sync.Once{} + agent2StartedOnce := sync.Once{} + agent2DoneOnce := sync.Once{} + + var agentRunCount int32 + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + count := atomic.AddInt32(&agentRunCount, 1) + switch count { + case 1: + agent1StartedOnce.Do(func() { close(agent1Started) }) + <-allowAgent1Done + agent1DoneOnce.Do(func() { close(agent1Done) }) + case 2: + agent2StartedOnce.Do(func() { close(agent2Started) }) + select { + case <-allowAgent2Done: + case <-ctx.Done(): + t.Error("Agent2 was unexpectedly cancelled") + return nil, ctx.Err() + } + agent2DoneOnce.Do(func() { close(agent2Done) }) + } + return &AgentOutput{}, nil + }, + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: genInputConsumeFirst, + }) + + loop.Push("first") + + waitOrFail(t, agent1Started, "agent1 did not start") + + _, ack := loop.Push("second", WithPreempt[string, *schema.Message](AnySafePoint), WithPreemptDelay[string, *schema.Message](500*time.Millisecond)) + require.NotNil(t, ack) + close(allowAgent1Done) + + waitOrFail(t, agent1Done, "agent1 did not complete naturally") + + waitOrFail(t, agent2Started, "agent2 did not start") + + requireAckClosed(t, ack) + close(allowAgent2Done) + + waitOrFail(t, agent2Done, "agent2 did not complete - may have been incorrectly preempted") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.Equal(t, int32(2), atomic.LoadInt32(&agentRunCount)) +} + +func TestTurnLoop_PreemptDuringPlanningCancelsUpcomingAgent(t *testing.T) { + genInputEntered := make(chan struct{}) + allowGenInput := make(chan struct{}) + cancelSubmitted := make(chan struct{}) + agentCancelled := make(chan struct{}) + genInputOnce := sync.Once{} + cancelOnce := sync.Once{} + agentCancelledOnce := sync.Once{} + + var agentRunCount int32 + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + count := atomic.AddInt32(&agentRunCount, 1) + if count == 1 { + <-ctx.Done() + agentCancelledOnce.Do(func() { close(agentCancelled) }) + return &AgentOutput{}, nil + } + return &AgentOutput{}, nil + }, + onCancel: func(cc *cancelContext) { + cancelOnce.Do(func() { close(cancelSubmitted) }) + }, + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + genInputOnce.Do(func() { + close(genInputEntered) + <-allowGenInput + }) + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: []string{items[0]}, + Remaining: items[1:], + }, nil + }, + }) + + loop.Push("first") + + waitOrFail(t, genInputEntered, "GenInput did not enter planning phase") + + ok, ack := loop.Push("urgent", WithPreempt[string, *schema.Message](AnySafePoint)) + require.True(t, ok) + require.NotNil(t, ack) + requireAckOpen(t, ack) + + close(allowGenInput) + + waitOrFail(t, cancelSubmitted, "planning-phase preempt did not submit cancel") + requireAckClosed(t, ack) + + waitOrFail(t, agentCancelled, "agent was not cancelled by planning-phase preempt") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.GreaterOrEqual(t, atomic.LoadInt32(&agentRunCount), int32(1)) +} + +func TestTurnLoop_ConcurrentPush(t *testing.T) { + var count int32 + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + atomic.AddInt32(&count, int32(len(items))) + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + }) + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 10; j++ { + _, _ = loop.Push(fmt.Sprintf("msg-%d-%d", i, j)) + } + }(i) + } + + wg.Wait() + time.Sleep(200 * time.Millisecond) + + loop.Stop() + result := loop.Wait() + + processed := atomic.LoadInt32(&count) + unhandled := len(result.UnhandledItems) + + assert.True(t, processed > 0, "should have processed some items") + assert.True(t, int(processed)+unhandled <= 100, "total should not exceed pushed amount") +} + +func TestTurnLoop_StopAfterReceive_RecoverItem(t *testing.T) { + receiveStarted := make(chan struct{}) + cancelDone := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + close(receiveStarted) + <-cancelDone + time.Sleep(50 * time.Millisecond) + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + <-receiveStarted + + loop.Stop() + close(cancelDone) + + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_StopAfterGenInput_RecoverConsumed(t *testing.T) { + genInputDone := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + close(genInputDone) + time.Sleep(50 * time.Millisecond) + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items[:1], + Remaining: items[1:], + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + time.Sleep(100 * time.Millisecond) + return &turnLoopMockAgent{name: "test"}, nil + }, + }) + + loop.Push("msg1") + loop.Push("msg2") + + <-genInputDone + + time.Sleep(60 * time.Millisecond) + loop.Stop() + + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_GetAgentError_RecoverConsumed(t *testing.T) { + agentErr := errors.New("get agent error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items[:1], + Remaining: items[1:], + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], c []string) (Agent, error) { + return nil, agentErr + }, + }) + + loop.Push("msg1") + loop.Push("msg2") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, agentErr) + assert.NotEmpty(t, result.UnhandledItems, "should recover at least the consumed item and remaining") +} + +func TestTurnLoop_GenInputError_RecoverItems(t *testing.T) { + genErr := errors.New("gen input error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return nil, genErr + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + loop.Push("msg2") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, genErr) + assert.Len(t, result.UnhandledItems, 2, "should recover all items when GenInput fails") + assert.Contains(t, result.UnhandledItems, "msg1") + assert.Contains(t, result.UnhandledItems, "msg2") +} + +func TestTurnLoop_PrepareAgentError_RecoverItemsInOrder(t *testing.T) { + agentErr := errors.New("prepare agent error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + var urgent string + remaining := make([]string, 0, len(items)) + for _, item := range items { + if item == "urgent" { + urgent = item + } else { + remaining = append(remaining, item) + } + } + if urgent != "" { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: []string{urgent}, + Remaining: remaining, + }, nil + } + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items[:1], + Remaining: items[1:], + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return nil, agentErr + }, + }) + + loop.Push("msg1") + loop.Push("urgent") + loop.Push("msg2") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, agentErr) + assert.Len(t, result.UnhandledItems, 3, "should recover all items") + assert.Equal(t, []string{"msg1", "urgent", "msg2"}, result.UnhandledItems, + "should preserve original push order even when GenInput selects non-prefix items") +} + +// Context cancel tests: the TurnLoop monitors context cancellation by closing +// the internal buffer when ctx.Done() fires, which unblocks the blocking +// Receive() call. The loop then checks ctx.Err() and exits with the context error. + +func TestTurnLoop_ContextCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + genInputStarted := make(chan struct{}) + genInputDone := make(chan struct{}) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + close(genInputStarted) + <-genInputDone + if err := ctx.Err(); err != nil { + return nil, err + } + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + + <-genInputStarted + cancel() + close(genInputDone) + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, context.Canceled) +} + +func TestTurnLoop_ContextDeadlineExceeded(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + select { + case <-time.After(100 * time.Millisecond): + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + case <-ctx.Done(): + return nil, ctx.Err() + } + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, context.DeadlineExceeded) +} + +func TestTurnLoop_ContextCancelBeforeReceive(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + // Push before Run to guarantee the item is buffered before the + // context-monitoring goroutine can close the buffer. + _, _ = loop.Push("msg1") + loop.Run(ctx) + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, context.Canceled) + assert.Len(t, result.UnhandledItems, 1) +} + +func TestTurnLoop_ContextCancelDuringBlockingReceive(t *testing.T) { + // When context is cancelled while Receive() is blocking (no items in buffer), + // the context monitoring goroutine closes the buffer, which unblocks Receive(). + ctx, cancel := context.WithCancel(context.Background()) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + // Don't push any items — let Receive() block + time.Sleep(50 * time.Millisecond) + cancel() + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, context.Canceled) +} + +func TestTurnLoop_ContextCancelAfterGenInput_RecoverItems(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + genInputCount := 0 + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + genInputCount++ + if genInputCount == 1 { + cancel() + } + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items[:1], + Remaining: items[1:], + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], c []string) (Agent, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + return &turnLoopMockAgent{name: "test"}, nil + }, + }) + + loop.Push("msg1") + loop.Push("msg2") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, context.Canceled) + assert.NotEmpty(t, result.UnhandledItems, "should recover consumed and remaining items") +} + +func TestTurnLoop_OnAgentEventsReceivesEvents(t *testing.T) { + var receivedEvents []*AgentEvent + var receivedConsumed []string + var mu sync.Mutex + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + mu.Lock() + receivedConsumed = append(receivedConsumed, tc.Consumed...) + mu.Unlock() + + for { + event, ok := events.Next() + if !ok { + break + } + mu.Lock() + receivedEvents = append(receivedEvents, event) + mu.Unlock() + } + return nil + }, + }) + + loop.Push("msg1") + + time.Sleep(100 * time.Millisecond) + + loop.Stop() + result := loop.Wait() + + assert.NoError(t, result.ExitReason) + + mu.Lock() + defer mu.Unlock() + assert.NotEmpty(t, receivedConsumed, "should have received consumed items") +} + +// TestTurnLoop_BareStop_AgentRunsToCompletion verifies the core contract of +// bare Stop(): the running agent finishes naturally with an uncanceled context, +// the loop exits cleanly (ExitReason == nil), and no new turn starts even when +// additional items are buffered. +func TestTurnLoop_BareStop_AgentRunsToCompletion(t *testing.T) { + const agentWorkDuration = 200 * time.Millisecond + + agentStarted := make(chan struct{}) + agentCtxErr := make(chan error, 1) + agentOutput := make(chan string, 1) + + turnsExecuted := int32(0) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeFirst, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "worker", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + atomic.AddInt32(&turnsExecuted, 1) + close(agentStarted) + + // Simulate real work (NOT blocking on <-ctx.Done()) + time.Sleep(agentWorkDuration) + + // Record context state AFTER work completes + agentCtxErr <- ctx.Err() + agentOutput <- "work-done" + + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + // Push two items so the loop has a reason to start a second turn. + loop.Push("task1") + loop.Push("task2") + + // Wait for the agent to start processing task1. + waitOrFail(t, agentStarted, "agent did not start") + + // Call bare Stop() while the agent is doing work. + loop.Stop() + + result := loop.Wait() + + // 1. Agent's context was NOT canceled. + select { + case err := <-agentCtxErr: + assert.NoError(t, err, "bare Stop must not cancel the agent's context") + default: + t.Fatal("agent never reported context state") + } + + // 2. Agent completed its work. + select { + case out := <-agentOutput: + assert.Equal(t, "work-done", out) + default: + t.Fatal("agent never produced output") + } + + // 3. ExitReason is nil (clean exit, not a CancelError). + assert.NoError(t, result.ExitReason) + + // 4. InterruptedItems is empty (agent was not interrupted). + assert.Empty(t, result.InterruptedItems) + + // 5. Only one turn executed; the second item is unhandled. + assert.Equal(t, int32(1), atomic.LoadInt32(&turnsExecuted), + "bare Stop must prevent new turns from starting after the current one completes") + assert.Equal(t, []string{"task2"}, result.UnhandledItems, + "the second item should appear in UnhandledItems") +} + +func TestTurnLoop_StopCheckPointIDInCancelError(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + checkpointID := "turn-loop-cancel-ckpt-1" + store := newTestStore() + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: checkpointID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + + loop.Push("msg1") + + <-modelStarted + loop.Stop(WithImmediate()) + + result := loop.Wait() + + var cancelErr *CancelError + assert.True(t, errors.As(result.ExitReason, &cancelErr), "ExitReason should be a *CancelError") + + store.mu.Lock() + defer store.mu.Unlock() + _, ok := store.m[checkpointID] + assert.True(t, ok, "checkpoint should be saved under the configured CheckpointID") +} + +// TestTurnLoop_CancelError_CapturedIndependentlyOfCallback verifies that the TurnLoop +// correctly reports *CancelError as ExitReason and populates InterruptedItems even when +// the user's custom OnAgentEvents callback swallows the CancelError (returns nil). +// This tests the documented guarantee: "the callback should NEVER propagate CancelError +// — the framework handles it automatically." +func TestTurnLoop_CancelError_CapturedIndependentlyOfCallback(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + checkpointID := "cancel-capture-independent-1" + store := newTestStore() + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: checkpointID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + // Custom OnAgentEvents that deliberately swallows all errors including CancelError. + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*TypedAgentEvent[*schema.Message]]) error { + for { + _, ok := events.Next() + if !ok { + break + } + // Deliberately ignore event.Err — do NOT propagate CancelError. + } + return nil // swallow everything + }, + }) + + loop.Push("msg1") + + <-modelStarted + loop.Stop(WithImmediate()) + + result := loop.Wait() + + // The framework should capture CancelError independently of the callback's return value. + var cancelErr *CancelError + assert.True(t, errors.As(result.ExitReason, &cancelErr), + "ExitReason should be *CancelError even when OnAgentEvents swallows it, got: %v", result.ExitReason) + + // InterruptedItems should be populated. + assert.Equal(t, []string{"msg1"}, result.InterruptedItems, + "InterruptedItems should contain the items that were being processed") + + // Checkpoint should be saved. + store.mu.Lock() + defer store.mu.Unlock() + _, ok := store.m[checkpointID] + assert.True(t, ok, "checkpoint should be saved under the configured CheckpointID") +} + +// TestTurnLoop_CancelError_CustomErrorWins_InterruptedItemsStillSet verifies that when +// the user's OnAgentEvents callback returns a custom error during a cancel, the custom +// error becomes ExitReason (not overwritten by CancelError), but InterruptedItems is still +// populated because the items were factually mid-execution when the cancel signal arrived. +func TestTurnLoop_CancelError_CustomErrorWins_InterruptedItemsStillSet(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + checkpointID := "cancel-custom-error-wins-1" + store := newTestStore() + customErr := fmt.Errorf("user callback encountered a problem") + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: checkpointID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + // Custom OnAgentEvents that returns a custom error instead of the CancelError. + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*TypedAgentEvent[*schema.Message]]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + return customErr + }, + }) + + loop.Push("msg1") + + <-modelStarted + loop.Stop(WithImmediate()) + + result := loop.Wait() + + // User's custom error should win as ExitReason. + assert.ErrorIs(t, result.ExitReason, customErr, + "ExitReason should be the user's custom error, not CancelError") + + // But InterruptedItems should still be populated (items were factually in-flight). + assert.Equal(t, []string{"msg1"}, result.InterruptedItems, + "InterruptedItems should contain the items that were being processed") + + // Checkpoint should be saved (cancel was captured, items were in-flight). + store.mu.Lock() + defer store.mu.Unlock() + _, ok := store.m[checkpointID] + assert.True(t, ok, "checkpoint should be saved even when user returns custom error") +} + +func TestTurnLoop_StopWithoutCheckpointIDDoesNotPersist(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + store := newTestStore() + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + + loop.Push("msg1") + + <-modelStarted + loop.Stop(WithImmediate()) + + result := loop.Wait() + + var cancelErr *CancelError + assert.True(t, errors.As(result.ExitReason, &cancelErr), "ExitReason should be a *CancelError") + + store.mu.Lock() + defer store.mu.Unlock() + assert.Empty(t, store.m, "no checkpoint should be saved when CheckpointID is not configured") +} + +func TestTurnLoop_StopWhileIdle_SkipsCheckpoint(t *testing.T) { + ctx := context.Background() + store := &deletableCheckpointStore{ + turnLoopCheckpointStore: turnLoopCheckpointStore{m: make(map[string][]byte)}, + } + cpID := "idle-session" + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Stop() + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + + store.mu.Lock() + defer store.mu.Unlock() + _, exists := store.m[cpID] + assert.False(t, exists, "no checkpoint should be saved when TurnLoop is idle") +} + +func TestTurnLoop_StopBetweenTurnsAndResume(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "between-turns-session" + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("a") + loop.Push("b") + loop.Stop() + loop.Run(ctx) + + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + + var seen []string + var mu sync.Mutex + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + mu.Lock() + seen = append([]string{}, items...) + mu.Unlock() + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + + loop2.Push("c") + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.NoError(t, exit2.ExitReason) + + mu.Lock() + defer mu.Unlock() + assert.Equal(t, []string{"a", "b", "c"}, seen) +} + +// TestTurnLoop_StopCancel_InterruptedItems_PersistAndRestore verifies the full +// lifecycle: Stop(WithImmediate()) cancels a running agent, the exit state +// reports the correct InterruptedItems, the checkpoint persists them, and a new +// TurnLoop restoring from that checkpoint passes the exact same items to +// GenResume's interruptedItems parameter. +func TestTurnLoop_StopCancel_InterruptedItems_PersistAndRestore(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + store := newTestStore() + cpID := "inflight-persist-session" + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + require.NoError(t, err) + + // Phase 1: Run, cancel mid-turn, verify exit state. + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + + loop.Push("msg1") + <-modelStarted + loop.Stop(WithImmediate()) + exit := loop.Wait() + + // ExitReason must be CancelError. + var cancelErr *CancelError + require.True(t, errors.As(exit.ExitReason, &cancelErr), "expected *CancelError, got: %v", exit.ExitReason) + + // InterruptedItems must contain the consumed items. + assert.Equal(t, []string{"msg1"}, exit.InterruptedItems, + "InterruptedItems in exit state should contain the items that were mid-execution") + + // Checkpoint must be saved. + assert.True(t, exit.CheckpointAttempted, "checkpoint should be attempted") + assert.NoError(t, exit.CheckpointErr, "checkpoint save should succeed") + + store.mu.Lock() + _, cpExists := store.m[cpID] + store.mu.Unlock() + require.True(t, cpExists, "checkpoint should exist in store") + + // Phase 2: Restore from checkpoint, verify GenResume receives the exact InterruptedItems. + slowModel.setDelay(10 * time.Millisecond) + + var genResumeCalled bool + var resumeInterruptedItems []string + var resumeUnhandledItems []string + + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenResume: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], interruptedItems, unhandledItems, newItems []string) (*GenResumeResult[string, *schema.Message], error) { + genResumeCalled = true + resumeInterruptedItems = append([]string{}, interruptedItems...) + resumeUnhandledItems = append([]string{}, unhandledItems...) + return &GenResumeResult[string, *schema.Message]{ + Consumed: interruptedItems, + Remaining: append(append([]string{}, unhandledItems...), newItems...), + }, nil + }, + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + t.Fatal("GenInput should not be called when resuming from a cancel checkpoint") + return nil, nil + }, + PrepareAgent: prepareAgent(agent), + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + + loop2.Run(ctx) + exit2 := loop2.Wait() + + assert.NoError(t, exit2.ExitReason) + assert.True(t, genResumeCalled, "GenResume should be called when restoring from a cancel checkpoint") + assert.Equal(t, []string{"msg1"}, resumeInterruptedItems, + "GenResume's interruptedItems should match the original exit state's InterruptedItems") + assert.Empty(t, resumeUnhandledItems, + "unhandledItems should be empty (all items were consumed before cancel)") +} + +// TestTurnLoop_PreemptThenStop_InterruptedItems_ReflectsStoppedTurn verifies that +// when a preempt interrupts turn 1 and then Stop(WithImmediate()) cancels turn 2, +// the reported InterruptedItems correspond to turn 2's consumed items (not turn 1's). +func TestTurnLoop_PreemptThenStop_InterruptedItems_ReflectsStoppedTurn(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "preempt-then-stop-session" + + turnCount := int32(0) + modelStarted := make(chan struct{}, 10) + + slowModel := &cancelTestChatModel{ + delayNs: int64(5 * time.Second), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 10), + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + require.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + n := atomic.AddInt32(&turnCount, 1) + _ = n + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: prepareAgent(agent), + }) + + // Turn 1: push "a", wait for agent to start, then preempt with "b". + loop.Push("a") + <-modelStarted + + _, ack := loop.Push("b", WithPreemptTimeout[string, *schema.Message](AnySafePoint, 10*time.Millisecond)) + // Wait for the preempt to be acknowledged. + waitOrFail(t, ack, "preempt ack timed out") + + // Turn 2: the loop restarts with all buffered items. Wait for agent to start. + <-modelStarted + + // Now stop with immediate cancel on turn 2. + loop.Stop(WithImmediate()) + exit := loop.Wait() + + // ExitReason must be CancelError. + var cancelErr *CancelError + require.True(t, errors.As(exit.ExitReason, &cancelErr), "expected *CancelError, got: %v", exit.ExitReason) + + // KEY ASSERTION: InterruptedItems must reflect turn 2's consumed items (which + // includes both "a" and "b" since GenInput consumes all items), NOT turn 1's ["a"]. + // Turn 2 consumed whatever GenInput received (the re-buffered items from preempt + "b"). + assert.NotEmpty(t, exit.InterruptedItems, "InterruptedItems should not be empty after Stop cancel") + assert.Contains(t, exit.InterruptedItems, "b", + "InterruptedItems should contain 'b' which was part of turn 2's consumed items") +} + +func TestTurnLoop_BusinessInterrupt_PersistAndResume(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "interrupt-session" + + // Agent that produces a business interrupt via Interrupt() call. + interruptAgent := &turnLoopInterruptAgent{interruptInfo: "approval_needed"} + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return interruptAgent, nil + }, + }) + + loop.Push("msg1") + exit := loop.Wait() + + // 1. ExitReason is an *InterruptError (not nil, not *CancelError). + var intErr *InterruptError + require.True(t, errors.As(exit.ExitReason, &intErr), "expected *InterruptError, got: %v", exit.ExitReason) + + // 2. InterruptContexts is populated. + require.NotEmpty(t, intErr.InterruptContexts) + + // 3. InterruptedItems contains the items being processed. + assert.Equal(t, []string{"msg1"}, exit.InterruptedItems) + + // 4. Checkpoint was persisted. + assert.True(t, exit.CheckpointAttempted) + assert.NoError(t, exit.CheckpointErr) + + store.mu.Lock() + _, cpExists := store.m[cpID] + store.mu.Unlock() + assert.True(t, cpExists, "checkpoint should exist in store") + + // 5. Resume: new TurnLoop with same CheckpointID gets GenResume called. + var genResumeCalled bool + var resumeInterruptedItems []string + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenResume: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], interruptedItems []string, unhandledItems []string, newItems []string) (*GenResumeResult[string, *schema.Message], error) { + genResumeCalled = true + resumeInterruptedItems = append([]string{}, interruptedItems...) + return &GenResumeResult[string, *schema.Message]{ + Consumed: interruptedItems, + Remaining: append(append([]string{}, unhandledItems...), newItems...), + }, nil + }, + GenInput: genInputConsumeAll, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + // On resume, agent completes normally. + return &turnLoopMockAgent{ + name: "ResumeAgent", + events: []*AgentEvent{{Output: &AgentOutput{}}}, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.NoError(t, exit2.ExitReason) + assert.True(t, genResumeCalled, "GenResume should be called on checkpoint resume") + assert.Equal(t, []string{"msg1"}, resumeInterruptedItems, "interruptedItems should contain the original items") +} + +// turnLoopInterruptAgent is a test agent that produces a business interrupt event. +type turnLoopInterruptAgent struct { + interruptInfo any +} + +func (a *turnLoopInterruptAgent) Name(_ context.Context) string { return "InterruptAgent" } +func (a *turnLoopInterruptAgent) Description(_ context.Context) string { + return "agent that interrupts" +} +func (a *turnLoopInterruptAgent) Run(ctx context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + event := Interrupt(ctx, a.interruptInfo) + gen.Send(event) + }() + return iter +} + +func TestTurnLoop_CheckpointIDWithoutStore_FreshStart(t *testing.T) { + ctx := context.Background() + var genInputCalled bool + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + CheckpointID: "some-id", + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + genInputCalled = true + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + loop.Push("a") + loop.Run(ctx) + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + assert.True(t, genInputCalled) +} + +func TestTurnLoop_CheckpointNotFound_FreshStart(t *testing.T) { + ctx := context.Background() + store := newTestStore() + var genInputCalled bool + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: "nonexistent-id", + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + genInputCalled = true + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + loop.Push("a") + loop.Run(ctx) + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + assert.True(t, genInputCalled) +} + +func TestTurnLoop_CheckpointEmptyData_TreatedAsNoCheckpoint(t *testing.T) { + ctx := context.Background() + store := newTestStore() + store.m["cp-empty"] = nil + + var genInputCalled bool + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: "cp-empty", + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + genInputCalled = true + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + loop.Push("a") + loop.Run(ctx) + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + assert.True(t, genInputCalled) +} + +type errorCheckpointStore struct { + getErr error + setErr error +} + +func (s *errorCheckpointStore) Get(_ context.Context, _ string) ([]byte, bool, error) { + return nil, false, s.getErr +} + +func (s *errorCheckpointStore) Set(_ context.Context, _ string, _ []byte) error { + return s.setErr +} + +func TestTurnLoop_CheckpointLoadError_ReturnsError(t *testing.T) { + ctx := context.Background() + store := &errorCheckpointStore{getErr: fmt.Errorf("store unavailable")} + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: "cp-1", + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Run(ctx) + exit := loop.Wait() + assert.Error(t, exit.ExitReason) + assert.Contains(t, exit.ExitReason.Error(), "store unavailable") +} + +func TestTurnLoop_CheckpointCorruptData_ReturnsError(t *testing.T) { + ctx := context.Background() + store := newTestStore() + store.m["cp-corrupt"] = []byte("not-valid-gob-data") + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: "cp-corrupt", + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Run(ctx) + exit := loop.Wait() + assert.Error(t, exit.ExitReason) + assert.Contains(t, exit.ExitReason.Error(), "failed to unmarshal checkpoint") +} + +func TestTurnLoop_CheckpointSaveError_ReturnsError(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + saveStore := &errorCheckpointStore{setErr: fmt.Errorf("write failed")} + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: saveStore, + CheckpointID: "cp-1", + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + loop.Push("msg1") + <-modelStarted + loop.Stop(WithImmediate()) + exit := loop.Wait() + assert.Error(t, exit.ExitReason) + assert.True(t, exit.CheckpointAttempted) + assert.Error(t, exit.CheckpointErr) + assert.Contains(t, exit.CheckpointErr.Error(), "write failed") +} + +func TestTurnLoop_StaleCheckpointDeletion_OnCleanResume(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "stale-session" + + loop1 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop1.Push("a") + loop1.Stop() + loop1.Run(ctx) + loop1.Wait() + + store.mu.Lock() + _, exists := store.m[cpID] + store.mu.Unlock() + assert.True(t, exists, "checkpoint should exist after first loop saves it") + + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + loop2.Push("b") + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.NoError(t, exit2.ExitReason) + + store.mu.Lock() + _, exists = store.m[cpID] + store.mu.Unlock() + assert.True(t, exists, "checkpoint should still exist because loop2 was stopped and saved a new one") +} + +type deletableCheckpointStore struct { + turnLoopCheckpointStore + deleteCalled bool + deletedKey string +} + +func (s *deletableCheckpointStore) Delete(_ context.Context, key string) error { + s.mu.Lock() + defer s.mu.Unlock() + s.deleteCalled = true + s.deletedKey = key + delete(s.m, key) + return nil +} + +func TestTurnLoop_CheckpointDeleter_CalledOnContextCancel(t *testing.T) { + ctx := context.Background() + store := &deletableCheckpointStore{ + turnLoopCheckpointStore: turnLoopCheckpointStore{m: make(map[string][]byte)}, + } + cpID := "deleter-session" + + loop1 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop1.Push("a") + loop1.Stop() + loop1.Run(ctx) + loop1.Wait() + + store.mu.Lock() + _, exists := store.m[cpID] + store.mu.Unlock() + assert.True(t, exists, "checkpoint saved after loop1") + + ctx2, cancel2 := context.WithCancel(ctx) + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + cancel2() + return nil + }, + }) + loop2.Push("b") + loop2.Run(ctx2) + exit2 := loop2.Wait() + assert.ErrorIs(t, exit2.ExitReason, context.Canceled) + + store.mu.Lock() + defer store.mu.Unlock() + assert.True(t, store.deleteCalled, "CheckPointDeleter.Delete should be called") + assert.Equal(t, cpID, store.deletedKey) + _, exists = store.m[cpID] + assert.False(t, exists, "checkpoint should be removed from store") +} + +func TestTurnLoop_GenResumeNil_Error(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "resume-nil-session" + modelStarted := make(chan struct{}, 1) + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop1 := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + loop1.Push("msg1") + <-modelStarted + loop1.Stop(WithImmediate()) + loop1.Wait() + + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.Error(t, exit2.ExitReason) + assert.Contains(t, exit2.ExitReason.Error(), "GenResume is required") +} + +func TestTurnLoop_SameCheckpointID_OverwritePattern(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "overwrite-session" + + loop1 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop1.Push("a") + loop1.Push("b") + loop1.Stop() + loop1.Run(ctx) + loop1.Wait() + + store.mu.Lock() + data1 := append([]byte{}, store.m[cpID]...) + store.mu.Unlock() + assert.NotEmpty(t, data1) + + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop2.Push("c") + loop2.Stop() + loop2.Run(ctx) + loop2.Wait() + + store.mu.Lock() + data2 := append([]byte{}, store.m[cpID]...) + store.mu.Unlock() + assert.NotEmpty(t, data2) + assert.NotEqual(t, data1, data2, "checkpoint data should change because items are different") + + var seen []string + var mu sync.Mutex + loop3 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + mu.Lock() + seen = append([]string{}, items...) + mu.Unlock() + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + loop3.Push("d") + loop3.Run(ctx) + exit3 := loop3.Wait() + assert.NoError(t, exit3.ExitReason) + + mu.Lock() + defer mu.Unlock() + assert.Equal(t, []string{"a", "b", "c", "d"}, seen, "should see loop2's unhandled items (a,b,c from loop2's checkpoint) plus new d") +} + +func TestTurnLoop_CheckpointHasRunnerStateButEmptyBytes(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "empty-runner-bytes" + + cp := &turnLoopCheckpoint[string]{ + HasRunnerState: true, + RunnerCheckpoint: nil, + UnhandledItems: []string{"x"}, + } + data, err := marshalTurnLoopCheckpoint(cp) + assert.NoError(t, err) + store.m[cpID] = data + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Run(ctx) + exit := loop.Wait() + assert.Error(t, exit.ExitReason) + assert.Contains(t, exit.ExitReason.Error(), "has runner state but bytes are empty") +} + +func TestTurnLoop_GenResumeReturnsError(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "resume-err-session" + modelStarted := make(chan struct{}, 1) + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop1 := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + loop1.Push("msg1") + <-modelStarted + loop1.Stop(WithImmediate()) + loop1.Wait() + + genResumeErr := fmt.Errorf("resume callback failed") + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + GenResume: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], canceled, unhandled, newItems []string) (*GenResumeResult[string, *schema.Message], error) { + return nil, genResumeErr + }, + PrepareAgent: prepareTestAgent, + }) + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.Error(t, exit2.ExitReason) + assert.ErrorIs(t, exit2.ExitReason, genResumeErr) +} + +func TestTurnLoop_ResumeWaitsForInFlightPushBeforePlanning(t *testing.T) { + ctx := context.Background() + resumeErr := errors.New("stop after observing resume inputs") + strategyEntered := make(chan struct{}) + allowStrategy := make(chan struct{}) + pushDone := make(chan struct{}) + genResumeCalled := make(chan struct{}) + + var resumeNewItems []string + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + GenResume: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], interruptedItems, unhandledItems, newItems []string) (*GenResumeResult[string, *schema.Message], error) { + resumeNewItems = append([]string{}, newItems...) + close(genResumeCalled) + return nil, resumeErr + }, + PrepareAgent: prepareTestAgent, + }) + loop.pendingResume = &turnLoopPendingResume[string]{ + interrupted: []string{"interrupted"}, + newItems: []string{"pre-existing"}, + } + + go func() { + defer close(pushDone) + ok, ack := loop.Push("during-resume", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + close(strategyEntered) + <-allowStrategy + return nil + })) + assert.True(t, ok) + assert.Nil(t, ack) + }() + + waitOrFail(t, strategyEntered, "strategy did not enter") + + loop.Run(ctx) + + select { + case <-genResumeCalled: + t.Fatal("GenResume should wait for in-flight PushStrategy to finish") + default: + } + + close(allowStrategy) + waitOrFail(t, pushDone, "push did not finish") + + exit := loop.Wait() + assert.ErrorIs(t, exit.ExitReason, resumeErr) + assert.Equal(t, []string{"pre-existing", "during-resume"}, resumeNewItems) +} + +func TestTurnLoop_CheckpointSaveError_MergesWithExistingError(t *testing.T) { + ctx := context.Background() + modelStarted := make(chan struct{}, 1) + saveStore := &errorCheckpointStore{setErr: fmt.Errorf("disk full")} + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: saveStore, + CheckpointID: "cp-merge-err", + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + loop.Push("msg1") + <-modelStarted + loop.Stop(WithImmediate()) + exit := loop.Wait() + assert.Error(t, exit.ExitReason) + var ce *CancelError + assert.True(t, errors.As(exit.ExitReason, &ce), "ExitReason should be CancelError, not merged with checkpoint error") + assert.True(t, exit.CheckpointAttempted) + assert.Error(t, exit.CheckpointErr) + assert.Contains(t, exit.CheckpointErr.Error(), "disk full") +} + +func TestTurnLoop_ResumeWithParams(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "resume-params-session" + modelStarted := make(chan struct{}, 1) + + slowModel := &cancelTestChatModel{ + delayNs: int64(500 * time.Millisecond), + response: &schema.Message{ + Role: schema.Assistant, + Content: "Hello", + }, + startedChan: modelStarted, + doneChan: make(chan struct{}, 1), + } + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Instruction: "You are a test assistant", + Model: slowModel, + }) + assert.NoError(t, err) + + loop1 := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareAgent(agent), + }) + loop1.Push("msg1") + <-modelStarted + loop1.Stop(WithImmediate()) + exit1 := loop1.Wait() + var ce *CancelError + assert.True(t, errors.As(exit1.ExitReason, &ce)) + + var resumeParamsUsed *ResumeParams + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + GenResume: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], canceled, unhandled, newItems []string) (*GenResumeResult[string, *schema.Message], error) { + params := &ResumeParams{ + Targets: map[string]any{"some-address": "user-data"}, + } + resumeParamsUsed = params + return &GenResumeResult[string, *schema.Message]{ + ResumeParams: params, + Consumed: append(append(canceled, unhandled...), newItems...), + }, nil + }, + PrepareAgent: prepareAgent(agent), + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + }) + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.NotNil(t, resumeParamsUsed, "GenResume should have been called with ResumeParams") + assert.Contains(t, resumeParamsUsed.Targets, "some-address") + _ = exit2 +} + +func TestTurnLoop_ResumeInterruptAgain_PreservesEnableStreamingCheckpoint(t *testing.T) { + for _, enableStreaming := range []bool{true, false} { + t.Run(fmt.Sprintf("enable_streaming_%t", enableStreaming), func(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := fmt.Sprintf("streaming-resume-%t", enableStreaming) + originalMessage := "msg1" + + firstAgent := &myAgent{ + runFn: func(ctx context.Context, input *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.Equal(t, enableStreaming, input.EnableStreaming) + assert.Len(t, input.Messages, 1) + assert.Equal(t, originalMessage, input.Messages[0].Content) + + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + gen.Send(Interrupt(ctx, "first_interrupt")) + }() + return iter + }, + } + + loop1 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: func(_ context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{ + Messages: []Message{schema.UserMessage(items[0])}, + EnableStreaming: enableStreaming, + }, + Consumed: items, + }, nil + }, + PrepareAgent: prepareAgent(firstAgent), + }) + loop1.Push(originalMessage) + loop1.Run(ctx) + exit1 := loop1.Wait() + require.ErrorAs(t, exit1.ExitReason, new(*InterruptError)) + require.NoError(t, exit1.CheckpointErr) + + secondAgent := &myAgent{ + resumeFn: func(ctx context.Context, info *ResumeInfo, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.Equal(t, enableStreaming, info.EnableStreaming) + + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer gen.Close() + gen.Send(Interrupt(ctx, "second_interrupt")) + }() + return iter + }, + } + + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + GenResume: func(_ context.Context, _ *TurnLoop[string, *schema.Message], interrupted, unhandled, newItems []string) (*GenResumeResult[string, *schema.Message], error) { + return &GenResumeResult[string, *schema.Message]{ + Consumed: interrupted, + Remaining: append(append([]string{}, unhandled...), newItems...), + }, nil + }, + PrepareAgent: prepareAgent(secondAgent), + }) + loop2.Run(ctx) + exit2 := loop2.Wait() + require.ErrorAs(t, exit2.ExitReason, new(*InterruptError)) + require.NoError(t, exit2.CheckpointErr) + + // Verify the runner-level checkpoint persisted by the second interrupt + // still encodes the original streaming mode. This is the invariant the PR + // fixes: even though loop2's TypedRunner was constructed with the + // resume-path placeholder (false), runner uses resumeInfo.EnableStreaming + // from the previous checkpoint when re-saving. + store.mu.Lock() + data, ok := store.m[cpID] + store.mu.Unlock() + require.True(t, ok) + cp, err := unmarshalTurnLoopCheckpoint[string](data) + require.NoError(t, err) + require.True(t, cp.HasRunnerState) + _, _, info2, err := runnerLoadCheckPointImpl(newResumeBridgeStore(bridgeCheckpointID, cp.RunnerCheckpoint), context.Background(), bridgeCheckpointID) + require.NoError(t, err) + assert.Equal(t, enableStreaming, info2.EnableStreaming) + }) + } +} + +func TestTurnLoop_Stop_EscalatesCancelMode(t *testing.T) { + ctx := context.Background() + agentStarted := make(chan *cancelContext, 1) + probe := &turnLoopStopModeProbeAgent{ccCh: agentStarted} + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return probe, nil + }, + }) + + loop.Push("msg1") + cc := <-agentStarted + + loop.Stop(WithGracefulTimeout(10 * time.Second)) + loop.Stop(WithImmediate()) + + deadline := time.After(1 * time.Second) + for { + if cc.getMode() == CancelImmediate { + break + } + select { + case <-deadline: + t.Fatal("cancel mode did not escalate to CancelImmediate") + default: + } + time.Sleep(1 * time.Millisecond) + } + + exit := loop.Wait() + var ce *CancelError + require.True(t, errors.As(exit.ExitReason, &ce)) + assert.Equal(t, CancelImmediate, ce.Info.Mode) +} + +func TestTurnLoop_DefaultOnAgentEvents_ErrorPropagation(t *testing.T) { + agentErr := errors.New("agent execution error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + return nil, agentErr + }, + }, nil + }, + // No OnAgentEvents — use default handler + }) + + loop.Push("msg1") + + result := loop.Wait() + // The default handler should propagate the agent error as ExitReason + assert.Error(t, result.ExitReason) +} + +func TestTurnLoop_OnAgentEventsError(t *testing.T) { + handlerErr := errors.New("event handler error") + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + // Drain events then return error + for { + _, ok := events.Next() + if !ok { + break + } + } + return handlerErr + }, + }) + + loop.Push("msg1") + + result := loop.Wait() + assert.ErrorIs(t, result.ExitReason, handlerErr) +} + +func TestTurnLoop_StopCallFromGenInput(t *testing.T) { + // Test that calling Stop() from within GenInput works correctly + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, loop *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + loop.Stop() + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + + result := loop.Wait() + assert.NoError(t, result.ExitReason) +} + +func TestTurnLoop_PushFromOnAgentEvents(t *testing.T) { + // Test that calling Push() from within OnAgentEvents works + pushCount := int32(0) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeFirst, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + count := atomic.AddInt32(&pushCount, 1) + if count == 1 { + // Push a follow-up item from the callback + _, _ = tc.Loop.Push("follow-up") + } else { + tc.Loop.Stop() + } + return nil + }, + }) + + loop.Push("initial") + + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.Equal(t, int32(2), atomic.LoadInt32(&pushCount)) +} + +// Tests for NewTurnLoop: the permissive API where Push, Stop, and Wait are +// all valid on a not-yet-running loop. + +func TestNewTurnLoop_PushBeforeRun(t *testing.T) { + // Items pushed before Run are buffered and processed after Run starts. + var processedItems []string + var mu sync.Mutex + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + mu.Lock() + processedItems = append(processedItems, items...) + mu.Unlock() + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: prepareTestAgent, + }) + + // Push before Run — items should be buffered. + ok, _ := loop.Push("msg1") + assert.True(t, ok) + ok, _ = loop.Push("msg2") + assert.True(t, ok) + + loop.Run(context.Background()) + + time.Sleep(100 * time.Millisecond) + + loop.Stop() + result := loop.Wait() + + mu.Lock() + defer mu.Unlock() + + assert.NoError(t, result.ExitReason) + assert.Contains(t, processedItems, "msg1") + assert.Contains(t, processedItems, "msg2") +} + +func TestNewTurnLoop_WaitBeforeRun(t *testing.T) { + // Wait blocks until Run is called AND the loop exits. + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + waitDone := make(chan *TurnLoopExitState[string, *schema.Message], 1) + go func() { + waitDone <- loop.Wait() + }() + + // Wait should not return yet since Run hasn't been called. + select { + case <-waitDone: + t.Fatal("Wait returned before Run was called") + case <-time.After(50 * time.Millisecond): + // expected + } + + loop.Push("msg1") + loop.Stop() + loop.Run(context.Background()) + + select { + case result := <-waitDone: + assert.NoError(t, result.ExitReason) + assert.Equal(t, []string{"msg1"}, result.UnhandledItems) + case <-time.After(1 * time.Second): + t.Fatal("Wait did not return after Run + Stop") + } +} + +func TestNewTurnLoop_RunIsIdempotent(t *testing.T) { + var genInputCalls int32 + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + atomic.AddInt32(&genInputCalls, 1) + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("msg1") + loop.Run(context.Background()) + loop.Run(context.Background()) + loop.Run(context.Background()) + + time.Sleep(100 * time.Millisecond) + + loop.Stop() + result := loop.Wait() + + assert.NoError(t, result.ExitReason) + assert.True(t, atomic.LoadInt32(&genInputCalls) >= 1) +} + +func TestNewTurnLoop_ConcurrentPushAndRun(t *testing.T) { + // Concurrent Push and Run should not race. + for i := 0; i < 100; i++ { + var count int32 + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + atomic.AddInt32(&count, int32(len(items))) + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{name: "test"}, nil + }, + }) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + _, _ = loop.Push("item") + }() + + go func() { + defer wg.Done() + loop.Run(context.Background()) + }() + + wg.Wait() + + time.Sleep(50 * time.Millisecond) + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + + processed := atomic.LoadInt32(&count) + unhandled := len(result.UnhandledItems) + assert.True(t, int(processed)+unhandled <= 1, + "total should not exceed pushed amount") + } +} + +type turnCtxKey struct{} + +func TestTurnLoop_RunCtx_Propagation(t *testing.T) { + // Verify that GenInputResult.RunCtx is propagated to PrepareAgent, + // the agent run, and OnAgentEvents. + + const traceVal = "trace-123" + var prepareCtxVal, agentCtxVal, eventsCtxVal string + + cfg := TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, loop *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + // Derive a new context with per-item trace data + runCtx := context.WithValue(ctx, turnCtxKey{}, traceVal) + return &GenInputResult[string, *schema.Message]{ + RunCtx: runCtx, + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, loop *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + if v, ok := ctx.Value(turnCtxKey{}).(string); ok { + prepareCtxVal = v + } + return &turnLoopMockAgent{ + name: "trace-agent", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + if v, ok := ctx.Value(turnCtxKey{}).(string); ok { + agentCtxVal = v + } + return &AgentOutput{}, nil + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + if v, ok := ctx.Value(turnCtxKey{}).(string); ok { + eventsCtxVal = v + } + for { + if _, ok := events.Next(); !ok { + break + } + } + tc.Loop.Stop() + return nil + }, + } + + loop := NewTurnLoop(cfg) + loop.Push("hello") + loop.Run(context.Background()) + result := loop.Wait() + + assert.Nil(t, result.ExitReason) + assert.Equal(t, traceVal, prepareCtxVal, "PrepareAgent should receive RunCtx") + assert.Equal(t, traceVal, agentCtxVal, "Agent run should receive RunCtx") + assert.Equal(t, traceVal, eventsCtxVal, "OnAgentEvents should receive RunCtx") +} + +func TestTurnLoop_TurnContext_PreemptedChannel(t *testing.T) { + preemptedSeen := make(chan struct{}) + agentStarted := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + close(agentStarted) + select { + case <-tc.Preempted: + close(preemptedSeen) + case <-time.After(5 * time.Second): + t.Error("timed out waiting for Preempted channel") + } + // Drain events + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Push("msg2", WithPreemptTimeout[string, *schema.Message](AnySafePoint, time.Millisecond)) + + select { + case <-preemptedSeen: + // success + case <-time.After(5 * time.Second): + t.Fatal("preempted channel was never observed in OnAgentEvents") + } + + loop.Stop() + loop.Wait() +} + +// ============================================================================= +// preemptController unit tests +// ============================================================================= + +func requireAckClosed(t *testing.T, ack <-chan struct{}) { + t.Helper() + waitOrFail(t, ack, "ack should be closed") +} + +func requireAckOpen(t *testing.T, ack <-chan struct{}) { + t.Helper() + select { + case <-ack: + t.Fatal("ack should still be open") + default: + } +} + +func requirePreemptPhase(t *testing.T, c *preemptController, phase preemptTurnPhase) { + t.Helper() + require.Eventually(t, func() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.turnPhase == phase + }, time.Second, time.Millisecond) +} + +func TestPreemptController_BeginPushSnapshotsPlanningTurn(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + + snapshot := c.beginPush() + c.endPush() + + assert.True(t, snapshot.hasTargetTurn) + assert.NotZero(t, snapshot.turnID) + assert.Nil(t, snapshot.ctx) + assert.Nil(t, snapshot.tc) +} + +type testContextKey struct{} + +func TestPreemptController_BeginPushSnapshotsActiveTurn(t *testing.T) { + c := newPreemptController() + ctx := context.WithValue(context.Background(), testContextKey{}, "value") + tc := "turn-context" + + c.beginPlanningTurn() + c.beginActiveTurn(ctx, tc) + snapshot := c.beginPush() + c.endPush() + + assert.True(t, snapshot.hasTargetTurn) + assert.NotZero(t, snapshot.turnID) + assert.Equal(t, ctx, snapshot.ctx) + assert.Equal(t, tc, snapshot.tc) +} + +func TestPreemptController_RequestPreemptIdleTurnAcksImmediately(t *testing.T) { + c := newPreemptController() + snapshot := c.beginPush() + c.endPush() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + requireAckClosed(t, ack) + _, ok := c.receivePreempt() + assert.False(t, ok) +} + +func TestPreemptController_RejectsInvalidTurnPhaseTransitions(t *testing.T) { + c := newPreemptController() + + assert.PanicsWithValue(t, "adk: preemptController.beginActiveTurn called while turn phase is idle; expected planning", func() { + c.beginActiveTurn(context.Background(), "tc") + }) + assert.PanicsWithValue(t, "adk: preemptController.abortPlanningTurn called while turn phase is idle; expected planning", func() { + c.abortPlanningTurn() + }) + assert.PanicsWithValue(t, "adk: preemptController.endActiveTurn called while turn phase is idle; expected active", func() { + c.endActiveTurn() + }) + + c.beginPlanningTurn() + assert.PanicsWithValue(t, "adk: preemptController.beginPlanningTurn called while turn phase is planning; expected idle", func() { + c.beginPlanningTurn() + }) + c.abortPlanningTurn() + + c.pending = newPreemptRequest(nil, nil, time.Now()) + assert.PanicsWithValue(t, "adk: preemptController.beginPlanningTurn called with stale pending preempt request", func() { + c.beginPlanningTurn() + }) +} + +func TestPreemptController_RequestPreemptForPlanningTurnIsConsumedAfterActivation(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + snapshot := c.beginPush() + c.endPush() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + _, ok := c.receivePreempt() + assert.False(t, ok) + requireAckOpen(t, ack) + + c.beginActiveTurn(context.Background(), "tc") + req, ok := c.receivePreempt() + require.True(t, ok) + requireAckOpen(t, ack) + + req.ack() + requireAckClosed(t, ack) +} + +func TestPreemptController_RequestPreemptForActiveTurnIsConsumedOnce(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + req, ok := c.receivePreempt() + require.True(t, ok) + opts := req.cancelOptions(time.Now()) + cfg := parseAgentCancelOptions(opts...) + assert.Equal(t, CancelAfterChatModel, cfg.Mode) + requireAckOpen(t, ack) + + _, ok = c.receivePreempt() + assert.False(t, ok) + + req.ack() + requireAckClosed(t, ack) +} + +func TestPreemptController_AbortPlanningTurnAcksUnconsumedRequest(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + snapshot := c.beginPush() + c.endPush() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + req := c.abortPlanningTurn() + require.NotNil(t, req) + req.ack() + requireAckClosed(t, ack) + + _, ok := c.receivePreempt() + assert.False(t, ok) +} + +func TestPreemptController_EndActiveTurnAcksUnconsumedRequest(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + req := c.endActiveTurn() + require.NotNil(t, req) + req.ack() + requireAckClosed(t, ack) + + _, ok := c.receivePreempt() + assert.False(t, ok) +} + +func TestPreemptController_EndActiveTurnDoesNotAckConsumedRequest(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + req, ok := c.receivePreempt() + require.True(t, ok) + + assert.Nil(t, c.endActiveTurn()) + requireAckOpen(t, ack) + + req.ack() + requireAckClosed(t, ack) +} + +func TestPreemptController_TargetTurnMismatchAcksImmediately(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + snapshot := c.beginPush() + c.endPush() + c.abortPlanningTurn() + c.beginPlanningTurn() + + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + requireAckClosed(t, ack) + _, ok := c.receivePreempt() + assert.False(t, ok) +} + +func TestPreemptController_WaitForPushesBlocksUntilPushEnds(t *testing.T) { + c := newPreemptController() + c.beginPush() + + waitDone := make(chan struct{}) + go func() { + c.waitForPushes() + close(waitDone) + }() + + select { + case <-waitDone: + t.Fatal("waitForPushes should block while a push is in flight") + default: + } + + c.endPush() + waitOrFail(t, waitDone, "waitForPushes should unblock after endPush") +} + +func TestPreemptController_MultipleRequestsBeforeReceiveMergeAcksAndUseMergedOpts(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + ack1 := make(chan struct{}) + ack2 := make(chan struct{}) + c.requestPreempt(snapshot, ack1, WithAgentCancelMode(CancelAfterChatModel), WithAgentCancelTimeout(time.Minute)) + c.requestPreempt(snapshot, ack2, WithAgentCancelMode(CancelAfterToolCalls), WithRecursive(), WithAgentCancelTimeout(time.Second)) + + req, ok := c.receivePreempt() + require.True(t, ok) + opts := req.cancelOptions(time.Now()) + cfg := parseAgentCancelOptions(opts...) + assert.Equal(t, CancelAfterChatModel|CancelAfterToolCalls, cfg.Mode) + assert.True(t, cfg.Recursive) + require.NotNil(t, cfg.Timeout) + assert.LessOrEqual(t, *cfg.Timeout, time.Second) + + requireAckOpen(t, ack1) + requireAckOpen(t, ack2) + req.ack() + requireAckClosed(t, ack1) + requireAckClosed(t, ack2) +} + +func TestPreemptController_ConcurrentPreemptRequestsMergeAndAck(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + const requestCount = 50 + start := make(chan struct{}) + acks := make([]chan struct{}, requestCount) + var wg sync.WaitGroup + for i := 0; i < requestCount; i++ { + acks[i] = make(chan struct{}) + wg.Add(1) + go func(i int) { + defer wg.Done() + <-start + c.requestPreempt(snapshot, acks[i], WithAgentCancelMode(CancelAfterChatModel)) + }(i) + } + + close(start) + wg.Wait() + + req, ok := c.receivePreempt() + require.True(t, ok) + req.ack() + for _, ack := range acks { + requireAckClosed(t, ack) + } +} + +func TestPreemptController_CloseForLoopExitDuringDelayedPreempt(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + + // Capture a snapshot while the turn is active. + snapshot := c.beginPush() + c.endPush() + + // closeForLoopExit tears down controller state during TurnLoop cleanup. + c.closeForLoopExit() + + // A delayed goroutine fires requestPreempt AFTER closeForLoopExit. + // This must not panic or deadlock; ack should be closed immediately + // because the controller is now closed. + ack := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer close(done) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + }() + + waitOrFail(t, done, "requestPreempt after closeForLoopExit must not deadlock") + requireAckClosed(t, ack) +} + +func TestPreemptController_RequestPreemptAfterCloseForLoopExit(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + c.closeForLoopExit() + + // requestPreempt on a closed controller should close ack immediately. + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterToolCalls)) + requireAckClosed(t, ack) + + // receivePreempt should return nothing. + _, ok := c.receivePreempt() + assert.False(t, ok) +} + +func TestPreemptController_ConcurrentBeginPushAndWaitForPushes(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + + const pushCount = 100 + var wg sync.WaitGroup + + // Launch many goroutines doing beginPush / endPush concurrently. + for i := 0; i < pushCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = c.beginPush() + // Simulate some work. + time.Sleep(time.Microsecond) + c.endPush() + }() + } + + // Meanwhile, waitForPushes should eventually return once all are done. + waitDone := make(chan struct{}) + go func() { + c.waitForPushes() + close(waitDone) + }() + + wg.Wait() // all pushes complete + + waitOrFail(t, waitDone, "waitForPushes deadlocked with concurrent beginPush/endPush") +} + +func TestPreemptController_RequestPreemptWithNilAck(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + // requestPreempt with nil ack channel must not panic. + assert.NotPanics(t, func() { + c.requestPreempt(snapshot, nil, WithAgentCancelMode(CancelAfterChatModel)) + }) + + // The request should still be stored and consumable. + req, ok := c.receivePreempt() + require.True(t, ok) + // ack() with nil channels in the list must not panic. + assert.NotPanics(t, func() { + req.ack() + }) +} + +func TestPreemptController_MergeImmediateOverridesTimeout(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc") + snapshot := c.beginPush() + c.endPush() + + // First request: graceful with timeout. + ack1 := make(chan struct{}) + c.requestPreempt(snapshot, ack1, + WithAgentCancelMode(CancelAfterToolCalls), + WithAgentCancelTimeout(10*time.Second)) + + // Second request: CancelImmediate (no timeout). + ack2 := make(chan struct{}) + c.requestPreempt(snapshot, ack2, WithAgentCancelMode(CancelImmediate)) + + req, ok := c.receivePreempt() + require.True(t, ok) + + opts := req.cancelOptions(time.Now()) + cfg := parseAgentCancelOptions(opts...) + + // CancelImmediate should win and timeout should be nil. + assert.Equal(t, CancelImmediate, cfg.Mode) + assert.Nil(t, cfg.Timeout, "CancelImmediate merge should clear timeout") + + req.ack() + requireAckClosed(t, ack1) + requireAckClosed(t, ack2) +} + +func TestPreemptController_DelayedPreemptTargetGoneBetweenTurns(t *testing.T) { + c := newPreemptController() + + // Turn 1: planning → active → end + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc1") + oldSnapshot := c.beginPush() + c.endPush() + req := c.endActiveTurn() + assert.Nil(t, req) // no pending request + + // Turn 2: start a new turn + c.beginPlanningTurn() + c.beginActiveTurn(context.Background(), "tc2") + + // A delayed preempt from Turn 1 fires with stale snapshot. + // It should resolve as no-op (ack immediately) because turnID doesn't match. + ack := make(chan struct{}) + c.requestPreempt(oldSnapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + requireAckClosed(t, ack) + + // The new turn should have no pending preempt. + _, ok := c.receivePreempt() + assert.False(t, ok, "stale preempt must not affect new turn") + + c.endActiveTurn() +} + +func TestPreemptController_EndPushWithoutBeginPushPanics(t *testing.T) { + c := newPreemptController() + + // endPush without a matching beginPush should panic with the new invariant. + assert.PanicsWithValue(t, + "adk: preemptController.endPush called without matching beginPush", + func() { + c.endPush() + }, + ) +} + +func TestPreemptController_BeginActiveTurnNotifiesExistingPending(t *testing.T) { + c := newPreemptController() + c.beginPlanningTurn() + snapshot := c.beginPush() + c.endPush() + + // Send a preempt during planning phase. + ack := make(chan struct{}) + c.requestPreempt(snapshot, ack, WithAgentCancelMode(CancelAfterChatModel)) + + // During planning, receivePreempt returns nothing. + _, ok := c.receivePreempt() + assert.False(t, ok) + + // beginActiveTurn should notify the watcher via the notify channel. + c.beginActiveTurn(context.Background(), "tc") + + // The notify channel should have a message. + select { + case <-c.notify: + // Expected: watcher notification was sent. + case <-time.After(1 * time.Second): + t.Fatal("beginActiveTurn should notify watcher when there is a pending request") + } + + // Now receivePreempt should return the pending request. + req, ok := c.receivePreempt() + require.True(t, ok) + req.ack() + requireAckClosed(t, ack) +} + +// ============================================================================= +// Integration tests for race-prone preempt scenarios +// ============================================================================= + +func TestTurnLoop_ConcurrentPreemptsDuringTurn(t *testing.T) { + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + return &AgentOutput{}, nil + }, + } + + var genInputCount int32 + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + atomic.AddInt32(&genInputCount, 1) + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items, + }, nil + }, + }) + + loop.Push("seed") + + waitOrFail(t, agentStarted, "agent did not start") + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + ok, ack := loop.Push(fmt.Sprintf("urgent-%d", i), WithPreemptTimeout[string, *schema.Message](AnySafePoint, 10*time.Millisecond)) + if ok && ack != nil { + select { + case <-ack: + case <-time.After(5 * time.Second): + t.Error("ack channel not closed within timeout") + } + } + }(i) + } + + // Stop the loop concurrently. The run loop may be blocked on + // buffer.Receive after processing all preempts; Stop unblocks it + // and triggers closeForLoopExit which closes any orphaned ack channels. + go func() { + time.Sleep(500 * time.Millisecond) + loop.Stop(WithImmediate()) + }() + + wg.Wait() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.True(t, atomic.LoadInt32(&genInputCount) >= 2, "should have had at least the initial turn + one preempted turn") +} + +func TestTurnLoop_PreemptBetweenTurnsAcksImmediately(t *testing.T) { + var cancelCount int32 + var turnCount int32 + firstTurnDone := make(chan struct{}) + secondTurnDone := make(chan struct{}) + firstTurnOnce := sync.Once{} + secondTurnOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "fast", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + return &AgentOutput{}, nil + }, + onCancel: func(cc *cancelContext) { + atomic.AddInt32(&cancelCount, 1) + }, + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + atomic.AddInt32(&turnCount, 1) + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + switch atomic.LoadInt32(&turnCount) { + case 1: + firstTurnOnce.Do(func() { close(firstTurnDone) }) + case 2: + secondTurnOnce.Do(func() { close(secondTurnDone) }) + } + return nil + }, + }) + + loop.Push("first") + waitOrFail(t, firstTurnDone, "first turn did not complete") + requirePreemptPhase(t, loop.preemptCtrl, preemptTurnIdle) + + ok, ack := loop.Push("between-turns", WithPreempt[string, *schema.Message](AnySafePoint)) + require.True(t, ok) + require.NotNil(t, ack) + requireAckClosed(t, ack) + assert.Equal(t, int32(0), atomic.LoadInt32(&cancelCount), "between-turn preempt must not submit cancel") + + waitOrFail(t, secondTurnDone, "between-turn item was not processed") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.Equal(t, int32(2), atomic.LoadInt32(&turnCount)) +} + +func TestTurnLoop_PushStrategy_DuringTurnTransition(t *testing.T) { + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + allowFinish := make(chan struct{}) + strategyEntered := make(chan struct{}) + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + select { + case <-allowFinish: + return &AgentOutput{}, nil + case <-ctx.Done(): + return &AgentOutput{}, nil + } + }, + } + + var genInputCount int32 + secondTurnDone := make(chan struct{}) + secondTurnOnce := sync.Once{} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + count := atomic.AddInt32(&genInputCount, 1) + if count >= 2 { + secondTurnOnce.Do(func() { + close(secondTurnDone) + }) + } + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items, + }, nil + }, + }) + + loop.Push("first") + + waitOrFail(t, agentStarted, "agent did not start") + + strategyBlocker := make(chan struct{}) + var strategyTCNotNil int32 + + go func() { + loop.Push("strategic-item", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + if tc != nil { + atomic.StoreInt32(&strategyTCNotNil, 1) + } + close(strategyEntered) + <-strategyBlocker + return []PushOption[string, *schema.Message]{WithPreempt[string, *schema.Message](AnySafePoint)} + })) + }() + + waitOrFail(t, strategyEntered, "strategy did not enter") + + close(allowFinish) + + select { + case <-secondTurnDone: + t.Fatal("second turn should not be planned before strategy Push finishes") + default: + } + + close(strategyBlocker) + + waitOrFail(t, secondTurnDone, "second turn should eventually run after strategy resolves") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.True(t, atomic.LoadInt32(&genInputCount) >= 2) + assert.Equal(t, int32(1), atomic.LoadInt32(&strategyTCNotNil)) +} + +func TestTurnLoop_ConcurrentPreemptAndStop(t *testing.T) { + for iter := 0; iter < 20; iter++ { + t.Run(fmt.Sprintf("iter_%d", iter), func(t *testing.T) { + ctx := context.Background() + + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + return &AgentOutput{}, nil + }, + } + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return agent, nil + }, + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items, + }, nil + }, + }) + + loop.Push("seed") + + select { + case <-agentStarted: + case <-time.After(1 * time.Second): + t.Fatal("agent did not start") + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + _, ack := loop.Push("preempt-item", WithPreempt[string, *schema.Message](AnySafePoint)) + if ack != nil { + <-ack + } + }() + + go func() { + defer wg.Done() + loop.Stop(WithImmediate()) + }() + + wg.Wait() + loop.Wait() + }) + } +} + +func TestTurnLoop_ConcurrentPushStrategyAndStop(t *testing.T) { + for iter := 0; iter < 20; iter++ { + t.Run(fmt.Sprintf("iter_%d", iter), func(t *testing.T) { + ctx := context.Background() + + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + return &AgentOutput{}, nil + }, + } + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return agent, nil + }, + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{}, + Consumed: items, + }, nil + }, + }) + + loop.Push("seed") + + select { + case <-agentStarted: + case <-time.After(1 * time.Second): + t.Fatal("agent did not start") + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + _, ack := loop.Push("strategic-item", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + return []PushOption[string, *schema.Message]{WithPreempt[string, *schema.Message](AnySafePoint)} + })) + if ack != nil { + <-ack + } + }() + + go func() { + defer wg.Done() + loop.Stop(WithImmediate()) + }() + + wg.Wait() + loop.Wait() + }) + } +} + +func TestTurnLoop_TurnContext_StoppedChannel(t *testing.T) { + stoppedSeen := make(chan struct{}) + agentStarted := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + close(agentStarted) + select { + case <-tc.Stopped: + close(stoppedSeen) + case <-time.After(5 * time.Second): + t.Error("timed out waiting for Stopped channel") + } + // Drain events + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Stop(WithImmediate()) + + select { + case <-stoppedSeen: + // success + case <-time.After(5 * time.Second): + t.Fatal("stopped channel was never observed in OnAgentEvents") + } + + loop.Wait() +} + +func TestTurnLoop_TurnContext_BothPreemptedAndStopped(t *testing.T) { + t.Run("PreemptThenStop_OnlyPreemptContributes", func(t *testing.T) { + preemptedSeen := make(chan struct{}) + agentStarted := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*TypedAgentEvent[*schema.Message]]) error { + close(agentStarted) + select { + case <-tc.Preempted: + close(preemptedSeen) + case <-time.After(5 * time.Second): + t.Error("timed out waiting for Preempted") + } + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Push("msg2", WithPreemptTimeout[string, *schema.Message](AnySafePoint, time.Millisecond)) + + select { + case <-preemptedSeen: + case <-time.After(5 * time.Second): + t.Fatal("Preempted channel was never closed") + } + + loop.Stop(WithImmediate()) + loop.Wait() + }) + + t.Run("StopThenPreempt_OnlyStopContributes", func(t *testing.T) { + stoppedSeen := make(chan struct{}) + agentStarted := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*TypedAgentEvent[*schema.Message]]) error { + close(agentStarted) + select { + case <-tc.Stopped: + close(stoppedSeen) + case <-time.After(5 * time.Second): + t.Error("timed out waiting for Stopped") + } + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Stop(WithImmediate()) + + select { + case <-stoppedSeen: + case <-time.After(5 * time.Second): + t.Fatal("Stopped channel was never closed") + } + + loop.Push("msg2", WithPreemptTimeout[string, *schema.Message](AnySafePoint, time.Millisecond)) + loop.Wait() + }) +} + +func TestTurnLoop_PushStrategy_DuringTurn(t *testing.T) { + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + agentCancelled := make(chan struct{}) + agentCancelledOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + agentCancelledOnce.Do(func() { + close(agentCancelled) + }) + return &AgentOutput{}, nil + }, + } + + genInputCalls := int32(0) + secondGenInputCalled := make(chan struct{}) + secondGenInputOnce := sync.Once{} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + count := atomic.AddInt32(&genInputCalls, 1) + if count >= 2 { + secondGenInputOnce.Do(func() { + close(secondGenInputCalled) + }) + } + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: []string{items[0]}, + Remaining: items[1:], + }, nil + }, + }) + + loop.Push("first") + + waitOrFail(t, agentStarted, "agent did not start") + + // Strategy inspects TurnContext during a running turn and decides to preempt. + var strategyCalled int32 + var strategyTC *TurnContext[string, *schema.Message] + loop.Push("urgent", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + atomic.AddInt32(&strategyCalled, 1) + strategyTC = tc + return []PushOption[string, *schema.Message]{WithPreempt[string, *schema.Message](AnySafePoint)} + })) + + waitOrFail(t, agentCancelled, "agent was not cancelled by strategy-returned preempt") + + waitOrFail(t, secondGenInputCalled, "second GenInput was not called after preempt") + + loop.Stop(WithImmediate()) + loop.Wait() + + assert.Equal(t, int32(1), atomic.LoadInt32(&strategyCalled)) + assert.NotNil(t, strategyTC, "strategy should receive non-nil TurnContext during a turn") + assert.Equal(t, []string{"first"}, strategyTC.Consumed) +} + +func TestTurnLoop_PushStrategy_BetweenTurns(t *testing.T) { + // Push with strategy before Run() — TurnContext should be nil. + var strategyCalled int32 + var strategyTCWasNil bool + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + return &AgentOutput{}, nil + }, + } + + agentDone := make(chan struct{}) + agentDoneOnce := sync.Once{} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + Remaining: nil, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + agentDoneOnce.Do(func() { + close(agentDone) + }) + return nil + }, + }) + + // Push with strategy — no turn is active yet, so tc should be nil. + loop.Push("item", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + atomic.AddInt32(&strategyCalled, 1) + strategyTCWasNil = tc == nil + return nil // plain push, no preempt + })) + + waitOrFail(t, agentDone, "agent did not complete") + + loop.Stop() + loop.Wait() + + assert.Equal(t, int32(1), atomic.LoadInt32(&strategyCalled)) + assert.True(t, strategyTCWasNil, "strategy should receive nil TurnContext between turns") +} + +func TestTurnLoop_PushStrategy_OverridesOtherOptions(t *testing.T) { + // Push with both WithPreempt and WithPushStrategy — only strategy's result applies. + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + return &AgentOutput{}, nil + }, + } + + agentDone := make(chan struct{}) + agentDoneOnce := sync.Once{} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + Remaining: nil, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + agentDoneOnce.Do(func() { + close(agentDone) + }) + return nil + }, + }) + + // Strategy returns nil (no preempt), even though WithPreempt is also passed. + // The strategy should override — so the agent should NOT be preempted. + ok, ack := loop.Push("item", WithPreempt[string, *schema.Message](AnySafePoint), WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + return nil // no preempt + })) + assert.True(t, ok) + assert.Nil(t, ack, "ack should be nil since strategy returned no preempt") + + waitOrFail(t, agentDone, "agent did not complete normally") + + loop.Stop() + loop.Wait() +} + +func TestTurnLoop_PushStrategy_NestedStrategyStripped(t *testing.T) { + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + return &AgentOutput{}, nil + }, + } + + agentDone := make(chan struct{}) + agentDoneOnce := sync.Once{} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + Remaining: nil, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + _, ok := events.Next() + if !ok { + break + } + } + agentDoneOnce.Do(func() { + close(agentDone) + }) + return nil + }, + }) + + // Strategy returns another WithPushStrategy — the nested one should be stripped. + innerCalled := int32(0) + ok, ack := loop.Push("item", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + return []PushOption[string, *schema.Message]{ + WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + atomic.AddInt32(&innerCalled, 1) + return []PushOption[string, *schema.Message]{WithPreempt[string, *schema.Message](AnySafePoint)} + }), + } + })) + assert.True(t, ok) + assert.Nil(t, ack, "ack should be nil since nested strategy was stripped (no preempt)") + + waitOrFail(t, agentDone, "agent did not complete normally") + + loop.Stop() + loop.Wait() + + assert.Equal(t, int32(0), atomic.LoadInt32(&innerCalled), "nested strategy should not be called") +} + +func TestTurnLoop_PushStrategy_ConsumedInspection(t *testing.T) { + // Strategy preempts only when current turn is processing "low-priority" items. + agentStarted := make(chan struct{}) + agentStartedOnce := sync.Once{} + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + agentStartedOnce.Do(func() { + close(agentStarted) + }) + <-ctx.Done() + return &AgentOutput{}, nil + }, + } + + genInputCalls := int32(0) + secondGenInputItems := make(chan []string, 1) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + count := atomic.AddInt32(&genInputCalls, 1) + if count >= 2 { + select { + case secondGenInputItems <- append([]string{}, items...): + default: + } + } + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: []string{items[0]}, + Remaining: items[1:], + }, nil + }, + }) + + loop.Push("low-priority-task") + + waitOrFail(t, agentStarted, "agent did not start") + + // Strategy checks Consumed and preempts because current turn has "low-priority" items. + loop.Push("urgent-task", WithPushStrategy(func(ctx context.Context, tc *TurnContext[string, *schema.Message]) []PushOption[string, *schema.Message] { + if tc != nil && len(tc.Consumed) > 0 && tc.Consumed[0] == "low-priority-task" { + return []PushOption[string, *schema.Message]{WithPreempt[string, *schema.Message](AnySafePoint)} + } + return nil + })) + + select { + case items := <-secondGenInputItems: + assert.Contains(t, items, "urgent-task") + case <-time.After(2 * time.Second): + t.Fatal("second GenInput was not called after strategy-driven preempt") + } + + loop.Stop(WithImmediate()) + loop.Wait() +} + +func TestTurnLoop_PushAfterStop_BufferedAsLateItems(t *testing.T) { + ctx := context.Background() + processed := make(chan string, 10) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + processed <- tc.Consumed[0] + return nil + }, + }) + + loop.Push("msg1") + <-processed + loop.Stop() + result := loop.Wait() + + // Push after stop — should be buffered as late items + ok1, _ := loop.Push("late1") + ok2, _ := loop.Push("late2") + ok3, _ := loop.Push("late3") + assert.False(t, ok1) + assert.False(t, ok2) + assert.False(t, ok3) + + late := result.TakeLateItems() + assert.Equal(t, []string{"late1", "late2", "late3"}, late) +} + +func TestTurnLoop_TakeLateItems_Idempotent(t *testing.T) { + ctx := context.Background() + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Stop() + loop.Run(ctx) + result := loop.Wait() + + loop.Push("late1") + + first := result.TakeLateItems() + second := result.TakeLateItems() + third := result.TakeLateItems() + + assert.Equal(t, []string{"late1"}, first) + assert.Equal(t, first, second, "subsequent calls should return the same slice") + assert.Equal(t, first, third, "subsequent calls should return the same slice") +} + +func TestTurnLoop_PushAfterTakeLateItems_Panics(t *testing.T) { + ctx := context.Background() + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Stop() + loop.Run(ctx) + result := loop.Wait() + + result.TakeLateItems() + + assert.PanicsWithValue(t, "TurnLoop: Push called after TakeLateItems", func() { + loop.Push("too-late") + }) +} + +func TestTurnLoop_TakeLateItems_NeverCalled_NoImpact(t *testing.T) { + ctx := context.Background() + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Push("b") + loop.Stop() + loop.Run(ctx) + result := loop.Wait() + + // Don't call TakeLateItems — verify UnhandledItems works normally + assert.Contains(t, result.UnhandledItems, "b") + assert.Nil(t, result.ExitReason) +} + +func TestTurnLoop_CheckpointErr_SeparateFromExitReason(t *testing.T) { + ctx := context.Background() + saveStore := &errorCheckpointStore{setErr: fmt.Errorf("storage unavailable")} + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: saveStore, + CheckpointID: "cp-separate-err", + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Stop() + loop.Run(ctx) + result := loop.Wait() + + // ExitReason should be nil (clean stop), checkpoint error should be separate + assert.Nil(t, result.ExitReason) + assert.True(t, result.CheckpointAttempted) + assert.Error(t, result.CheckpointErr) + assert.Contains(t, result.CheckpointErr.Error(), "storage unavailable") +} + +func TestTurnLoop_CheckpointAttempted_FalseWhenNoStore(t *testing.T) { + ctx := context.Background() + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop.Push("a") + loop.Stop() + loop.Run(ctx) + result := loop.Wait() + + assert.False(t, result.CheckpointAttempted) + assert.Nil(t, result.CheckpointErr) +} + +func TestTurnLoop_CheckpointAttempted_FalseOnErrorExit(t *testing.T) { + ctx := context.Background() + store := newTestStore() + genInputErr := errors.New("gen input failed") + + firstTurnDone := make(chan struct{}) + var callCount int32 + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: "cp-err-exit", + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + n := atomic.AddInt32(&callCount, 1) + if n > 1 { + return nil, genInputErr + } + return &GenInputResult[string, *schema.Message]{Input: &AgentInput{}, Consumed: items}, nil + }, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + close(firstTurnDone) + return nil + }, + }) + loop.Push("msg1") + <-firstTurnDone + loop.Push("msg2") + result := loop.Wait() + + // Loop exited from error, not Stop() — checkpoint should not be saved + assert.ErrorIs(t, result.ExitReason, genInputErr) + assert.False(t, result.CheckpointAttempted) + assert.Nil(t, result.CheckpointErr) +} + +func TestTurnLoop_StopConcurrentWithCallbackError_NoCheckpoint(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "stop-concurrent-err" + + prepareErr := errors.New("prepare agent failed") + firstTurnDone := make(chan struct{}) + stopCalled := make(chan struct{}) + var prepareCount int32 + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + n := atomic.AddInt32(&prepareCount, 1) + if n > 1 { + // Wait until Stop() has been called so stopCtrl.isCommitted() is true. + <-stopCalled + return nil, prepareErr + } + return &turnLoopMockAgent{name: "test"}, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + close(firstTurnDone) + return nil + }, + }) + + loop.Push("msg1") + <-firstTurnDone + loop.Push("msg2") + + // Call Stop() and signal PrepareAgent to proceed with error + go func() { + loop.Stop() + close(stopCalled) + }() + + result := loop.Wait() + + // The loop may exit via Stop (clean) or via PrepareAgent error. + // If it exited via PrepareAgent error with Stop also called: + // checkpoint should NOT be saved. + if result.ExitReason != nil && !errors.As(result.ExitReason, new(*CancelError)) { + assert.ErrorIs(t, result.ExitReason, prepareErr) + assert.False(t, result.CheckpointAttempted, "should not checkpoint when exit is caused by callback error") + } + // If Stop won the race, that's fine — checkpoint may or may not be saved + // depending on idle state. The test is about the error path. +} + +func TestTurnLoop_DeleteWithoutCheckPointDeleter_NoOp(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "no-deleter" + + // First loop: save a checkpoint + loop1 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop1.Push("a") + loop1.Stop() + loop1.Run(ctx) + loop1.Wait() + + store.mu.Lock() + _, exists := store.m[cpID] + store.mu.Unlock() + assert.True(t, exists, "checkpoint should be saved") + + // Second loop: exit via context cancel — should try to delete but store + // doesn't implement CheckPointDeleter, so checkpoint persists (no-op) + ctx2, cancel2 := context.WithCancel(ctx) + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + cancel2() + return nil + }, + }) + loop2.Push("b") + loop2.Run(ctx2) + loop2.Wait() + + // Without CheckPointDeleter, the stale checkpoint should NOT be deleted + store.mu.Lock() + v, exists := store.m[cpID] + store.mu.Unlock() + assert.True(t, exists, "checkpoint should still exist without CheckPointDeleter") + assert.NotNil(t, v, "checkpoint should not be set to nil") +} + +func TestTurnLoop_StopWithSkipCheckpoint(t *testing.T) { + ctx := context.Background() + store := newTestStore() + cpID := "skip-cp-session" + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("a") + loop.Push("b") + loop.Stop(WithSkipCheckpoint()) + loop.Run(ctx) + + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + assert.False(t, exit.CheckpointAttempted, "checkpoint should be skipped when WithSkipCheckpoint is used") + + store.mu.Lock() + _, exists := store.m[cpID] + store.mu.Unlock() + assert.False(t, exists, "no checkpoint should be saved when WithSkipCheckpoint is used") +} + +func TestTurnLoop_StopWithSkipCheckpoint_DeletesStaleCheckpoint(t *testing.T) { + ctx := context.Background() + store := &deletableCheckpointStore{ + turnLoopCheckpointStore: turnLoopCheckpointStore{m: make(map[string][]byte)}, + } + cpID := "skip-stale-session" + + loop1 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop1.Push("a") + loop1.Stop() + loop1.Run(ctx) + exit1 := loop1.Wait() + assert.True(t, exit1.CheckpointAttempted) + + store.mu.Lock() + _, exists := store.m[cpID] + store.mu.Unlock() + assert.True(t, exists, "first loop should save checkpoint") + + loop2 := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + loop2.Push("b") + loop2.Stop(WithSkipCheckpoint()) + loop2.Run(ctx) + exit2 := loop2.Wait() + assert.False(t, exit2.CheckpointAttempted, "second loop should skip checkpoint") + + store.mu.Lock() + deleteCalled := store.deleteCalled + store.mu.Unlock() + assert.True(t, deleteCalled, "stale checkpoint should be deleted when SkipCheckpoint is used") +} + +func TestTurnLoop_StopWithStopCause(t *testing.T) { + ctx := context.Background() + cause := "user session timeout" + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Push("a") + loop.Stop(WithStopCause(cause)) + + exit := loop.Wait() + assert.Equal(t, cause, exit.StopCause) +} + +func TestTurnLoop_StopCause_EmptyWhenNoStop(t *testing.T) { + ctx := context.Background() + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAll, + PrepareAgent: prepareTestAgent, + }) + + loop.Stop() + exit := loop.Wait() + assert.Empty(t, exit.StopCause) +} + +func TestTurnLoop_StopCause_InTurnContext(t *testing.T) { + cause := "business shutdown" + gotCause := make(chan string, 1) + agentStarted := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + close(agentStarted) + select { + case <-tc.Stopped: + gotCause <- tc.StopCause() + case <-time.After(5 * time.Second): + t.Error("timed out waiting for Stopped channel") + } + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Stop(WithImmediate(), WithStopCause(cause)) + + select { + case c := <-gotCause: + assert.Equal(t, cause, c) + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for StopCause in TurnContext") + } + + exit := loop.Wait() + assert.Equal(t, cause, exit.StopCause) +} + +func TestTurnLoop_StopCause_FirstNonEmptyWins(t *testing.T) { + agentStarted := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + close(agentStarted) + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Stop(WithGraceful(), WithStopCause("first cause")) + loop.Stop(WithStopCause("second cause")) + + exit := loop.Wait() + assert.Equal(t, "first cause", exit.StopCause, "first non-empty StopCause should win") +} + +func TestTurnLoop_StopBeforeRun_PushThenStop(t *testing.T) { + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + t.Fatal("GenInput should not be called when Stop is called before Run") + return nil, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + t.Fatal("PrepareAgent should not be called when Stop is called before Run") + return nil, nil + }, + }) + + ok, _ := loop.Push("item1") + assert.True(t, ok) + ok, _ = loop.Push("item2") + assert.True(t, ok) + + loop.Stop() + loop.Run(context.Background()) + result := loop.Wait() + + assert.NoError(t, result.ExitReason) + assert.Equal(t, []string{"item1", "item2"}, result.UnhandledItems) + assert.Empty(t, result.InterruptedItems) + assert.Empty(t, result.TakeLateItems()) +} + +func TestTurnLoop_StopBeforeRun_StopThenPush(t *testing.T) { + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + t.Fatal("GenInput should not be called when Stop is called before Run") + return nil, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + t.Fatal("PrepareAgent should not be called when Stop is called before Run") + return nil, nil + }, + }) + + loop.Stop() + + ok, _ := loop.Push("item1") + assert.False(t, ok) + ok, _ = loop.Push("item2") + assert.False(t, ok) + + loop.Run(context.Background()) + result := loop.Wait() + + assert.NoError(t, result.ExitReason) + assert.Empty(t, result.UnhandledItems) + assert.Empty(t, result.InterruptedItems) + assert.Equal(t, []string{"item1", "item2"}, result.TakeLateItems()) +} + +func TestTurnLoop_SkipCheckpoint_Sticky(t *testing.T) { + agentStarted := make(chan struct{}) + + store := newTestStore() + cpID := "sticky-skip-session" + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: cpID, + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "slow", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + close(agentStarted) + for { + if _, ok := events.Next(); !ok { + break + } + } + return nil + }, + }) + + loop.Push("msg1") + <-agentStarted + loop.Stop(WithGraceful(), WithSkipCheckpoint()) + loop.Stop() + + exit := loop.Wait() + assert.False(t, exit.CheckpointAttempted, "SkipCheckpoint should be sticky across multiple Stop calls") + + store.mu.Lock() + _, exists := store.m[cpID] + store.mu.Unlock() + assert.False(t, exists, "no checkpoint should be saved when SkipCheckpoint was set in any Stop call") +} + +func TestWithGracefulTimeout_NonPositive_Panics(t *testing.T) { + assert.PanicsWithValue(t, "adk: WithGracefulTimeout: gracePeriod must be positive", + func() { WithGracefulTimeout(0) }) + assert.PanicsWithValue(t, "adk: WithGracefulTimeout: gracePeriod must be positive", + func() { WithGracefulTimeout(-1 * time.Second) }) +} + +func TestWithPreempt_ZeroSafePoint_Panics(t *testing.T) { + assert.PanicsWithValue(t, "adk: SafePoint must not be zero; use AfterToolCalls, AfterChatModel, or AnySafePoint", + func() { WithPreempt[string, *schema.Message](SafePoint(0)) }) +} + +func TestWithPreemptTimeout_ZeroSafePoint_Panics(t *testing.T) { + assert.PanicsWithValue(t, "adk: SafePoint must not be zero; use AfterToolCalls, AfterChatModel, or AnySafePoint", + func() { WithPreemptTimeout[string, *schema.Message](SafePoint(0), time.Second) }) +} + +func TestSafePoint_ToCancelMode(t *testing.T) { + assert.Equal(t, CancelAfterToolCalls, AfterToolCalls.toCancelMode()) + assert.Equal(t, CancelAfterChatModel, AfterChatModel.toCancelMode()) + assert.Equal(t, CancelAfterToolCalls|CancelAfterChatModel, AnySafePoint.toCancelMode()) +} + +func TestNewTurnLoop_NilGenInput_Panics(t *testing.T) { + assert.PanicsWithValue(t, "adk: NewTurnLoop: GenInput is required", func() { + NewTurnLoop(TurnLoopConfig[string, *schema.Message]{PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return nil, nil + }}) + }) +} + +func TestNewTurnLoop_NilPrepareAgent_Panics(t *testing.T) { + assert.PanicsWithValue(t, "adk: NewTurnLoop: PrepareAgent is required", func() { + NewTurnLoop(TurnLoopConfig[string, *schema.Message]{GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return nil, nil + }}) + }) +} + +func TestDeriveAgentToolCancelContext_NilParent_ReturnsNil(t *testing.T) { + var cc *cancelContext + assert.Nil(t, cc.deriveAgentToolCancelContext(context.Background())) +} + +func TestUntilIdleFor(t *testing.T) { + t.Run("FiresAfterIdleDuration", func(t *testing.T) { + turnDone := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(turnDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-turnDone + + loop.Stop(UntilIdleFor(50 * time.Millisecond)) + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("loop did not exit after idle timeout") + } + }) + + t.Run("ResetsOnPush", func(t *testing.T) { + turnCount := int32(0) + turnDone := make(chan struct{}, 10) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + atomic.AddInt32(&turnCount, 1) + turnDone <- struct{}{} + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-turnDone + + loop.Stop(UntilIdleFor(200 * time.Millisecond)) + + time.Sleep(100 * time.Millisecond) + loop.Push("msg2") + <-turnDone + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("loop did not exit after idle timeout") + } + + assert.Equal(t, int32(2), atomic.LoadInt32(&turnCount)) + }) + + t.Run("EscalatedByStopWithImmediate", func(t *testing.T) { + agentStarted := make(chan *cancelContext, 1) + probe := &turnLoopStopModeProbeAgent{ccCh: agentStarted} + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return probe, nil + }, + }) + + loop.Push("msg1") + cc := <-agentStarted + + loop.Stop(UntilIdleFor(10 * time.Minute)) + loop.Stop(WithImmediate()) + + deadline := time.After(2 * time.Second) + for { + if cc.getMode() == CancelImmediate { + break + } + select { + case <-deadline: + t.Fatal("cancel mode did not escalate to CancelImmediate") + default: + } + time.Sleep(1 * time.Millisecond) + } + + exit := loop.Wait() + var ce *CancelError + require.True(t, errors.As(exit.ExitReason, &ce)) + assert.Equal(t, CancelImmediate, ce.Info.Mode) + }) + + t.Run("EscalatedByStopWithGraceful", func(t *testing.T) { + agentStarted := make(chan struct{}) + agentDone := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + <-ctx.Done() + close(agentDone) + return nil, ctx.Err() + }, + }, nil + }, + }) + + loop.Push("msg1") + <-agentStarted + + loop.Stop(UntilIdleFor(10 * time.Minute)) + loop.Stop(WithGracefulTimeout(50 * time.Millisecond)) + + select { + case <-agentDone: + case <-time.After(2 * time.Second): + t.Fatal("agent was not cancelled") + } + + exit := loop.Wait() + assert.Error(t, exit.ExitReason) + }) +} + +// TestUntilIdleFor_DoesNotCancelRunningAgent verifies that Stop(UntilIdleFor) +// records an idle stop policy but does NOT create a pending cancel request for +// the running agent. +func TestUntilIdleFor_DoesNotCancelRunningAgent(t *testing.T) { + t.Run("BeforeRun", func(t *testing.T) { + agentStarted := make(chan struct{}) + agentCtxCanceled := int32(0) + agentDone := make(chan struct{}) + + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + // Block until context is canceled or a short timeout. + select { + case <-ctx.Done(): + atomic.StoreInt32(&agentCtxCanceled, 1) + case <-time.After(200 * time.Millisecond): + } + close(agentDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + // Call Stop(UntilIdleFor) BEFORE Run. + loop.Stop(UntilIdleFor(50 * time.Millisecond)) + loop.Run(context.Background()) + + <-agentStarted + <-agentDone + + exit := loop.Wait() + assert.Nil(t, exit.ExitReason, "UntilIdleFor should not produce a CancelError") + assert.Equal(t, int32(0), atomic.LoadInt32(&agentCtxCanceled), + "agent context should not have been canceled by UntilIdleFor") + }) + + t.Run("DuringRun", func(t *testing.T) { + agentStarted := make(chan struct{}) + agentCtxCanceled := int32(0) + agentDone := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + select { + case <-ctx.Done(): + atomic.StoreInt32(&agentCtxCanceled, 1) + case <-time.After(200 * time.Millisecond): + } + close(agentDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-agentStarted + + // Call Stop(UntilIdleFor) while the agent is running. + loop.Stop(UntilIdleFor(50 * time.Millisecond)) + <-agentDone + + exit := loop.Wait() + assert.Nil(t, exit.ExitReason, "UntilIdleFor should not produce a CancelError") + assert.Equal(t, int32(0), atomic.LoadInt32(&agentCtxCanceled), + "agent context should not have been canceled by UntilIdleFor") + }) + + // Cancel opts paired with UntilIdleFor in the same call are silently + // dropped. The agent must run to completion even when WithImmediate is + // combined with UntilIdleFor. + t.Run("CancelOptsDroppedInSameCall", func(t *testing.T) { + agentStarted := make(chan struct{}) + agentCtxCanceled := int32(0) + agentDone := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage(items[0])}}, + Consumed: items, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + select { + case <-ctx.Done(): + atomic.StoreInt32(&agentCtxCanceled, 1) + case <-time.After(200 * time.Millisecond): + } + close(agentDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-agentStarted + + // WithImmediate in the same call as UntilIdleFor must be ignored. + loop.Stop(UntilIdleFor(50*time.Millisecond), WithImmediate()) + <-agentDone + + exit := loop.Wait() + assert.Nil(t, exit.ExitReason, "cancel opts should be dropped when combined with UntilIdleFor") + assert.Equal(t, int32(0), atomic.LoadInt32(&agentCtxCanceled), + "agent context should not have been canceled") + }) +} + +func TestUntilIdleFor_ContextCancelDuringIdleWait(t *testing.T) { + turnDone := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(turnDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-turnDone + + // Start idle timer, then cancel the parent context while idle. + loop.Stop(UntilIdleFor(10 * time.Minute)) + time.Sleep(20 * time.Millisecond) + cancel() + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + waitOrFail(t, done, "loop should exit when context is canceled during idle wait") + + exit := loop.Wait() + assert.ErrorIs(t, exit.ExitReason, context.Canceled) +} + +func TestCancelRequestState_ImmediateDominatesSafePointModes(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(time.Minute), + }, now) + + state.merge([]AgentCancelOption{WithAgentCancelMode(CancelImmediate)}, now) + + cfg := parseAgentCancelOptions(state.cancelOptions(now)...) + assert.Equal(t, CancelImmediate, cfg.Mode) + assert.Nil(t, cfg.Timeout) +} + +func TestCancelRequestState_NilMergeDoesNotCreateCancelIntent(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{WithAgentCancelMode(CancelAfterChatModel)}, now) + + state.merge(nil, now) + + cfg := parseAgentCancelOptions(state.cancelOptions(now)...) + assert.Equal(t, CancelAfterChatModel, cfg.Mode) +} + +func TestCancelRequestState_EmptyMergeMeansExplicitImmediate(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{WithAgentCancelMode(CancelAfterChatModel)}, now) + + state.merge([]AgentCancelOption{}, now) + + cfg := parseAgentCancelOptions(state.cancelOptions(now)...) + assert.Equal(t, CancelImmediate, cfg.Mode) +} + +func TestCancelRequestState_SafePointModesJoin(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{WithAgentCancelMode(CancelAfterChatModel)}, now) + + state.merge([]AgentCancelOption{WithAgentCancelMode(CancelAfterToolCalls)}, now) + + cfg := parseAgentCancelOptions(state.cancelOptions(now)...) + assert.Equal(t, CancelAfterChatModel|CancelAfterToolCalls, cfg.Mode) +} + +func TestCancelRequestState_RecursiveIsMonotonic(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{WithAgentCancelMode(CancelAfterChatModel)}, now) + + state.merge([]AgentCancelOption{WithAgentCancelMode(CancelAfterToolCalls), WithRecursive()}, now) + state.merge([]AgentCancelOption{WithAgentCancelMode(CancelAfterChatModel)}, now) + + cfg := parseAgentCancelOptions(state.cancelOptions(now)...) + assert.True(t, cfg.Recursive) +} + +func TestCancelRequestState_TimeoutUsesEarliestDeadline(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(10 * time.Second), + }, now) + + state.merge([]AgentCancelOption{ + WithAgentCancelMode(CancelAfterToolCalls), + WithAgentCancelTimeout(time.Second), + }, now.Add(100*time.Millisecond)) + + cfg := parseAgentCancelOptions(state.cancelOptions(now.Add(100 * time.Millisecond))...) + require.NotNil(t, cfg.Timeout) + assert.LessOrEqual(t, *cfg.Timeout, time.Second) +} + +func TestCancelRequestState_ExpiredTimeoutConvertsToImmediate(t *testing.T) { + now := time.Now() + state := newCancelRequestState([]AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(time.Nanosecond), + }, now) + + cfg := parseAgentCancelOptions(state.cancelOptions(now.Add(time.Second))...) + assert.Equal(t, CancelImmediate, cfg.Mode) + assert.Nil(t, cfg.Timeout) +} + +func TestStopController_BareStopCommitsWithoutCancelRequest(t *testing.T) { + c := newStopController() + + decision := c.requestStop(&stopConfig{}) + + assert.True(t, decision.commit) + assert.True(t, c.isCommitted()) + c.beginActiveTurn() + _, ok := c.receiveCancel() + assert.False(t, ok) +} + +func TestStopController_UntilIdleForDoesNotCreateCancelRequest(t *testing.T) { + c := newStopController() + + decision := c.requestStop(&stopConfig{idleFor: time.Second}) + + assert.False(t, decision.commit) + assert.True(t, decision.wakeIdle) + assert.Equal(t, time.Second, c.idleDuration()) + assert.False(t, c.isCommitted()) + c.beginActiveTurn() + _, ok := c.receiveCancel() + assert.False(t, ok) +} + +func TestStopController_CancelOptsDroppedWhenCombinedWithUntilIdleFor(t *testing.T) { + c := newStopController() + + decision := c.requestStop(&stopConfig{ + idleFor: time.Second, + agentCancelOpts: []AgentCancelOption{WithRecursive()}, + }) + + assert.False(t, decision.commit) + c.beginActiveTurn() + _, ok := c.receiveCancel() + assert.False(t, ok) +} + +func TestStopController_ImmediateStopCreatesPendingCancelForActiveTurn(t *testing.T) { + c := newStopController() + c.beginActiveTurn() + + decision := c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithRecursive()}}) + + assert.True(t, decision.commit) + req, ok := c.receiveCancel() + require.True(t, ok) + cfg := parseAgentCancelOptions(req.cancelOptions(time.Now())...) + assert.Equal(t, CancelImmediate, cfg.Mode) + assert.True(t, cfg.Recursive) +} + +func TestStopController_StopBeforeWatcherStartsConsumedAfterBeginActiveTurn(t *testing.T) { + c := newStopController() + + decision := c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithRecursive()}}) + assert.True(t, decision.commit) + + c.beginActiveTurn() + req, ok := c.receiveCancel() + require.True(t, ok) + cfg := parseAgentCancelOptions(req.cancelOptions(time.Now())...) + assert.Equal(t, CancelImmediate, cfg.Mode) +} + +func TestStopController_EndActiveTurnDropsUnconsumedCancel(t *testing.T) { + c := newStopController() + c.beginActiveTurn() + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithRecursive()}}) + + req := c.endActiveTurn() + + require.NotNil(t, req) + _, ok := c.receiveCancel() + assert.False(t, ok) +} + +func TestStopController_RepeatedStopsMergeWithoutDeescalation(t *testing.T) { + c := newStopController() + c.beginActiveTurn() + + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithRecursive()}}) + c.requestStop(&stopConfig{}) + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel | CancelAfterToolCalls), + WithRecursive(), + }}) + + req, ok := c.receiveCancel() + require.True(t, ok) + cfg := parseAgentCancelOptions(req.cancelOptions(time.Now())...) + assert.Equal(t, CancelImmediate, cfg.Mode) + assert.True(t, cfg.Recursive) +} + +func TestStopController_RepeatedStopsUseSharedCancelMergeState(t *testing.T) { + c := newStopController() + c.beginActiveTurn() + + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithAgentCancelMode(CancelAfterChatModel)}}) + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithAgentCancelMode(CancelAfterToolCalls)}}) + + req, ok := c.receiveCancel() + require.True(t, ok) + cfg := parseAgentCancelOptions(req.cancelOptions(time.Now())...) + assert.Equal(t, CancelAfterChatModel|CancelAfterToolCalls, cfg.Mode) +} + +func TestStopController_StopCauseFirstNonEmptyWins(t *testing.T) { + c := newStopController() + + c.requestStop(&stopConfig{}) + c.requestStop(&stopConfig{stopCause: "first"}) + c.requestStop(&stopConfig{stopCause: "second"}) + + assert.Equal(t, "first", c.cause()) +} + +func TestStopController_SkipCheckpointSticky(t *testing.T) { + c := newStopController() + + c.requestStop(&stopConfig{skipCheckpoint: true}) + c.requestStop(&stopConfig{}) + + assert.True(t, c.skipCheckpointEnabled()) +} + +func TestStopController_ConcurrentStopRequestsRaceSafe(t *testing.T) { + c := newStopController() + c.beginActiveTurn() + + var wg sync.WaitGroup + for i := 0; i < 20; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + switch i % 5 { + case 0: + c.requestStop(&stopConfig{}) + case 1: + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithRecursive()}}) + case 2: + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{ + WithAgentCancelMode(CancelAfterChatModel), + WithAgentCancelTimeout(time.Second), + WithRecursive(), + }}) + case 3: + c.requestStop(&stopConfig{idleFor: time.Second}) + case 4: + c.requestStop(&stopConfig{skipCheckpoint: true, stopCause: "cause"}) + } + }(i) + } + wg.Wait() + + assert.True(t, c.isCommitted()) + assert.True(t, c.skipCheckpointEnabled()) +} + +func TestStopController_CloseForLoopExitClearsPendingCancel(t *testing.T) { + c := newStopController() + c.beginActiveTurn() + c.requestStop(&stopConfig{agentCancelOpts: []AgentCancelOption{WithRecursive()}}) + + c.closeForLoopExit() + + _, ok := c.receiveCancel() + assert.False(t, ok) +} + +func TestAttack_UntilIdleFor_ConcurrentPushDuringIdleTimer(t *testing.T) { + turnCount := int32(0) + turnDone := make(chan struct{}, 10) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + atomic.AddInt32(&turnCount, 1) + turnDone <- struct{}{} + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-turnDone + + loop.Stop(UntilIdleFor(200 * time.Millisecond)) + + for i := 0; i < 5; i++ { + time.Sleep(50 * time.Millisecond) + loop.Push("concurrent-" + string(rune('a'+i))) + <-turnDone + } + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + waitOrFail(t, done, "loop did not exit after idle timeout — Push did not reset timer correctly") + + finalCount := atomic.LoadInt32(&turnCount) + assert.Equal(t, int32(6), finalCount, "all 6 pushes should have been processed") +} + +func TestAttack_UntilIdleFor_MultipleStopCallsFirstWins(t *testing.T) { + turnDone := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(turnDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-turnDone + + loop.Stop(UntilIdleFor(100 * time.Millisecond)) + loop.Stop(UntilIdleFor(10 * time.Minute)) + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + waitOrFail(t, done, "second UntilIdleFor should have been ignored; loop should have exited with 100ms timer") +} + +func TestAttack_BareStopOverridesUntilIdleFor(t *testing.T) { + agentStarted := make(chan struct{}) + agentDone := make(chan struct{}) + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + <-agentDone + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-agentStarted + + loop.Stop(UntilIdleFor(10 * time.Minute)) + + loop.Stop() + close(agentDone) + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + waitOrFail(t, done, "bare Stop should override UntilIdleFor and cause immediate shutdown") + + exit := loop.Wait() + assert.NoError(t, exit.ExitReason, "bare Stop should exit cleanly") +} + +func TestAttack_BareStopDoesNotDeescalateExistingCancelIntent(t *testing.T) { + agentStarted := make(chan *cancelContext, 1) + probe := &turnLoopStopModeProbeAgent{ccCh: agentStarted} + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return probe, nil + }, + }) + + loop.Push("msg1") + cc := <-agentStarted + + loop.Stop(WithImmediate()) + + time.Sleep(20 * time.Millisecond) + + loop.Stop() + + time.Sleep(20 * time.Millisecond) + mode := cc.getMode() + assert.Equal(t, CancelImmediate, mode, "bare Stop after WithImmediate must not de-escalate cancel mode") + + exit := loop.Wait() + var ce *CancelError + require.True(t, errors.As(exit.ExitReason, &ce)) + assert.Equal(t, CancelImmediate, ce.Info.Mode) +} + +func TestAttack_InterruptedItems_EmptyWhenAgentFinishesNormally(t *testing.T) { + agentStarted := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-agentStarted + time.Sleep(50 * time.Millisecond) + loop.Stop() + + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + assert.Empty(t, exit.InterruptedItems, "InterruptedItems must be empty when agent finished normally") +} + +func TestAttack_TurnBuffer_WakeupDoesNotLoseItems(t *testing.T) { + tb := newTurnBuffer[string]() + + tb.Send("a") + tb.Send("b") + tb.Wakeup() + tb.Send("c") + + var got []string + for i := 0; i < 3; i++ { + val, ok := tb.Receive() + require.True(t, ok) + got = append(got, val) + } + + assert.Equal(t, []string{"a", "b", "c"}, got, "Wakeup must not cause items to be lost") +} + +func TestAttack_TurnBuffer_ClearWakeupPreventsSpuriousReturn(t *testing.T) { + tb := newTurnBuffer[string]() + + tb.Wakeup() + tb.ClearWakeup() + + received := make(chan string, 1) + go func() { + val, ok := tb.Receive() + if ok { + received <- val + } + }() + + time.Sleep(50 * time.Millisecond) + tb.Send("real") + + select { + case val := <-received: + assert.Equal(t, "real", val, "ClearWakeup should prevent spurious empty return") + case <-time.After(2 * time.Second): + t.Fatal("Receive blocked forever despite Send") + } +} + +func TestAttack_StopBeforeRun_UntilIdleFor_ExitsImmediately(t *testing.T) { + loop := NewTurnLoop(TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: prepareTestAgent, + }) + + loop.Stop(UntilIdleFor(10 * time.Minute)) + loop.Stop() + + loop.Run(context.Background()) + + done := make(chan struct{}) + go func() { + loop.Wait() + close(done) + }() + + waitOrFail(t, done, "loop should exit immediately when Stop() called before Run()") +} + +func TestAttack_PushAfterStop_UntilIdleFor_RoutedToLateItems(t *testing.T) { + turnDone := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(turnDone) + return &AgentOutput{}, nil + }, + }, nil + }, + }) + + loop.Push("msg1") + <-turnDone + + loop.Stop(UntilIdleFor(50 * time.Millisecond)) + exit := loop.Wait() + assert.NoError(t, exit.ExitReason) + + ok, _ := loop.Push("after-stop") + assert.False(t, ok, "Push after loop exited should return false") + + late := exit.TakeLateItems() + assert.Equal(t, []string{"after-stop"}, late) +} + +func TestAttack_ConcurrentStopEscalation_RaceDetector(t *testing.T) { + agentStarted := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + }) + + loop.Push("msg1") + <-agentStarted + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + switch i % 4 { + case 0: + loop.Stop() + case 1: + loop.Stop(WithImmediate()) + case 2: + loop.Stop(WithGracefulTimeout(100 * time.Millisecond)) + case 3: + loop.Stop(UntilIdleFor(50 * time.Millisecond)) + } + }(i) + } + + wg.Wait() + exit := loop.Wait() + t.Log("ExitReason:", exit.ExitReason) +} + +func TestAttack_SkipCheckpoint_Sticky(t *testing.T) { + agentStarted := make(chan struct{}) + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + close(agentStarted) + <-ctx.Done() + return nil, ctx.Err() + }, + }, nil + }, + Store: newTestStore(), + CheckpointID: "test-sticky", + }) + + loop.Push("msg1") + <-agentStarted + + loop.Stop(WithSkipCheckpoint()) + loop.Stop(WithImmediate()) + + exit := loop.Wait() + assert.False(t, exit.CheckpointAttempted, "SkipCheckpoint is sticky; checkpoint should be skipped") +} + +// turnLoopNestedProbeAgent simulates an agent with a nested sub-agent +// by deriving a child cancelContext. This allows tests to verify that +// TurnLoop's Stop/Push options correctly propagate recursive cancellation. +// +// IMPORTANT: child.markDone() is NOT called by the probe. The test MUST +// call it (e.g. via t.Cleanup) after verifying propagation to avoid a +// race between markDone closing child.doneChan and the deriveAgentToolCancelContext +// goroutines propagating the cancel signal. +type turnLoopNestedProbeAgent struct { + parentCCCh chan *cancelContext + childCCCh chan *cancelContext +} + +func (a *turnLoopNestedProbeAgent) Name(_ context.Context) string { return "nested-probe" } +func (a *turnLoopNestedProbeAgent) Description(_ context.Context) string { return "nested-probe" } +func (a *turnLoopNestedProbeAgent) Run(ctx context.Context, _ *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + o := getCommonOptions(nil, opts...) + cc := o.cancelCtx + + child := cc.deriveAgentToolCancelContext(ctx) + a.parentCCCh <- cc + a.childCCCh <- child + + go func() { + defer gen.Close() + <-cc.cancelChan + for { + if cc.getMode() == CancelImmediate { + gen.Send(&AgentEvent{Err: cc.createCancelError()}) + return + } + time.Sleep(1 * time.Millisecond) + } + }() + return iter +} + +func TestTurnLoop_Stop_WithImmediate_RecursivePropagation(t *testing.T) { + parentCCCh := make(chan *cancelContext, 1) + childCCCh := make(chan *cancelContext, 1) + probe := &turnLoopNestedProbeAgent{parentCCCh: parentCCCh, childCCCh: childCCCh} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return probe, nil + }, + }) + + loop.Push("msg1") + cc := <-parentCCCh + child := <-childCCCh + t.Cleanup(func() { child.markDone() }) + + loop.Stop(WithImmediate()) + + // Child should receive the cancel signal via recursive propagation. + select { + case <-child.cancelChan: + case <-time.After(2 * time.Second): + t.Fatal("child did not receive cancel via recursive propagation") + } + + // Child should also receive the immediate cancel signal. + select { + case <-child.immediateChan: + case <-time.After(2 * time.Second): + t.Fatal("child did not receive immediate cancel via recursive propagation") + } + + assert.True(t, cc.isRecursive(), "WithImmediate should set recursive on parent") + assert.True(t, child.shouldCancel(), "child should be cancelled") + assert.True(t, child.isImmediateCancelled(), "child should have received immediate cancel") + + exit := loop.Wait() + var ce *CancelError + require.True(t, errors.As(exit.ExitReason, &ce)) + assert.Equal(t, CancelImmediate, ce.Info.Mode) +} + +func TestTurnLoop_Push_WithPreemptTimeout_RecursivePropagation(t *testing.T) { + parentCCCh := make(chan *cancelContext, 2) + childCCCh := make(chan *cancelContext, 2) + probe := &turnLoopNestedProbeAgent{parentCCCh: parentCCCh, childCCCh: childCCCh} + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return probe, nil + }, + }) + + loop.Push("first") + cc := <-parentCCCh + child := <-childCCCh + t.Cleanup(func() { child.markDone() }) + + // Preempt with a very short timeout so it escalates to CancelImmediate quickly. + loop.Push("urgent", WithPreemptTimeout[string, *schema.Message](AfterChatModel, 10*time.Millisecond)) + + // After timeout escalation, child should receive the immediate cancel + // via recursive propagation. + select { + case <-child.immediateChan: + case <-time.After(2 * time.Second): + t.Fatal("child did not receive immediate cancel after preempt timeout escalation") + } + + assert.True(t, cc.isRecursive(), "WithPreemptTimeout should set recursive on parent") + assert.True(t, child.isImmediateCancelled(), "child should have received immediate cancel") + + loop.Stop(WithImmediate()) + loop.Wait() +} + +func TestUntilIdleFor_NonPositive_Panics(t *testing.T) { + assert.PanicsWithValue(t, "adk: UntilIdleFor: duration must be positive", + func() { UntilIdleFor(0) }) + assert.PanicsWithValue(t, "adk: UntilIdleFor: duration must be positive", + func() { UntilIdleFor(-1 * time.Second) }) +} + +func TestSaveTurnLoopCheckpoint_NilStore(t *testing.T) { + l := &TurnLoop[string, *schema.Message]{config: TurnLoopConfig[string, *schema.Message]{Store: nil}} + err := l.saveTurnLoopCheckpoint(context.Background(), "cp-1", &turnLoopCheckpoint[string]{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "checkpoint store is nil") +} + +func TestSetupBridgeStore_NilStore_Resume(t *testing.T) { + l := &TurnLoop[string, *schema.Message]{config: TurnLoopConfig[string, *schema.Message]{Store: nil}} + spec := &turnRunSpec[string, *schema.Message]{isResume: true} + _, _, err := l.setupBridgeStore(spec, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "checkpoint store is nil") +} + +// TestTurnLoop_Preempt_LoopStalledAfterSecondPreemptPush covers a liveness +// regression where a preempted turn was followed by another preemptive Push and +// the loop stopped making progress before processing the later item. +func TestTurnLoop_Preempt_LoopStalledAfterSecondPreemptPush(t *testing.T) { + // turnCount tracks how many turns have been fully processed. + var turnCount int32 + + // Channels to synchronize the test with each turn's lifecycle. + firstAgentStarted := make(chan struct{}) + secondTurnDone := make(chan struct{}) + thirdTurnDone := make(chan struct{}) + + var firstAgentStartedOnce, secondTurnDoneOnce, thirdTurnDoneOnce sync.Once + + agent := &turnLoopCancellableMockAgent{ + name: "test", + runFunc: func(ctx context.Context, input *AgentInput) (*AgentOutput, error) { + turn := atomic.AddInt32(&turnCount, 1) + switch turn { + case 1: + // First turn: signal started, then block until preempted. + firstAgentStartedOnce.Do(func() { close(firstAgentStarted) }) + <-ctx.Done() + case 2, 3: + // Subsequent turns: complete immediately. + } + return &AgentOutput{}, nil + }, + } + + loop := newAndRunTurnLoop(context.Background(), TurnLoopConfig[string, *schema.Message]{ + PrepareAgent: prepareAgent(agent), + GenInput: genInputConsumeFirst, + OnAgentEvents: func(ctx context.Context, tc *TurnContext[string, *schema.Message], events *AsyncIterator[*AgentEvent]) error { + for { + if _, ok := events.Next(); !ok { + break + } + } + turn := atomic.LoadInt32(&turnCount) + switch turn { + case 2: + secondTurnDoneOnce.Do(func() { close(secondTurnDone) }) + case 3: + thirdTurnDoneOnce.Do(func() { close(thirdTurnDone) }) + } + return nil + }, + }) + + // Step 1: Push item A (no preempt). Wait for agent to start. + loop.Push("A") + waitOrFail(t, firstAgentStarted, "agent did not start for item A") + + // Step 2: Push item B with preempt. This cancels the first turn. + loop.Push("B", WithPreempt[string, *schema.Message](AnySafePoint)) + + // Wait for the second turn (item B) to complete successfully. + waitOrFail(t, secondTurnDone, "second turn (item B) did not complete") + + // Step 3: Push item C with preempt. This is the scenario that triggers + // the bug — the loop should process item C but instead gets stuck. + loop.Push("C", WithPreempt[string, *schema.Message](AnySafePoint)) + + // The loop should process item C. If the bug is present, this will timeout. + waitOrFail(t, thirdTurnDone, "third turn (item C) was never processed — loop is stuck between turns") + + loop.Stop() + result := loop.Wait() + assert.NoError(t, result.ExitReason) + assert.Equal(t, int32(3), atomic.LoadInt32(&turnCount), "expected 3 turns to be processed") +} + +func TestAttack_BusinessInterrupt_NoStore_ExitsWithoutPanic(t *testing.T) { + ctx := context.Background() + interruptAgent := &turnLoopInterruptAgent{interruptInfo: "no_store_test"} + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + GenInput: genInputConsumeAllWithMsg, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return interruptAgent, nil + }, + }) + + loop.Push("msg1") + exit := loop.Wait() + + var intErr *InterruptError + require.True(t, errors.As(exit.ExitReason, &intErr), "expected *InterruptError, got: %v", exit.ExitReason) + assert.Equal(t, []string{"msg1"}, exit.InterruptedItems) + assert.False(t, exit.CheckpointAttempted, "no store → no checkpoint attempt") +} + +func TestAttack_BusinessInterrupt_EmptyConsumed_NoCheckpoint(t *testing.T) { + ctx := context.Background() + store := newTestStore() + interruptAgent := &turnLoopInterruptAgent{interruptInfo: "idle_test"} + + loop := newAndRunTurnLoop(ctx, TurnLoopConfig[string, *schema.Message]{ + Store: store, + CheckpointID: "idle-cp", + GenInput: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], items []string) (*GenInputResult[string, *schema.Message], error) { + return &GenInputResult[string, *schema.Message]{ + Input: &AgentInput{Messages: []Message{schema.UserMessage("x")}}, + Consumed: []string{}, + }, nil + }, + PrepareAgent: func(ctx context.Context, _ *TurnLoop[string, *schema.Message], consumed []string) (Agent, error) { + return interruptAgent, nil + }, + }) + + loop.Push("msg1") + exit := loop.Wait() + + var intErr *InterruptError + require.True(t, errors.As(exit.ExitReason, &intErr), "expected *InterruptError, got: %v", exit.ExitReason) + assert.Empty(t, exit.InterruptedItems, "consumed was empty → InterruptedItems should be empty") +} diff --git a/adk/utils.go b/adk/utils.go new file mode 100644 index 0000000..991abd0 --- /dev/null +++ b/adk/utils.go @@ -0,0 +1,313 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "io" + "strings" + + "github.com/google/uuid" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/schema" +) + +type AsyncIterator[T any] struct { + ch *internal.UnboundedChan[T] +} + +func (ai *AsyncIterator[T]) Next() (T, bool) { + return ai.ch.Receive() +} + +type AsyncGenerator[T any] struct { + ch *internal.UnboundedChan[T] +} + +func (ag *AsyncGenerator[T]) Send(v T) { + ag.ch.Send(v) +} + +func (ag *AsyncGenerator[T]) trySend(v T) bool { + return ag.ch.TrySend(v) +} + +func (ag *AsyncGenerator[T]) Close() { + ag.ch.Close() +} + +// NewAsyncIteratorPair returns a paired async iterator and generator +// that share the same underlying channel. +func NewAsyncIteratorPair[T any]() (*AsyncIterator[T], *AsyncGenerator[T]) { + ch := internal.NewUnboundedChan[T]() + return &AsyncIterator[T]{ch}, &AsyncGenerator[T]{ch} +} + +func copyMap[K comparable, V any](m map[K]V) map[K]V { + res := make(map[K]V, len(m)) + for k, v := range m { + res[k] = v + } + return res +} + +func cloneSlice[T any](s []T) []T { + if s == nil { + return nil + } + res := make([]T, len(s)) + copy(res, s) + return res +} + +func concatInstructions(instructions ...string) string { + var sb strings.Builder + sb.WriteString(instructions[0]) + for i := 1; i < len(instructions); i++ { + sb.WriteString("\n\n") + sb.WriteString(instructions[i]) + } + + return sb.String() +} + +// GenTransferMessages generates assistant and tool messages to instruct a +// transfer-to-agent tool call targeting the destination agent. +// +// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven +// to be more effective empirically. Consider using ChatModelAgent with AgentTool +// or DeepAgent instead for most multi-agent scenarios. +func GenTransferMessages(_ context.Context, destAgentName string) (Message, Message) { + toolCallID := uuid.NewString() + tooCall := schema.ToolCall{ID: toolCallID, Function: schema.FunctionCall{Name: TransferToAgentToolName, Arguments: destAgentName}} + assistantMessage := schema.AssistantMessage("", []schema.ToolCall{tooCall}) + msg := transferToAgentToolOutput(destAgentName) + toolMessage := schema.ToolMessage(msg, toolCallID, schema.WithToolName(TransferToAgentToolName)) + return assistantMessage, toolMessage +} + +func typedSetAutomaticClose[M MessageType](e *TypedAgentEvent[M]) { + if e.Output == nil || e.Output.MessageOutput == nil || !e.Output.MessageOutput.IsStreaming { + return + } + + e.Output.MessageOutput.MessageStream.SetAutomaticClose() +} + +// set automatic close for event's message stream +func setAutomaticClose(e *AgentEvent) { + typedSetAutomaticClose(e) +} + +// getMessageFromWrappedEvent extracts the message from an AgentEvent. +// If the stream contains an error chunk, this function returns (nil, err) and +// sets StreamErr to prevent re-consumption. The nil message ensures that +// failed stream responses are not included in subsequent agents' context windows. +func getMessageFromTypedWrappedEvent[M MessageType](e *typedAgentEventWrapper[M]) (M, error) { + var zero M + if e.event.Output == nil || e.event.Output.MessageOutput == nil { + return zero, nil + } + + if !e.event.Output.MessageOutput.IsStreaming { + return e.event.Output.MessageOutput.Message, nil + } + + if e.StreamErr != nil { + return zero, e.StreamErr + } + + if !isNilMessage(e.concatenatedMessage) { + return e.concatenatedMessage, nil + } + + e.consumeStream() + + if e.StreamErr != nil { + return zero, e.StreamErr + } + return e.concatenatedMessage, nil +} + +func getMessageFromWrappedEvent(e *agentEventWrapper) (Message, error) { + if e.AgentEvent.Output == nil || e.AgentEvent.Output.MessageOutput == nil { + return nil, nil + } + + if !e.AgentEvent.Output.MessageOutput.IsStreaming { + return e.AgentEvent.Output.MessageOutput.Message, nil + } + + if e.StreamErr != nil { + return nil, e.StreamErr + } + + if e.concatenatedMessage != nil { + return e.concatenatedMessage, nil + } + + e.consumeStream() + + if e.StreamErr != nil { + return nil, e.StreamErr + } + return e.concatenatedMessage, nil +} + +// consumeStream drains the message stream, setting concatenatedMessage on +// success or StreamErr on failure. The stream is always replaced with an +// error-free, materialized version safe for gob encoding. +// Must be called at most once (guarded by callers checking concatenatedMessage/StreamErr). +func (e *agentEventWrapper) consumeStream() { + e.mu.Lock() + defer e.mu.Unlock() + + if e.concatenatedMessage != nil { + return + } + + s := e.AgentEvent.Output.MessageOutput.MessageStream + var msgs []Message + + defer s.Close() + for { + msg, err := s.Recv() + if err != nil { + if err == io.EOF { + break + } + e.StreamErr = err + e.AgentEvent.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs) + return + } + msgs = append(msgs, msg) + } + + if len(msgs) == 0 { + e.StreamErr = errors.New("no messages in MessageVariant.MessageStream") + // Defensively replace the stream. The defer s.Close() above already + // ensures subsequent Recv() returns io.EOF, but we replace it anyway + // to make the invariant explicit: after consumeStream, MessageStream + // is always safe for MessageVariant.GobEncode to consume. + e.AgentEvent.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs) + return + } + + if len(msgs) == 1 { + e.concatenatedMessage = msgs[0] + } else { + var err error + e.concatenatedMessage, err = schema.ConcatMessages(msgs) + if err != nil { + e.StreamErr = err + e.AgentEvent.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs) + return + } + } + + e.AgentEvent.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray([]Message{e.concatenatedMessage}) +} + +// copyTypedAgentEvent copies a TypedAgentEvent. +// If the MessageVariant is streaming, the MessageStream will be copied. +// RunPath will be deep copied. +// The result of Copy will be a new TypedAgentEvent that is: +// - safe to set fields of TypedAgentEvent +// - safe to extend RunPath +// - safe to receive from MessageStream +// NOTE: even if the event is copied, it's still not recommended to modify +// the Message itself or Chunks of the MessageStream, as they are not copied. +// NOTE: if you have CustomizedOutput or CustomizedAction, they are NOT copied. +func copyTypedAgentEvent[M MessageType](ae *TypedAgentEvent[M]) *TypedAgentEvent[M] { + rp := make([]RunStep, len(ae.RunPath)) + copy(rp, ae.RunPath) + + copied := &TypedAgentEvent[M]{ + AgentName: ae.AgentName, + RunPath: rp, + Action: ae.Action, + Err: ae.Err, + } + + if ae.Output == nil { + return copied + } + + copied.Output = &TypedAgentOutput[M]{ + CustomizedOutput: ae.Output.CustomizedOutput, + } + + mv := ae.Output.MessageOutput + if mv == nil { + return copied + } + + copied.Output.MessageOutput = &TypedMessageVariant[M]{ + IsStreaming: mv.IsStreaming, + Role: mv.Role, + AgenticRole: mv.AgenticRole, + ToolName: mv.ToolName, + } + if mv.IsStreaming { + sts := ae.Output.MessageOutput.MessageStream.Copy(2) + mv.MessageStream = sts[0] + copied.Output.MessageOutput.MessageStream = sts[1] + } else { + copied.Output.MessageOutput.Message = mv.Message + } + + return copied +} + +// TypedGetMessage extracts the message from a TypedAgentEvent, concatenating a stream if present. +func TypedGetMessage[M MessageType](e *TypedAgentEvent[M]) (M, *TypedAgentEvent[M], error) { + var zero M + if e.Output == nil || e.Output.MessageOutput == nil { + return zero, e, nil + } + + msgOutput := e.Output.MessageOutput + if msgOutput.IsStreaming { + ss := msgOutput.MessageStream.Copy(2) + e.Output.MessageOutput.MessageStream = ss[0] + + msg, err := concatMessageStream(ss[1]) + + return msg, e, err + } + + return msgOutput.Message, e, nil +} + +// GetMessage extracts the Message from an AgentEvent. For streaming output, +// it duplicates the stream and concatenates it into a single Message. +func GetMessage(e *AgentEvent) (Message, *AgentEvent, error) { + return TypedGetMessage(e) +} + +func typedErrorIter[M MessageType](err error) *AsyncIterator[*TypedAgentEvent[M]] { + iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]() + generator.Send(&TypedAgentEvent[M]{Err: err}) + generator.Close() + return iterator +} + +func genErrorIter(err error) *AsyncIterator[*AgentEvent] { + return typedErrorIter[*schema.Message](err) +} diff --git a/adk/utils_test.go b/adk/utils_test.go new file mode 100644 index 0000000..b4c2cbc --- /dev/null +++ b/adk/utils_test.go @@ -0,0 +1,455 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestAsyncIteratorPair_Basic(t *testing.T) { + // Create a new iterator-generator pair + iterator, generator := NewAsyncIteratorPair[string]() + + // Test sending and receiving a value + generator.Send("test1") + val, ok := iterator.Next() + if !ok { + t.Error("receive should succeed") + } + if val != "test1" { + t.Errorf("expected 'test1', got '%s'", val) + } + + // Test sending and receiving multiple values + generator.Send("test2") + generator.Send("test3") + + val, ok = iterator.Next() + if !ok { + t.Error("receive should succeed") + } + if val != "test2" { + t.Errorf("expected 'test2', got '%s'", val) + } + + val, ok = iterator.Next() + if !ok { + t.Error("receive should succeed") + } + if val != "test3" { + t.Errorf("expected 'test3', got '%s'", val) + } +} + +func TestAsyncIteratorPair_Close(t *testing.T) { + iterator, generator := NewAsyncIteratorPair[int]() + + // Send some values + generator.Send(1) + generator.Send(2) + + // Close the generator + generator.Close() + + // Should still be able to read existing values + val, ok := iterator.Next() + if !ok { + t.Error("receive should succeed") + } + if val != 1 { + t.Errorf("expected 1, got %d", val) + } + + val, ok = iterator.Next() + if !ok { + t.Error("receive should succeed") + } + if val != 2 { + t.Errorf("expected 2, got %d", val) + } + + // After consuming all values, Next should return false + _, ok = iterator.Next() + if ok { + t.Error("receive from closed, empty channel should return ok=false") + } +} + +func TestAsyncIteratorPair_Concurrency(t *testing.T) { + iterator, generator := NewAsyncIteratorPair[int]() + const numSenders = 5 + const numReceivers = 3 + const messagesPerSender = 100 + + var rwg, swg sync.WaitGroup + rwg.Add(numReceivers) + swg.Add(numSenders) + + // Start senders + for i := 0; i < numSenders; i++ { + go func(id int) { + defer swg.Done() + for j := 0; j < messagesPerSender; j++ { + generator.Send(id*messagesPerSender + j) + time.Sleep(time.Microsecond) // Small delay to increase concurrency chance + } + }(i) + } + + // Start receivers + received := make([]int, 0, numSenders*messagesPerSender) + var mu sync.Mutex + + for i := 0; i < numReceivers; i++ { + go func() { + defer rwg.Done() + for { + val, ok := iterator.Next() + if !ok { + return + } + mu.Lock() + received = append(received, val) + mu.Unlock() + } + }() + } + + // Wait for senders to finish + swg.Wait() + generator.Close() + + // Wait for all goroutines to finish + rwg.Wait() + + // Verify we received all messages + if len(received) != numSenders*messagesPerSender { + t.Errorf("expected %d messages, got %d", numSenders*messagesPerSender, len(received)) + } + + // Create a map to check for duplicates and missing values + receivedMap := make(map[int]bool) + for _, val := range received { + receivedMap[val] = true + } + + if len(receivedMap) != numSenders*messagesPerSender { + t.Error("duplicate or missing messages detected") + } +} + +func TestGenErrorIter(t *testing.T) { + iter := genErrorIter(fmt.Errorf("test")) + e, ok := iter.Next() + assert.True(t, ok) + assert.Equal(t, "test", e.Err.Error()) + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestGetMessageFromWrappedEvent_StreamError_MultipleCallsGuard(t *testing.T) { + streamErr := errors.New("stream error") + + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("chunk1", nil), nil) + sw.Send(schema.AssistantMessage("chunk2", nil), nil) + sw.Send(nil, streamErr) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + } + + msg1, err1 := getMessageFromWrappedEvent(wrapper) + assert.Nil(t, msg1) + assert.NotNil(t, err1) + assert.Equal(t, "stream error", err1.Error()) + + assert.NotEmpty(t, wrapper.StreamErr) + assert.Equal(t, err1, wrapper.StreamErr) + + msg2, err2 := getMessageFromWrappedEvent(wrapper) + assert.Nil(t, msg2) + assert.Equal(t, err1, err2) +} + +func TestGetMessageFromWrappedEvent_StreamSuccess_MultipleCallsCached(t *testing.T) { + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("chunk1", nil), nil) + sw.Send(schema.AssistantMessage("chunk2", nil), nil) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + } + + msg1, err1 := getMessageFromWrappedEvent(wrapper) + assert.NotNil(t, msg1) + assert.Nil(t, err1) + assert.Equal(t, "chunk1chunk2", msg1.Content) + + assert.NotNil(t, wrapper.concatenatedMessage) + + msg2, err2 := getMessageFromWrappedEvent(wrapper) + assert.NotNil(t, msg2) + assert.Nil(t, err2) + assert.Equal(t, "chunk1chunk2", msg2.Content) + assert.Same(t, msg1, msg2) +} + +func TestGetMessageFromWrappedEvent_StreamError_PartialMessagesPreserved(t *testing.T) { + streamErr := errors.New("stream error at chunk3") + + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("chunk1", nil), nil) + sw.Send(schema.AssistantMessage("chunk2", nil), nil) + sw.Send(nil, streamErr) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + } + + _, err := getMessageFromWrappedEvent(wrapper) + assert.NotNil(t, err) + assert.Equal(t, streamErr, wrapper.StreamErr) + + newStream := wrapper.AgentEvent.Output.MessageOutput.MessageStream + assert.NotNil(t, newStream) + + var msgs []Message + for { + msg, err := newStream.Recv() + if err != nil { + break + } + msgs = append(msgs, msg) + } + + assert.Equal(t, 2, len(msgs)) + assert.Equal(t, "chunk1", msgs[0].Content) + assert.Equal(t, "chunk2", msgs[1].Content) +} + +func TestAgentEventWrapper_GobEncoding_WithWillRetryError(t *testing.T) { + streamErr := &WillRetryError{ErrStr: "stream error", RetryAttempt: 2} + + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("partial1", nil), nil) + sw.Send(schema.AssistantMessage("partial2", nil), nil) + sw.Send(nil, streamErr) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + TS: 12345, + } + + _, err := getMessageFromWrappedEvent(wrapper) + assert.NotNil(t, err) + var wrapperErr *WillRetryError + assert.True(t, errors.As(wrapper.StreamErr, &wrapperErr)) + assert.Equal(t, streamErr.ErrStr, wrapperErr.ErrStr) + assert.Equal(t, streamErr.RetryAttempt, wrapperErr.RetryAttempt) + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err = enc.Encode(wrapper) + assert.NoError(t, err) + + var decoded agentEventWrapper + dec := gob.NewDecoder(&buf) + err = dec.Decode(&decoded) + assert.NoError(t, err) + + assert.Equal(t, "TestAgent", decoded.AgentName) + assert.Equal(t, int64(12345), decoded.TS) + var decodedErr *WillRetryError + assert.True(t, errors.As(decoded.StreamErr, &decodedErr)) + assert.Equal(t, streamErr.ErrStr, decodedErr.ErrStr) + assert.Equal(t, streamErr.RetryAttempt, decodedErr.RetryAttempt) +} + +func TestAgentEventWrapper_GobEncoding_WithUnregisteredError(t *testing.T) { + streamErr := errors.New("unregistered error type") + + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("partial1", nil), nil) + sw.Send(nil, streamErr) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + TS: 22222, + } + + _, err := getMessageFromWrappedEvent(wrapper) + assert.NotNil(t, err) + assert.Equal(t, streamErr, wrapper.StreamErr) + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err = enc.Encode(wrapper) + assert.Error(t, err, "gob encoding should fail for unregistered error types") +} + +func TestAgentEventWrapper_GobEncoding_WithStreamSuccess(t *testing.T) { + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("success1", nil), nil) + sw.Send(schema.AssistantMessage("success2", nil), nil) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + AgentName: "TestAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + TS: 67890, + } + + msg, err := getMessageFromWrappedEvent(wrapper) + assert.NoError(t, err) + assert.Equal(t, "success1success2", msg.Content) + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err = enc.Encode(wrapper) + assert.NoError(t, err) + + var decoded agentEventWrapper + dec := gob.NewDecoder(&buf) + err = dec.Decode(&decoded) + assert.NoError(t, err) + + assert.Equal(t, "TestAgent", decoded.AgentName) + assert.Equal(t, int64(67890), decoded.TS) + assert.Empty(t, decoded.StreamErr) +} + +func TestConsumeStream_EdgeCases(t *testing.T) { + t.Run("double call is no-op", func(t *testing.T) { + sr, sw := schema.Pipe[Message](10) + go func() { + defer sw.Close() + sw.Send(schema.AssistantMessage("msg1", nil), nil) + }() + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + } + + wrapper.consumeStream() + assert.NotNil(t, wrapper.concatenatedMessage) + assert.Equal(t, "msg1", wrapper.concatenatedMessage.Content) + + // Second call should be a no-op + wrapper.consumeStream() + assert.Equal(t, "msg1", wrapper.concatenatedMessage.Content) + }) + + t.Run("empty stream sets StreamErr", func(t *testing.T) { + sr, sw := schema.Pipe[Message](10) + sw.Close() // immediately close => EOF with 0 messages + + wrapper := &agentEventWrapper{ + AgentEvent: &AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: true, + MessageStream: sr, + }, + }, + }, + } + + wrapper.consumeStream() + assert.Nil(t, wrapper.concatenatedMessage) + assert.Error(t, wrapper.StreamErr) + assert.Contains(t, wrapper.StreamErr.Error(), "no messages") + }) +} diff --git a/adk/workflow.go b/adk/workflow.go new file mode 100644 index 0000000..161c434 --- /dev/null +++ b/adk/workflow.go @@ -0,0 +1,706 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "fmt" + "runtime/debug" + "sync" + + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +type workflowAgentMode int + +const ( + workflowAgentModeUnknown workflowAgentMode = iota + workflowAgentModeSequential + workflowAgentModeLoop + workflowAgentModeParallel +) + +type workflowAgent struct { + name string + description string + subAgents []*flowAgent + + mode workflowAgentMode + + maxIterations int +} + +func (a *workflowAgent) Name(_ context.Context) string { + return a.name +} + +func (a *workflowAgent) Description(_ context.Context) string { + return a.description +} + +func (a *workflowAgent) GetType() string { + switch a.mode { + case workflowAgentModeSequential: + return "Sequential" + case workflowAgentModeParallel: + return "Parallel" + case workflowAgentModeLoop: + return "Loop" + default: + return "WorkflowAgent" + } +} + +func (a *workflowAgent) Run(ctx context.Context, _ *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + + var err error + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&AgentEvent{Err: e}) + } else if err != nil { + generator.Send(&AgentEvent{Err: err}) + } + + generator.Close() + }() + + // Different workflow execution based on mode + switch a.mode { + case workflowAgentModeSequential: + err = a.runSequential(ctx, generator, nil, nil, opts...) + case workflowAgentModeLoop: + err = a.runLoop(ctx, generator, nil, nil, opts...) + case workflowAgentModeParallel: + err = a.runParallel(ctx, generator, nil, nil, opts...) + default: + err = fmt.Errorf("unsupported workflow agent mode: %d", a.mode) + } + }() + + return iterator +} + +type sequentialWorkflowState struct { + InterruptIndex int +} + +type parallelWorkflowState struct { + SubAgentEvents map[int][]*agentEventWrapper +} + +type loopWorkflowState struct { + LoopIterations int + SubAgentIndex int +} + +func init() { + schema.RegisterName[*sequentialWorkflowState]("eino_adk_sequential_workflow_state") + schema.RegisterName[*parallelWorkflowState]("eino_adk_parallel_workflow_state") + schema.RegisterName[*loopWorkflowState]("eino_adk_loop_workflow_state") +} + +func (a *workflowAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + var err error + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&AgentEvent{Err: e}) + } else if err != nil { + generator.Send(&AgentEvent{Err: err}) + } + + generator.Close() + }() + + state := info.InterruptState + if state == nil { + panic(fmt.Sprintf("workflowAgent.Resume: agent '%s' was asked to resume but has no state", a.Name(ctx))) + } + + // Different workflow execution based on the type of our restored state. + switch s := state.(type) { + case *sequentialWorkflowState: + err = a.runSequential(ctx, generator, s, info, opts...) + case *parallelWorkflowState: + err = a.runParallel(ctx, generator, s, info, opts...) + case *loopWorkflowState: + err = a.runLoop(ctx, generator, s, info, opts...) + default: + err = fmt.Errorf("unsupported workflow agent state type: %T", s) + } + }() + return iterator +} + +// WorkflowInterruptInfo stores interrupt information for workflow agents. +// CheckpointSchema: persisted via InterruptInfo.Data (gob). +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +type WorkflowInterruptInfo struct { + OrigInput *AgentInput + + SequentialInterruptIndex int + SequentialInterruptInfo *InterruptInfo + + LoopIterations int + + ParallelInterruptInfo map[int] /*index*/ *InterruptInfo +} + +func (a *workflowAgent) runSequential(ctx context.Context, + generator *AsyncGenerator[*AgentEvent], seqState *sequentialWorkflowState, info *ResumeInfo, + opts ...AgentRunOption) (err error) { + + startIdx := 0 + + seqCtx := ctx + + // If we are resuming, find which sub-agent to start from and prepare its context. + if seqState != nil { + startIdx = seqState.InterruptIndex + + var steps []string + for i := 0; i < startIdx; i++ { + steps = append(steps, a.subAgents[i].Name(seqCtx)) + } + + seqCtx = updateRunPathOnly(seqCtx, steps...) + } + + for i := startIdx; i < len(a.subAgents); i++ { + subAgent := a.subAgents[i] + + // Cancel check at transition boundary between sub-agents. + // Transition boundaries are always safe to cancel at — no sub-agent + // work is in progress, so any cancel mode is honoured. + if cancelCtx := getCancelContext(ctx); cancelCtx != nil && cancelCtx.shouldCancel() { + state := &sequentialWorkflowState{InterruptIndex: i} + event := cancelAtTransition(ctx, "Sequential workflow cancel at transition", state) + generator.Send(event) + return nil + } + + var subIterator *AsyncIterator[*AgentEvent] + if seqState != nil { + wfInfo, _ := info.Data.(*WorkflowInterruptInfo) + if wfInfo != nil && wfInfo.SequentialInterruptInfo != nil { + // Sub-agent was interrupted — resume it. + subIterator = subAgent.Resume(seqCtx, &ResumeInfo{ + EnableStreaming: info.EnableStreaming, + InterruptInfo: wfInfo.SequentialInterruptInfo, + }, opts...) + } else { + subIterator = subAgent.Run(seqCtx, nil, opts...) + } + seqState = nil + } else { + subIterator = subAgent.Run(seqCtx, nil, opts...) + } + + seqCtx = updateRunPathOnly(seqCtx, subAgent.Name(seqCtx)) + + var lastActionEvent *AgentEvent + for { + event, ok := subIterator.Next() + if !ok { + break + } + + if event.Err != nil { + // exit if report error + generator.Send(event) + return nil + } + + if lastActionEvent != nil { + generator.Send(lastActionEvent) + lastActionEvent = nil + } + + if event.Action != nil { + lastActionEvent = event + continue + } + generator.Send(event) + } + + if lastActionEvent != nil { + if lastActionEvent.Action.internalInterrupted != nil { + // A sub-agent interrupted. Wrap it with our own state, including the index. + state := &sequentialWorkflowState{ + InterruptIndex: i, + } + // Use CompositeInterrupt to funnel the sub-interrupt and add our own state. + // The context for the composite interrupt must be the one from *before* the sub-agent ran. + event := CompositeInterrupt(ctx, "Sequential workflow interrupted", state, + lastActionEvent.Action.internalInterrupted) + + // For backward compatibility, populate the deprecated Data field. + event.Action.Interrupted.Data = &WorkflowInterruptInfo{ + OrigInput: getRunCtx(ctx).RootInput, + SequentialInterruptIndex: i, + SequentialInterruptInfo: lastActionEvent.Action.Interrupted, + } + event.AgentName = lastActionEvent.AgentName + event.RunPath = lastActionEvent.RunPath + + generator.Send(event) + return nil + } + + if lastActionEvent.Action.Exit { + // Forward the event + generator.Send(lastActionEvent) + return nil + } + + generator.Send(lastActionEvent) + } + } + + return nil +} + +// BreakLoopAction is a programmatic-only agent action used to prematurely +// terminate the execution of a loop workflow agent. +// When a loop workflow agent receives this action from a sub-agent, it will stop its +// current iteration and will not proceed to the next one. +// It will mark the BreakLoopAction as Done, signalling to any 'upper level' loop agent +// that this action has been processed and should be ignored further up. +// This action is not intended to be used by LLMs. +type BreakLoopAction struct { + // From records the name of the agent that initiated the break loop action. + From string + // Done is a state flag that can be used by the framework to mark when the + // action has been handled. + Done bool + // CurrentIterations is populated by the framework to record at which + // iteration the loop was broken. + CurrentIterations int +} + +// NewBreakLoopAction creates a new BreakLoopAction, signaling a request +// to terminate the current loop. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +func NewBreakLoopAction(agentName string) *AgentAction { + return &AgentAction{BreakLoop: &BreakLoopAction{ + From: agentName, + }} +} + +func (a *workflowAgent) runLoop(ctx context.Context, generator *AsyncGenerator[*AgentEvent], + loopState *loopWorkflowState, resumeInfo *ResumeInfo, opts ...AgentRunOption) (err error) { + + if len(a.subAgents) == 0 { + return nil + } + + startIter := 0 + startIdx := 0 + + loopCtx := ctx + + if loopState != nil { + // We are resuming. + startIter = loopState.LoopIterations + startIdx = loopState.SubAgentIndex + + // Rebuild the loopCtx to have the correct RunPath up to the point of resumption. + var steps []string + for i := 0; i < startIter; i++ { + for _, subAgent := range a.subAgents { + steps = append(steps, subAgent.Name(loopCtx)) + } + } + for i := 0; i < startIdx; i++ { + steps = append(steps, a.subAgents[i].Name(loopCtx)) + } + loopCtx = updateRunPathOnly(loopCtx, steps...) + } + + for i := startIter; i < a.maxIterations || a.maxIterations == 0; i++ { + for j := startIdx; j < len(a.subAgents); j++ { + subAgent := a.subAgents[j] + + if cancelCtx := getCancelContext(ctx); cancelCtx != nil && cancelCtx.shouldCancel() { + state := &loopWorkflowState{LoopIterations: i, SubAgentIndex: j} + event := cancelAtTransition(ctx, "Loop workflow cancel at transition", state) + generator.Send(event) + return nil + } + + var subIterator *AsyncIterator[*AgentEvent] + if loopState != nil { + wfInfo, _ := resumeInfo.Data.(*WorkflowInterruptInfo) + if wfInfo != nil && wfInfo.SequentialInterruptInfo != nil { + // Sub-agent was interrupted — resume it. + subIterator = subAgent.Resume(loopCtx, &ResumeInfo{ + EnableStreaming: resumeInfo.EnableStreaming, + InterruptInfo: wfInfo.SequentialInterruptInfo, + }, opts...) + } else { + subIterator = subAgent.Run(loopCtx, nil, opts...) + } + loopState = nil // Only resume the first time. + } else { + subIterator = subAgent.Run(loopCtx, nil, opts...) + } + + loopCtx = updateRunPathOnly(loopCtx, subAgent.Name(loopCtx)) + + var lastActionEvent *AgentEvent + var breakLoopEvent *AgentEvent + for { + event, ok := subIterator.Next() + if !ok { + break + } + + if event.Err != nil { + generator.Send(event) + return nil + } + + if lastActionEvent != nil { + if lastActionEvent.Action.BreakLoop != nil && !lastActionEvent.Action.BreakLoop.Done { + lastActionEvent.Action.BreakLoop.Done = true + lastActionEvent.Action.BreakLoop.CurrentIterations = i + breakLoopEvent = lastActionEvent + } + generator.Send(lastActionEvent) + lastActionEvent = nil + } + + if event.Action != nil { + lastActionEvent = event + continue + } + generator.Send(event) + } + + if lastActionEvent != nil { + if lastActionEvent.Action.BreakLoop != nil && !lastActionEvent.Action.BreakLoop.Done { + lastActionEvent.Action.BreakLoop.Done = true + lastActionEvent.Action.BreakLoop.CurrentIterations = i + breakLoopEvent = lastActionEvent + } + + if lastActionEvent.Action.internalInterrupted != nil { + // A sub-agent interrupted. Wrap it with our own loop state. + state := &loopWorkflowState{ + LoopIterations: i, + SubAgentIndex: j, + } + // Use CompositeInterrupt to funnel the sub-interrupt and add our own state. + event := CompositeInterrupt(ctx, "Loop workflow interrupted", state, + lastActionEvent.Action.internalInterrupted) + + // For backward compatibility, populate the deprecated Data field. + event.Action.Interrupted.Data = &WorkflowInterruptInfo{ + OrigInput: getRunCtx(ctx).RootInput, + LoopIterations: i, + SequentialInterruptIndex: j, + SequentialInterruptInfo: lastActionEvent.Action.Interrupted, + } + event.AgentName = lastActionEvent.AgentName + event.RunPath = lastActionEvent.RunPath + + generator.Send(event) + return + } + + if lastActionEvent.Action.Exit { + generator.Send(lastActionEvent) + return + } + + generator.Send(lastActionEvent) + } + + if breakLoopEvent != nil { + return + } + } + + // Reset the sub-agent index for the next iteration of the outer loop. + startIdx = 0 + } + + return nil +} + +func (a *workflowAgent) runParallel(ctx context.Context, generator *AsyncGenerator[*AgentEvent], + parState *parallelWorkflowState, resumeInfo *ResumeInfo, opts ...AgentRunOption) error { + + if len(a.subAgents) == 0 { + return nil + } + + var ( + wg sync.WaitGroup + subInterruptSignals []*core.InterruptSignal + dataMap = make(map[int]*InterruptInfo) + mu sync.Mutex + agentNames map[string]bool + err error + childContexts = make([]context.Context, len(a.subAgents)) + ) + + // If resuming, get the scoped ResumeInfo for each child that needs to be resumed. + if parState != nil { + agentNames, err = getNextResumeAgents(ctx, resumeInfo) + if err != nil { + return err + } + } + + // Fork contexts for each sub-agent + for i := range a.subAgents { + childContexts[i] = forkRunCtx(ctx) + + // If we're resuming and this agent has existing events, add them to the child context + if parState != nil && parState.SubAgentEvents != nil { + if existingEvents, ok := parState.SubAgentEvents[i]; ok { + // Add existing events to the child's lane events + childRunCtx := getRunCtx(childContexts[i]) + if childRunCtx != nil && childRunCtx.Session != nil { + if childRunCtx.Session.LaneEvents == nil { + childRunCtx.Session.LaneEvents = &laneEvents{} + } + childRunCtx.Session.LaneEvents.Events = append(childRunCtx.Session.LaneEvents.Events, existingEvents...) + } + } + } + } + + // Cancel check before spawning parallel goroutines. No sub-agent work + // is in progress, so any cancel mode is honoured at this boundary. + if cancelCtx := getCancelContext(ctx); cancelCtx != nil && cancelCtx.shouldCancel() { + state := ¶llelWorkflowState{} + event := cancelAtTransition(ctx, "Parallel workflow cancel before spawn", state) + generator.Send(event) + return nil + } + + for i := range a.subAgents { + wg.Add(1) + go func(idx int, agent *flowAgent) { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + generator.Send(&AgentEvent{Err: e}) + } + wg.Done() + }() + + var iterator *AsyncIterator[*AgentEvent] + + if _, ok := agentNames[agent.Name(ctx)]; ok { + childResumeInfo := &ResumeInfo{ + EnableStreaming: resumeInfo.EnableStreaming, + } + if wfInfo, ok := resumeInfo.Data.(*WorkflowInterruptInfo); ok && wfInfo != nil { + childResumeInfo.InterruptInfo = wfInfo.ParallelInterruptInfo[idx] + } + iterator = agent.Resume(childContexts[idx], childResumeInfo, opts...) + } else if parState != nil { + // We are resuming, but this child is not in the next points map. + // This means it finished successfully, so we don't run it. + return + } else { + iterator = agent.Run(childContexts[idx], nil, opts...) + } + + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Action != nil && event.Action.internalInterrupted != nil { + mu.Lock() + subInterruptSignals = append(subInterruptSignals, event.Action.internalInterrupted) + dataMap[idx] = event.Action.Interrupted + mu.Unlock() + break + } + generator.Send(event) + } + }(i, a.subAgents[i]) + } + + wg.Wait() + + if len(subInterruptSignals) == 0 { + // Join all child contexts back to the parent + joinRunCtxs(ctx, childContexts...) + return nil + } + + if len(subInterruptSignals) > 0 { + // Before interrupting, collect the current events from each child context + subAgentEvents := make(map[int][]*agentEventWrapper) + for i, childCtx := range childContexts { + childRunCtx := getRunCtx(childCtx) + if childRunCtx != nil && childRunCtx.Session != nil && childRunCtx.Session.LaneEvents != nil { + subAgentEvents[i] = childRunCtx.Session.LaneEvents.Events + } + } + + state := ¶llelWorkflowState{ + SubAgentEvents: subAgentEvents, + } + event := CompositeInterrupt(ctx, "Parallel workflow interrupted", state, subInterruptSignals...) + + // For backward compatibility, populate the deprecated Data field. + event.Action.Interrupted.Data = &WorkflowInterruptInfo{ + OrigInput: getRunCtx(ctx).RootInput, + ParallelInterruptInfo: dataMap, + } + event.AgentName = a.Name(ctx) + event.RunPath = getRunCtx(ctx).RunPath + + generator.Send(event) + } + + return nil +} + +func cancelAtTransition(ctx context.Context, info string, state any) *AgentEvent { + // state is the workflow checkpoint state (e.g. sequentialWorkflowState); + // nil for subContexts because this is a leaf interrupt with no child signals. + is, err := core.Interrupt(ctx, info, state, nil, + core.WithLayerPayload(getRunCtx(ctx).RunPath)) + if err != nil { + return &AgentEvent{Err: err} + } + + contexts := core.ToInterruptContexts(is, allowedAddressSegmentTypes) + + return &AgentEvent{ + Action: &AgentAction{ + Interrupted: &InterruptInfo{ + InterruptContexts: contexts, + }, + internalInterrupted: is, + }, + } +} + +// SequentialAgentConfig is the configuration for NewSequentialAgent. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +type SequentialAgentConfig struct { + Name string + Description string + SubAgents []Agent +} + +// ParallelAgentConfig is the configuration for NewParallelAgent. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +type ParallelAgentConfig struct { + Name string + Description string + SubAgents []Agent +} + +// LoopAgentConfig is the configuration for NewLoopAgent. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +type LoopAgentConfig struct { + Name string + Description string + SubAgents []Agent + + MaxIterations int +} + +func newWorkflowAgent(ctx context.Context, name, desc string, + subAgents []Agent, mode workflowAgentMode, maxIterations int) (*flowAgent, error) { + + wa := &workflowAgent{ + name: name, + description: desc, + mode: mode, + + maxIterations: maxIterations, + } + + fas := make([]Agent, len(subAgents)) + for i, subAgent := range subAgents { + fas[i] = toFlowAgent(ctx, subAgent, WithDisallowTransferToParent()) + } + + fa, err := setSubAgents(ctx, wa, fas) + if err != nil { + return nil, err + } + + wa.subAgents = fa.subAgents + + return fa, nil +} + +// NewSequentialAgent creates an agent that runs sub-agents sequentially. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +func NewSequentialAgent(ctx context.Context, config *SequentialAgentConfig) (ResumableAgent, error) { + return newWorkflowAgent(ctx, config.Name, config.Description, config.SubAgents, workflowAgentModeSequential, 0) +} + +// NewParallelAgent creates an agent that runs sub-agents in parallel. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +func NewParallelAgent(ctx context.Context, config *ParallelAgentConfig) (ResumableAgent, error) { + return newWorkflowAgent(ctx, config.Name, config.Description, config.SubAgents, workflowAgentModeParallel, 0) +} + +// NewLoopAgent creates an agent that loops over sub-agents with a max iteration limit. +// +// NOT RECOMMENDED: Workflow agents are built on agent transfer with full context sharing, +// which has not proven to be more effective empirically. Consider using +// ChatModelAgent with AgentTool or DeepAgent instead for most multi-agent scenarios. +func NewLoopAgent(ctx context.Context, config *LoopAgentConfig) (ResumableAgent, error) { + return newWorkflowAgent(ctx, config.Name, config.Description, config.SubAgents, workflowAgentModeLoop, config.MaxIterations) +} diff --git a/adk/workflow_test.go b/adk/workflow_test.go new file mode 100644 index 0000000..6e68d21 --- /dev/null +++ b/adk/workflow_test.go @@ -0,0 +1,1361 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// mockAgent is a simple implementation of the Agent interface for testing +type mockAgent struct { + name string + description string + responses []*AgentEvent +} + +func (a *mockAgent) Name(_ context.Context) string { + return a.name +} + +func (a *mockAgent) Description(_ context.Context) string { + return a.description +} + +func (a *mockAgent) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iterator, generator := NewAsyncIteratorPair[*AgentEvent]() + + go func() { + defer generator.Close() + + for _, event := range a.responses { + generator.Send(event) + + // If the event has an Exit action, stop sending events + if event.Action != nil && event.Action.Exit { + break + } + } + }() + + return iterator +} + +// newMockAgent creates a new mock agent with the given name, description, and responses +func newMockAgent(name, description string, responses []*AgentEvent) *mockAgent { + return &mockAgent{ + name: name, + description: description, + responses: responses, + } +} + +// TestSequentialAgent tests the sequential workflow agent +func TestSequentialAgent(t *testing.T) { + ctx := context.Background() + + // Create mock agents with predefined responses + agent1 := newMockAgent("Agent1", "First agent", []*AgentEvent{ + { + AgentName: "Agent1", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from Agent1", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + agent2 := newMockAgent("Agent2", "Second agent", []*AgentEvent{ + { + AgentName: "Agent2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from Agent2", nil), + Role: schema.Assistant, + }, + }}, + }) + + // Create a sequential agent with the mock agents + config := &SequentialAgentConfig{ + Name: "SequentialTestAgent", + Description: "Test sequential agent", + SubAgents: []Agent{agent1, agent2}, + } + + sequentialAgent, err := NewSequentialAgent(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, sequentialAgent) + + assert.Equal(t, "Test sequential agent", sequentialAgent.Description(ctx)) + + // Run the sequential agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + + // Initialize the run context + ctx, _ = initRunCtx(ctx, sequentialAgent.Name(ctx), input) + + iterator := sequentialAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // First event should be from agent1 + event1, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event1) + assert.Nil(t, event1.Err) + assert.NotNil(t, event1.Output) + assert.NotNil(t, event1.Output.MessageOutput) + + // Get the message content from agent1 + msg1 := event1.Output.MessageOutput.Message + assert.NotNil(t, msg1) + assert.Equal(t, "Response from Agent1", msg1.Content) + + // Second event should be from agent2 + event2, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event2) + assert.Nil(t, event2.Err) + assert.NotNil(t, event2.Output) + assert.NotNil(t, event2.Output.MessageOutput) + + // Get the message content from agent2 + msg2 := event2.Output.MessageOutput.Message + assert.NotNil(t, msg2) + assert.Equal(t, "Response from Agent2", msg2.Content) + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) +} + +// TestSequentialAgentWithExit tests the sequential workflow agent with an exit action +func TestSequentialAgentWithExit(t *testing.T) { + ctx := context.Background() + + // Create mock agents with predefined responses + agent1 := newMockAgent("Agent1", "First agent", []*AgentEvent{ + { + AgentName: "Agent1", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from Agent1", nil), + Role: schema.Assistant, + }, + }, + Action: &AgentAction{ + Exit: true, + }, + }, + }) + + agent2 := newMockAgent("Agent2", "Second agent", []*AgentEvent{ + { + AgentName: "Agent2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from Agent2", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // Create a sequential agent with the mock agents + config := &SequentialAgentConfig{ + Name: "SequentialTestAgent", + Description: "Test sequential agent", + SubAgents: []Agent{agent1, agent2}, + } + + sequentialAgent, err := NewSequentialAgent(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, sequentialAgent) + + // Run the sequential agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + + ctx, _ = initRunCtx(ctx, sequentialAgent.Name(ctx), input) + + iterator := sequentialAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // First event should be from agent1 with exit action + event1, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event1) + assert.Nil(t, event1.Err) + assert.NotNil(t, event1.Output) + assert.NotNil(t, event1.Output.MessageOutput) + assert.NotNil(t, event1.Action) + assert.True(t, event1.Action.Exit) + + // No more events due to exit action + _, ok = iterator.Next() + assert.False(t, ok) +} + +// TestParallelAgent tests the parallel workflow agent +func TestParallelAgent(t *testing.T) { + ctx := context.Background() + + // Create mock agents with predefined responses + agent1 := newMockAgent("Agent1", "First agent", []*AgentEvent{ + { + AgentName: "Agent1", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from Agent1", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + agent2 := newMockAgent("Agent2", "Second agent", []*AgentEvent{ + { + AgentName: "Agent2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Response from Agent2", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // Create a parallel agent with the mock agents + config := &ParallelAgentConfig{ + Name: "ParallelTestAgent", + Description: "Test parallel agent", + SubAgents: []Agent{agent1, agent2}, + } + + parallelAgent, err := NewParallelAgent(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, parallelAgent) + + // Run the parallel agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + + ctx, _ = initRunCtx(ctx, parallelAgent.Name(ctx), input) + + iterator := parallelAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Collect all events + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + // Should have two events, one from each agent + assert.Equal(t, 2, len(events)) + + // Verify the events + for _, event := range events { + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.NoError(t, err) + + // Check the source agent name and message content + switch event.AgentName { + case "Agent1": + assert.Equal(t, "Response from Agent1", msg.Content) + case "Agent2": + assert.Equal(t, "Response from Agent2", msg.Content) + default: + t.Fatalf("Unexpected source agent name: %s", event.AgentName) + } + } +} + +// TestLoopAgent tests the loop workflow agent +func TestLoopAgent(t *testing.T) { + ctx := context.Background() + + // Create a mock agent that will be called multiple times + agent := newMockAgent("LoopAgent", "Loop agent", []*AgentEvent{ + { + AgentName: "LoopAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Loop iteration", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + // Create a loop agent with the mock agent and max iterations set to 3 + config := &LoopAgentConfig{ + Name: "LoopTestAgent", + Description: "Test loop agent", + SubAgents: []Agent{agent}, + + MaxIterations: 3, + } + + loopAgent, err := NewLoopAgent(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, loopAgent) + + // Run the loop agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + + ctx, _ = initRunCtx(ctx, loopAgent.Name(ctx), input) + + iterator := loopAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Collect all events + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + // Should have 3 events (one for each iteration) + assert.Equal(t, 3, len(events)) + + // Verify all events + for _, event := range events { + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.Equal(t, "Loop iteration", msg.Content) + } +} + +// TestLoopAgentWithBreakLoop tests the loop workflow agent with an break loop action +func TestLoopAgentWithBreakLoop(t *testing.T) { + ctx := context.Background() + + // Create a mock agent that will break the loop after the first iteration + agent := newMockAgent("LoopAgent", "Loop agent", []*AgentEvent{ + { + AgentName: "LoopAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Loop iteration with break loop", nil), + Role: schema.Assistant, + }, + }, + Action: NewBreakLoopAction("LoopAgent"), + }, + }) + + // Create a loop agent with the mock agent and max iterations set to 3 + config := &LoopAgentConfig{ + Name: "LoopTestAgent", + Description: "Test loop agent", + SubAgents: []Agent{agent}, + MaxIterations: 3, + } + + loopAgent, err := NewLoopAgent(ctx, config) + assert.NoError(t, err) + assert.NotNil(t, loopAgent) + + // Run the loop agent + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + ctx, _ = initRunCtx(ctx, loopAgent.Name(ctx), input) + + iterator := loopAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Collect all events + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + // Should have only 1 event due to break loop action + assert.Equal(t, 1, len(events)) + + // Verify the event + event := events[0] + assert.Nil(t, event.Err) + assert.NotNil(t, event.Output) + assert.NotNil(t, event.Output.MessageOutput) + assert.NotNil(t, event.Action) + assert.NotNil(t, event.Action.BreakLoop) + assert.True(t, event.Action.BreakLoop.Done) + assert.Equal(t, "LoopAgent", event.Action.BreakLoop.From) + assert.Equal(t, 0, event.Action.BreakLoop.CurrentIterations) + + msg := event.Output.MessageOutput.Message + assert.NotNil(t, msg) + assert.Equal(t, "Loop iteration with break loop", msg.Content) +} + +// Add these test functions to the existing workflow_test.go file + +// Replace the existing TestWorkflowAgentPanicRecovery function +func TestWorkflowAgentPanicRecovery(t *testing.T) { + ctx := context.Background() + + // Create a panic agent that panics in Run method + panicAgent := &panicMockAgent{ + mockAgent: mockAgent{ + name: "PanicAgent", + description: "Agent that panics", + responses: []*AgentEvent{}, + }, + } + + // Create a sequential agent with the panic agent + config := &SequentialAgentConfig{ + Name: "PanicTestAgent", + Description: "Test agent with panic", + SubAgents: []Agent{panicAgent}, + } + + sequentialAgent, err := NewSequentialAgent(ctx, config) + assert.NoError(t, err) + + // Run the agent and expect panic recovery + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + + ctx, _ = initRunCtx(ctx, sequentialAgent.Name(ctx), input) + iterator := sequentialAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Should receive an error event due to panic recovery + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.Contains(t, event.Err.Error(), "panic") + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) +} + +// Add these new mock agent types that properly panic +type panicMockAgent struct { + mockAgent +} + +func (a *panicMockAgent) Run(_ context.Context, _ *AgentInput, _ ...AgentRunOption) *AsyncIterator[*AgentEvent] { + panic("test panic in agent") +} + +func TestParallelWorkflowResumeWithEvents(t *testing.T) { + ctx := context.Background() + + // Create interruptible agents + sa1 := &myAgent{ + name: "sa1", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + // Send a normal message event first, called event1 + generator.Send(&AgentEvent{ + AgentName: "sa1", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa1 normal message"), + }, + }, + }) + intEvent := Interrupt(ctx, "sa1 interrupt data") + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.WasInterrupted) + assert.Nil(t, info.InterruptState) + assert.True(t, info.IsResumeTarget) + assert.Equal(t, "resume sa1", info.ResumeData) + + // Get the events from session and verify visibility + runCtx := getRunCtx(ctx) + assert.NotNil(t, runCtx.Session, "sa1 resumer should have session") + allEvents := runCtx.Session.getEvents() + + // Assert that allEvents only have 1 event, that is event1 + assert.Equal(t, 1, len(allEvents), "sa1 should only see its own event in session") + assert.Equal(t, "sa1", allEvents[0].AgentEvent.AgentName, "sa1 should see its own event") + assert.Equal(t, "sa1 normal message", allEvents[0].AgentEvent.Output.MessageOutput.Message.Content, "sa1 should see its own message content") + + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } + + sa2 := &myAgent{ + name: "sa2", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + // Send a normal message event first, called event2 + generator.Send(&AgentEvent{ + AgentName: "sa2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa2 normal message"), + }, + }, + }) + intEvent := StatefulInterrupt(ctx, "sa2 interrupt data", "sa2 interrupt") + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.WasInterrupted) + assert.NotNil(t, info.InterruptState) + assert.Equal(t, "sa2 interrupt", info.InterruptState) + assert.True(t, info.IsResumeTarget) + assert.Equal(t, "resume sa2", info.ResumeData) + + // Get the events from session and verify visibility + runCtx := getRunCtx(ctx) + assert.NotNil(t, runCtx.Session, "sa2 resumer should have session") + allEvents := runCtx.Session.getEvents() + + // Assert that allEvents only have 1 event, that is event2 + assert.Equal(t, 1, len(allEvents), "sa2 should only see its own event in session") + assert.Equal(t, "sa2", allEvents[0].AgentEvent.AgentName, "sa2 should see its own event") + assert.Equal(t, "sa2 normal message", allEvents[0].AgentEvent.Output.MessageOutput.Message.Content, "sa2 should see its own message content") + + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } + + sa3 := &myAgent{ + name: "sa3", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "sa3", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa3 completed"), + }, + }, + }) + generator.Close() + return iter + }, + } + + sa4 := &myAgent{ + name: "sa4", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "sa4", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("sa4 completed"), + }, + }, + }) + generator.Close() + return iter + }, + } + + t.Run("test parallel workflow agent", func(t *testing.T) { + // parallel + a, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "parallel agent", + SubAgents: []Agent{sa1, sa2, sa3, sa4}, + }) + assert.NoError(t, err) + runner := NewRunner(ctx, RunnerConfig{ + Agent: a, + CheckPointStore: newMyStore(), + }) + iter := runner.Query(ctx, "hello world", WithCheckPointID("1")) + var ( + events []*AgentEvent + interruptEvent *AgentEvent + ) + + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + continue + } + events = append(events, event) + } + assert.Equal(t, 4, len(events), "should have 4 events (2 normal messages + 2 completed agents)") + + // Verify specific properties of each event + var sa3Event, sa4Event *AgentEvent + for _, event := range events { + switch event.AgentName { + case "sa3": + sa3Event = event + case "sa4": + sa4Event = event + } + } + + // Verify sa3 event properties + assert.NotNil(t, sa3Event, "should have event from sa3") + assert.Equal(t, "sa3", sa3Event.AgentName, "sa3 event should have correct agent name") + assert.Equal(t, []RunStep{{"parallel agent"}, {"sa3"}}, sa3Event.RunPath, "sa3 event should have correct run path") + assert.NotNil(t, sa3Event.Output, "sa3 event should have output") + assert.NotNil(t, sa3Event.Output.MessageOutput, "sa3 event should have message output") + assert.Equal(t, "sa3 completed", sa3Event.Output.MessageOutput.Message.Content, "sa3 event should have correct message content") + + // Verify sa4 event properties + assert.NotNil(t, sa4Event, "should have event from sa4") + assert.Equal(t, "sa4", sa4Event.AgentName, "sa4 event should have correct agent name") + assert.Equal(t, []RunStep{{"parallel agent"}, {"sa4"}}, sa4Event.RunPath, "sa4 event should have correct run path") + assert.NotNil(t, sa4Event.Output, "sa4 event should have output") + assert.NotNil(t, sa4Event.Output.MessageOutput, "sa4 event should have message output") + assert.Equal(t, "sa4 completed", sa4Event.Output.MessageOutput.Message.Content, "sa4 event should have correct message content") + + assert.NotNil(t, interruptEvent) + assert.Equal(t, "parallel agent", interruptEvent.AgentName) + assert.Equal(t, []RunStep{{"parallel agent"}}, interruptEvent.RunPath) + assert.NotNil(t, interruptEvent.Action.Interrupted) + + var sa1InfoFound, sa2InfoFound bool + for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + switch ctx.Info { + case "sa1 interrupt data": + sa1InfoFound = true + case "sa2 interrupt data": + sa2InfoFound = true + } + } + + assert.Equal(t, 2, len(interruptEvent.Action.Interrupted.InterruptContexts)) + assert.True(t, sa1InfoFound) + assert.True(t, sa2InfoFound) + + var parallelInterruptID1, parallelInterruptID2 string + for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + switch ctx.Info { + case "sa1 interrupt data": + parallelInterruptID1 = ctx.ID + case "sa2 interrupt data": + parallelInterruptID2 = ctx.ID + } + } + assert.NotEmpty(t, parallelInterruptID1) + assert.NotEmpty(t, parallelInterruptID2) + + iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{ + Targets: map[string]any{ + parallelInterruptID1: "resume sa1", + parallelInterruptID2: "resume sa2", + }, + }) + assert.NoError(t, err) + _, ok := iter.Next() + assert.False(t, ok) + }) +} + +func TestNestedParallelWorkflow(t *testing.T) { + ctx := context.Background() + + // Create predecessor agent that runs before the parallel structure + predecessorAgent := &myAgent{ + name: "predecessor", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "predecessor", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("predecessor completed"), + }, + }, + }) + generator.Close() + return iter + }, + } + + // Create interruptible inner agents + innerAgent1 := &myAgent{ + name: "inner1", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + + // Verify inner1 can see predecessor's event + runCtx := getRunCtx(ctx) + allEvents := runCtx.Session.getEvents() + assert.Equal(t, 1, len(allEvents), "inner1 should see exactly 1 event (predecessor)") + + assert.Equal(t, "predecessor", allEvents[0].AgentEvent.AgentName, "inner1 should see predecessor event") + assert.Equal(t, "predecessor completed", allEvents[0].AgentEvent.Output.MessageOutput.Message.Content, "inner1 should see predecessor message content") + + generator.Send(&AgentEvent{ + AgentName: "inner1", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("inner1 normal"), + }, + }, + }) + intEvent := Interrupt(ctx, "inner1 interrupt") + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.WasInterrupted) + assert.Equal(t, "resume inner1", info.ResumeData) + + // Verify inner1 can see predecessor's event during resume + runCtx := getRunCtx(ctx) + allEvents := runCtx.Session.getEvents() + assert.Equal(t, 2, len(allEvents), "inner1 should see exactly 2 events (predecessor + own normal message) during resume") + + // Find and verify predecessor event + var foundPredecessor bool + for _, event := range allEvents { + if event.AgentEvent != nil && event.AgentEvent.AgentName == "predecessor" { + foundPredecessor = true + assert.Equal(t, "predecessor completed", event.AgentEvent.Output.MessageOutput.Message.Content) + } + } + assert.True(t, foundPredecessor, "inner1 should see predecessor event during resume") + + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } + + innerAgent2 := &myAgent{ + name: "inner2", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + + // Verify inner2 can see predecessor's event + runCtx := getRunCtx(ctx) + allEvents := runCtx.Session.getEvents() + assert.Equal(t, 1, len(allEvents), "inner2 should see exactly 1 event (predecessor)") + + assert.Equal(t, "predecessor", allEvents[0].AgentEvent.AgentName, "inner2 should see predecessor event") + assert.Equal(t, "predecessor completed", allEvents[0].AgentEvent.Output.MessageOutput.Message.Content, "inner2 should see predecessor message content") + + generator.Send(&AgentEvent{ + AgentName: "inner2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("inner2 normal"), + }, + }, + }) + intEvent := StatefulInterrupt(ctx, "inner2 interrupt", "inner2 state") + generator.Send(intEvent) + generator.Close() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + assert.True(t, info.WasInterrupted) + assert.Equal(t, "inner2 state", info.InterruptState) + assert.Equal(t, "resume inner2", info.ResumeData) + + // Verify inner2 can see predecessor's event during resume + runCtx := getRunCtx(ctx) + allEvents := runCtx.Session.getEvents() + assert.Equal(t, 2, len(allEvents), "inner2 should see exactly 2 events (predecessor + own normal message) during resume") + + // Find and verify predecessor event + var foundPredecessor bool + for _, event := range allEvents { + if event.AgentEvent != nil && event.AgentEvent.AgentName == "predecessor" { + foundPredecessor = true + assert.Equal(t, "predecessor completed", event.AgentEvent.Output.MessageOutput.Message.Content) + } + } + assert.True(t, foundPredecessor, "inner2 should see predecessor event during resume") + + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Close() + return iter + }, + } + + // Create inner parallel workflow + innerParallel, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "inner parallel", + SubAgents: []Agent{innerAgent1, innerAgent2}, + }) + assert.NoError(t, err) + + // Create simple outer agents + outerAgent1 := &myAgent{ + name: "outer1", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "outer1", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("outer1 completed"), + }, + }, + }) + generator.Close() + return iter + }, + } + + outerAgent2 := &myAgent{ + name: "outer2", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + generator.Send(&AgentEvent{ + AgentName: "outer2", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("outer2 completed"), + }, + }, + }) + generator.Close() + return iter + }, + } + + // Create outer parallel workflow with nested parallel agent + outerParallel, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + Name: "outer parallel", + SubAgents: []Agent{outerAgent1, innerParallel, outerAgent2}, + }) + assert.NoError(t, err) + + // Create successor agent that runs after the parallel structure + successorAgent := &myAgent{ + name: "successor", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + + // Verify successor can see all events from predecessor and parallel agents + runCtx := getRunCtx(ctx) + allEvents := runCtx.Session.getEvents() + assert.GreaterOrEqual(t, len(allEvents), 5, "successor should see all events") + + var foundPredecessor, foundOuter1, foundOuter2, foundInner1, foundInner2 bool + for _, event := range allEvents { + if event.AgentEvent != nil { + switch event.AgentEvent.AgentName { + case "predecessor": + foundPredecessor = true + assert.Equal(t, "predecessor completed", event.AgentEvent.Output.MessageOutput.Message.Content) + case "outer1": + foundOuter1 = true + case "outer2": + foundOuter2 = true + case "inner1": + foundInner1 = true + case "inner2": + foundInner2 = true + } + } + } + + assert.True(t, foundPredecessor, "successor should see predecessor event") + assert.True(t, foundOuter1, "successor should see outer1 event") + assert.True(t, foundOuter2, "successor should see outer2 event") + assert.True(t, foundInner1, "successor should see inner1 event") + assert.True(t, foundInner2, "successor should see inner2 event") + + generator.Send(&AgentEvent{ + AgentName: "successor", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.UserMessage("successor completed"), + }, + }, + }) + generator.Close() + return iter + }, + } + + // Create sequential workflow: predecessor -> parallel -> successor + sequentialWorkflow, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + Name: "sequential workflow", + SubAgents: []Agent{predecessorAgent, outerParallel, successorAgent}, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: sequentialWorkflow, + CheckPointStore: newMyStore(), + }) + + iter := runner.Query(ctx, "test nested parallel with predecessor and successor", WithCheckPointID("nested-parallel-test")) + + var events []*AgentEvent + var interruptEvent *AgentEvent + for event, ok := iter.Next(); ok; event, ok = iter.Next() { + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + continue + } + events = append(events, event) + } + + // Should get events from predecessor, outer agents, and inner normal messages (successor doesn't run due to interruption) + assert.Equal(t, 5, len(events), "should have 5 events (predecessor + 2 outer + 2 inner)") + if interruptEvent == nil { + t.Fatal("should have interrupt event") + } + + // Resume the inner parallel workflow + var innerInterruptID1, innerInterruptID2 string + for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts { + switch ctx.Info { + case "inner1 interrupt": + innerInterruptID1 = ctx.ID + case "inner2 interrupt": + innerInterruptID2 = ctx.ID + } + } + + iter, err = runner.ResumeWithParams(ctx, "nested-parallel-test", &ResumeParams{ + Targets: map[string]any{ + innerInterruptID1: "resume inner1", + innerInterruptID2: "resume inner2", + }, + }) + assert.NoError(t, err) + + // Verify resume completes successfully and successor runs + var resumeEvents []*AgentEvent + for event, ok := iter.Next(); ok; event, ok = iter.Next() { + resumeEvents = append(resumeEvents, event) + } + + // Should get successor event after resume + assert.Equal(t, 1, len(resumeEvents), "should have successor event after resume") + assert.Equal(t, "successor", resumeEvents[0].AgentName) +} + +// TestWorkflowAgentUnsupportedMode tests unsupported workflow mode error (lines 65-71) +func TestWorkflowAgentUnsupportedMode(t *testing.T) { + ctx := context.Background() + + // Create a workflow agent with unsupported mode + agent := &workflowAgent{ + name: "UnsupportedModeAgent", + description: "Agent with unsupported mode", + subAgents: []*flowAgent{}, + mode: workflowAgentMode(999), + } + + // Run the agent and expect error + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + + ctx, _ = initRunCtx(ctx, agent.Name(ctx), input) + iterator := agent.Run(ctx, input) + assert.NotNil(t, iterator) + + // Should receive an error event due to unsupported mode + event, ok := iterator.Next() + assert.True(t, ok) + assert.NotNil(t, event) + assert.NotNil(t, event.Err) + assert.Contains(t, event.Err.Error(), "unsupported workflow agent mode") + + // No more events + _, ok = iterator.Next() + assert.False(t, ok) +} + +func TestFilterOptions(t *testing.T) { + a1 := &myAgent{ + name: "Agent1", + runFn: func(ctx context.Context, input *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + o := GetImplSpecificOptions[myAgentOptions](nil, opts...) + assert.Equal(t, "Agent1", o.value) + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + gen.Close() + return iter + }, + } + a2 := &myAgent{ + name: "Agent2", + runFn: func(ctx context.Context, input *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + o := GetImplSpecificOptions[myAgentOptions](nil, opts...) + assert.Equal(t, "Agent2", o.value) + iter, gen := NewAsyncIteratorPair[*AgentEvent]() + gen.Close() + return iter + }, + } + ctx := context.Background() + // sequential + seqAgent, err := NewSequentialAgent(ctx, &SequentialAgentConfig{ + SubAgents: []Agent{a1, a2}, + }) + assert.NoError(t, err) + iter := seqAgent.Run(ctx, &AgentInput{}, withValue("Agent1").DesignateAgent("Agent1"), withValue("Agent2").DesignateAgent("Agent2")) + _, ok := iter.Next() + assert.False(t, ok) + + // parallel + parAgent, err := NewParallelAgent(ctx, &ParallelAgentConfig{ + SubAgents: []Agent{a1, a2}, + }) + assert.NoError(t, err) + iter = parAgent.Run(ctx, &AgentInput{}, withValue("Agent1").DesignateAgent("Agent1"), withValue("Agent2").DesignateAgent("Agent2")) + _, ok = iter.Next() + assert.False(t, ok) +} + +func TestLoopAgentWithError(t *testing.T) { + ctx := context.Background() + + iterationCount := 0 + agent := &myAgent{ + name: "ErrorAgent", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer generator.Close() + iterationCount++ + if iterationCount == 3 { + generator.Send(&AgentEvent{Err: fmt.Errorf("error on iteration %d", iterationCount)}) + return + } + generator.Send(&AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.AssistantMessage(fmt.Sprintf("iteration %d", iterationCount), nil), + Role: schema.Assistant, + }, + }, + }) + }() + return iter + }, + } + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "LoopErrorTestAgent", + SubAgents: []Agent{agent}, + MaxIterations: 10, + }) + assert.NoError(t, err) + + input := &AgentInput{Messages: []Message{schema.UserMessage("test")}} + ctx, _ = initRunCtx(ctx, loopAgent.Name(ctx), input) + iterator := loopAgent.Run(ctx, input) + + var events []*AgentEvent + var errorEvent *AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Err != nil { + errorEvent = event + } else { + events = append(events, event) + } + } + + assert.Equal(t, 2, len(events), "should have 2 successful iterations before error") + assert.NotNil(t, errorEvent, "should have received error event") + assert.Contains(t, errorEvent.Err.Error(), "error on iteration 3") + assert.Equal(t, 3, iterationCount, "loop should stop at iteration 3") +} + +func TestWorkflowCallbackHandlerNotDoubled(t *testing.T) { + ctx := context.Background() + store := newMyStore() + + var globalCallbackCount int + var designatedCallbackCount int + var mu sync.Mutex + + globalHandler := callbacks.NewHandlerBuilder().OnStartFn( + func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component == ComponentOfAgent && info.Name == "SubSubAgent" { + mu.Lock() + globalCallbackCount++ + mu.Unlock() + } + return ctx + }).Build() + + designatedHandler := callbacks.NewHandlerBuilder().OnStartFn( + func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Component == ComponentOfAgent && info.Name == "SubSubAgent" { + mu.Lock() + designatedCallbackCount++ + mu.Unlock() + } + return ctx + }).Build() + + iterationCount := 0 + shouldInterrupt := true + subSubAgent := &myAgent{ + name: "SubSubAgent", + runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer generator.Close() + iterationCount++ + if shouldInterrupt && iterationCount == 2 { + generator.Send(Interrupt(ctx, "test_interrupt")) + return + } + generator.Send(&AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.AssistantMessage(fmt.Sprintf("iteration %d", iterationCount), nil), + Role: schema.Assistant, + }, + }, + }) + }() + return iter + }, + resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] { + iter, generator := NewAsyncIteratorPair[*AgentEvent]() + go func() { + defer generator.Close() + iterationCount++ + generator.Send(&AgentEvent{ + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + Message: schema.AssistantMessage(fmt.Sprintf("resumed iteration %d", iterationCount), nil), + Role: schema.Assistant, + }, + }, + }) + }() + return iter + }, + } + + subWorkflow, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "SubWorkflow", + SubAgents: []Agent{subSubAgent}, + MaxIterations: 2, + }) + assert.NoError(t, err) + + parentWorkflow, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "ParentWorkflow", + SubAgents: []Agent{subWorkflow}, + MaxIterations: 2, + }) + assert.NoError(t, err) + + runner := NewRunner(ctx, RunnerConfig{ + Agent: parentWorkflow, + CheckPointStore: store, + }) + + opts := []AgentRunOption{ + WithCallbacks(globalHandler), + WithCallbacks(designatedHandler).DesignateAgent("ParentWorkflow", "SubSubAgent"), + WithCheckPointID("cp1"), + } + + iterator := runner.Run(ctx, []Message{schema.UserMessage("test")}, opts...) + + var interruptEvent *AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Action != nil && event.Action.Interrupted != nil { + interruptEvent = event + } + } + + assert.NotNil(t, interruptEvent) + assert.Equal(t, 2, iterationCount) + assert.Equal(t, 2, globalCallbackCount) + assert.Equal(t, 2, designatedCallbackCount) + + shouldInterrupt = false + var rootCauseID string + for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts { + if intCtx.IsRootCause { + rootCauseID = intCtx.ID + break + } + } + + resumeIter, err := runner.ResumeWithParams(ctx, "cp1", &ResumeParams{ + Targets: map[string]any{rootCauseID: nil}, + }, opts...) + assert.NoError(t, err) + + for { + _, ok := resumeIter.Next() + if !ok { + break + } + } + + assert.Equal(t, 5, iterationCount) + assert.Equal(t, 5, globalCallbackCount) + assert.Equal(t, 5, designatedCallbackCount) +} + +func TestLoopAgentWithBreakLoopFollowedByMoreEvents(t *testing.T) { + ctx := context.Background() + + agent := newMockAgent("SubAgent", "Sub agent", []*AgentEvent{ + { + AgentName: "SubAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.ToolMessage("tool result", "call_123"), + Role: schema.Tool, + }, + }, + Action: NewBreakLoopAction("SubAgent"), + }, + { + AgentName: "SubAgent", + Output: &AgentOutput{ + MessageOutput: &MessageVariant{ + IsStreaming: false, + Message: schema.AssistantMessage("Final response after tool", nil), + Role: schema.Assistant, + }, + }, + }, + }) + + loopAgent, err := NewLoopAgent(ctx, &LoopAgentConfig{ + Name: "LoopTestAgent", + Description: "Test loop agent", + SubAgents: []Agent{agent}, + MaxIterations: 3, + }) + assert.NoError(t, err) + assert.NotNil(t, loopAgent) + + input := &AgentInput{ + Messages: []Message{ + schema.UserMessage("Test input"), + }, + } + ctx, _ = initRunCtx(ctx, loopAgent.Name(ctx), input) + + iterator := loopAgent.Run(ctx, input) + assert.NotNil(t, iterator) + + var events []*AgentEvent + for { + event, ok := iterator.Next() + if !ok { + break + } + events = append(events, event) + } + + assert.Equal(t, 2, len(events), "should have 2 events (tool event with BreakLoop + final response) and loop should break") + + assert.NotNil(t, events[0].Action, "first event should have an action") + assert.NotNil(t, events[0].Action.BreakLoop, "first event should have BreakLoop action") + assert.True(t, events[0].Action.BreakLoop.Done, "BreakLoop should be marked as Done") + assert.Equal(t, "SubAgent", events[0].Action.BreakLoop.From) + assert.Equal(t, 0, events[0].Action.BreakLoop.CurrentIterations) + assert.Equal(t, schema.Tool, events[0].Output.MessageOutput.Role, "first event should be tool message") + + assert.Nil(t, events[1].Action, "second event should not have an action") + assert.Equal(t, schema.Assistant, events[1].Output.MessageOutput.Role, "second event should be assistant message") + assert.Equal(t, "Final response after tool", events[1].Output.MessageOutput.Message.Content) +} diff --git a/adk/wrappers.go b/adk/wrappers.go new file mode 100644 index 0000000..ef5c035 --- /dev/null +++ b/adk/wrappers.go @@ -0,0 +1,1448 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "io" + "reflect" + "sync" + + "github.com/google/uuid" + + "github.com/cloudwego/eino/adk/internal" + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +type typedGenerateEndpoint[M MessageType] func(ctx context.Context, input []M, opts ...model.Option) (M, error) +type typedStreamEndpoint[M MessageType] func(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) + +type typedModelWrapperConfig[M MessageType] struct { + handlers []TypedChatModelAgentMiddleware[M] + middlewares []AgentMiddleware + retryConfig *TypedModelRetryConfig[M] + failoverConfig *ModelFailoverConfig[M] + toolInfos []*schema.ToolInfo + cancelContext *cancelContext +} + +type modelWrapperConfig = typedModelWrapperConfig[*schema.Message] + +func buildModelWrappers[M MessageType](m model.BaseModel[M], config *typedModelWrapperConfig[M]) model.BaseModel[M] { + return buildModelWrappersImpl(m, config) +} + +func buildModelWrappersImpl[M MessageType](m model.BaseModel[M], config *typedModelWrapperConfig[M]) model.BaseModel[M] { + var wrapped = m + + if config.failoverConfig != nil { + wrapped = &typedFailoverProxyModel[M]{} + } + + if !components.IsCallbacksEnabled(wrapped) { + wrapped = typedCallbackInjectionModelWrapper[M]{}.wrapModel(wrapped) + } + + wrapped = &typedStateModelWrapper[M]{ + inner: wrapped, + original: m, + handlers: config.handlers, + middlewares: config.middlewares, + toolInfos: config.toolInfos, + modelRetryConfig: config.retryConfig, + modelFailoverConfig: config.failoverConfig, + cancelContext: config.cancelContext, + } + + return wrapped +} + +type typedCallbackInjectionModelWrapper[M MessageType] struct{} + +func (w typedCallbackInjectionModelWrapper[M]) wrapModel(m model.BaseModel[M]) model.BaseModel[M] { + return &typedCallbackInjectedModel[M]{inner: m} +} + +type typedCallbackInjectedModel[M MessageType] struct { + inner model.BaseModel[M] +} + +func (m *typedCallbackInjectedModel[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + ctx = callbacks.OnStart(ctx, input) + result, err := m.inner.Generate(ctx, input, opts...) + if err != nil { + callbacks.OnError(ctx, err) + var zero M + return zero, err + } + callbacks.OnEnd(ctx, result) + return result, nil +} + +func (m *typedCallbackInjectedModel[M]) Stream(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + ctx = callbacks.OnStart(ctx, input) + result, err := m.inner.Stream(ctx, input, opts...) + if err != nil { + callbacks.OnError(ctx, err) + return nil, err + } + _, wrappedStream := callbacks.OnEndWithStreamOutput(ctx, result) + return wrappedStream, nil +} + +func handlersToToolMiddlewares[M MessageType](handlers []TypedChatModelAgentMiddleware[M]) []compose.ToolMiddleware { + var middlewares []compose.ToolMiddleware + // Forward iteration: compose.wrapToolCall applies middlewares in reverse order + // (len-1 down to 0), so keeping the original handler order here means + // handlers[0] ends up outermost — matching the model wrapping convention. + for _, handler := range handlers { + + m := compose.ToolMiddleware{} + + h := handler + m.Invokable = func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + tCtx := &ToolContext{ + Name: input.Name, + CallID: input.CallID, + } + wrappedEndpoint, err := h.WrapInvokableToolCall( + ctx, + func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + output, err := next(ctx, &compose.ToolInput{ + Name: input.Name, + CallID: input.CallID, + Arguments: argumentsInJSON, + CallOptions: opts, + }) + if err != nil { + return "", err + } + return output.Result, nil + }, + tCtx, + ) + if err != nil { + return nil, err + } + result, err := wrappedEndpoint(ctx, input.Arguments, input.CallOptions...) + if err != nil { + return nil, err + } + return &compose.ToolOutput{Result: result}, nil + } + } + + m.Streamable = func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + tCtx := &ToolContext{ + Name: input.Name, + CallID: input.CallID, + } + wrappedEndpoint, err := h.WrapStreamableToolCall( + ctx, + func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + output, err := next(ctx, &compose.ToolInput{ + Name: input.Name, + CallID: input.CallID, + Arguments: argumentsInJSON, + CallOptions: opts, + }) + if err != nil { + return nil, err + } + return output.Result, nil + }, + tCtx, + ) + if err != nil { + return nil, err + } + result, err := wrappedEndpoint(ctx, input.Arguments, input.CallOptions...) + if err != nil { + return nil, err + } + return &compose.StreamToolOutput{Result: result}, nil + } + } + + m.EnhancedInvokable = func(next compose.EnhancedInvokableToolEndpoint) compose.EnhancedInvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + tCtx := &ToolContext{ + Name: input.Name, + CallID: input.CallID, + } + wrappedEndpoint, err := h.WrapEnhancedInvokableToolCall( + ctx, + func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + output, err := next(ctx, &compose.ToolInput{ + Name: input.Name, + CallID: input.CallID, + Arguments: toolArgument.Text, + CallOptions: opts, + }) + if err != nil { + return nil, err + } + return output.Result, nil + }, + tCtx, + ) + if err != nil { + return nil, err + } + result, err := wrappedEndpoint(ctx, &schema.ToolArgument{Text: input.Arguments}, input.CallOptions...) + if err != nil { + return nil, err + } + return &compose.EnhancedInvokableToolOutput{Result: result}, nil + } + } + + m.EnhancedStreamable = func(next compose.EnhancedStreamableToolEndpoint) compose.EnhancedStreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + tCtx := &ToolContext{ + Name: input.Name, + CallID: input.CallID, + } + wrappedEndpoint, err := h.WrapEnhancedStreamableToolCall( + ctx, + func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + output, err := next(ctx, &compose.ToolInput{ + Name: input.Name, + CallID: input.CallID, + Arguments: toolArgument.Text, + CallOptions: opts, + }) + if err != nil { + return nil, err + } + return output.Result, nil + }, + tCtx, + ) + if err != nil { + return nil, err + } + result, err := wrappedEndpoint(ctx, &schema.ToolArgument{Text: input.Arguments}, input.CallOptions...) + if err != nil { + return nil, err + } + return &compose.EnhancedStreamableToolOutput{Result: result}, nil + } + } + + middlewares = append(middlewares, m) + } + return middlewares +} + +type typedEventSenderModelWrapper[M MessageType] struct { + *TypedBaseChatModelAgentMiddleware[M] +} + +// NewEventSenderModelWrapper creates a ChatModelAgentMiddleware that sends model output as agent events. +func NewEventSenderModelWrapper() ChatModelAgentMiddleware { + return &typedEventSenderModelWrapper[*schema.Message]{ + TypedBaseChatModelAgentMiddleware: &TypedBaseChatModelAgentMiddleware[*schema.Message]{}, + } +} + +func (w *typedEventSenderModelWrapper[M]) WrapModel(_ context.Context, m model.BaseModel[M], mc *TypedModelContext[M]) (model.BaseModel[M], error) { + inner := m + if mc != nil && mc.cancelContext != nil { + inner = &typedCancelMonitoredModel[M]{ + inner: inner, + cancelContext: mc.cancelContext, + } + } + var retryConfig *TypedModelRetryConfig[M] + if mc != nil { + retryConfig = mc.ModelRetryConfig + } + var failoverConfig *ModelFailoverConfig[M] + if mc != nil { + failoverConfig = mc.ModelFailoverConfig + } + return &typedEventSenderModel[M]{inner: inner, modelRetryConfig: retryConfig, modelFailoverConfig: failoverConfig}, nil +} + +type typedEventSenderModel[M MessageType] struct { + inner model.BaseModel[M] + modelRetryConfig *TypedModelRetryConfig[M] + modelFailoverConfig *ModelFailoverConfig[M] +} + +func (m *typedEventSenderModel[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + result, err := m.inner.Generate(ctx, input, opts...) + if err != nil { + var zero M + return zero, err + } + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + if execCtx != nil && execCtx.suppressEventSend { + return result, nil + } + if execCtx == nil || execCtx.generator == nil { + var zero M + return zero, errors.New("generator is nil when sending event in Generate: ensure agent state is properly initialized") + } + + event := typedModelOutputEvent(copyMessage(result), nil) + execCtx.send(event) + + return result, nil +} + +func (m *typedEventSenderModel[M]) Stream(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + result, err := m.inner.Stream(ctx, input, opts...) + if err != nil { + return nil, err + } + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + if execCtx == nil || execCtx.generator == nil { + result.Close() + return nil, errors.New("generator is nil when sending event in Stream: ensure agent state is properly initialized") + } + + streams := result.Copy(2) + + eventStream := streams[0] + if convertOpts := m.buildStreamConvertOptions(ctx); len(convertOpts) > 0 { + eventStream = schema.StreamReaderWithConvert(streams[0], + func(msg M) (M, error) { return msg, nil }, + convertOpts...) + } + + var zero M + event := typedModelOutputEvent[M](zero, eventStream) + execCtx.send(event) + + return streams[1], nil +} + +// buildStreamConvertOptions constructs ConvertOption hooks that gate stream termination behind +// the retry verdict signal protocol. +// +// Verdict signal lifecycle: +// - streamWithShouldRetry creates a new retryVerdictSignal per retry attempt, stores it in +// execCtx.retryVerdictSignal, and sends exactly one retryVerdict after ShouldRetry decides. +// - The closures below capture a *retryVerdictSignal that is nil at closure-creation time; they +// read the live value from execCtx.retryVerdictSignal, which is set before each model call. +// +// Two hooks cooperate to cover all stream termination paths: +// - WithErrWrapper intercepts mid-stream errors. It blocks on the verdict to decide +// whether to wrap the error as WillRetryError (rejected attempt) or pass it through (accepted). +// - WithOnEOF intercepts clean EOF (successful stream). It blocks on the verdict to +// either inject a WillRetryError (rejected) or pass through io.EOF (accepted). +// +// Both hooks share a sync.Once-guarded reader so the verdict channel is read at most once. +// This prevents a goroutine leak when a mid-stream error is followed by EOF: errWrapper fires +// first (caching the verdict), and onEOF reuses the cached value instead of blocking on a +// drained channel. +func (m *typedEventSenderModel[M]) buildStreamConvertOptions(ctx context.Context) []schema.ConvertOption { + var retryAttempt int + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + retryAttempt = st.getRetryAttempt() + return nil + }) + + wrapWithCancelGuard := func(inner func(error) error) func(error) error { + return func(err error) error { + if errors.Is(err, ErrStreamCanceled) { + return err + } + return inner(err) + } + } + + var opts []schema.ConvertOption + + var retryWrapper func(error) error + if m.modelRetryConfig != nil { + if m.modelRetryConfig.ShouldRetry != nil { + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + signal := (*retryVerdictSignal)(nil) + if execCtx != nil { + signal = execCtx.retryVerdictSignal + } + if signal != nil { + var ( + verdictOnce sync.Once + cachedVerdict retryVerdict + ) + readVerdict := func() retryVerdict { + verdictOnce.Do(func() { + cachedVerdict = <-signal.ch + }) + return cachedVerdict + } + + retryWrapper = wrapWithCancelGuard(func(err error) error { + verdict := readVerdict() + if verdict.WillRetry { + return &WillRetryError{ + ErrStr: err.Error(), + RetryAttempt: verdict.RetryAttempt, + rejectReason: verdict.RejectReason, + err: err, + } + } + return err + }) + + opts = append(opts, schema.WithOnEOF(func() (any, error) { + verdict := readVerdict() + if verdict.WillRetry { + return nil, &WillRetryError{ + ErrStr: verdict.Err.Error(), + RetryAttempt: verdict.RetryAttempt, + rejectReason: verdict.RejectReason, + err: verdict.Err, + } + } + return nil, io.EOF + })) + } + } else { + retryWrapper = wrapWithCancelGuard( + genErrWrapper(ctx, m.modelRetryConfig.MaxRetries, retryAttempt, m.modelRetryConfig.IsRetryAble), + ) + } + } + + hasFailover := m.modelFailoverConfig != nil + // failoverHasMoreAttempts is set by failoverModelWrapper before each inner call. + // It is true when additional failover attempts remain after the current one, + // meaning stream errors should be wrapped as WillRetryError so the flow layer + // skips them. On the final attempt it is false, so the error propagates normally. + failoverHasMore := getFailoverHasMoreAttempts(ctx) + + if retryWrapper == nil && !(hasFailover && failoverHasMore) { + return opts + } + + combinedErrWrapper := func(err error) error { + // If retry is configured and will retry this error, use the retry wrapper's WillRetryError. + if retryWrapper != nil { + wrapped := retryWrapper(err) + if errors.As(wrapped, new(*WillRetryError)) { + return wrapped + } + } + // Retry won't handle this error (either exhausted or not configured), but + // failover still has more attempts remaining. Wrap it as WillRetryError so + // the flow layer skips this event from the failed attempt. + if hasFailover && failoverHasMore { + if errors.Is(err, ErrStreamCanceled) { + return err + } + return &WillRetryError{ErrStr: err.Error(), err: err} + } + return err + } + opts = append(opts, schema.WithErrWrapper(combinedErrWrapper)) + + return opts +} + +func copyMessage[M MessageType](msg M) M { + switch v := any(msg).(type) { + case *schema.Message: + cp := *v + return any(&cp).(M) + case *schema.AgenticMessage: + cp := *v + return any(&cp).(M) + default: + return msg + } +} + +// typedSetMessageID sets a specific message ID in Extra. +func typedSetMessageID[M MessageType](msg M, id string) { + switch v := any(msg).(type) { + case *schema.Message: + v.Extra = internal.SetMessageID(v.Extra, id) + case *schema.AgenticMessage: + v.Extra = internal.SetMessageID(v.Extra, id) + } +} + +// GetMessageID returns the eino-internal message ID from the given message, or "". +func GetMessageID[M MessageType](msg M) string { + switch v := any(msg).(type) { + case *schema.Message: + return internal.GetMessageID(v.Extra) + case *schema.AgenticMessage: + return internal.GetMessageID(v.Extra) + default: + return "" + } +} + +// EnsureMessageID assigns a UUID v4 message ID if the message doesn't have one. +// Idempotent: if ID already set, no-op. +// Middleware authors should call this before SendEvent if they create messages. +func EnsureMessageID[M MessageType](msg M) { + switch v := any(msg).(type) { + case *schema.Message: + v.Extra = internal.EnsureMessageID(v.Extra) + case *schema.AgenticMessage: + v.Extra = internal.EnsureMessageID(v.Extra) + } +} + +func typedPopToolGenAction[M MessageType](ctx context.Context, toolName string) *AgentAction { + toolCallID := compose.GetToolCallID(ctx) + + var action *AgentAction + _ = compose.ProcessState(ctx, func(ctx context.Context, st *typedState[M]) error { + if len(toolCallID) > 0 { + if a := st.popToolGenAction(toolCallID); a != nil { + action = a + return nil + } + } + + if a := st.popToolGenAction(toolName); a != nil { + action = a + } + + return nil + }) + + return action +} + +type typedEventSenderToolWrapper[M MessageType] struct { + *TypedBaseChatModelAgentMiddleware[M] +} + +func (*typedEventSenderToolWrapper[M]) isEventSenderToolWrapper() {} + +// eventSenderToolWrapperMarker enables cross-type detection of eventSenderToolWrapper +// in generic contexts. hasUserEventSenderToolWrapper[M] receives +// []TypedChatModelAgentMiddleware[M], so when M is *schema.AgenticMessage, a direct +// type assertion to *eventSenderToolWrapper (which implements the *schema.Message alias) +// would fail. The marker interface bridges this gap. +type eventSenderToolWrapperMarker interface{ isEventSenderToolWrapper() } + +// NewEventSenderToolWrapper returns a ChatModelAgentMiddleware that sends tool result events. +// By default, the framework places this before all user middlewares (outermost), so events +// reflect the fully processed tool output. To control exactly where events are emitted, +// include this in ChatModelAgentConfig.Handlers at the desired position. +// When detected in Handlers, the framework skips the default event sender to avoid duplicates. +func NewEventSenderToolWrapper() ChatModelAgentMiddleware { + return newTypedEventSenderToolWrapper[*schema.Message]() +} + +// newTypedEventSenderToolWrapper creates a typed event sender wrapper for the given message type. +// This is used internally to ensure the default event sender matches the agent's message type +// (e.g. *schema.AgenticMessage agents need an AgenticMessage-typed wrapper so that +// compose.ProcessState can access the correct state type). +func newTypedEventSenderToolWrapper[M MessageType]() *typedEventSenderToolWrapper[M] { + return &typedEventSenderToolWrapper[M]{ + TypedBaseChatModelAgentMiddleware: &TypedBaseChatModelAgentMiddleware[M]{}, + } +} + +// textToFunctionToolResultBlocks wraps a plain text string into FunctionToolResultBlocks. +func textToFunctionToolResultBlocks(text string) []*schema.FunctionToolResultContentBlock { + if text == "" { + return nil + } + return []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: text}}, + } +} + +// functionToolResultAgenticMessage constructs a function tool result message with AgenticRoleType "user". +func functionToolResultAgenticMessage(callID, name string, content []*schema.FunctionToolResultContentBlock) *schema.AgenticMessage { + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.FunctionToolResult{ + CallID: callID, + Name: name, + Content: content, + }), + }, + } +} + +func markAgenticMessageStreamingMeta(msg *schema.AgenticMessage, index int) { + if msg == nil { + return + } + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + block.StreamingMeta = &schema.StreamingMeta{Index: index} + } +} + +func toolSearchResultAgenticMessage(callID, name string, tr *schema.ToolResult) (*schema.AgenticMessage, bool) { + if tr == nil || len(tr.Parts) != 1 { + return nil, false + } + + part := tr.Parts[0] + if part.Type != schema.ToolPartTypeToolSearchResult || part.ToolSearchResult == nil { + return nil, false + } + + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.ToolSearchFunctionToolResult{ + CallID: callID, + Name: name, + Result: part.ToolSearchResult, + }), + }, + }, true +} + +func toolResultAgenticMessage(callID, name string, tr *schema.ToolResult) *schema.AgenticMessage { + if msg, ok := toolSearchResultAgenticMessage(callID, name, tr); ok { + return msg + } + return functionToolResultAgenticMessage(callID, name, toolResultToBlocks(tr)) +} + +// toolResultToBlocks converts a ToolResult's multimodal parts into FunctionToolResultBlocks. +// This preserves all media types (text, image, audio, video, file), unlike toolResultText +// which only extracts text. +func toolResultToBlocks(tr *schema.ToolResult) []*schema.FunctionToolResultContentBlock { + if tr == nil || len(tr.Parts) == 0 { + return nil + } + blocks := make([]*schema.FunctionToolResultContentBlock, 0, len(tr.Parts)) + for _, p := range tr.Parts { + var block *schema.FunctionToolResultContentBlock + switch p.Type { + case schema.ToolPartTypeText: + block = &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeText, + Text: &schema.UserInputText{Text: p.Text}, + Extra: p.Extra, + } + case schema.ToolPartTypeImage: + if p.Image != nil { + block = &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeImage, + Image: &schema.UserInputImage{ + URL: derefString(p.Image.URL), + Base64Data: derefString(p.Image.Base64Data), + MIMEType: p.Image.MIMEType, + }, + Extra: p.Extra, + } + } + case schema.ToolPartTypeAudio: + if p.Audio != nil { + block = &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeAudio, + Audio: &schema.UserInputAudio{ + URL: derefString(p.Audio.URL), + Base64Data: derefString(p.Audio.Base64Data), + MIMEType: p.Audio.MIMEType, + }, + Extra: p.Extra, + } + } + case schema.ToolPartTypeVideo: + if p.Video != nil { + block = &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeVideo, + Video: &schema.UserInputVideo{ + URL: derefString(p.Video.URL), + Base64Data: derefString(p.Video.Base64Data), + MIMEType: p.Video.MIMEType, + }, + Extra: p.Extra, + } + } + case schema.ToolPartTypeFile: + if p.File != nil { + block = &schema.FunctionToolResultContentBlock{ + Type: schema.FunctionToolResultContentBlockTypeFile, + File: &schema.UserInputFile{ + URL: derefString(p.File.URL), + Base64Data: derefString(p.File.Base64Data), + MIMEType: p.File.MIMEType, + }, + Extra: p.Extra, + } + } + } + if block != nil { + blocks = append(blocks, block) + } + } + return blocks +} + +func derefString(s *string) string { + if s == nil { + return "" + } + return *s +} + +// typedToolInvokeEvent constructs the tool result event for the invoke path, +// dispatching on M to create the correct message and event types. +func typedToolInvokeEvent[M MessageType](callID, toolName, result, toolMsgID string) *TypedAgentEvent[M] { + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.ToolMessage(result, callID, schema.WithToolName(toolName)) + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + event := EventFromMessage(msg, nil, schema.Tool, toolName) + return any(event).(*TypedAgentEvent[M]) + case *schema.AgenticMessage: + msg := functionToolResultAgenticMessage(callID, toolName, textToFunctionToolResultBlocks(result)) + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + event := EventFromAgenticMessage(msg, nil, schema.AgenticRoleTypeUser) + return any(event).(*TypedAgentEvent[M]) + default: + return nil + } +} + +// typedToolStreamEvent constructs the tool result event for the stream path, +// dispatching on M to create the correct message stream and event types. +func typedToolStreamEvent[M MessageType](callID, toolName, toolMsgID string, stream *schema.StreamReader[string]) *TypedAgentEvent[M] { + var zero M + switch any(zero).(type) { + case *schema.Message: + first := true + cvt := func(in string) (Message, error) { + msg := schema.ToolMessage(in, callID, schema.WithToolName(toolName)) + if first { + first = false + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + } + return msg, nil + } + msgStream := schema.StreamReaderWithConvert(stream, cvt) + event := EventFromMessage(nil, msgStream, schema.Tool, toolName) + return any(event).(*TypedAgentEvent[M]) + case *schema.AgenticMessage: + first := true + cvt := func(in string) (*schema.AgenticMessage, error) { + msg := functionToolResultAgenticMessage(callID, toolName, textToFunctionToolResultBlocks(in)) + markAgenticMessageStreamingMeta(msg, 0) + if first { + first = false + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + } + return msg, nil + } + msgStream := schema.StreamReaderWithConvert(stream, cvt) + event := EventFromAgenticMessage(nil, msgStream, schema.AgenticRoleTypeUser) + return any(event).(*TypedAgentEvent[M]) + default: + return nil + } +} + +// typedToolEnhancedInvokeEvent constructs the tool result event for the enhanced invoke path. +// For *schema.Message it builds a multimodal tool message; for *schema.AgenticMessage it +// uses the string content of the result (AgenticToolsNode only uses the string path). +func typedToolEnhancedInvokeEvent[M MessageType](callID, toolName, toolMsgID string, result *schema.ToolResult) (*TypedAgentEvent[M], error) { + var zero M + switch any(zero).(type) { + case *schema.Message: + msg := schema.ToolMessage("", callID, schema.WithToolName(toolName)) + var err error + msg.UserInputMultiContent, err = result.ToMessageInputParts() + if err != nil { + return nil, err + } + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + event := EventFromMessage(msg, nil, schema.Tool, toolName) + return any(event).(*TypedAgentEvent[M]), nil + case *schema.AgenticMessage: + msg := toolResultAgenticMessage(callID, toolName, result) + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + event := EventFromAgenticMessage(msg, nil, schema.AgenticRoleTypeUser) + return any(event).(*TypedAgentEvent[M]), nil + default: + return nil, nil + } +} + +// typedToolEnhancedStreamEvent constructs the tool result event for the enhanced stream path. +// For *schema.Message it builds multimodal tool messages; for *schema.AgenticMessage it +// converts each chunk's multimodal parts into FunctionToolResultBlocks. +func typedToolEnhancedStreamEvent[M MessageType](callID, toolName, toolMsgID string, stream *schema.StreamReader[*schema.ToolResult]) *TypedAgentEvent[M] { + var zero M + switch any(zero).(type) { + case *schema.Message: + first := true + cvt := func(in *schema.ToolResult) (Message, error) { + msg := schema.ToolMessage("", callID, schema.WithToolName(toolName)) + var cvtErr error + msg.UserInputMultiContent, cvtErr = in.ToMessageInputParts() + if cvtErr != nil { + return nil, cvtErr + } + if first { + first = false + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + } + return msg, nil + } + msgStream := schema.StreamReaderWithConvert(stream, cvt) + event := EventFromMessage(nil, msgStream, schema.Tool, toolName) + return any(event).(*TypedAgentEvent[M]) + case *schema.AgenticMessage: + first := true + cvt := func(in *schema.ToolResult) (*schema.AgenticMessage, error) { + msg := toolResultAgenticMessage(callID, toolName, in) + markAgenticMessageStreamingMeta(msg, 0) + if first { + first = false + msg.Extra = internal.SetMessageID(msg.Extra, toolMsgID) + } + return msg, nil + } + msgStream := schema.StreamReaderWithConvert(stream, cvt) + event := EventFromAgenticMessage(nil, msgStream, schema.AgenticRoleTypeUser) + return any(event).(*TypedAgentEvent[M]) + default: + return nil + } +} + +func (w *typedEventSenderToolWrapper[M]) WrapInvokableToolCall(_ context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error) { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + result, err := endpoint(ctx, argumentsInJSON, opts...) + if err != nil { + return "", err + } + + toolName := tCtx.Name + callID := tCtx.CallID + + prePopAction := typedPopToolGenAction[M](ctx, toolName) + toolMsgID := uuid.NewString() + event := typedToolInvokeEvent[M](callID, toolName, result, toolMsgID) + if prePopAction != nil { + event.Action = prePopAction + } + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setToolMsgID(toolName, callID, toolMsgID) + if st.getReturnDirectlyToolCallID() == callID { + st.setReturnDirectlyEvent(event) + } else { + execCtx.send(event) + } + return nil + }) + + return result, nil + }, nil +} + +func (w *typedEventSenderToolWrapper[M]) WrapStreamableToolCall(_ context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error) { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + result, err := endpoint(ctx, argumentsInJSON, opts...) + if err != nil { + return nil, err + } + + toolName := tCtx.Name + callID := tCtx.CallID + + prePopAction := typedPopToolGenAction[M](ctx, toolName) + streams := result.Copy(2) + + toolMsgID := uuid.NewString() + event := typedToolStreamEvent[M](callID, toolName, toolMsgID, streams[0]) + event.Action = prePopAction + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setToolMsgID(toolName, callID, toolMsgID) + if st.getReturnDirectlyToolCallID() == callID { + st.setReturnDirectlyEvent(event) + } else { + execCtx.send(event) + } + return nil + }) + + return streams[1], nil + }, nil +} + +func (w *typedEventSenderToolWrapper[M]) WrapEnhancedInvokableToolCall(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error) { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + result, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + + toolName := tCtx.Name + callID := tCtx.CallID + + prePopAction := typedPopToolGenAction[M](ctx, toolName) + toolMsgID := uuid.NewString() + event, eventErr := typedToolEnhancedInvokeEvent[M](callID, toolName, toolMsgID, result) + if eventErr != nil { + return nil, eventErr + } + if prePopAction != nil { + event.Action = prePopAction + } + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setToolMsgID(toolName, callID, toolMsgID) + if st.getReturnDirectlyToolCallID() == callID { + st.setReturnDirectlyEvent(event) + } else { + execCtx.send(event) + } + return nil + }) + + return result, nil + }, nil +} + +func (w *typedEventSenderToolWrapper[M]) WrapEnhancedStreamableToolCall(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error) { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + result, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + + toolName := tCtx.Name + callID := tCtx.CallID + + prePopAction := typedPopToolGenAction[M](ctx, toolName) + streams := result.Copy(2) + + toolMsgID := uuid.NewString() + event := typedToolEnhancedStreamEvent[M](callID, toolName, toolMsgID, streams[0]) + event.Action = prePopAction + + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.setToolMsgID(toolName, callID, toolMsgID) + if st.getReturnDirectlyToolCallID() == callID { + st.setReturnDirectlyEvent(event) + } else { + execCtx.send(event) + } + return nil + }) + + return streams[1], nil + }, nil +} + +func hasUserEventSenderToolWrapper[M MessageType](handlers []TypedChatModelAgentMiddleware[M]) bool { + for _, handler := range handlers { + if _, ok := any(handler).(eventSenderToolWrapperMarker); ok { + return true + } + } + return false +} + +type typedStateModelWrapper[M MessageType] struct { + inner model.BaseModel[M] + original model.BaseModel[M] + handlers []TypedChatModelAgentMiddleware[M] + middlewares []AgentMiddleware + toolInfos []*schema.ToolInfo + modelRetryConfig *TypedModelRetryConfig[M] + modelFailoverConfig *ModelFailoverConfig[M] + cancelContext *cancelContext +} + +type stateModelWrapper = typedStateModelWrapper[*schema.Message] + +func (w *typedStateModelWrapper[M]) IsCallbacksEnabled() bool { + return true +} + +func (w *typedStateModelWrapper[M]) GetType() string { + if typer, ok := any(w.original).(components.Typer); ok { + return typer.GetType() + } + return generic.ParseTypeName(reflect.ValueOf(w.original)) +} + +func (w *typedStateModelWrapper[M]) hasUserEventSender() bool { + for _, handler := range w.handlers { + if _, ok := any(handler).(*typedEventSenderModelWrapper[M]); ok { + return true + } + } + return false +} + +func (w *typedStateModelWrapper[M]) wrapGenerateEndpoint(endpoint typedGenerateEndpoint[M]) typedGenerateEndpoint[M] { + // === ID Assignment layer (innermost, framework-controlled) === + // Ensures model output has a message ID before any WrapModel handler or event sender sees it. + // Copies the result to avoid mutating a potentially shared pointer returned by the model. + { + realInner := endpoint + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (M, error) { + result, err := realInner(ctx, input, opts...) + if err != nil { + return result, err + } + if GetMessageID(result) == "" { + result = copyMessage(result) + EnsureMessageID(result) + } + return result, nil + } + } + + hasUserEventSender := w.hasUserEventSender() + retryConfig := w.modelRetryConfig + failoverConfig := w.modelFailoverConfig + cc := w.cancelContext + + for i := len(w.handlers) - 1; i >= 0; i-- { + handler := w.handlers[i] + innerEndpoint := endpoint + baseToolInfos := w.toolInfos + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (M, error) { + baseOpts := &model.Options{Tools: baseToolInfos} + commonOpts := model.GetCommonOptions(baseOpts, opts...) + mc := &TypedModelContext[M]{Tools: commonOpts.Tools, ModelRetryConfig: retryConfig, cancelContext: cc} + wrappedModel, err := handler.WrapModel(ctx, &typedEndpointModel[M]{generate: innerEndpoint}, mc) + if err != nil { + var zero M + return zero, err + } + return wrappedModel.Generate(ctx, input, opts...) + } + } + + if !hasUserEventSender { + innerEndpoint := endpoint + eventSender := &typedEventSenderModelWrapper[M]{ + TypedBaseChatModelAgentMiddleware: &TypedBaseChatModelAgentMiddleware[M]{}, + } + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (M, error) { + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + if execCtx == nil || execCtx.generator == nil { + return innerEndpoint(ctx, input, opts...) + } + mc := &TypedModelContext[M]{ModelRetryConfig: retryConfig, ModelFailoverConfig: failoverConfig, cancelContext: cc} + wrappedModel, err := eventSender.WrapModel(ctx, &typedEndpointModel[M]{generate: innerEndpoint}, mc) + if err != nil { + var zero M + return zero, err + } + return wrappedModel.Generate(ctx, input, opts...) + } + } + + if w.modelRetryConfig != nil { + innerEndpoint := endpoint + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (M, error) { + retryWrapper := newTypedRetryModelWrapper[M](&typedEndpointModel[M]{generate: innerEndpoint}, w.modelRetryConfig) + return retryWrapper.Generate(ctx, input, opts...) + } + } + + if w.modelFailoverConfig != nil { + config := w.modelFailoverConfig + innerEndpoint := endpoint + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (M, error) { + failoverWrapper := newFailoverModelWrapper[M](&typedEndpointModel[M]{generate: innerEndpoint}, config) + return failoverWrapper.Generate(ctx, input, opts...) + } + } + + return endpoint +} + +func (w *typedStateModelWrapper[M]) wrapStreamEndpoint(endpoint typedStreamEndpoint[M]) typedStreamEndpoint[M] { + // === ID Assignment layer (innermost, framework-controlled) === + // Pre-allocates a UUID and injects it into the first chunk only. + // Only the first chunk carries the ID in Extra to avoid concatStrings corruption + // during ConcatMessages (which string-concatenates duplicate Extra keys). + { + realInner := endpoint + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + reader, err := realInner(ctx, input, opts...) + if err != nil { + return nil, err + } + msgID := uuid.NewString() + first := true + return schema.StreamReaderWithConvert(reader, func(msg M) (M, error) { + if first { + first = false + if GetMessageID(msg) == "" { + typedSetMessageID(msg, msgID) + } + } + return msg, nil + }), nil + } + } + + hasUserEventSender := w.hasUserEventSender() + retryConfig := w.modelRetryConfig + failoverConfig := w.modelFailoverConfig + cc := w.cancelContext + + for i := len(w.handlers) - 1; i >= 0; i-- { + handler := w.handlers[i] + innerEndpoint := endpoint + baseToolInfos := w.toolInfos + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + baseOpts := &model.Options{Tools: baseToolInfos} + commonOpts := model.GetCommonOptions(baseOpts, opts...) + mc := &TypedModelContext[M]{Tools: commonOpts.Tools, ModelRetryConfig: retryConfig, cancelContext: cc} + wrappedModel, err := handler.WrapModel(ctx, &typedEndpointModel[M]{stream: innerEndpoint}, mc) + if err != nil { + return nil, err + } + return wrappedModel.Stream(ctx, input, opts...) + } + } + + if !hasUserEventSender { + innerEndpoint := endpoint + eventSender := &typedEventSenderModelWrapper[M]{ + TypedBaseChatModelAgentMiddleware: &TypedBaseChatModelAgentMiddleware[M]{}, + } + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + execCtx := getTypedChatModelAgentExecCtx[M](ctx) + if execCtx == nil || execCtx.generator == nil { + return innerEndpoint(ctx, input, opts...) + } + mc := &TypedModelContext[M]{ModelRetryConfig: retryConfig, ModelFailoverConfig: failoverConfig, cancelContext: cc} + wrappedModel, err := eventSender.WrapModel(ctx, &typedEndpointModel[M]{stream: innerEndpoint}, mc) + if err != nil { + return nil, err + } + return wrappedModel.Stream(ctx, input, opts...) + } + } + + if w.modelRetryConfig != nil { + innerEndpoint := endpoint + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + retryWrapper := newTypedRetryModelWrapper[M](&typedEndpointModel[M]{stream: innerEndpoint}, w.modelRetryConfig) + return retryWrapper.Stream(ctx, input, opts...) + } + } + + if w.modelFailoverConfig != nil { + config := w.modelFailoverConfig + innerEndpoint := endpoint + endpoint = func(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + failoverWrapper := newFailoverModelWrapper[M](&typedEndpointModel[M]{stream: innerEndpoint}, config) + return failoverWrapper.Stream(ctx, input, opts...) + } + } + + return endpoint +} + +func (w *typedStateModelWrapper[M]) Generate(ctx context.Context, _ []M, opts ...model.Option) (M, error) { + var ( + stateMessages []M + stateToolInfos []*schema.ToolInfo + stateDeferredToolInfos []*schema.ToolInfo + ) + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + stateMessages = st.Messages + stateToolInfos = st.ToolInfos + stateDeferredToolInfos = st.DeferredToolInfos + return nil + }) + + // Backfill: old checkpoints or fresh starts have nil ToolInfos. + // Use compose-level tools from opts (which always reflects the latest bc.toolInfos) + // rather than w.toolInfos (which may be stale if the graph was reused). + if stateToolInfos == nil { + composeLevelOpts := model.GetCommonOptions(&model.Options{}, opts...) + if composeLevelOpts.Tools != nil { + stateToolInfos = composeLevelOpts.Tools + } else { + stateToolInfos = w.toolInfos + } + } + + state := &TypedChatModelAgentState[M]{ + Messages: stateMessages, + ToolInfos: stateToolInfos, + DeferredToolInfos: stateDeferredToolInfos, + } + + if msgState, ok := any(state).(*ChatModelAgentState); ok { + for _, m := range w.middlewares { + if m.BeforeChatModel != nil { + if err := m.BeforeChatModel(ctx, msgState); err != nil { + var zero M + return zero, err + } + } + } + } + + baseOpts := &model.Options{Tools: w.toolInfos} + commonOpts := model.GetCommonOptions(baseOpts, opts...) + mc := &TypedModelContext[M]{Tools: commonOpts.Tools, ModelRetryConfig: w.modelRetryConfig, cancelContext: w.cancelContext} + for _, handler := range w.handlers { + var err error + ctx, state, err = handler.BeforeModelRewriteState(ctx, state, mc) + if err != nil { + var zero M + return zero, err + } + } + + // Persist state (including tool infos) after BeforeModelRewriteState. + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.Messages = state.Messages + st.ToolInfos = state.ToolInfos + st.DeferredToolInfos = state.DeferredToolInfos + return nil + }) + + // Derive model options from state. Append after caller opts so state takes precedence + // (model.GetCommonOptions applies left-to-right, last wins). + // Use explicit copy to avoid mutating the caller's opts slice. + derivedOpts := make([]model.Option, len(opts), len(opts)+2) + copy(derivedOpts, opts) + derivedOpts = append(derivedOpts, model.WithTools(state.ToolInfos)) + if state.DeferredToolInfos != nil { + derivedOpts = append(derivedOpts, model.WithDeferredTools(state.DeferredToolInfos)) + } + + wrappedEndpoint := w.wrapGenerateEndpoint(w.inner.Generate) + result, err := wrappedEndpoint(ctx, state.Messages, derivedOpts...) + if err != nil { + var zero M + return zero, err + } + + // Re-read State.Messages after Generate completes: when ShouldRetry uses + // PersistModifiedInputMessages, applyDecisionForRetry writes modified messages to State. + // We must pick up those changes before appending the model result. + if w.modelRetryConfig != nil && w.modelRetryConfig.ShouldRetry != nil { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + state.Messages = st.Messages + return nil + }) + } + + state.Messages = append(state.Messages, result) + + for _, handler := range w.handlers { + ctx, state, err = handler.AfterModelRewriteState(ctx, state, mc) + if err != nil { + var zero M + return zero, err + } + } + + if msgState, ok := any(state).(*ChatModelAgentState); ok { + for _, m := range w.middlewares { + if m.AfterChatModel != nil { + if err := m.AfterChatModel(ctx, msgState); err != nil { + var zero M + return zero, err + } + } + } + } + + // Persist state (including tool infos) after AfterModelRewriteState. + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.Messages = state.Messages + st.ToolInfos = state.ToolInfos + st.DeferredToolInfos = state.DeferredToolInfos + return nil + }) + + if len(state.Messages) == 0 { + var zero M + return zero, errors.New("no messages left in state after model call") + } + return state.Messages[len(state.Messages)-1], nil +} + +func (w *typedStateModelWrapper[M]) Stream(ctx context.Context, _ []M, opts ...model.Option) (*schema.StreamReader[M], error) { + var ( + stateMessages []M + stateToolInfos []*schema.ToolInfo + stateDeferredToolInfos []*schema.ToolInfo + ) + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + stateMessages = st.Messages + stateToolInfos = st.ToolInfos + stateDeferredToolInfos = st.DeferredToolInfos + return nil + }) + + // Backfill: old checkpoints or fresh starts have nil ToolInfos. + // Use compose-level tools from opts (which always reflects the latest bc.toolInfos) + // rather than w.toolInfos (which may be stale if the graph was reused). + if stateToolInfos == nil { + composeLevelOpts := model.GetCommonOptions(&model.Options{}, opts...) + if composeLevelOpts.Tools != nil { + stateToolInfos = composeLevelOpts.Tools + } else { + stateToolInfos = w.toolInfos + } + } + + state := &TypedChatModelAgentState[M]{ + Messages: stateMessages, + ToolInfos: stateToolInfos, + DeferredToolInfos: stateDeferredToolInfos, + } + + if msgState, ok := any(state).(*ChatModelAgentState); ok { + for _, m := range w.middlewares { + if m.BeforeChatModel != nil { + if err := m.BeforeChatModel(ctx, msgState); err != nil { + return nil, err + } + } + } + } + + baseOpts := &model.Options{Tools: w.toolInfos} + commonOpts := model.GetCommonOptions(baseOpts, opts...) + mc := &TypedModelContext[M]{Tools: commonOpts.Tools, ModelRetryConfig: w.modelRetryConfig, cancelContext: w.cancelContext} + for _, handler := range w.handlers { + var err error + ctx, state, err = handler.BeforeModelRewriteState(ctx, state, mc) + if err != nil { + return nil, err + } + } + + // Persist state (including tool infos) after BeforeModelRewriteState. + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.Messages = state.Messages + st.ToolInfos = state.ToolInfos + st.DeferredToolInfos = state.DeferredToolInfos + return nil + }) + + // Derive model options from state. Append after caller opts so state takes precedence + // (model.GetCommonOptions applies left-to-right, last wins). + // Use explicit copy to avoid mutating the caller's opts slice. + derivedOpts := make([]model.Option, len(opts), len(opts)+2) + copy(derivedOpts, opts) + derivedOpts = append(derivedOpts, model.WithTools(state.ToolInfos)) + if state.DeferredToolInfos != nil { + derivedOpts = append(derivedOpts, model.WithDeferredTools(state.DeferredToolInfos)) + } + + wrappedEndpoint := w.wrapStreamEndpoint(w.inner.Stream) + stream, err := wrappedEndpoint(ctx, state.Messages, derivedOpts...) + if err != nil { + return nil, err + } + result, err := concatMessageStream(stream) + if err != nil { + return nil, err + } + + // Re-read State.Messages after Stream completes: same rationale as in Generate above. + if w.modelRetryConfig != nil && w.modelRetryConfig.ShouldRetry != nil { + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + state.Messages = st.Messages + return nil + }) + } + + state.Messages = append(state.Messages, result) + + for _, handler := range w.handlers { + ctx, state, err = handler.AfterModelRewriteState(ctx, state, mc) + if err != nil { + return nil, err + } + } + + if msgState, ok := any(state).(*ChatModelAgentState); ok { + for _, m := range w.middlewares { + if m.AfterChatModel != nil { + if err := m.AfterChatModel(ctx, msgState); err != nil { + return nil, err + } + } + } + } + + // Persist state (including tool infos) after AfterModelRewriteState. + _ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error { + st.Messages = state.Messages + st.ToolInfos = state.ToolInfos + st.DeferredToolInfos = state.DeferredToolInfos + return nil + }) + + if len(state.Messages) == 0 { + return nil, errors.New("no messages left in state after model call") + } + return schema.StreamReaderFromArray([]M{state.Messages[len(state.Messages)-1]}), nil +} + +type typedEndpointModel[M MessageType] struct { + generate typedGenerateEndpoint[M] + stream typedStreamEndpoint[M] +} + +func (m *typedEndpointModel[M]) Generate(ctx context.Context, input []M, opts ...model.Option) (M, error) { + if m.generate != nil { + return m.generate(ctx, input, opts...) + } + var zero M + return zero, errors.New("generate endpoint not set") +} + +func (m *typedEndpointModel[M]) Stream(ctx context.Context, input []M, opts ...model.Option) (*schema.StreamReader[M], error) { + if m.stream != nil { + return m.stream(ctx, input, opts...) + } + return nil, errors.New("stream endpoint not set") +} diff --git a/adk/wrappers_failover_test.go b/adk/wrappers_failover_test.go new file mode 100644 index 0000000..45fb0c2 --- /dev/null +++ b/adk/wrappers_failover_test.go @@ -0,0 +1,215 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +func TestBuildModelWrappers_FailoverProxyInner(t *testing.T) { + base := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return schema.AssistantMessage("ok", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil + }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 0, + ShouldFailover: func(context.Context, *schema.Message, error) bool { return false }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return base, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](base, &modelWrapperConfig{ + failoverConfig: failoverCfg, + }) + + smw, ok := wrapped.(*stateModelWrapper) + require.True(t, ok) + _, ok = smw.inner.(*failoverProxyModel) + require.True(t, ok) + require.Same(t, base, smw.original) + require.Same(t, failoverCfg, smw.modelFailoverConfig) +} + +func TestStateModelWrapper_Generate_WithFailover(t *testing.T) { + wantErr := errors.New("first failed") + var shouldCalls int32 + var m1Calls int32 + var m2Calls int32 + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return schema.AssistantMessage("partial", nil), wantErr + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + }, + } + m2 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m2Calls, 1) + return schema.AssistantMessage("ok", nil), nil + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, out *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + require.ErrorIs(t, err, wantErr) + require.NotNil(t, out) + require.Equal(t, "partial", out.Content) + return true + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.Equal(t, uint(1), failoverCtx.FailoverAttempt) + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + got, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, "ok", got.Content) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) +} + +func TestStateModelWrapper_Stream_WithFailover(t *testing.T) { + streamErr := errors.New("mid error") + var shouldCalls int32 + var m1Calls int32 + var m2Calls int32 + + m1 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return streamWithMidError([]*schema.Message{ + schema.AssistantMessage("p1", nil), + schema.AssistantMessage("p2", nil), + }, streamErr), nil + }, + } + m2 := &fakeChatModel{ + callbacksEnabled: true, + generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + }, + stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("final", nil)}), nil + }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, out *schema.Message, err error) bool { + atomic.AddInt32(&shouldCalls, 1) + require.ErrorIs(t, err, streamErr) + require.NotNil(t, out) + require.Equal(t, "p1p2", out.Content) + return true + }, + GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.Equal(t, uint(1), failoverCtx.FailoverAttempt) + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + msgs, err := drainMessageStream(sr) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.Equal(t, "final", msgs[0].Content) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls)) +} + +func TestFailoverAcceptsAgenticAgent(t *testing.T) { + ctx := context.Background() + + m := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("ok"), nil + }, + } + + fallbackModel := &mockAgenticModel{ + generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) { + return agenticMsg("fallback"), nil + }, + } + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "FailoverAgent", + Description: "Agent with failover config", + Model: m, + ModelFailoverConfig: &ModelFailoverConfig[*schema.AgenticMessage]{ + MaxRetries: 1, + ShouldFailover: func(ctx context.Context, outputMessage *schema.AgenticMessage, outputErr error) bool { + return true + }, + GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext[*schema.AgenticMessage]) (model.BaseModel[*schema.AgenticMessage], []*schema.AgenticMessage, error) { + return fallbackModel, nil, nil + }, + }, + }) + require.NoError(t, err) + assert.NotNil(t, agent) +} diff --git a/adk/wrappers_retry_failover_test.go b/adk/wrappers_retry_failover_test.go new file mode 100644 index 0000000..c1a291d --- /dev/null +++ b/adk/wrappers_retry_failover_test.go @@ -0,0 +1,613 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +func newFakeChatModel( + gen func(context.Context, []*schema.Message, ...model.Option) (*schema.Message, error), + stream func(context.Context, []*schema.Message, ...model.Option) (*schema.StreamReader[*schema.Message], error), +) *fakeChatModel { + if gen == nil { + gen = func(context.Context, []*schema.Message, ...model.Option) (*schema.Message, error) { + return nil, errors.New("unused") + } + } + if stream == nil { + stream = func(context.Context, []*schema.Message, ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return nil, errors.New("unused") + } + } + return &fakeChatModel{callbacksEnabled: true, generate: gen, stream: stream} +} + +func TestRetryThenFailover(t *testing.T) { + t.Run("Generate_RetryExhaustedTriggersFailover", func(t *testing.T) { + modelErr := errors.New("model error") + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, modelErr + }, nil) + m2 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m2Calls, 1) + return schema.AssistantMessage("ok from m2", nil), nil + }, nil) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 2, + IsRetryAble: func(_ context.Context, err error) bool { return true }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, fc *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.NotNil(t, fc.LastErr) + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + msg, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "ok from m2", msg.Content) + + // m1: 1 (lastSuccess) + 2 retries = 3 calls on lastSuccess attempt, + // then failover to m2 which also goes through retry wrapper: 1 call succeeds. + require.Equal(t, int32(3), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("Generate_AllExhausted", func(t *testing.T) { + modelErr := errors.New("always fails") + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, modelErr + }, nil) + m2 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m2Calls, 1) + return nil, modelErr + }, nil) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 1, + IsRetryAble: func(_ context.Context, err error) bool { return true }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + + // Should be RetryExhaustedError from m2's retry wrapper + var retryErr *RetryExhaustedError + require.True(t, errors.As(err, &retryErr)) + + // m1: 1 initial + 1 retry = 2 calls + require.Equal(t, int32(2), atomic.LoadInt32(&m1Calls)) + // m2: 1 initial + 1 retry = 2 calls + require.Equal(t, int32(2), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("Generate_RetrySucceedsNoFailover", func(t *testing.T) { + var m1Calls int32 + var failoverCalled int32 + + m1 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + n := atomic.AddInt32(&m1Calls, 1) + if n == 1 { + return nil, errors.New("transient error") + } + return schema.AssistantMessage("ok on retry", nil), nil + }, nil) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 2, + IsRetryAble: func(_ context.Context, err error) bool { return true }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&failoverCalled, 1) + return true + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + t.Fatal("GetFailoverModel should not be called when retry succeeds") + return nil, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + msg, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "ok on retry", msg.Content) + + // 2 calls: first fails, second succeeds via retry + require.Equal(t, int32(2), atomic.LoadInt32(&m1Calls)) + // ShouldFailover should never be called + require.Equal(t, int32(0), atomic.LoadInt32(&failoverCalled)) + }) + + t.Run("Generate_NonRetryableErrorTriggersFailover", func(t *testing.T) { + nonRetryableErr := errors.New("non-retryable") + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, nonRetryableErr + }, nil) + m2 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m2Calls, 1) + return schema.AssistantMessage("ok from m2", nil), nil + }, nil) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 3, + IsRetryAble: func(_ context.Context, err error) bool { + // Only non-retryable errors + return !errors.Is(err, nonRetryableErr) + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + msg, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "ok from m2", msg.Content) + + // m1 called only once — non-retryable error skips retry + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("Stream_RetryExhaustedTriggersFailover", func(t *testing.T) { + streamErr := errors.New("stream mid error") + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return streamWithMidError([]*schema.Message{ + schema.AssistantMessage("partial", nil), + }, streamErr), nil + }) + m2 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok from m2", nil)}), nil + }) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 1, + IsRetryAble: func(_ context.Context, err error) bool { return true }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, fc *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + require.NotNil(t, fc.LastErr) + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + msgs, err := drainMessageStream(sr) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.Equal(t, "ok from m2", msgs[0].Content) + + // m1: 1 initial + 1 retry = 2 calls on lastSuccess attempt + require.Equal(t, int32(2), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("Stream_AllExhausted", func(t *testing.T) { + streamErr := errors.New("always fails mid-stream") + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return streamWithMidError([]*schema.Message{ + schema.AssistantMessage("p", nil), + }, streamErr), nil + }) + m2 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return streamWithMidError([]*schema.Message{ + schema.AssistantMessage("p", nil), + }, streamErr), nil + }) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 1, + IsRetryAble: func(_ context.Context, err error) bool { return true }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + + var retryErr *RetryExhaustedError + require.True(t, errors.As(err, &retryErr)) + + // m1: 1 initial + 1 retry = 2 calls + require.Equal(t, int32(2), atomic.LoadInt32(&m1Calls)) + // m2: 1 initial + 1 retry = 2 calls + require.Equal(t, int32(2), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("ShouldRetry_Stream_TriggersFailover", func(t *testing.T) { + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("bad from m1", nil)}), nil + }) + m2 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m2Calls, 1) + return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("good from m2", nil)}), nil + }) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(_ context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && retryCtx.OutputMessage.Content == "bad from m1" { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + sr, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + msgs, err := drainMessageStream(sr) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.Equal(t, "good from m2", msgs[0].Content) + require.Equal(t, int32(2), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("ShouldRetry_Generate_TriggersFailover", func(t *testing.T) { + var m1Calls int32 + var m2Calls int32 + + m1 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return schema.AssistantMessage("bad from m1", nil), nil + }, nil) + m2 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m2Calls, 1) + return schema.AssistantMessage("good from m2", nil), nil + }, nil) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 1, + ShouldRetry: func(_ context.Context, retryCtx *RetryContext) *RetryDecision { + if retryCtx.OutputMessage != nil && retryCtx.OutputMessage.Content == "bad from m1" { + return &RetryDecision{Retry: true} + } + return &RetryDecision{Retry: false} + }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return m2, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + msg, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.NoError(t, err) + require.Equal(t, "good from m2", msg.Content) + require.Equal(t, int32(2), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls)) + }) + + t.Run("Stream_GetFailoverModelReturnsNilModel", func(t *testing.T) { + streamErr := errors.New("m1 always fails") + var m1Calls int32 + + m1 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return nil, streamErr + }) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 0, + IsRetryAble: func(_ context.Context, err error) bool { return false }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 1, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + return nil, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + require.Contains(t, err.Error(), "returned nil model at attempt") + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + }) + + t.Run("Stream_ContextCanceledDuringFailover", func(t *testing.T) { + streamErr := errors.New("m1 fails") + var m1Calls int32 + var failoverModelCalled int32 + + m1 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return nil, streamErr + }) + + ctx, cancel := context.WithCancel(context.Background()) + + retryCfg := &ModelRetryConfig{ + MaxRetries: 0, + IsRetryAble: func(_ context.Context, err error) bool { return false }, + BackoffFunc: func(_ context.Context, _ int) time.Duration { return 0 }, + } + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 3, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + cancel() + return err != nil + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + atomic.AddInt32(&failoverModelCalled, 1) + return nil, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + retryConfig: retryCfg, + failoverConfig: failoverCfg, + }) + + ctx = withTypedChatModelAgentExecCtx(ctx, &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + require.ErrorIs(t, err, context.Canceled) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(0), atomic.LoadInt32(&failoverModelCalled)) + }) +} + +func TestErrStreamCanceled_Failover(t *testing.T) { + t.Run("Stream_NeverFailedOver", func(t *testing.T) { + var m1Calls int32 + var failoverCalled int32 + + m1 := newFakeChatModel(nil, func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) { + atomic.AddInt32(&m1Calls, 1) + return streamWithMidError([]*schema.Message{ + schema.AssistantMessage("partial", nil), + }, ErrStreamCanceled), nil + }) + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 2, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&failoverCalled, 1) + return true + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + t.Fatal("GetFailoverModel should not be called for ErrStreamCanceled") + return nil, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + require.True(t, errors.Is(err, ErrStreamCanceled)) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(0), atomic.LoadInt32(&failoverCalled)) + }) + + t.Run("Generate_NeverFailedOver", func(t *testing.T) { + var m1Calls int32 + var failoverCalled int32 + + m1 := newFakeChatModel(func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + atomic.AddInt32(&m1Calls, 1) + return nil, ErrStreamCanceled + }, nil) + + failoverCfg := &ModelFailoverConfig[*schema.Message]{ + MaxRetries: 2, + ShouldFailover: func(_ context.Context, _ *schema.Message, err error) bool { + atomic.AddInt32(&failoverCalled, 1) + return true + }, + GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) { + t.Fatal("GetFailoverModel should not be called for ErrStreamCanceled") + return nil, nil, nil + }, + } + + wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{ + failoverConfig: failoverCfg, + }) + + ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{ + failoverLastSuccessModel: m1, + }) + _, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")}) + require.Error(t, err) + require.True(t, errors.Is(err, ErrStreamCanceled)) + require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls)) + require.Equal(t, int32(0), atomic.LoadInt32(&failoverCalled)) + }) +} diff --git a/adk/wrappers_test.go b/adk/wrappers_test.go new file mode 100644 index 0000000..11dacbd --- /dev/null +++ b/adk/wrappers_test.go @@ -0,0 +1,2188 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package adk + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type testEnhancedToolWrapperHandler struct { + *BaseChatModelAgentMiddleware + wrapEnhancedInvokableFn func(context.Context, EnhancedInvokableToolCallEndpoint, *ToolContext) EnhancedInvokableToolCallEndpoint + wrapEnhancedStreamableFn func(context.Context, EnhancedStreamableToolCallEndpoint, *ToolContext) EnhancedStreamableToolCallEndpoint +} + +func (h *testEnhancedToolWrapperHandler) WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error) { + if h.wrapEnhancedInvokableFn != nil { + return h.wrapEnhancedInvokableFn(ctx, endpoint, tCtx), nil + } + return endpoint, nil +} + +func (h *testEnhancedToolWrapperHandler) WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error) { + if h.wrapEnhancedStreamableFn != nil { + return h.wrapEnhancedStreamableFn(ctx, endpoint, tCtx), nil + } + return endpoint, nil +} + +func newTestEnhancedInvokableToolCallWrapper(beforeFn, afterFn func()) func(context.Context, EnhancedInvokableToolCallEndpoint, *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + if beforeFn != nil { + beforeFn() + } + result, err := endpoint(ctx, toolArgument, opts...) + if afterFn != nil { + afterFn() + } + return result, err + } + } +} + +func newTestEnhancedStreamableToolCallWrapper(beforeFn, afterFn func()) func(context.Context, EnhancedStreamableToolCallEndpoint, *ToolContext) EnhancedStreamableToolCallEndpoint { + return func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + if beforeFn != nil { + beforeFn() + } + result, err := endpoint(ctx, toolArgument, opts...) + if afterFn != nil { + afterFn() + } + return result, err + } + } +} + +func TestHandlersToToolMiddlewaresEnhanced(t *testing.T) { + t.Run("OnlyEnhancedInvokableHandler", func(t *testing.T) { + var called bool + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + called = true + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 1) + assert.NotNil(t, middlewares[0].EnhancedInvokable) + assert.NotNil(t, middlewares[0].Invokable) + assert.NotNil(t, middlewares[0].Streamable) + assert.NotNil(t, middlewares[0].EnhancedStreamable) + + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{ + Result: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "test"}}, + }, + }, nil + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + _, err := wrapped(context.Background(), &compose.ToolInput{ + Name: "test_tool", + CallID: "call-1", + Arguments: `{"input": "test"}`, + }) + assert.NoError(t, err) + assert.True(t, called) + }) + + t.Run("OnlyEnhancedStreamableHandler", func(t *testing.T) { + var called bool + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + called = true + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 1) + assert.NotNil(t, middlewares[0].EnhancedStreamable) + assert.NotNil(t, middlewares[0].Invokable) + assert.NotNil(t, middlewares[0].Streamable) + assert.NotNil(t, middlewares[0].EnhancedInvokable) + + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{ + Result: schema.StreamReaderFromArray([]*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "test"}}}, + }), + }, nil + } + + wrapped := middlewares[0].EnhancedStreamable(mockEndpoint) + _, err := wrapped(context.Background(), &compose.ToolInput{ + Name: "test_tool", + CallID: "call-1", + Arguments: `{"input": "test"}`, + }) + assert.NoError(t, err) + assert.True(t, called) + }) + + t.Run("MixedHandlers", func(t *testing.T) { + var invokableCalled, streamableCalled, enhancedInvokableCalled, enhancedStreamableCalled bool + + handlers := []ChatModelAgentMiddleware{ + &testToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapInvokableFn: func(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) InvokableToolCallEndpoint { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + invokableCalled = true + return endpoint(ctx, argumentsInJSON, opts...) + } + }, + }, + &testToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapStreamableFn: func(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) StreamableToolCallEndpoint { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + streamableCalled = true + return endpoint(ctx, argumentsInJSON, opts...) + } + }, + }, + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + enhancedInvokableCalled = true + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + enhancedStreamableCalled = true + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 4) + + invokableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + return &compose.ToolOutput{Result: "test"}, nil + } + _, _ = middlewares[0].Invokable(invokableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + streamableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + return &compose.StreamToolOutput{Result: schema.StreamReaderFromArray([]string{"test"})}, nil + } + _, _ = middlewares[1].Streamable(streamableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + enhancedInvokableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil + } + _, _ = middlewares[2].EnhancedInvokable(enhancedInvokableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + enhancedStreamableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{{}})}, nil + } + _, _ = middlewares[3].EnhancedStreamable(enhancedStreamableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + assert.True(t, invokableCalled) + assert.True(t, streamableCalled) + assert.True(t, enhancedInvokableCalled) + assert.True(t, enhancedStreamableCalled) + }) + + t.Run("NoHandlers", func(t *testing.T) { + handlers := []ChatModelAgentMiddleware{} + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 0) + }) + + t.Run("HandlerWithNoToolWrappers", func(t *testing.T) { + handlers := []ChatModelAgentMiddleware{ + &BaseChatModelAgentMiddleware{}, + } + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 1) + }) + + t.Run("EnhancedInvokableToolCallErrorPropagation", func(t *testing.T) { + expectedErr := errors.New("test error") + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + return nil, expectedErr + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + _, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("EnhancedStreamableToolCallErrorPropagation", func(t *testing.T) { + expectedErr := errors.New("test error") + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return nil, expectedErr + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{})}, nil + } + + wrapped := middlewares[0].EnhancedStreamable(mockEndpoint) + _, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("MultipleEnhancedInvokableWrappers", func(t *testing.T) { + var executionOrder []string + var mu sync.Mutex + + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: newTestEnhancedInvokableToolCallWrapper( + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler1-before") + mu.Unlock() + }, + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler1-after") + mu.Unlock() + }, + ), + }, + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: newTestEnhancedInvokableToolCallWrapper( + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler2-before") + mu.Unlock() + }, + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler2-after") + mu.Unlock() + }, + ), + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 2) + + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil + } + + wrapped := middlewares[0].EnhancedInvokable(middlewares[1].EnhancedInvokable(mockEndpoint)) + _, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + assert.NoError(t, err) + assert.Equal(t, []string{"handler1-before", "handler2-before", "handler2-after", "handler1-after"}, executionOrder) + }) + + t.Run("MultipleEnhancedStreamableWrappers", func(t *testing.T) { + var executionOrder []string + var mu sync.Mutex + + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: newTestEnhancedStreamableToolCallWrapper( + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler1-before") + mu.Unlock() + }, + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler1-after") + mu.Unlock() + }, + ), + }, + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: newTestEnhancedStreamableToolCallWrapper( + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler2-before") + mu.Unlock() + }, + func() { + mu.Lock() + executionOrder = append(executionOrder, "handler2-after") + mu.Unlock() + }, + ), + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + assert.Len(t, middlewares, 2) + + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{{}})}, nil + } + + wrapped := middlewares[0].EnhancedStreamable(middlewares[1].EnhancedStreamable(mockEndpoint)) + _, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + assert.NoError(t, err) + assert.Equal(t, []string{"handler1-before", "handler2-before", "handler2-after", "handler1-after"}, executionOrder) + }) +} + +func TestEnhancedToolContextPropagation(t *testing.T) { + t.Run("ToolContextContainsCorrectInfo", func(t *testing.T) { + var capturedCtx *ToolContext + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) EnhancedInvokableToolCallEndpoint { + capturedCtx = tCtx + return endpoint + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + _, _ = wrapped(context.Background(), &compose.ToolInput{ + Name: "my_tool", + CallID: "call-123", + Arguments: `{"key": "value"}`, + }) + + assert.NotNil(t, capturedCtx) + assert.Equal(t, "my_tool", capturedCtx.Name) + assert.Equal(t, "call-123", capturedCtx.CallID) + }) + + t.Run("StreamableToolContextContainsCorrectInfo", func(t *testing.T) { + var capturedCtx *ToolContext + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) EnhancedStreamableToolCallEndpoint { + capturedCtx = tCtx + return endpoint + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{{}})}, nil + } + + wrapped := middlewares[0].EnhancedStreamable(mockEndpoint) + _, _ = wrapped(context.Background(), &compose.ToolInput{ + Name: "stream_tool", + CallID: "call-456", + Arguments: `{"data": "test"}`, + }) + + assert.NotNil(t, capturedCtx) + assert.Equal(t, "stream_tool", capturedCtx.Name) + assert.Equal(t, "call-456", capturedCtx.CallID) + }) +} + +func TestBaseChatModelAgentMiddlewareEnhancedDefaults(t *testing.T) { + t.Run("DefaultEnhancedInvokableReturnsEndpoint", func(t *testing.T) { + base := &BaseChatModelAgentMiddleware{} + + var called bool + endpoint := func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + called = true + return &schema.ToolResult{}, nil + } + + wrapped, wrapErr := base.WrapEnhancedInvokableToolCall(context.Background(), endpoint, &ToolContext{Name: "test", CallID: "1"}) + assert.NoError(t, wrapErr) + _, err := wrapped(context.Background(), &schema.ToolArgument{Text: "{}"}) + + assert.NoError(t, err) + assert.True(t, called) + }) + + t.Run("DefaultEnhancedStreamableReturnsEndpoint", func(t *testing.T) { + base := &BaseChatModelAgentMiddleware{} + + var called bool + endpoint := func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + called = true + return schema.StreamReaderFromArray([]*schema.ToolResult{}), nil + } + + wrapped, wrapErr := base.WrapEnhancedStreamableToolCall(context.Background(), endpoint, &ToolContext{Name: "test", CallID: "1"}) + assert.NoError(t, wrapErr) + _, err := wrapped(context.Background(), &schema.ToolArgument{Text: "{}"}) + + assert.NoError(t, err) + assert.True(t, called) + }) +} + +func TestEnhancedToolArgumentsPropagation(t *testing.T) { + t.Run("ArgumentsPassedCorrectly", func(t *testing.T) { + var capturedArgs string + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + capturedArgs = toolArgument.Text + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + _, _ = wrapped(context.Background(), &compose.ToolInput{ + Name: "test_tool", + CallID: "call-1", + Arguments: `{"name": "test", "value": 123}`, + }) + + assert.Equal(t, `{"name": "test", "value": 123}`, capturedArgs) + }) +} + +func TestEnhancedToolResultPropagation(t *testing.T) { + t.Run("ResultPassedThroughMiddleware", func(t *testing.T) { + expectedResult := &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "original result"}, + }, + } + + var capturedResult *schema.ToolResult + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + result, err := endpoint(ctx, toolArgument, opts...) + capturedResult = result + return result, err + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: expectedResult}, nil + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + output, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, capturedResult) + assert.Equal(t, expectedResult, output.Result) + }) + + t.Run("ModifiedResultPropagated", func(t *testing.T) { + modifiedResult := &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "modified result"}, + }, + } + + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + _, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + return modifiedResult, nil + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "original"}}, + }}, nil + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + output, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + assert.NoError(t, err) + assert.Equal(t, modifiedResult, output.Result) + assert.Equal(t, "modified result", output.Result.Parts[0].Text) + }) +} + +func TestEnhancedToolEndpointErrorFromNext(t *testing.T) { + t.Run("EnhancedInvokableNextError", func(t *testing.T) { + expectedErr := errors.New("next endpoint error") + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + return nil, expectedErr + } + + wrapped := middlewares[0].EnhancedInvokable(mockEndpoint) + _, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("EnhancedStreamableNextError", func(t *testing.T) { + expectedErr := errors.New("next endpoint error") + handlers := []ChatModelAgentMiddleware{ + &testEnhancedToolWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return endpoint(ctx, toolArgument, opts...) + } + }, + }, + } + + middlewares := handlersToToolMiddlewares(handlers) + mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + return nil, expectedErr + } + + wrapped := middlewares[0].EnhancedStreamable(mockEndpoint) + _, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"}) + + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestWrapModelStreamChunksPreserved(t *testing.T) { + t.Run("AgentEventMessageStreamShouldPreserveChunksWithNoopWrapModel", func(t *testing.T) { + ctx := context.Background() + + chunk1 := schema.AssistantMessage("Hello ", nil) + chunk2 := schema.AssistantMessage("World", nil) + + mockModel := &mockStreamingModel{ + chunks: []*schema.Message{chunk1, chunk2}, + } + + noopWrapModelHandler := &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return m + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + Handlers: []ChatModelAgentMiddleware{noopWrapModelHandler}, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var streamingEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.IsStreaming && + event.Output.MessageOutput.Role == schema.Assistant { + streamingEvents = append(streamingEvents, event) + } + } + + assert.GreaterOrEqual(t, len(streamingEvents), 1, "Should have at least one streaming event") + + if len(streamingEvents) > 0 { + event := streamingEvents[0] + assert.NotNil(t, event.Output.MessageOutput.MessageStream, "Event should have message stream") + + var receivedChunks []*schema.Message + for { + chunk, recvErr := event.Output.MessageOutput.MessageStream.Recv() + if recvErr != nil { + break + } + receivedChunks = append(receivedChunks, chunk) + } + + assert.Equal(t, 2, len(receivedChunks), + "AgentEvent's MessageStream should contain 2 separate chunks, not 1 concatenated chunk. "+ + "Got %d chunks instead. This indicates the stream is being concatenated before being sent to AgentEvent.", + len(receivedChunks)) + + if len(receivedChunks) >= 2 { + assert.Equal(t, "Hello ", receivedChunks[0].Content, "First chunk content should be preserved") + assert.Equal(t, "World", receivedChunks[1].Content, "Second chunk content should be preserved") + } + } + }) + + t.Run("AgentEventMessageStreamShouldReflectUserMiddlewareModifications", func(t *testing.T) { + ctx := context.Background() + + chunk1 := schema.AssistantMessage("Hello ", nil) + chunk2 := schema.AssistantMessage("World", nil) + + mockModel := &mockStreamingModel{ + chunks: []*schema.Message{chunk1, chunk2}, + } + + streamConsumingWrapModelHandler := &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return &streamConsumingModelWrapper{inner: m} + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + Handlers: []ChatModelAgentMiddleware{streamConsumingWrapModelHandler}, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var streamingEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.IsStreaming && + event.Output.MessageOutput.Role == schema.Assistant { + streamingEvents = append(streamingEvents, event) + } + } + + assert.GreaterOrEqual(t, len(streamingEvents), 1, "Should have at least one streaming event") + + if len(streamingEvents) > 0 { + event := streamingEvents[0] + assert.NotNil(t, event.Output.MessageOutput.MessageStream, "Event should have message stream") + + var receivedChunks []*schema.Message + for { + chunk, recvErr := event.Output.MessageOutput.MessageStream.Recv() + if recvErr != nil { + break + } + receivedChunks = append(receivedChunks, chunk) + } + + assert.Equal(t, 1, len(receivedChunks), + "AgentEvent's MessageStream should contain 1 concatenated chunk (modified by user middleware). "+ + "Got %d chunks instead.", + len(receivedChunks)) + + if len(receivedChunks) >= 1 { + assert.Equal(t, "Hello World", receivedChunks[0].Content, "Chunk content should be concatenated by user middleware") + } + } + }) + + t.Run("AgentEventMessageStreamShouldReflectMultipleUserMiddlewareModifications", func(t *testing.T) { + ctx := context.Background() + + chunk1 := schema.AssistantMessage("Hello ", nil) + chunk2 := schema.AssistantMessage("World", nil) + + mockModel := &mockStreamingModel{ + chunks: []*schema.Message{chunk1, chunk2}, + } + + handler1 := &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return &streamConsumingModelWrapper{inner: m} + }, + } + + handler2 := &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return m + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + Handlers: []ChatModelAgentMiddleware{handler1, handler2}, + ModelRetryConfig: &ModelRetryConfig{ + MaxRetries: 3, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var streamingEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.IsStreaming && + event.Output.MessageOutput.Role == schema.Assistant { + streamingEvents = append(streamingEvents, event) + } + } + + assert.GreaterOrEqual(t, len(streamingEvents), 1, "Should have at least one streaming event") + + if len(streamingEvents) > 0 { + event := streamingEvents[0] + assert.NotNil(t, event.Output.MessageOutput.MessageStream, "Event should have message stream") + + var receivedChunks []*schema.Message + for { + chunk, recvErr := event.Output.MessageOutput.MessageStream.Recv() + if recvErr != nil { + break + } + receivedChunks = append(receivedChunks, chunk) + } + + assert.Equal(t, 1, len(receivedChunks), + "AgentEvent's MessageStream should contain 1 concatenated chunk (modified by user middleware). "+ + "Got %d chunks instead.", + len(receivedChunks)) + + if len(receivedChunks) >= 1 { + assert.Equal(t, "Hello World", receivedChunks[0].Content, "Chunk content should be concatenated by user middleware") + } + } + }) +} + +type mockStreamingModel struct { + chunks []*schema.Message +} + +func (m *mockStreamingModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return schema.ConcatMessages(m.chunks) +} + +func (m *mockStreamingModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](len(m.chunks)) + go func() { + defer sw.Close() + for _, chunk := range m.chunks { + sw.Send(chunk, nil) + } + }() + return sr, nil +} + +type streamConsumingModelWrapper struct { + inner model.BaseChatModel +} + +func (m *streamConsumingModelWrapper) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return m.inner.Generate(ctx, input, opts...) +} + +func (m *streamConsumingModelWrapper) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + stream, err := m.inner.Stream(ctx, input, opts...) + if err != nil { + return nil, err + } + result, err := schema.ConcatMessageStream(stream) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.Message{result}), nil +} + +func TestEventSenderModelWrapperCustomPosition(t *testing.T) { + t.Run("UserConfiguredEventSenderSkipsDefaultEventSender", func(t *testing.T) { + ctx := context.Background() + + chunk1 := schema.AssistantMessage("Hello ", nil) + chunk2 := schema.AssistantMessage("World", nil) + + mockModel := &mockStreamingModel{ + chunks: []*schema.Message{chunk1, chunk2}, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + Handlers: []ChatModelAgentMiddleware{NewEventSenderModelWrapper()}, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var streamingEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.IsStreaming && + event.Output.MessageOutput.Role == schema.Assistant { + streamingEvents = append(streamingEvents, event) + } + } + + assert.Equal(t, 1, len(streamingEvents), "Should have exactly one streaming event (no duplicate from default event sender)") + }) + + t.Run("EventSenderAfterUserMiddlewareByDefault", func(t *testing.T) { + ctx := context.Background() + + mockModel := &mockStreamingModel{ + chunks: []*schema.Message{ + schema.AssistantMessage("Original", nil), + }, + } + + modifiedContent := "Modified" + contentModifyingHandler := &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return &contentModifyingModelWrapper{inner: m, newContent: modifiedContent} + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + Handlers: []ChatModelAgentMiddleware{contentModifyingHandler}, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + }) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var assistantEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.Role == schema.Assistant { + assistantEvents = append(assistantEvents, event) + } + } + + assert.GreaterOrEqual(t, len(assistantEvents), 1, "Should have at least one assistant event") + if len(assistantEvents) > 0 { + msg := assistantEvents[0].Output.MessageOutput.Message + assert.Equal(t, modifiedContent, msg.Content, "Event should contain modified content from user middleware") + } + }) + + t.Run("EventSenderInnermostGetsOriginalOutput", func(t *testing.T) { + ctx := context.Background() + + originalContent := "Original" + mockModel := &mockStreamingModel{ + chunks: []*schema.Message{ + schema.AssistantMessage(originalContent, nil), + }, + } + + modifiedContent := "Modified" + contentModifyingHandler := &testModelWrapperHandler{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel { + return &contentModifyingModelWrapper{inner: m, newContent: modifiedContent} + }, + } + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + Handlers: []ChatModelAgentMiddleware{ + contentModifyingHandler, + NewEventSenderModelWrapper(), + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{ + Agent: agent, + EnableStreaming: false, + }) + iter := r.Run(ctx, []Message{schema.UserMessage("test")}) + + var assistantEvents []*AgentEvent + for { + event, ok := iter.Next() + if !ok { + break + } + if event.Output != nil && event.Output.MessageOutput != nil && + event.Output.MessageOutput.Role == schema.Assistant { + assistantEvents = append(assistantEvents, event) + } + } + + assert.GreaterOrEqual(t, len(assistantEvents), 1, "Should have at least one assistant event") + if len(assistantEvents) > 0 { + msg := assistantEvents[0].Output.MessageOutput.Message + assert.Equal(t, originalContent, msg.Content, "Event should contain original content (EventSenderModelWrapper is innermost)") + } + }) +} + +type contentModifyingModelWrapper struct { + inner model.BaseChatModel + newContent string +} + +func (m *contentModifyingModelWrapper) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + result, err := m.inner.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + result.Content = m.newContent + return result, nil +} + +func (m *contentModifyingModelWrapper) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + stream, err := m.inner.Stream(ctx, input, opts...) + if err != nil { + return nil, err + } + result, err := schema.ConcatMessageStream(stream) + if err != nil { + return nil, err + } + result.Content = m.newContent + return schema.StreamReaderFromArray([]*schema.Message{result}), nil +} + +type mockToolCallingModel struct { + mu sync.Mutex + generateCalls int + toolCallName string +} + +func (m *mockToolCallingModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) { + m.mu.Lock() + m.generateCalls++ + calls := m.generateCalls + m.mu.Unlock() + if calls == 1 { + return schema.AssistantMessage("calling tool", []schema.ToolCall{ + {ID: "tc-1", Function: schema.FunctionCall{Name: m.toolCallName, Arguments: `{"input":"test"}`}}, + }), nil + } + return schema.AssistantMessage("done", nil), nil +} + +func (m *mockToolCallingModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + msg, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + return schema.StreamReaderFromArray([]*schema.Message{msg}), nil +} + +func (m *mockToolCallingModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + return m, nil +} + +type invokableTestTool struct { + name string + result string +} + +func (t *invokableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "test tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Desc: "input", Required: true, Type: schema.String}, + }), + }, nil +} + +func (t *invokableTestTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return t.result, nil +} + +type streamableTestTool struct { + name string + result string +} + +func (t *streamableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "test tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Desc: "input", Required: true, Type: schema.String}, + }), + }, nil +} + +func (t *streamableTestTool) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) { + return schema.StreamReaderFromArray([]string{t.result}), nil +} + +type enhancedInvokableTestTool struct { + name string + result string +} + +func (t *enhancedInvokableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "test tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Desc: "input", Required: true, Type: schema.String}, + }), + }, nil +} + +func (t *enhancedInvokableTestTool) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: t.result}}, + }, nil +} + +type enhancedStreamableTestTool struct { + name string + result string +} + +func (t *enhancedStreamableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Desc: "test tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Desc: "input", Required: true, Type: schema.String}, + }), + }, nil +} + +func (t *enhancedStreamableTestTool) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return schema.StreamReaderFromArray([]*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: t.result}}}, + }), nil +} + +type invokableResultModifier struct { + *BaseChatModelAgentMiddleware + modifiedResult string +} + +func (h *invokableResultModifier) WrapInvokableToolCall(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) (InvokableToolCallEndpoint, error) { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + _, err := endpoint(ctx, argumentsInJSON, opts...) + if err != nil { + return "", err + } + return h.modifiedResult, nil + }, nil +} + +type streamableResultModifier struct { + *BaseChatModelAgentMiddleware + modifiedResult string +} + +func (h *streamableResultModifier) WrapStreamableToolCall(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) (StreamableToolCallEndpoint, error) { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + sr, err := endpoint(ctx, argumentsInJSON, opts...) + if err != nil { + return nil, err + } + sr.Close() + return schema.StreamReaderFromArray([]string{h.modifiedResult}), nil + }, nil +} + +type enhancedInvokableResultModifier struct { + *BaseChatModelAgentMiddleware + modifiedResult string +} + +func (h *enhancedInvokableResultModifier) WrapEnhancedInvokableToolCall(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) (EnhancedInvokableToolCallEndpoint, error) { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + _, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: h.modifiedResult}}, + }, nil + }, nil +} + +type enhancedStreamableResultModifier struct { + *BaseChatModelAgentMiddleware + modifiedResult string +} + +func (h *enhancedStreamableResultModifier) WrapEnhancedStreamableToolCall(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) (EnhancedStreamableToolCallEndpoint, error) { + return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + sr, err := endpoint(ctx, toolArgument, opts...) + if err != nil { + return nil, err + } + sr.Close() + return schema.StreamReaderFromArray([]*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: h.modifiedResult}}}, + }), nil + }, nil +} + +func collectToolEvents(it *AsyncIterator[*AgentEvent]) []*AgentEvent { + var toolEvents []*AgentEvent + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + mo := ev.Output.MessageOutput + if mo.Message != nil && mo.Message.Role == schema.Tool { + toolEvents = append(toolEvents, ev) + continue + } + if mo.IsStreaming && mo.Role == schema.Tool && mo.MessageStream != nil { + toolEvents = append(toolEvents, ev) + } + } + return toolEvents +} + +func collectToolContent(events []*AgentEvent) []string { + var contents []string + for _, ev := range events { + mo := ev.Output.MessageOutput + if !mo.IsStreaming && mo.Message != nil { + if mo.Message.Content != "" { + contents = append(contents, mo.Message.Content) + } else if len(mo.Message.UserInputMultiContent) > 0 { + for _, part := range mo.Message.UserInputMultiContent { + if part.Text != "" { + contents = append(contents, part.Text) + } + } + } + continue + } + if mo.IsStreaming && mo.MessageStream != nil { + var msgs []*schema.Message + for { + msg, err := mo.MessageStream.Recv() + if err != nil { + break + } + msgs = append(msgs, msg) + } + if len(msgs) > 0 { + concated, err := schema.ConcatMessages(msgs) + if err == nil { + if concated.Content != "" { + contents = append(contents, concated.Content) + } else if len(concated.UserInputMultiContent) > 0 { + for _, part := range concated.UserInputMultiContent { + if part.Text != "" { + contents = append(contents, part.Text) + } + } + } + } + } + } + } + return contents +} + +func TestEventSenderToolHandler(t *testing.T) { + t.Run("Invokable", func(t *testing.T) { + t.Run("DefaultSendsEvent", func(t *testing.T) { + ctx := context.Background() + testTool := &invokableTestTool{name: "test_tool", result: "invokable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, "invokable_output") + }) + + t.Run("UserConfiguredSkipsDefault", func(t *testing.T) { + ctx := context.Background() + testTool := &invokableTestTool{name: "test_tool", result: "invokable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()}, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + }) + + t.Run("InnermostGetsOriginalOutput", func(t *testing.T) { + ctx := context.Background() + originalResult := "original_invokable_output" + modifiedResult := "modified_invokable_output" + testTool := &invokableTestTool{name: "test_tool", result: originalResult} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &invokableResultModifier{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + modifiedResult: modifiedResult, + }, + NewEventSenderToolWrapper(), + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.GreaterOrEqual(t, len(toolEvents), 1) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, originalResult) + }) + }) + + t.Run("Streamable", func(t *testing.T) { + t.Run("DefaultSendsEvent", func(t *testing.T) { + ctx := context.Background() + testTool := &streamableTestTool{name: "test_tool", result: "streamable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, "streamable_output") + }) + + t.Run("UserConfiguredSkipsDefault", func(t *testing.T) { + ctx := context.Background() + testTool := &streamableTestTool{name: "test_tool", result: "streamable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()}, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + }) + + t.Run("InnermostGetsOriginalOutput", func(t *testing.T) { + ctx := context.Background() + originalResult := "original_streamable_output" + modifiedResult := "modified_streamable_output" + testTool := &streamableTestTool{name: "test_tool", result: originalResult} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &streamableResultModifier{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + modifiedResult: modifiedResult, + }, + NewEventSenderToolWrapper(), + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.GreaterOrEqual(t, len(toolEvents), 1) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, originalResult) + }) + }) + + t.Run("EnhancedInvokable", func(t *testing.T) { + t.Run("DefaultSendsEvent", func(t *testing.T) { + ctx := context.Background() + testTool := &enhancedInvokableTestTool{name: "test_tool", result: "enhanced_invokable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, "enhanced_invokable_output") + }) + + t.Run("UserConfiguredSkipsDefault", func(t *testing.T) { + ctx := context.Background() + testTool := &enhancedInvokableTestTool{name: "test_tool", result: "enhanced_invokable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()}, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + }) + + t.Run("InnermostGetsOriginalOutput", func(t *testing.T) { + ctx := context.Background() + originalResult := "original_enhanced_invokable_output" + modifiedResult := "modified_enhanced_invokable_output" + testTool := &enhancedInvokableTestTool{name: "test_tool", result: originalResult} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &enhancedInvokableResultModifier{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + modifiedResult: modifiedResult, + }, + NewEventSenderToolWrapper(), + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.GreaterOrEqual(t, len(toolEvents), 1) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, originalResult) + }) + }) + + t.Run("EnhancedStreamable", func(t *testing.T) { + t.Run("DefaultSendsEvent", func(t *testing.T) { + ctx := context.Background() + testTool := &enhancedStreamableTestTool{name: "test_tool", result: "enhanced_streamable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, "enhanced_streamable_output") + }) + + t.Run("UserConfiguredSkipsDefault", func(t *testing.T) { + ctx := context.Background() + testTool := &enhancedStreamableTestTool{name: "test_tool", result: "enhanced_streamable_output"} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()}, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + }) + + t.Run("InnermostGetsOriginalOutput", func(t *testing.T) { + ctx := context.Background() + originalResult := "original_enhanced_streamable_output" + modifiedResult := "modified_enhanced_streamable_output" + testTool := &enhancedStreamableTestTool{name: "test_tool", result: originalResult} + mockModel := &mockToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{ + Name: "TestAgent", + Description: "Test agent", + Model: mockModel, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + }, + }, + Handlers: []ChatModelAgentMiddleware{ + &enhancedStreamableResultModifier{ + BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{}, + modifiedResult: modifiedResult, + }, + NewEventSenderToolWrapper(), + }, + }) + assert.NoError(t, err) + + r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true}) + it := r.Run(ctx, []Message{schema.UserMessage("test")}) + + toolEvents := collectToolEvents(it) + assert.GreaterOrEqual(t, len(toolEvents), 1) + contents := collectToolContent(toolEvents) + assert.Contains(t, contents, originalResult) + }) + }) +} + +// mockAgenticToolCallingModel is a model.BaseModel[*schema.AgenticMessage] that +// returns a tool call on the first Generate, then a final answer on the second. +type mockAgenticToolCallingModel struct { + toolCallName string + callCount int32 +} + +func (m *mockAgenticToolCallingModel) Generate(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) { + idx := atomic.AddInt32(&m.callCount, 1) + if idx == 1 { + return agenticToolCallMsg(m.toolCallName, "tc-1", `{"input":"test"}`), nil + } + return agenticMsg("done"), nil +} + +func (m *mockAgenticToolCallingModel) Stream(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) { + msg, err := m.Generate(ctx, input, opts...) + if err != nil { + return nil, err + } + r, w := schema.Pipe[*schema.AgenticMessage](1) + go func() { defer w.Close(); w.Send(msg, nil) }() + return r, nil +} + +// collectAgenticToolEvents filters tool result events from the agentic iterator. +// Agentic tool results have AgenticRole == AgenticRoleTypeUser and contain +// FunctionToolResult content blocks. +func collectAgenticToolEvents(it *AsyncIterator[*agenticAgentEvent]) []*agenticAgentEvent { + var toolEvents []*agenticAgentEvent + for { + ev, ok := it.Next() + if !ok { + break + } + if ev.Output == nil || ev.Output.MessageOutput == nil { + continue + } + mo := ev.Output.MessageOutput + if mo.AgenticRole == schema.AgenticRoleTypeUser { + toolEvents = append(toolEvents, ev) + } + } + return toolEvents +} + +// collectAgenticToolContent extracts text from agentic tool result events. +func collectAgenticToolContent(events []*agenticAgentEvent) []string { + var contents []string + for _, ev := range events { + mo := ev.Output.MessageOutput + if !mo.IsStreaming && mo.Message != nil { + for _, cb := range mo.Message.ContentBlocks { + if cb.FunctionToolResult != nil { + for _, b := range cb.FunctionToolResult.Content { + if b.Text != nil { + contents = append(contents, b.Text.Text) + } + } + } + } + continue + } + if mo.IsStreaming && mo.MessageStream != nil { + for { + msg, err := mo.MessageStream.Recv() + if err != nil { + break + } + for _, cb := range msg.ContentBlocks { + if cb.FunctionToolResult != nil { + for _, b := range cb.FunctionToolResult.Content { + if b.Text != nil { + contents = append(contents, b.Text.Text) + } + } + } + } + } + } + } + return contents +} + +func newAgenticEventSenderToolWrapper() TypedChatModelAgentMiddleware[*schema.AgenticMessage] { + return &typedEventSenderToolWrapper[*schema.AgenticMessage]{ + TypedBaseChatModelAgentMiddleware: &TypedBaseChatModelAgentMiddleware[*schema.AgenticMessage]{}, + } +} + +// TestAgenticEventSenderToolHandler exercises the *schema.AgenticMessage branches +// in typedToolInvokeEvent, typedToolStreamEvent, typedToolEnhancedInvokeEvent, +// typedToolEnhancedStreamEvent, plus the helpers textToFunctionToolResultBlocks, +// toolResultToBlocks, and derefString. +func TestAgenticEventSenderToolHandler(t *testing.T) { + t.Run("Invokable", func(t *testing.T) { + ctx := context.Background() + testTool := &invokableTestTool{name: "test_tool", result: "invokable_output"} + mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}}, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()}, + }) + require.NoError(t, err) + + r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: false}) + it := r.Query(ctx, "test") + + toolEvents := collectAgenticToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectAgenticToolContent(toolEvents) + assert.Contains(t, contents, "invokable_output") + }) + + t.Run("Streamable", func(t *testing.T) { + ctx := context.Background() + testTool := &streamableTestTool{name: "test_tool", result: "streamable_output"} + mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}}, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()}, + }) + require.NoError(t, err) + + r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: true}) + it := r.Query(ctx, "test") + + toolEvents := collectAgenticToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectAgenticToolContent(toolEvents) + assert.Contains(t, contents, "streamable_output") + }) + + t.Run("EnhancedInvokable", func(t *testing.T) { + ctx := context.Background() + testTool := &enhancedInvokableTestTool{name: "test_tool", result: "enhanced_output"} + mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}}, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()}, + }) + require.NoError(t, err) + + r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: false}) + it := r.Query(ctx, "test") + + toolEvents := collectAgenticToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectAgenticToolContent(toolEvents) + assert.Contains(t, contents, "enhanced_output") + }) + + t.Run("EnhancedStreamable", func(t *testing.T) { + ctx := context.Background() + testTool := &enhancedStreamableTestTool{name: "test_tool", result: "enhanced_stream_output"} + mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}}, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()}, + }) + require.NoError(t, err) + + r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: true}) + it := r.Query(ctx, "test") + + toolEvents := collectAgenticToolEvents(it) + assert.Equal(t, 1, len(toolEvents)) + contents := collectAgenticToolContent(toolEvents) + assert.Contains(t, contents, "enhanced_stream_output") + }) + + t.Run("EnhancedInvokableMultimodal", func(t *testing.T) { + ctx := context.Background() + imgURL := "https://example.com/img.png" + testTool := &multimodalEnhancedInvokableTestTool{ + name: "test_tool", + result: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "caption"}, + {Type: schema.ToolPartTypeImage, Image: &schema.ToolOutputImage{MessagePartCommon: schema.MessagePartCommon{URL: &imgURL}}}, + }, + }, + } + mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}}, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()}, + }) + require.NoError(t, err) + + r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: false}) + it := r.Query(ctx, "test") + + toolEvents := collectAgenticToolEvents(it) + require.Equal(t, 1, len(toolEvents)) + + // Verify multimodal content + msg := toolEvents[0].Output.MessageOutput.Message + require.NotNil(t, msg) + require.Len(t, msg.ContentBlocks, 1) + ftr := msg.ContentBlocks[0].FunctionToolResult + require.NotNil(t, ftr) + require.Len(t, ftr.Content, 2) + assert.Equal(t, "caption", ftr.Content[0].Text.Text) + assert.Equal(t, "https://example.com/img.png", ftr.Content[1].Image.URL) + }) + + t.Run("EnhancedStreamableMultimodal", func(t *testing.T) { + ctx := context.Background() + audioURL := "https://example.com/audio.mp3" + testTool := &multimodalEnhancedStreamableTestTool{ + name: "test_tool", + result: &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "transcript"}, + {Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{URL: &audioURL}}}, + }, + }, + } + mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"} + + agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{ + Name: "TestAgent", + Description: "test", + Model: mdl, + ToolsConfig: ToolsConfig{ + ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}}, + }, + Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()}, + }) + require.NoError(t, err) + + r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: true}) + it := r.Query(ctx, "test") + + toolEvents := collectAgenticToolEvents(it) + require.Equal(t, 1, len(toolEvents)) + + // Drain the stream and verify multimodal content + mo := toolEvents[0].Output.MessageOutput + require.True(t, mo.IsStreaming) + var allBlocks []*schema.FunctionToolResultContentBlock + for { + msg, err := mo.MessageStream.Recv() + if err != nil { + break + } + for _, cb := range msg.ContentBlocks { + if cb.FunctionToolResult != nil { + allBlocks = append(allBlocks, cb.FunctionToolResult.Content...) + } + } + } + require.Len(t, allBlocks, 2) + assert.Equal(t, "transcript", allBlocks[0].Text.Text) + assert.Equal(t, "https://example.com/audio.mp3", allBlocks[1].Audio.URL) + }) +} + +func TestTypedToolStreamEventAgenticMessageSetsStreamingMeta(t *testing.T) { + event := typedToolStreamEvent[*schema.AgenticMessage]( + "call_1", + "execute", + "msg_1", + schema.StreamReaderFromArray([]string{"first\n", "second\n"}), + ) + require.NotNil(t, event) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + require.True(t, event.Output.MessageOutput.IsStreaming) + require.NotNil(t, event.Output.MessageOutput.MessageStream) + + first, err := event.Output.MessageOutput.MessageStream.Recv() + require.NoError(t, err) + require.Len(t, first.ContentBlocks, 1) + assert.Equal(t, &schema.StreamingMeta{Index: 0}, first.ContentBlocks[0].StreamingMeta) + + second, err := event.Output.MessageOutput.MessageStream.Recv() + require.NoError(t, err) + require.Len(t, second.ContentBlocks, 1) + assert.Equal(t, &schema.StreamingMeta{Index: 0}, second.ContentBlocks[0].StreamingMeta) + + result, err := schema.ConcatAgenticMessages([]*schema.AgenticMessage{first, second}) + require.NoError(t, err) + require.Len(t, result.ContentBlocks, 1) + assert.Nil(t, result.ContentBlocks[0].StreamingMeta) + require.NotNil(t, result.ContentBlocks[0].FunctionToolResult) + require.Len(t, result.ContentBlocks[0].FunctionToolResult.Content, 2) + assert.Equal(t, "first\n", result.ContentBlocks[0].FunctionToolResult.Content[0].Text.Text) + assert.Equal(t, "second\n", result.ContentBlocks[0].FunctionToolResult.Content[1].Text.Text) +} + +func TestTypedToolEnhancedStreamEventAgenticMessageSetsStreamingMeta(t *testing.T) { + event := typedToolEnhancedStreamEvent[*schema.AgenticMessage]( + "call_1", + "execute", + "msg_1", + schema.StreamReaderFromArray([]*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "first\n"}}}, + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "second\n"}}}, + }), + ) + require.NotNil(t, event) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + require.True(t, event.Output.MessageOutput.IsStreaming) + require.NotNil(t, event.Output.MessageOutput.MessageStream) + + first, err := event.Output.MessageOutput.MessageStream.Recv() + require.NoError(t, err) + require.Len(t, first.ContentBlocks, 1) + assert.Equal(t, &schema.StreamingMeta{Index: 0}, first.ContentBlocks[0].StreamingMeta) + + second, err := event.Output.MessageOutput.MessageStream.Recv() + require.NoError(t, err) + require.Len(t, second.ContentBlocks, 1) + assert.Equal(t, &schema.StreamingMeta{Index: 0}, second.ContentBlocks[0].StreamingMeta) + + result, err := schema.ConcatAgenticMessages([]*schema.AgenticMessage{first, second}) + require.NoError(t, err) + require.Len(t, result.ContentBlocks, 1) + assert.Nil(t, result.ContentBlocks[0].StreamingMeta) + require.NotNil(t, result.ContentBlocks[0].FunctionToolResult) + require.Len(t, result.ContentBlocks[0].FunctionToolResult.Content, 2) + assert.Equal(t, "first\n", result.ContentBlocks[0].FunctionToolResult.Content[0].Text.Text) + assert.Equal(t, "second\n", result.ContentBlocks[0].FunctionToolResult.Content[1].Text.Text) +} + +// multimodalEnhancedInvokableTestTool returns a pre-built multimodal ToolResult. +type multimodalEnhancedInvokableTestTool struct { + name string + result *schema.ToolResult +} + +func (t *multimodalEnhancedInvokableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, Desc: "multimodal test tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Desc: "input", Required: true, Type: schema.String}, + }), + }, nil +} + +func (t *multimodalEnhancedInvokableTestTool) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return t.result, nil +} + +// multimodalEnhancedStreamableTestTool returns a pre-built multimodal ToolResult as a stream. +type multimodalEnhancedStreamableTestTool struct { + name string + result *schema.ToolResult +} + +func (t *multimodalEnhancedStreamableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, Desc: "multimodal streaming test tool", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Desc: "input", Required: true, Type: schema.String}, + }), + }, nil +} + +func (t *multimodalEnhancedStreamableTestTool) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return schema.StreamReaderFromArray([]*schema.ToolResult{t.result}), nil +} + +func Test_functionToolResultAgenticMessage(t *testing.T) { + t.Run("basic", func(t *testing.T) { + blocks := []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "result_str"}}, + } + msg := functionToolResultAgenticMessage("call_1", "tool_name", blocks) + assert.Equal(t, schema.AgenticRoleTypeUser, msg.Role) + assert.Len(t, msg.ContentBlocks, 1) + assert.Equal(t, schema.ContentBlockTypeFunctionToolResult, msg.ContentBlocks[0].Type) + ftr := msg.ContentBlocks[0].FunctionToolResult + assert.Equal(t, "call_1", ftr.CallID) + assert.Equal(t, "tool_name", ftr.Name) + assert.Len(t, ftr.Content, 1) + assert.Equal(t, "result_str", ftr.Content[0].Text.Text) + }) + + t.Run("multimodal", func(t *testing.T) { + blocks := []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "description"}}, + {Type: schema.FunctionToolResultContentBlockTypeImage, Image: &schema.UserInputImage{URL: "https://example.com/img.png"}}, + } + msg := functionToolResultAgenticMessage("call_2", "vision_tool", blocks) + assert.Equal(t, schema.AgenticRoleTypeUser, msg.Role) + ftr := msg.ContentBlocks[0].FunctionToolResult + assert.Equal(t, "call_2", ftr.CallID) + assert.Equal(t, "vision_tool", ftr.Name) + assert.Len(t, ftr.Content, 2) + assert.Equal(t, "description", ftr.Content[0].Text.Text) + assert.Equal(t, "https://example.com/img.png", ftr.Content[1].Image.URL) + }) +} + +func TestTypedToolEnhancedEventAgenticToolSearchResult(t *testing.T) { + result := &schema.ToolResult{Parts: []schema.ToolOutputPart{ + { + Type: schema.ToolPartTypeToolSearchResult, + ToolSearchResult: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{ + {Name: "dynamic_tool", Desc: "dynamic tool"}, + }}, + }, + }} + + t.Run("invoke", func(t *testing.T) { + event, err := typedToolEnhancedInvokeEvent[*schema.AgenticMessage]("call_1", "tool_search", "msg_1", result) + require.NoError(t, err) + require.NotNil(t, event) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + + msg := event.Output.MessageOutput.Message + require.NotNil(t, msg) + require.Len(t, msg.ContentBlocks, 1) + block := msg.ContentBlocks[0] + assert.Equal(t, schema.ContentBlockTypeToolSearchResult, block.Type) + require.NotNil(t, block.ToolSearchFunctionToolResult) + assert.Equal(t, "call_1", block.ToolSearchFunctionToolResult.CallID) + assert.Equal(t, "tool_search", block.ToolSearchFunctionToolResult.Name) + require.NotNil(t, block.ToolSearchFunctionToolResult.Result) + require.Len(t, block.ToolSearchFunctionToolResult.Result.Tools, 1) + assert.Equal(t, "dynamic_tool", block.ToolSearchFunctionToolResult.Result.Tools[0].Name) + }) + + t.Run("stream", func(t *testing.T) { + event := typedToolEnhancedStreamEvent[*schema.AgenticMessage]( + "call_2", + "tool_search", + "msg_2", + schema.StreamReaderFromArray([]*schema.ToolResult{result}), + ) + require.NotNil(t, event) + require.NotNil(t, event.Output) + require.NotNil(t, event.Output.MessageOutput) + + msg, err := event.Output.MessageOutput.MessageStream.Recv() + require.NoError(t, err) + require.NotNil(t, msg) + require.Len(t, msg.ContentBlocks, 1) + block := msg.ContentBlocks[0] + assert.Equal(t, schema.ContentBlockTypeToolSearchResult, block.Type) + assert.Equal(t, &schema.StreamingMeta{Index: 0}, block.StreamingMeta) + require.NotNil(t, block.ToolSearchFunctionToolResult) + assert.Equal(t, "call_2", block.ToolSearchFunctionToolResult.CallID) + assert.Equal(t, "tool_search", block.ToolSearchFunctionToolResult.Name) + require.NotNil(t, block.ToolSearchFunctionToolResult.Result) + require.Len(t, block.ToolSearchFunctionToolResult.Result.Tools, 1) + assert.Equal(t, "dynamic_tool", block.ToolSearchFunctionToolResult.Result.Tools[0].Name) + }) +} + +func TestExtractToolIdentifiersToolSearchResult(t *testing.T) { + msg := &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + schema.NewContentBlock(&schema.ToolSearchFunctionToolResult{ + CallID: "call_1", + Name: "tool_search", + Result: &schema.ToolSearchResult{}, + }), + }, + } + + toolName, callID := extractToolIdentifiers(msg) + assert.Equal(t, "tool_search", toolName) + assert.Equal(t, "call_1", callID) +} diff --git a/callbacks/aspect_inject.go b/callbacks/aspect_inject.go new file mode 100644 index 0000000..2b91e25 --- /dev/null +++ b/callbacks/aspect_inject.go @@ -0,0 +1,174 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/internal/callbacks" + "github.com/cloudwego/eino/schema" +) + +// OnStart Fast inject callback input / output aspect for component developer +// e.g. +// +// func (t *testChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (resp *schema.Message, err error) { +// defer func() { +// if err != nil { +// callbacks.OnError(ctx, err) +// } +// }() +// +// ctx = callbacks.OnStart(ctx, &model.CallbackInput{ +// Messages: input, +// Tools: nil, +// Extra: nil, +// }) +// +// // do smt +// +// ctx = callbacks.OnEnd(ctx, &model.CallbackOutput{ +// Message: resp, +// Extra: nil, +// }) +// +// return resp, nil +// } + +// OnStart invokes the OnStart timing for all registered handlers in the +// context. This is called by component implementations that manage their own +// callbacks (i.e. implement [components.Checker] and return true from +// IsCallbacksEnabled). The returned context must be propagated to subsequent +// OnEnd/OnError calls so handlers can correlate start and end events. +// +// Handlers are invoked in reverse registration order (last registered = first +// called) to match the middleware wrapping convention. +// +// Example — typical usage inside a component's Generate method: +// +// func (m *myChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { +// ctx = callbacks.OnStart(ctx, &model.CallbackInput{Messages: input}) +// resp, err := m.doGenerate(ctx, input, opts...) +// if err != nil { +// callbacks.OnError(ctx, err) +// return nil, err +// } +// callbacks.OnEnd(ctx, &model.CallbackOutput{Message: resp}) +// return resp, nil +// } +func OnStart[T any](ctx context.Context, input T) context.Context { + ctx, _ = callbacks.On(ctx, input, callbacks.OnStartHandle[T], TimingOnStart, true) + + return ctx +} + +// OnEnd invokes the OnEnd timing for all registered handlers. Call this after +// the component produces a successful result. Handlers run in registration +// order (first registered = first called). +// +// Do not call both OnEnd and OnError for the same invocation — OnEnd signals +// success; OnError signals failure. +func OnEnd[T any](ctx context.Context, output T) context.Context { + ctx, _ = callbacks.On(ctx, output, callbacks.OnEndHandle[T], TimingOnEnd, false) + + return ctx +} + +// OnStartWithStreamInput invokes the OnStartWithStreamInput timing. Use this +// when the component's input is itself a stream (Collect / Transform +// paradigms). The framework automatically copies the stream so each handler +// receives an independent reader; handlers MUST close their copy or the +// underlying goroutine will leak. +// +// Returns the updated context and a new StreamReader that the component should +// use going forward (the original is consumed by the framework). +func OnStartWithStreamInput[T any](ctx context.Context, input *schema.StreamReader[T]) ( + nextCtx context.Context, newStreamReader *schema.StreamReader[T]) { + + return callbacks.On(ctx, input, callbacks.OnStartWithStreamInputHandle[T], TimingOnStartWithStreamInput, true) +} + +// OnEndWithStreamOutput invokes the OnEndWithStreamOutput timing. Use this +// when the component produces a streaming output (Stream / Transform +// paradigms). Like OnStartWithStreamInput, stream copies are made per +// handler; each handler must close its copy. +// +// Returns the updated context and the StreamReader the component should return +// to its caller. +func OnEndWithStreamOutput[T any](ctx context.Context, output *schema.StreamReader[T]) ( + nextCtx context.Context, newStreamReader *schema.StreamReader[T]) { + + return callbacks.On(ctx, output, callbacks.OnEndWithStreamOutputHandle[T], TimingOnEndWithStreamOutput, false) +} + +// OnError invokes the OnError timing for all registered handlers. Call this +// when the component returns an error. Errors that occur mid-stream (after the +// StreamReader has been returned) are NOT routed through OnError; they surface +// as errors inside Recv. +// +// Handlers run in registration order (same as OnEnd). +func OnError(ctx context.Context, err error) context.Context { + ctx, _ = callbacks.On(ctx, err, callbacks.OnErrorHandle, TimingOnError, false) + + return ctx +} + +// EnsureRunInfo ensures the context carries a [RunInfo] for the given type and +// component kind. If the context already has a matching RunInfo, it is +// returned unchanged. Otherwise, a new callback manager is created that +// inherits the global handlers plus any handlers already in ctx. +// +// Component implementations that set IsCallbacksEnabled() = true should call +// this at the start of every public method (Generate, Stream, etc.) before +// calling [OnStart], so that the RunInfo is never missing from callbacks. +func EnsureRunInfo(ctx context.Context, typ string, comp components.Component) context.Context { + return callbacks.EnsureRunInfo(ctx, typ, comp) +} + +// ReuseHandlers creates a new context that inherits all handlers already +// present in ctx and sets a new RunInfo. Global handlers are added if ctx +// carries none yet. +// +// Use this when a component calls another component internally and wants the +// inner component's callbacks to share the same set of handlers as the outer +// component, but with the inner component's own identity in RunInfo: +// +// innerCtx := callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{ +// Type: "InnerChatModel", +// Component: components.ComponentOfChatModel, +// Name: "inner-cm", +// }) +func ReuseHandlers(ctx context.Context, info *RunInfo) context.Context { + return callbacks.ReuseHandlers(ctx, info) +} + +// InitCallbacks creates a new context with the given RunInfo and handlers, +// completely replacing any RunInfo and handlers already in ctx. +// +// Use this when running a component standalone outside a Graph — the Graph +// normally manages RunInfo injection automatically, but standalone callers must +// set it up themselves: +// +// ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{ +// Type: myModel.GetType(), +// Component: components.ComponentOfChatModel, +// Name: "my-model", +// }, myHandler) +func InitCallbacks(ctx context.Context, info *RunInfo, handlers ...Handler) context.Context { + return callbacks.InitCallbacks(ctx, info, handlers...) +} diff --git a/callbacks/aspect_inject_test.go b/callbacks/aspect_inject_test.go new file mode 100644 index 0000000..76d73f7 --- /dev/null +++ b/callbacks/aspect_inject_test.go @@ -0,0 +1,330 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + "fmt" + "io" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/callbacks" + "github.com/cloudwego/eino/schema" +) + +func TestAspectInject(t *testing.T) { + t.Run("ctx without manager", func(t *testing.T) { + ctx := context.Background() + ctx = OnStart(ctx, 1) + ctx = OnEnd(ctx, 2) + ctx = OnError(ctx, fmt.Errorf("3")) + isr, isw := schema.Pipe[int](2) + go func() { + for i := 0; i < 10; i++ { + isw.Send(i, nil) + } + isw.Close() + }() + + var nisr *schema.StreamReader[int] + ctx, nisr = OnStartWithStreamInput(ctx, isr) + j := 0 + for { + i, err := nisr.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, j, i) + j++ + } + nisr.Close() + + osr, osw := schema.Pipe[int](2) + go func() { + for i := 0; i < 10; i++ { + osw.Send(i, nil) + } + osw.Close() + }() + + var nosr *schema.StreamReader[int] + ctx, nosr = OnEndWithStreamOutput(ctx, osr) + j = 0 + for { + i, err := nosr.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, j, i) + j++ + } + nosr.Close() + }) + + t.Run("ctx with manager", func(t *testing.T) { + ctx := context.Background() + cnt := 0 + + hb := NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { + cnt += input.(int) + return ctx + }). + OnEndFn(func(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context { + cnt += output.(int) + return ctx + }). + OnErrorFn(func(ctx context.Context, info *RunInfo, err error) context.Context { + v, _ := strconv.ParseInt(err.Error(), 10, 64) + cnt += int(v) + return ctx + }). + OnStartWithStreamInputFn(func(ctx context.Context, info *RunInfo, input *schema.StreamReader[CallbackInput]) context.Context { + for { + i, err := input.Recv() + if err == io.EOF { + break + } + + cnt += i.(int) + } + + input.Close() + return ctx + }). + OnEndWithStreamOutputFn(func(ctx context.Context, info *RunInfo, output *schema.StreamReader[CallbackOutput]) context.Context { + for { + o, err := output.Recv() + if err == io.EOF { + break + } + + cnt += o.(int) + } + + output.Close() + return ctx + }).Build() + + ctx = InitCallbacks(ctx, nil, hb) + + ctx = OnStart(ctx, 1) + ctx = OnEnd(ctx, 2) + ctx = OnError(ctx, fmt.Errorf("3")) + isr, isw := schema.Pipe[int](2) + go func() { + for i := 0; i < 10; i++ { + isw.Send(i, nil) + } + isw.Close() + }() + + ctx = ReuseHandlers(ctx, &RunInfo{}) + var nisr *schema.StreamReader[int] + ctx, nisr = OnStartWithStreamInput(ctx, isr) + j := 0 + for { + i, err := nisr.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, j, i) + j++ + cnt += i + } + nisr.Close() + + osr, osw := schema.Pipe[int](2) + go func() { + for i := 0; i < 10; i++ { + osw.Send(i, nil) + } + osw.Close() + }() + + var nosr *schema.StreamReader[int] + ctx, nosr = OnEndWithStreamOutput(ctx, osr) + j = 0 + for { + i, err := nosr.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, j, i) + j++ + cnt += i + } + nosr.Close() + assert.Equal(t, 186, cnt) + }) +} + +func TestGlobalCallbacksRepeated(t *testing.T) { + times := 0 + testHandler := NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + times++ + return ctx + }).Build() + callbacks.GlobalHandlers = append(callbacks.GlobalHandlers, testHandler) + + ctx := context.Background() + ctx = callbacks.AppendHandlers(ctx, &RunInfo{}) + ctx = callbacks.AppendHandlers(ctx, &RunInfo{}) + + callbacks.On(ctx, "test", callbacks.OnStartHandle[string], TimingOnStart, true) + assert.Equal(t, times, 1) +} + +func TestEnsureRunInfo(t *testing.T) { + ctx := context.Background() + + var name, typ, comp string + ctx = InitCallbacks(ctx, &RunInfo{Name: "name", Type: "type", Component: "component"}, NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { + name = info.Name + typ = info.Type + comp = string(info.Component) + return ctx + }).Build()) + + ctx = OnStart(ctx, "") + assert.Equal(t, "name", name) + assert.Equal(t, "type", typ) + assert.Equal(t, "component", comp) + ctx2 := EnsureRunInfo(ctx, "type2", "component2") + OnStart(ctx2, "") + assert.Equal(t, "", name) + assert.Equal(t, "type2", typ) + assert.Equal(t, "component2", comp) + + // EnsureRunInfo on an empty Context + AppendGlobalHandlers(NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { + typ = info.Type + comp = string(info.Component) + return ctx + }).Build()) + ctx3 := EnsureRunInfo(context.Background(), "type3", "component3") + OnStart(ctx3, 0) + assert.Equal(t, "type3", typ) + assert.Equal(t, "component3", comp) + callbacks.GlobalHandlers = []Handler{} +} + +func TestNesting(t *testing.T) { + ctx := context.Background() + cb := &myCallback{t: t} + ctx = InitCallbacks(ctx, &RunInfo{ + Name: "test", + }, cb) + + // jumped + ctx1 := OnStart(ctx, 0) + ctx2 := OnStart(ctx1, 1) + OnEnd(ctx2, 1) + OnEnd(ctx1, 0) + assert.Equal(t, 4, cb.times) + + // reused + cb.times = 0 + ctx1 = OnStart(ctx, 0) + ctx2 = ReuseHandlers(ctx1, &RunInfo{Name: "test2"}) + ctx3 := OnStart(ctx2, 1) + OnEnd(ctx3, 1) + OnEnd(ctx1, 0) + assert.Equal(t, 4, cb.times) + +} + +func TestReuseHandlersOnEmptyCtx(t *testing.T) { + callbacks.GlobalHandlers = []Handler{} + cb := &myCallback{t: t} + AppendGlobalHandlers(cb) + ctx := ReuseHandlers(context.Background(), &RunInfo{Name: "test"}) + OnStart(ctx, 0) + assert.Equal(t, 1, cb.times) +} + +func TestAppendHandlersTwiceOnSameCtx(t *testing.T) { + callbacks.GlobalHandlers = []Handler{} + cb := &myCallback{t: t} + cb1 := &myCallback{t: t} + cb2 := &myCallback{t: t} + ctx := InitCallbacks(context.Background(), &RunInfo{Name: "test"}, cb) + ctx1 := callbacks.AppendHandlers(ctx, &RunInfo{Name: "test"}, cb1) + ctx2 := callbacks.AppendHandlers(ctx, &RunInfo{Name: "test"}, cb2) + OnStart(ctx1, 0) + OnStart(ctx2, 0) + assert.Equal(t, 2, cb.times) + assert.Equal(t, 1, cb1.times) + assert.Equal(t, 1, cb2.times) +} + +type myCallback struct { + t *testing.T + times int +} + +func (m *myCallback) OnStart(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { + m.times++ + if info == nil { + assert.Equal(m.t, 2, m.times) + return ctx + } + if info.Name == "test" { + assert.Equal(m.t, 0, input) + } else { + assert.Equal(m.t, 1, input) + } + + return ctx +} + +func (m *myCallback) OnEnd(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context { + m.times++ + if info == nil { + assert.Equal(m.t, 3, m.times) + return ctx + } + if info.Name == "test" { + assert.Equal(m.t, 0, output) + } else { + assert.Equal(m.t, 1, output) + } + return ctx +} + +func (m *myCallback) OnError(ctx context.Context, info *RunInfo, err error) context.Context { + panic("implement me") +} + +func (m *myCallback) OnStartWithStreamInput(ctx context.Context, info *RunInfo, input *schema.StreamReader[CallbackInput]) context.Context { + panic("implement me") +} + +func (m *myCallback) OnEndWithStreamOutput(ctx context.Context, info *RunInfo, output *schema.StreamReader[CallbackOutput]) context.Context { + panic("implement me") +} diff --git a/callbacks/doc.go b/callbacks/doc.go new file mode 100644 index 0000000..e2a3ca0 --- /dev/null +++ b/callbacks/doc.go @@ -0,0 +1,129 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package callbacks provides observability hooks for component execution in Eino. +// +// Callbacks fire at five lifecycle timings around every component invocation: +// - [TimingOnStart] / [TimingOnEnd]: non-streaming input and output. +// - [TimingOnStartWithStreamInput] / [TimingOnEndWithStreamOutput]: streaming +// variants — handlers receive a copy of the stream and MUST close it. +// - [TimingOnError]: component returned a non-nil error (stream-internal +// errors are NOT reported here). +// +// # Attaching Handlers +// +// Global handlers (observe every node in every graph): +// +// callbacks.AppendGlobalHandlers(myHandler) // call once, at startup — NOT thread-safe +// +// Per-invocation handlers (observe one graph run): +// +// runnable.Invoke(ctx, input, compose.WithCallbacks(myHandler)) +// +// Target a specific node: +// +// compose.WithCallbacks(myHandler).DesignateNode("nodeName") +// +// Handler inheritance: if the context passed to a graph run already carries +// handlers (e.g. from a parent graph), those handlers are inherited by the +// entire child run automatically. +// +// # Building Handlers +// +// Option 1 — [NewHandlerBuilder]: register raw functions for the timings you +// need. Input/output are untyped; use the component package's ConvCallbackInput +// helper to cast to a concrete type: +// +// handler := callbacks.NewHandlerBuilder(). +// OnStartFn(func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { +// // Handle component start +// return ctx +// }). +// OnEndFn(func(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context { +// // Handle component end +// return ctx +// }). +// OnErrorFn(func(ctx context.Context, info *RunInfo, err error) context.Context { +// // Handle component error +// return ctx +// }). +// OnStartWithStreamInputFn(func(ctx context.Context, info *RunInfo, input *schema.StreamReader[CallbackInput]) context.Context { +// defer input.Close() // MUST close — failure causes pipeline goroutine leak +// return ctx +// }). +// OnEndWithStreamOutputFn(func(ctx context.Context, info *RunInfo, output *schema.StreamReader[CallbackOutput]) context.Context { +// defer output.Close() // MUST close +// return ctx +// }). +// Build() +// +// Option 2 — utils/callbacks.NewHandlerHelper: dispatches by component type, so +// each handler function receives the concrete typed input/output directly: +// +// handler := callbacks.NewHandlerHelper(). +// ChatModel(&model.CallbackHandler{ +// OnStart: func(ctx context.Context, info *RunInfo, input *model.CallbackInput) context.Context { +// log.Printf("Model started: %s, messages: %d", info.Name, len(input.Messages)) +// return ctx +// }, +// }). +// Prompt(&prompt.CallbackHandler{ +// OnEnd: func(ctx context.Context, info *RunInfo, output *prompt.CallbackOutput) context.Context { +// log.Printf("Prompt completed") +// return ctx +// }, +// }). +// Handler() +// +// # Passing State Within a Handler +// +// The ctx returned by one timing is passed to the next timing of the SAME +// handler, enabling OnStart→OnEnd state transfer via context.WithValue: +// +// NewHandlerBuilder(). +// OnStartFn(func(ctx context.Context, info *RunInfo, _ CallbackInput) context.Context { +// return context.WithValue(ctx, startTimeKey{}, time.Now()) +// }). +// OnEndFn(func(ctx context.Context, info *RunInfo, _ CallbackOutput) context.Context { +// start := ctx.Value(startTimeKey{}).(time.Time) +// log.Printf("duration: %v", time.Since(start)) +// return ctx +// }).Build() +// +// Between DIFFERENT handlers there is no guaranteed execution order and no +// context chain. To share state between handlers, store it in a +// concurrency-safe variable in the outermost context instead. +// +// # Common Pitfalls +// +// - Stream copies must be closed: when N handlers register for a streaming +// timing, the stream is copied N+1 times (one per handler + one for +// downstream). If any handler's copy is not closed, the original stream +// cannot be freed and the entire pipeline leaks. +// +// - Do NOT mutate Input/Output: all downstream nodes and handlers share the +// same pointer. Mutations cause data races in concurrent graph execution. +// +// - AppendGlobalHandlers is NOT thread-safe: call only during initialization, +// never concurrently with graph execution. +// +// - Stream errors are invisible to OnError: errors that occur while a +// consumer reads from a StreamReader are not routed through OnError. +// +// - RunInfo may be nil: always nil-check before dereferencing in handlers, +// especially when a component is used standalone outside a graph without +// InitCallbacks being called. +package callbacks diff --git a/callbacks/handler_builder.go b/callbacks/handler_builder.go new file mode 100644 index 0000000..438b808 --- /dev/null +++ b/callbacks/handler_builder.go @@ -0,0 +1,163 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// HandlerBuilder constructs a [Handler] by registering callback functions for +// individual timings. Only set the timings you care about; the built handler +// implements [TimingChecker] and returns false for unregistered timings, so +// the framework skips those timings with no overhead. +// +// The input/output values are untyped (CallbackInput / CallbackOutput). To +// work with a specific component's payload, use the component package's +// ConvCallbackInput / ConvCallbackOutput helpers inside your function. For a +// higher-level API that dispatches by component type automatically, see +// utils/callbacks.NewHandlerHelper. +// +// Example: +// +// handler := callbacks.NewHandlerBuilder(). +// OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { +// mi := model.ConvCallbackInput(input) +// if mi != nil { +// log.Printf("[%s] model start: %d messages", info.Name, len(mi.Messages)) +// } +// return ctx +// }). +// OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { +// mo := model.ConvCallbackOutput(output) +// if mo != nil && mo.Message.ResponseMeta != nil { +// log.Printf("[%s] tokens: %d", info.Name, mo.Message.ResponseMeta.Usage.TotalTokens) +// } +// return ctx +// }). +// Build() +type HandlerBuilder struct { + onStartFn func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context + onEndFn func(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context + onErrorFn func(ctx context.Context, info *RunInfo, err error) context.Context + onStartWithStreamInputFn func(ctx context.Context, info *RunInfo, input *schema.StreamReader[CallbackInput]) context.Context + onEndWithStreamOutputFn func(ctx context.Context, info *RunInfo, output *schema.StreamReader[CallbackOutput]) context.Context +} + +type handlerImpl struct { + HandlerBuilder +} + +func (hb *handlerImpl) OnStart(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { + return hb.onStartFn(ctx, info, input) +} + +func (hb *handlerImpl) OnEnd(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context { + return hb.onEndFn(ctx, info, output) +} + +func (hb *handlerImpl) OnError(ctx context.Context, info *RunInfo, err error) context.Context { + return hb.onErrorFn(ctx, info, err) +} + +func (hb *handlerImpl) OnStartWithStreamInput(ctx context.Context, info *RunInfo, + input *schema.StreamReader[CallbackInput]) context.Context { + + return hb.onStartWithStreamInputFn(ctx, info, input) +} + +func (hb *handlerImpl) OnEndWithStreamOutput(ctx context.Context, info *RunInfo, + output *schema.StreamReader[CallbackOutput]) context.Context { + + return hb.onEndWithStreamOutputFn(ctx, info, output) +} + +func (hb *handlerImpl) Needed(_ context.Context, _ *RunInfo, timing CallbackTiming) bool { + switch timing { + case TimingOnStart: + return hb.onStartFn != nil + case TimingOnEnd: + return hb.onEndFn != nil + case TimingOnError: + return hb.onErrorFn != nil + case TimingOnStartWithStreamInput: + return hb.onStartWithStreamInputFn != nil + case TimingOnEndWithStreamOutput: + return hb.onEndWithStreamOutputFn != nil + default: + return false + } +} + +// NewHandlerBuilder creates and returns a new HandlerBuilder instance. +// HandlerBuilder is used to construct a Handler with custom callback functions +func NewHandlerBuilder() *HandlerBuilder { + return &HandlerBuilder{} +} + +// OnStartFn sets the handler for the start timing. +func (hb *HandlerBuilder) OnStartFn( + fn func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context) *HandlerBuilder { + + hb.onStartFn = fn + return hb +} + +// OnEndFn sets the handler for the end timing. +func (hb *HandlerBuilder) OnEndFn( + fn func(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context) *HandlerBuilder { + + hb.onEndFn = fn + return hb +} + +// OnErrorFn sets the handler for the error timing. +func (hb *HandlerBuilder) OnErrorFn( + fn func(ctx context.Context, info *RunInfo, err error) context.Context) *HandlerBuilder { + + hb.onErrorFn = fn + return hb +} + +// OnStartWithStreamInputFn sets the callback invoked when a component receives +// streaming input. The handler receives a [*schema.StreamReader] that is a +// private copy; it MUST close the reader after consuming it to avoid goroutine +// and memory leaks. +func (hb *HandlerBuilder) OnStartWithStreamInputFn( + fn func(ctx context.Context, info *RunInfo, input *schema.StreamReader[CallbackInput]) context.Context) *HandlerBuilder { + + hb.onStartWithStreamInputFn = fn + return hb +} + +// OnEndWithStreamOutputFn sets the callback invoked when a component produces +// streaming output. Like OnStartWithStreamInputFn, the handler receives a +// private copy of the stream and MUST close it after reading to prevent +// goroutine and memory leaks. This is the right place to implement streaming +// token-usage accounting or streaming log capture. +func (hb *HandlerBuilder) OnEndWithStreamOutputFn( + fn func(ctx context.Context, info *RunInfo, output *schema.StreamReader[CallbackOutput]) context.Context) *HandlerBuilder { + + hb.onEndWithStreamOutputFn = fn + return hb +} + +// Build returns a Handler with the functions set in the builder. +func (hb *HandlerBuilder) Build() Handler { + return &handlerImpl{*hb} +} diff --git a/callbacks/interface.go b/callbacks/interface.go new file mode 100644 index 0000000..b80554b --- /dev/null +++ b/callbacks/interface.go @@ -0,0 +1,145 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "github.com/cloudwego/eino/internal/callbacks" +) + +// RunInfo describes the entity that triggered a callback. Always nil-check +// before dereferencing — a component that calls OnStart without first calling +// EnsureRunInfo or InitCallbacks will leave RunInfo absent in the context. +// +// Fields: +// - Name: business-meaningful name specified by the user. For nodes in a +// graph this is the node name (compose.WithNodeName). For standalone +// components it must be set explicitly via [InitCallbacks] or +// [ReuseHandlers]; it is empty string if not set. +// - Type: implementation identity, e.g. "OpenAI". Set by the component via +// [components.Typer]; falls back to reflection (struct/func name) if the +// interface is not implemented. Empty for Graph itself. +// - Component: category constant, e.g. components.ComponentOfChatModel. +// Fixed value "Lambda" for lambdas, "Graph"/"Chain"/"Workflow" for graphs. +// Use this to branch on component kind without caring about implementation. +// +// Handlers should filter using RunInfo rather than assuming a fixed execution +// order — there is no guaranteed ordering between different Handlers. +type RunInfo = callbacks.RunInfo + +// CallbackInput is the value passed to OnStart and OnStartWithStreamInput +// handlers. The concrete type is defined by the component — for example, +// ChatModel callbacks carry *model.CallbackInput. Use the component package's +// ConvCallbackInput helper (e.g. model.ConvCallbackInput) to cast safely; it +// returns nil if the type does not match, so you can ignore irrelevant +// component types: +// +// modelInput := model.ConvCallbackInput(in) +// if modelInput == nil { +// return ctx // not a model invocation, skip +// } +// log.Printf("prompt: %v", modelInput.Messages) +type CallbackInput = callbacks.CallbackInput + +// CallbackOutput is the value passed to OnEnd and OnEndWithStreamOutput +// handlers. Like CallbackInput, the concrete type is component-defined. +// Use the component package's ConvCallbackOutput helper to cast safely. +type CallbackOutput = callbacks.CallbackOutput + +// Handler is the unified callback handler interface. Implement all five +// methods (OnStart, OnEnd, OnError, OnStartWithStreamInput, +// OnEndWithStreamOutput) or use [NewHandlerBuilder] to set only the timings +// you care about. +// +// Each method receives the context returned by the previous timing of the +// SAME handler, which lets a single handler pass state between its OnStart +// and OnEnd calls via context.WithValue. There is NO guaranteed execution +// order between DIFFERENT handlers, and the context chain does not flow +// from one handler to the next — do not rely on handler ordering. +// +// Implement [TimingChecker] (the Needed method) on your handler so the +// framework can skip timings you have not registered; this avoids unnecessary +// stream copies and goroutine allocations on every component invocation. +// +// Stream handlers (OnStartWithStreamInput, OnEndWithStreamOutput) receive a +// [*schema.StreamReader] that has already been copied; they MUST close their +// copy after reading. If any handler's copy is not closed, the original stream +// cannot be freed, causing a goroutine/memory leak for the entire pipeline. +// +// Important: do NOT mutate the Input or Output values. All downstream nodes +// and handlers share the same pointer (direct assignment, not a deep copy). +// Mutations cause data races in concurrent graph execution. +type Handler = callbacks.Handler + +// InitCallbackHandlers sets the global callback handlers. +// It should be called BEFORE any callback handler by user. +// It's useful when you want to inject some basic callbacks to all nodes. +// Deprecated: Use AppendGlobalHandlers instead. +func InitCallbackHandlers(handlers []Handler) { + callbacks.GlobalHandlers = handlers +} + +// AppendGlobalHandlers appends handlers to the process-wide list of callback +// handlers. Global handlers run before per-invocation handlers provided via +// compose.WithCallbacks, giving them higher priority for instrumentation that +// must observe every component invocation (e.g. distributed tracing, metrics). +// +// This function is NOT thread-safe. Call it once during program initialization +// (e.g. in main or TestMain), before any graph executions begin. +// Calling it concurrently with ongoing graph executions leads to data races. +func AppendGlobalHandlers(handlers ...Handler) { + callbacks.GlobalHandlers = append(callbacks.GlobalHandlers, handlers...) +} + +// CallbackTiming enumerates the lifecycle moments at which a callback handler +// is invoked. Implement [TimingChecker] on your handler and return false for +// timings you do not handle, so the framework skips the overhead of stream +// copying and goroutine spawning for those timings. +type CallbackTiming = callbacks.CallbackTiming + +// Callback timing constants. +const ( + // TimingOnStart fires just before the component begins processing. + // Receives a fully-formed input value (non-streaming). + TimingOnStart CallbackTiming = iota + // TimingOnEnd fires after the component returns a result successfully. + // Receives the output value. Only fires on success — not on error. + TimingOnEnd + // TimingOnError fires when the component returns a non-nil error. + // Stream errors (mid-stream panics) are NOT reported here; they surface + // as errors inside the stream reader. + TimingOnError + // TimingOnStartWithStreamInput fires when the component receives a + // streaming input (Collect / Transform paradigms). The handler receives a + // copy of the input stream and must close it after reading. + TimingOnStartWithStreamInput + // TimingOnEndWithStreamOutput fires after the component returns a + // streaming output (Stream / Transform paradigms). The handler receives a + // copy of the output stream and must close it after reading. This is + // typically where you implement streaming metrics or logging. + TimingOnEndWithStreamOutput +) + +// TimingChecker is an optional interface for [Handler] implementations. +// When a handler implements Needed, the framework calls it before each +// component invocation to decide whether to set up callback infrastructure +// (stream copying, goroutine allocation) for that timing. Returning false +// avoids unnecessary overhead. +// +// Handlers built with [NewHandlerBuilder] or +// utils/callbacks.NewHandlerHelper automatically implement TimingChecker +// based on which callback functions were set. +type TimingChecker = callbacks.TimingChecker diff --git a/callbacks/interface_test.go b/callbacks/interface_test.go new file mode 100644 index 0000000..6bb7e0f --- /dev/null +++ b/callbacks/interface_test.go @@ -0,0 +1,56 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/callbacks" +) + +func TestAppendGlobalHandlers(t *testing.T) { + // Clear global handlers before test + callbacks.GlobalHandlers = nil + + // Create test handlers + handler1 := NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context { + return ctx + }).Build() + handler2 := NewHandlerBuilder(). + OnEndFn(func(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context { + return ctx + }).Build() + + // Test appending first handler + AppendGlobalHandlers(handler1) + assert.Equal(t, 1, len(callbacks.GlobalHandlers)) + assert.Contains(t, callbacks.GlobalHandlers, handler1) + + // Test appending second handler + AppendGlobalHandlers(handler2) + assert.Equal(t, 2, len(callbacks.GlobalHandlers)) + assert.Contains(t, callbacks.GlobalHandlers, handler1) + assert.Contains(t, callbacks.GlobalHandlers, handler2) + + // Test appending nil handler + AppendGlobalHandlers([]Handler{}...) + assert.Equal(t, 2, len(callbacks.GlobalHandlers)) +} diff --git a/components/document/callback_extra_loader.go b/components/document/callback_extra_loader.go new file mode 100644 index 0000000..ee1a167 --- /dev/null +++ b/components/document/callback_extra_loader.go @@ -0,0 +1,71 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package document + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// LoaderCallbackInput is the input for the loader callback. +type LoaderCallbackInput struct { + // Source is the source of the documents. + Source Source + + // Extra is the extra information for the callback. + Extra map[string]any +} + +// LoaderCallbackOutput is the output for the loader callback. +type LoaderCallbackOutput struct { + // Source is the source of the documents. + Source Source + + // Docs is the documents to be loaded. + Docs []*schema.Document + + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvLoaderCallbackInput converts the callback input to the loader callback input. +func ConvLoaderCallbackInput(src callbacks.CallbackInput) *LoaderCallbackInput { + switch t := src.(type) { + case *LoaderCallbackInput: + return t + case Source: + return &LoaderCallbackInput{ + Source: t, + } + default: + return nil + } +} + +// ConvLoaderCallbackOutput converts the callback output to the loader callback output. +func ConvLoaderCallbackOutput(src callbacks.CallbackOutput) *LoaderCallbackOutput { + switch t := src.(type) { + case *LoaderCallbackOutput: + return t + case []*schema.Document: + return &LoaderCallbackOutput{ + Docs: t, + } + default: + return nil + } +} diff --git a/components/document/callback_extra_transformer.go b/components/document/callback_extra_transformer.go new file mode 100644 index 0000000..cd98f4a --- /dev/null +++ b/components/document/callback_extra_transformer.go @@ -0,0 +1,68 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package document + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// TransformerCallbackInput is the input for the transformer callback. +type TransformerCallbackInput struct { + // Input is the input documents. + Input []*schema.Document + + // Extra is the extra information for the callback. + Extra map[string]any +} + +// TransformerCallbackOutput is the output for the transformer callback. +type TransformerCallbackOutput struct { + // Output is the output documents. + Output []*schema.Document + + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvTransformerCallbackInput converts the callback input to the transformer callback input. +func ConvTransformerCallbackInput(src callbacks.CallbackInput) *TransformerCallbackInput { + switch t := src.(type) { + case *TransformerCallbackInput: + return t + case []*schema.Document: + return &TransformerCallbackInput{ + Input: t, + } + default: + return nil + } +} + +// ConvTransformerCallbackOutput converts the callback output to the transformer callback output. +func ConvTransformerCallbackOutput(src callbacks.CallbackOutput) *TransformerCallbackOutput { + switch t := src.(type) { + case *TransformerCallbackOutput: + return t + case []*schema.Document: + return &TransformerCallbackOutput{ + Output: t, + } + default: + return nil + } +} diff --git a/components/document/doc.go b/components/document/doc.go new file mode 100644 index 0000000..2e91f5b --- /dev/null +++ b/components/document/doc.go @@ -0,0 +1,41 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package document defines the Loader and Transformer component interfaces +// for ingesting and processing documents in an eino pipeline. +// +// # Components +// +// - [Loader]: reads raw content from an external source (file, URL, S3, …) +// and returns [schema.Document] values. Parsing is typically delegated to +// a [parser.Parser] configured on the loader. +// - [Transformer]: takes a slice of [schema.Document] values and transforms +// them — splitting, filtering, merging, re-ranking, etc. +// +// Concrete implementations live in eino-ext: +// +// github.com/cloudwego/eino-ext/components/document/ +// +// # Document Metadata +// +// [schema.Document].MetaData is the primary mechanism for carrying contextual +// information (source URI, scores, chunk indices, embeddings) through the +// pipeline. Transformers should preserve existing metadata and merge rather +// than replace when adding their own keys. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/document_loader_guide/ +// See https://www.cloudwego.io/docs/eino/core_modules/components/document_transformer_guide/ +package document diff --git a/components/document/interface.go b/components/document/interface.go new file mode 100644 index 0000000..6be8d73 --- /dev/null +++ b/components/document/interface.go @@ -0,0 +1,55 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package document + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// Source identifies the external location of a document. +// URI can be a local file path or a remote URL reachable by the loader. +type Source struct { + URI string +} + +//go:generate mockgen -destination ../../internal/mock/components/document/document_mock.go --package document -source interface.go + +// Loader reads raw content from an external source and returns it as a slice +// of [schema.Document] values. +// +// The Source.URI may be a local file path or a remote URL. The loader is +// responsible for fetching the raw bytes; actual format parsing is typically +// delegated to a [parser.Parser] configured on the loader via +// [WithParserOptions]. +// +// Document metadata ([schema.Document].MetaData) should be populated with at +// least the source URI so that downstream nodes can trace document provenance. +type Loader interface { + Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error) +} + +// Transformer converts a slice of [schema.Document] values into another slice, +// applying operations such as splitting, filtering, merging, or re-ranking. +// +// Implementations should preserve existing MetaData keys and merge rather than +// replace when adding their own metadata. Downstream nodes (e.g. Indexer, +// Retriever) may depend on metadata set by earlier pipeline stages. +type Transformer interface { + Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error) +} diff --git a/components/document/option.go b/components/document/option.go new file mode 100644 index 0000000..777b2cc --- /dev/null +++ b/components/document/option.go @@ -0,0 +1,167 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package document + +import "github.com/cloudwego/eino/components/document/parser" + +// LoaderOptions configures document loaders, including parser options. +type LoaderOptions struct { + ParserOptions []parser.Option +} + +// LoaderOption defines call option for Loader component, which is part of the component interface signature. +// Each Loader implementation could define its own options struct and option funcs within its own package, +// then wrap the impl specific option funcs into this type, before passing to Load. +type LoaderOption struct { + apply func(opts *LoaderOptions) + + implSpecificOptFn any +} + +// WrapLoaderImplSpecificOptFn wraps the impl specific option functions into LoaderOption type. +// T: the type of the impl specific options struct. +// Loader implementations are required to use this function to convert its own option functions into the unified LoaderOption type. +// For example, if the Loader impl defines its own options struct: +// +// type customOptions struct { +// conf string +// } +// +// Then the impl needs to provide an option function as such: +// +// func WithConf(conf string) Option { +// return WrapLoaderImplSpecificOptFn(func(o *customOptions) { +// o.conf = conf +// } +// } +func WrapLoaderImplSpecificOptFn[T any](optFn func(*T)) LoaderOption { + return LoaderOption{ + implSpecificOptFn: optFn, + } +} + +// GetLoaderImplSpecificOptions provides Loader author the ability to extract their own custom options from the unified LoaderOption type. +// T: the type of the impl specific options struct. +// This function should be used within the Loader implementation's Load function. +// It is recommended to provide a base T as the first argument, within which the Loader author can provide default values for the impl specific options. +// eg. +// +// myOption := &MyOption{ +// Field1: "default_value", +// } +// myOption := loader.GetLoaderImplSpecificOptions(myOption, opts...) +func GetLoaderImplSpecificOptions[T any](base *T, opts ...LoaderOption) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + s, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + s(base) + } + } + } + + return base +} + +// GetLoaderCommonOptions extract loader Options from Option list, optionally providing a base Options with default values. +func GetLoaderCommonOptions(base *LoaderOptions, opts ...LoaderOption) *LoaderOptions { + if base == nil { + base = &LoaderOptions{} + } + + for i := range opts { + opt := opts[i] + if opt.apply != nil { + opt.apply(base) + } + } + + return base +} + +// WithParserOptions attaches parser options to a loader request. +func WithParserOptions(opts ...parser.Option) LoaderOption { + return LoaderOption{ + apply: func(o *LoaderOptions) { + o.ParserOptions = opts + }, + } +} + +// TransformerOption defines call option for Transformer component, which is part of the component interface signature. +// Each Transformer implementation could define its own options struct and option funcs within its own package, +// then wrap the impl specific option funcs into this type, before passing to Transform. +type TransformerOption struct { + implSpecificOptFn any +} + +// WrapTransformerImplSpecificOptFn wraps the impl specific option functions into TransformerOption type. +// T: the type of the impl specific options struct. +// Transformer implementations are required to use this function to convert its own option functions into the unified TransformerOption type. +// For example, if the Transformer impl defines its own options struct: +// +// type customOptions struct { +// conf string +// } +// +// Then the impl needs to provide an option function as such: +// +// func WithConf(conf string) TransformerOption { +// return WrapTransformerImplSpecificOptFn(func(o *customOptions) { +// o.conf = conf +// } +// } +// +// . +func WrapTransformerImplSpecificOptFn[T any](optFn func(*T)) TransformerOption { + return TransformerOption{ + implSpecificOptFn: optFn, + } +} + +// GetTransformerImplSpecificOptions provides Transformer author the ability to extract their own custom options from the unified TransformerOption type. +// T: the type of the impl specific options struct. +// This function should be used within the Transformer implementation's Transform function. +// It is recommended to provide a base T as the first argument, within which the Transformer author can provide default values for the impl specific options. +// eg. +// +// myOption := &MyOption{ +// Field1: "default_value", +// } +// myOption := transformer.GetTransformerImplSpecificOptions(myOption, opts...) +func GetTransformerImplSpecificOptions[T any](base *T, opts ...TransformerOption) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + s, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + s(base) + } + } + } + + return base +} diff --git a/components/document/option_test.go b/components/document/option_test.go new file mode 100644 index 0000000..2bff574 --- /dev/null +++ b/components/document/option_test.go @@ -0,0 +1,78 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package document + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" + + "github.com/cloudwego/eino/components/document/parser" +) + +func TestImplSpecificOpts(t *testing.T) { + type implSpecificOptions struct { + conf string + index int + } + + withConf := func(conf string) func(o *implSpecificOptions) { + return func(o *implSpecificOptions) { + o.conf = conf + } + } + + withIndex := func(index int) func(o *implSpecificOptions) { + return func(o *implSpecificOptions) { + o.index = index + } + } + + convey.Convey("TestLoaderImplSpecificOpts", t, func() { + documentOption1 := WrapLoaderImplSpecificOptFn(withConf("test_conf")) + documentOption2 := WrapLoaderImplSpecificOptFn(withIndex(1)) + + implSpecificOpts := GetLoaderImplSpecificOptions(&implSpecificOptions{}, documentOption1, documentOption2) + + convey.So(implSpecificOpts, convey.ShouldResemble, &implSpecificOptions{ + conf: "test_conf", + index: 1, + }) + }) + convey.Convey("TestTransformerImplSpecificOpts", t, func() { + documentOption1 := WrapTransformerImplSpecificOptFn(withConf("test_conf")) + documentOption2 := WrapTransformerImplSpecificOptFn(withIndex(1)) + + implSpecificOpts := GetTransformerImplSpecificOptions(&implSpecificOptions{}, documentOption1, documentOption2) + + convey.So(implSpecificOpts, convey.ShouldResemble, &implSpecificOptions{ + conf: "test_conf", + index: 1, + }) + }) +} + +func TestCommonOptions(t *testing.T) { + convey.Convey("TestCommonOptions", t, func() { + o := &LoaderOptions{ParserOptions: []parser.Option{{}}} + o1 := GetLoaderCommonOptions(o) + convey.So(len(o1.ParserOptions), convey.ShouldEqual, 1) + + o2 := GetLoaderCommonOptions(o, WithParserOptions(parser.Option{}, parser.Option{})) + convey.So(len(o2.ParserOptions), convey.ShouldEqual, 2) + }) +} diff --git a/components/document/parser/doc.go b/components/document/parser/doc.go new file mode 100644 index 0000000..541989f --- /dev/null +++ b/components/document/parser/doc.go @@ -0,0 +1,48 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package parser defines the Parser interface for converting raw byte streams +// into [schema.Document] values. +// +// # Overview +// +// A Parser is not a standalone pipeline component — it is used inside a +// [document.Loader] to handle format-specific decoding. The loader fetches +// raw bytes; the parser converts them into documents. +// +// # Built-in Implementations +// +// - TextParser: treats the entire reader as plain text, one document per call +// - ExtParser: selects a parser by file extension (from [Options.URI]), with +// a configurable fallback for unknown extensions +// +// Use ExtParser when you want format-agnostic loading: pass the source URI +// via [WithURI] and ExtParser picks the right sub-parser automatically. +// +// # Reader Contract +// +// The [io.Reader] passed to [Parser.Parse] is consumed during the call — +// it cannot be read again. Loaders must not reuse the same reader across +// multiple Parse calls. +// +// # Metadata Propagation +// +// Use [WithExtraMeta] to attach key-value pairs that are merged into every +// document's MetaData. This is the standard way to tag documents with source +// information (URI, content type, etc.) at parse time. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/document_loader_guide/document_parser_interface_guide/ +package parser diff --git a/components/document/parser/ext_parser.go b/components/document/parser/ext_parser.go new file mode 100644 index 0000000..7d1f2f7 --- /dev/null +++ b/components/document/parser/ext_parser.go @@ -0,0 +1,130 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +import ( + "context" + "errors" + "io" + "path/filepath" + + "github.com/cloudwego/eino/schema" +) + +// ExtParserConfig defines the configuration for the ExtParser. +type ExtParserConfig struct { + // ext -> parser. + // eg: map[string]Parser{ + // ".pdf": &PDFParser{}, + // ".md": &MarkdownParser{}, + // } + Parsers map[string]Parser + + // Fallback parser to use when no other parser is found. + // Default is TextParser if not set. + FallbackParser Parser +} + +// ExtParser is a parser that uses the file extension to determine which parser to use. +// You can register your own parsers by calling RegisterParser. +// Default parser is TextParser. +// Note: +// +// parse 时,是通过 filepath.Ext(uri) 的方式找到对应的 parser,因此使用时需要: +// ① 必须使用 parser.WithURI 在请求时传入 URI +// ② URI 必须能通过 filepath.Ext 来解析出符合预期的 ext +// +// eg: +// +// pdf, _ := os.Open("./testdata/test.pdf") +// docs, err := ExtParser.Parse(ctx, pdf, parser.WithURI("./testdata/test.pdf")) +type ExtParser struct { + parsers map[string]Parser + + fallbackParser Parser +} + +// NewExtParser creates a new ExtParser. +func NewExtParser(ctx context.Context, conf *ExtParserConfig) (*ExtParser, error) { + if conf == nil { + conf = &ExtParserConfig{} + } + + p := &ExtParser{ + parsers: conf.Parsers, + fallbackParser: conf.FallbackParser, + } + + if p.fallbackParser == nil { + p.fallbackParser = TextParser{} + } + + if p.parsers == nil { + p.parsers = make(map[string]Parser) + } + + return p, nil +} + +// GetParsers returns a copy of the registered parsers. +// It is safe to modify the returned parsers. +func (p *ExtParser) GetParsers() map[string]Parser { + res := make(map[string]Parser, len(p.parsers)) + for k, v := range p.parsers { + res[k] = v + } + + return res +} + +// Parse parses the given reader and returns a list of documents. +func (p *ExtParser) Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error) { + opt := GetCommonOptions(&Options{}, opts...) + + ext := filepath.Ext(opt.URI) + + parser, ok := p.parsers[ext] + + if !ok { + parser = p.fallbackParser + } + + if parser == nil { + return nil, errors.New("no parser found for extension " + ext) + } + + docs, err := parser.Parse(ctx, reader, opts...) + if err != nil { + return nil, err + } + + for _, doc := range docs { + if doc == nil { + continue + } + + if doc.MetaData == nil { + doc.MetaData = make(map[string]any) + } + + for k, v := range opt.ExtraMeta { + doc.MetaData[k] = v + } + } + + return docs, nil +} diff --git a/components/document/parser/interface.go b/components/document/parser/interface.go new file mode 100644 index 0000000..e2e9082 --- /dev/null +++ b/components/document/parser/interface.go @@ -0,0 +1,36 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +import ( + "context" + "io" + + "github.com/cloudwego/eino/schema" +) + +// Parser converts raw content from an [io.Reader] into [schema.Document] values. +// +// Parse may return multiple documents from a single reader (e.g. a PDF with +// per-page splitting). The reader is consumed during Parse and must not be +// reused. +// +// Parsers are typically not called directly — they are configured on a +// [document.Loader] and invoked via [document.WithParserOptions]. +type Parser interface { + Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error) +} diff --git a/components/document/parser/option.go b/components/document/parser/option.go new file mode 100644 index 0000000..ee9706b --- /dev/null +++ b/components/document/parser/option.go @@ -0,0 +1,116 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +// Options configures the document parser with source URI and extra metadata. +type Options struct { + // uri of source. + URI string + + // extra metadata will merge to each document. + ExtraMeta map[string]any +} + +// Option defines call option for Parser component, which is part of the component interface signature. +// Each Parser implementation could define its own options struct and option funcs within its own package, +// then wrap the impl specific option funcs into this type, before passing to Transform. +type Option struct { + apply func(opts *Options) + + implSpecificOptFn any +} + +// WithURI specifies the source URI of the document. +// It will be used as to select parser in ExtParser. +func WithURI(uri string) Option { + return Option{ + apply: func(opts *Options) { + opts.URI = uri + }, + } +} + +// WithExtraMeta attaches extra metadata to the parsed document. +func WithExtraMeta(meta map[string]any) Option { + return Option{ + apply: func(opts *Options) { + opts.ExtraMeta = meta + }, + } +} + +// GetCommonOptions extract parser Options from Option list, optionally providing a base Options with default values. +func GetCommonOptions(base *Options, opts ...Option) *Options { + if base == nil { + base = &Options{} + } + + for i := range opts { + opt := opts[i] + if opt.apply != nil { + opt.apply(base) + } + } + + return base +} + +// WrapImplSpecificOptFn wraps the impl specific option functions into Option type. +// T: the type of the impl specific options struct. +// Parser implementations are required to use this function to convert its own option functions into the unified Option type. +// For example, if the Parser impl defines its own options struct: +// +// type customOptions struct { +// conf string +// } +// +// Then the impl needs to provide an option function as such: +// +// func WithConf(conf string) Option { +// return WrapImplSpecificOptFn(func(o *customOptions) { +// o.conf = conf +// } +// } +// +// . +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions provides Parser author the ability to extract their own custom options from the unified Option type. +// T: the type of the impl specific options struct. +// This function should be used within the Parser implementation's Transform function. +// It is recommended to provide a base T as the first argument, within which the Parser author can provide default values for the impl specific options. +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + s, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + s(base) + } + } + } + + return base +} diff --git a/components/document/parser/option_test.go b/components/document/parser/option_test.go new file mode 100644 index 0000000..d2ed699 --- /dev/null +++ b/components/document/parser/option_test.go @@ -0,0 +1,54 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" +) + +func TestImplSpecificOpts(t *testing.T) { + type implSpecificOptions struct { + conf string + index int + } + + withConf := func(conf string) func(o *implSpecificOptions) { + return func(o *implSpecificOptions) { + o.conf = conf + } + } + + withIndex := func(index int) func(o *implSpecificOptions) { + return func(o *implSpecificOptions) { + o.index = index + } + } + + convey.Convey("TestImplSpecificOpts", t, func() { + parserOption1 := WrapImplSpecificOptFn(withConf("test_conf")) + parserOption2 := WrapImplSpecificOptFn(withIndex(1)) + + implSpecificOpts := GetImplSpecificOptions(&implSpecificOptions{}, parserOption1, parserOption2) + + convey.So(implSpecificOpts, convey.ShouldResemble, &implSpecificOptions{ + conf: "test_conf", + index: 1, + }) + }) +} diff --git a/components/document/parser/parser_test.go b/components/document/parser/parser_test.go new file mode 100644 index 0000000..2324e3c --- /dev/null +++ b/components/document/parser/parser_test.go @@ -0,0 +1,118 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +import ( + "context" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +type ParserForTest struct { + mock func() ([]*schema.Document, error) +} + +func (p *ParserForTest) Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error) { + return p.mock() +} + +func TestParser(t *testing.T) { + ctx := context.Background() + + t.Run("Test default parser", func(t *testing.T) { + conf := &ExtParserConfig{} + + p, err := NewExtParser(ctx, conf) + if err != nil { + t.Fatal(err) + } + + f, err := os.Open("testdata/test.md") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + docs, err := p.Parse(ctx, f, WithURI("testdata/test.md")) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, 1, len(docs)) + assert.Equal(t, "# Title\nhello world", docs[0].Content) + }) + + t.Run("test types", func(t *testing.T) { + mockParser := &ParserForTest{ + mock: func() ([]*schema.Document, error) { + return []*schema.Document{ + { + Content: "hello world", + MetaData: map[string]any{ + "type": "text", + }, + }, + }, nil + }, + } + + conf := &ExtParserConfig{ + Parsers: map[string]Parser{ + ".md": mockParser, + }, + } + + p, err := NewExtParser(ctx, conf) + if err != nil { + t.Fatal(err) + } + + f, err := os.Open("testdata/test.md") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + docs, err := p.Parse(ctx, f, WithURI("x/test.md")) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, 1, len(docs)) + assert.Equal(t, "hello world", docs[0].Content) + assert.Equal(t, "text", docs[0].MetaData["type"]) + }) + + t.Run("test get parsers", func(t *testing.T) { + p, err := NewExtParser(ctx, &ExtParserConfig{ + Parsers: map[string]Parser{ + ".md": &TextParser{}, + }, + }) + if err != nil { + t.Fatal(err) + } + + ps := p.GetParsers() + assert.Equal(t, 1, len(ps)) + }) +} diff --git a/components/document/parser/testdata/test.md b/components/document/parser/testdata/test.md new file mode 100644 index 0000000..fb76356 --- /dev/null +++ b/components/document/parser/testdata/test.md @@ -0,0 +1,2 @@ +# Title +hello world \ No newline at end of file diff --git a/components/document/parser/text_parser.go b/components/document/parser/text_parser.go new file mode 100644 index 0000000..3cd956e --- /dev/null +++ b/components/document/parser/text_parser.go @@ -0,0 +1,60 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +import ( + "context" + "io" + + "github.com/cloudwego/eino/schema" +) + +const ( + // MetaKeySource is the metadata key storing the document's source URI. + MetaKeySource = "_source" +) + +// TextParser is a simple parser that reads the text from a reader and returns a single document. +// eg: +// +// docs, err := TextParser.Parse(ctx, strings.NewReader("hello world")) +// fmt.Println(docs[0].Content) // "hello world" +type TextParser struct{} + +// Parse reads the text from a reader and returns a single document. +func (dp TextParser) Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + opt := GetCommonOptions(&Options{}, opts...) + + meta := make(map[string]any) + meta[MetaKeySource] = opt.URI + + for k, v := range opt.ExtraMeta { + meta[k] = v + } + + doc := &schema.Document{ + Content: string(data), + MetaData: meta, + } + + return []*schema.Document{doc}, nil +} diff --git a/components/embedding/callback_extra.go b/components/embedding/callback_extra.go new file mode 100644 index 0000000..cab5151 --- /dev/null +++ b/components/embedding/callback_extra.go @@ -0,0 +1,97 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package embedding + +import ( + "github.com/cloudwego/eino/callbacks" +) + +// TokenUsage is the token usage for the embedding. +type TokenUsage struct { + // PromptTokens is the number of prompt tokens. + PromptTokens int + // CompletionTokens is the number of completion tokens. + CompletionTokens int + // TotalTokens is the total number of tokens. + TotalTokens int +} + +// Config is the config for the embedding. +type Config struct { + // Model is the model name. + Model string + // EncodingFormat is the encoding format. + EncodingFormat string +} + +// ComponentExtra is the extra information for the embedding. +type ComponentExtra struct { + // Config is the config for the embedding. + Config *Config + // TokenUsage is the token usage for the embedding. + TokenUsage *TokenUsage +} + +// CallbackInput is the input for the embedding callback. +type CallbackInput struct { + // Texts is the texts to be embedded. + Texts []string + // Config is the config for the embedding. + Config *Config + // Extra is the extra information for the callback. + Extra map[string]any +} + +// CallbackOutput is the output for the embedding callback. +type CallbackOutput struct { + // Embeddings is the embeddings. + Embeddings [][]float64 + // Config is the config for creating the embedding. + Config *Config + // TokenUsage is the token usage for the embedding. + TokenUsage *TokenUsage + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvCallbackInput converts the callback input to the embedding callback input. +func ConvCallbackInput(src callbacks.CallbackInput) *CallbackInput { + switch t := src.(type) { + case *CallbackInput: + return t + case []string: + return &CallbackInput{ + Texts: t, + } + default: + return nil + } +} + +// ConvCallbackOutput converts the callback output to the embedding callback output. +func ConvCallbackOutput(src callbacks.CallbackOutput) *CallbackOutput { + switch t := src.(type) { + case *CallbackOutput: + return t + case [][]float64: + return &CallbackOutput{ + Embeddings: t, + } + default: + return nil + } +} diff --git a/components/embedding/callback_extra_test.go b/components/embedding/callback_extra_test.go new file mode 100644 index 0000000..90e5cb9 --- /dev/null +++ b/components/embedding/callback_extra_test.go @@ -0,0 +1,33 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package embedding + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConvEmbedding(t *testing.T) { + assert.NotNil(t, ConvCallbackInput(&CallbackInput{})) + assert.NotNil(t, ConvCallbackInput([]string{})) + assert.Nil(t, ConvCallbackInput("asd")) + + assert.NotNil(t, ConvCallbackOutput(&CallbackOutput{})) + assert.NotNil(t, ConvCallbackOutput([][]float64{})) + assert.Nil(t, ConvCallbackOutput("asd")) +} diff --git a/components/embedding/doc.go b/components/embedding/doc.go new file mode 100644 index 0000000..628cd96 --- /dev/null +++ b/components/embedding/doc.go @@ -0,0 +1,44 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package embedding defines the Embedder component interface for converting +// text into vector representations. +// +// # Overview +// +// An Embedder converts a batch of strings into dense float vectors. Semantically +// similar texts produce vectors that are close in the vector space, making +// embeddings the backbone of semantic search, RAG pipelines, and clustering. +// +// Concrete implementations (OpenAI, Ark, Ollama, …) live in eino-ext: +// +// github.com/cloudwego/eino-ext/components/embedding/ +// +// # Output Format +// +// [Embedder.EmbedStrings] returns `[][]float64` where: +// - outer index corresponds to the input text at the same position +// - inner slice is the embedding vector; its length (dimensions) is fixed by +// the model and is the same for every text +// +// # Consistency Requirement +// +// The same model must be used for both indexing and retrieval. Mixing models +// produces vectors in different spaces — similarity scores become meaningless +// and semantic search breaks silently. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/embedding_guide/ +package embedding diff --git a/components/embedding/interface.go b/components/embedding/interface.go new file mode 100644 index 0000000..dc0cbe7 --- /dev/null +++ b/components/embedding/interface.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package embedding + +import "context" + +// Embedder converts a batch of strings into dense vector representations. +// +// EmbedStrings returns one vector per input text, in the same order. The +// vector length (dimensions) is fixed by the underlying model and identical +// for every text in the batch. +// +// The returned [][]float64 maps as: +// +// embeddings[i] → vector for texts[i] +// len(embeddings[i]) → model's embedding dimension (e.g. 1536 for ada-002) +// +// Both [Indexer] and [Retriever] use an Embedder to convert documents and +// queries into vectors. They must share the exact same model — mismatched +// dimensions or model families break semantic similarity. +// +//go:generate mockgen -destination ../../internal/mock/components/embedding/Embedding_mock.go --package embedding -source interface.go +type Embedder interface { + EmbedStrings(ctx context.Context, texts []string, opts ...Option) ([][]float64, error) // invoke +} diff --git a/components/embedding/option.go b/components/embedding/option.go new file mode 100644 index 0000000..b8d7fe9 --- /dev/null +++ b/components/embedding/option.go @@ -0,0 +1,102 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package embedding + +// Options is the options for the embedding. +type Options struct { + // Model is the model name for the embedding. + Model *string +} + +// Option is a call-time option for an Embedder. +type Option struct { + apply func(opts *Options) + + implSpecificOptFn any +} + +// WithModel is the option to set the model for the embedding. +func WithModel(model string) Option { + return Option{ + apply: func(opts *Options) { + opts.Model = &model + }, + } +} + +// GetCommonOptions extract embedding Options from Option list, optionally providing a base Options with default values. +// eg. +// +// defaultModelName := "default_model" +// embeddingOption := &embedding.Options{ +// Model: &defaultModelName, +// } +// embeddingOption := embedding.GetCommonOptions(embeddingOption, opts...) +func GetCommonOptions(base *Options, opts ...Option) *Options { + if base == nil { + base = &Options{} + } + + for i := range opts { + opt := opts[i] + if opt.apply != nil { + opt.apply(base) + } + } + + return base +} + +// WrapImplSpecificOptFn wraps an implementation-specific option function so it +// can be passed alongside standard options. For use by Embedder implementors: +// +// func WithMyParam(v string) embedding.Option { +// return embedding.WrapImplSpecificOptFn(func(o *MyOptions) { +// o.MyParam = v +// }) +// } +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions extracts implementation-specific options from opts, +// merging them onto base. Call alongside [GetCommonOptions] inside EmbedStrings: +// +// func (e *MyEmbedder) EmbedStrings(ctx context.Context, texts []string, opts ...embedding.Option) ([][]float64, error) { +// common := embedding.GetCommonOptions(nil, opts...) +// mine := embedding.GetImplSpecificOptions(&MyOptions{}, opts...) +// // use common.Model, mine.MyParam, etc. +// } +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} diff --git a/components/embedding/option_test.go b/components/embedding/option_test.go new file mode 100644 index 0000000..63c71af --- /dev/null +++ b/components/embedding/option_test.go @@ -0,0 +1,30 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package embedding + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptions(t *testing.T) { + defaultModel := "default_model" + opts := GetCommonOptions(&Options{Model: &defaultModel}, WithModel("test_model")) + assert.NotNil(t, opts.Model) + assert.Equal(t, *opts.Model, "test_model") +} diff --git a/components/indexer/callback_extra.go b/components/indexer/callback_extra.go new file mode 100644 index 0000000..88de567 --- /dev/null +++ b/components/indexer/callback_extra.go @@ -0,0 +1,66 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package indexer + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// CallbackInput is the input for the indexer callback. +type CallbackInput struct { + // Docs is the documents to be indexed. + Docs []*schema.Document + // Extra is the extra information for the callback. + Extra map[string]any +} + +// CallbackOutput is the output for the indexer callback. +type CallbackOutput struct { + // IDs is the ids of the indexed documents returned by the indexer. + IDs []string + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvCallbackInput converts the callback input to the indexer callback input. +func ConvCallbackInput(src callbacks.CallbackInput) *CallbackInput { + switch t := src.(type) { + case *CallbackInput: + return t + case []*schema.Document: + return &CallbackInput{ + Docs: t, + } + default: + return nil + } +} + +// ConvCallbackOutput converts the callback output to the indexer callback output. +func ConvCallbackOutput(src callbacks.CallbackOutput) *CallbackOutput { + switch t := src.(type) { + case *CallbackOutput: + return t + case []string: + return &CallbackOutput{ + IDs: t, + } + default: + return nil + } +} diff --git a/components/indexer/callback_extra_test.go b/components/indexer/callback_extra_test.go new file mode 100644 index 0000000..2ac79be --- /dev/null +++ b/components/indexer/callback_extra_test.go @@ -0,0 +1,35 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package indexer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestConvIndexer(t *testing.T) { + assert.NotNil(t, ConvCallbackInput(&CallbackInput{})) + assert.NotNil(t, ConvCallbackInput([]*schema.Document{})) + assert.Nil(t, ConvCallbackInput("asd")) + + assert.NotNil(t, ConvCallbackOutput(&CallbackOutput{})) + assert.NotNil(t, ConvCallbackOutput([]string{})) + assert.Nil(t, ConvCallbackOutput("asd")) +} diff --git a/components/indexer/doc.go b/components/indexer/doc.go new file mode 100644 index 0000000..968ff9e --- /dev/null +++ b/components/indexer/doc.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package indexer defines the Indexer component interface for storing documents +// and their vector representations in a backend store. +// +// # Overview +// +// An Indexer is the write path of a RAG pipeline. It takes [schema.Document] +// values, optionally generates vector embeddings, and persists them in a +// backend (vector DB, search engine, etc.) for later retrieval. +// +// Concrete implementations (VikingDB, Milvus, Elasticsearch, …) live in +// eino-ext: +// +// github.com/cloudwego/eino-ext/components/indexer/ +// +// # Vector Dimension Consistency +// +// When using the [Options.Embedding] option, the embedding model must be +// identical to the one used by the paired [retriever.Retriever]. Mismatched +// models produce vectors in different spaces — queries will not match stored +// documents. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/indexer_guide/ +package indexer diff --git a/components/indexer/interface.go b/components/indexer/interface.go new file mode 100644 index 0000000..59154fd --- /dev/null +++ b/components/indexer/interface.go @@ -0,0 +1,41 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package indexer + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// Indexer stores documents (and optionally their vector embeddings) in a +// backend for later retrieval. +// +// Store accepts a batch of [schema.Document] values and returns the IDs +// assigned to them by the backend. When [Options.Embedding] is provided, +// the implementation generates vectors before storing — the same embedder +// must be used by the paired [retriever.Retriever]. +// +// Use [Options.Index] to choose a backend index at call time, and +// [Options.SubIndexes] to write documents into logical sub-partitions within +// the same store. +// +//go:generate mockgen -destination ../../internal/mock/components/indexer/indexer_mock.go --package indexer -source interface.go +type Indexer interface { + // Store stores the documents and returns their assigned IDs. + Store(ctx context.Context, docs []*schema.Document, opts ...Option) (ids []string, err error) // invoke +} diff --git a/components/indexer/option.go b/components/indexer/option.go new file mode 100644 index 0000000..9d095ad --- /dev/null +++ b/components/indexer/option.go @@ -0,0 +1,113 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package indexer + +import "github.com/cloudwego/eino/components/embedding" + +// Options is the options for the indexer. +type Options struct { + // Index is the index for the indexer, index in different indexers may be different. + Index *string + // SubIndexes is the sub indexes to be indexed. + SubIndexes []string + // Embedding is the embedding component. + Embedding embedding.Embedder +} + +// WithIndex wraps the index option. +func WithIndex(index string) Option { + return Option{ + apply: func(opts *Options) { + opts.Index = &index + }, + } +} + +// WithSubIndexes is the option to set the sub indexes for the indexer. +func WithSubIndexes(subIndexes []string) Option { + return Option{ + apply: func(opts *Options) { + opts.SubIndexes = subIndexes + }, + } +} + +// WithEmbedding is the option to set the embedder for the indexer, which convert document to embeddings. +func WithEmbedding(emb embedding.Embedder) Option { + return Option{ + apply: func(opts *Options) { + opts.Embedding = emb + }, + } +} + +// Option is a call-time option for an Indexer. +type Option struct { + apply func(opts *Options) + + implSpecificOptFn any +} + +// GetCommonOptions extracts standard [Options] from opts, merging onto base. +// Implementors must call this inside Store: +// +// func (idx *MyIndexer) Store(ctx context.Context, docs []*schema.Document, opts ...indexer.Option) ([]string, error) { +// options := indexer.GetCommonOptions(nil, opts...) +// // use options.Embedding to generate vectors before storage +// } +func GetCommonOptions(base *Options, opts ...Option) *Options { + if base == nil { + base = &Options{} + } + + for i := range opts { + opt := opts[i] + if opt.apply != nil { + opt.apply(base) + } + } + + return base +} + +// WrapImplSpecificOptFn wraps an implementation-specific option function so it +// can be passed alongside standard options. For use by Indexer implementors. +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions extracts implementation-specific options from opts, +// merging onto base. Call alongside [GetCommonOptions] inside Store. +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} diff --git a/components/indexer/option_test.go b/components/indexer/option_test.go new file mode 100644 index 0000000..1ef37b5 --- /dev/null +++ b/components/indexer/option_test.go @@ -0,0 +1,48 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package indexer + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" + + "github.com/cloudwego/eino/internal/mock/components/embedding" +) + +func TestOptions(t *testing.T) { + convey.Convey("test options", t, func() { + var ( + index = "index" + subIndexes = []string{"index_1", "index_2"} + e = &embedding.MockEmbedder{} + ) + + opts := GetCommonOptions( + &Options{}, + WithIndex(index), + WithSubIndexes(subIndexes), + WithEmbedding(e), + ) + + convey.So(opts, convey.ShouldResemble, &Options{ + Index: &index, + SubIndexes: subIndexes, + Embedding: e, + }) + }) +} diff --git a/components/model/agentic_callback_extra.go b/components/model/agentic_callback_extra.go new file mode 100644 index 0000000..9a769cf --- /dev/null +++ b/components/model/agentic_callback_extra.go @@ -0,0 +1,94 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// AgenticConfig is the config for the agentic model. +type AgenticConfig struct { + // Model is the model name. + Model string + // MaxTokens is the max number of output tokens, if reached the max tokens, the model will stop generating. + MaxTokens int + // Temperature is the temperature, which controls the randomness of the agentic model. + Temperature float32 + // TopP is the top p, which controls the diversity of the agentic model. + TopP float32 +} + +// AgenticCallbackInput is the input for the agentic model callback. +type AgenticCallbackInput struct { + // Messages is the agentic messages to be sent to the agentic model. + Messages []*schema.AgenticMessage + // Tools is the tools to be used in the agentic model. + Tools []*schema.ToolInfo + // Config is the config for the agentic model. + Config *AgenticConfig + // Extra is the extra information for the callback. + Extra map[string]any +} + +// AgenticCallbackOutput is the output for the agentic model callback. +type AgenticCallbackOutput struct { + // Message is the agentic message generated by the agentic model. + Message *schema.AgenticMessage + // Config is the config for the agentic model. + Config *AgenticConfig + // TokenUsage is the token usage of this request. + TokenUsage *TokenUsage + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvAgenticCallbackInput converts the callback input to the agentic model callback input. +func ConvAgenticCallbackInput(src callbacks.CallbackInput) *AgenticCallbackInput { + switch t := src.(type) { + case *AgenticCallbackInput: + // when callback is triggered within component implementation, + // the input is usually already a typed *model.AgenticCallbackInput + return t + case []*schema.AgenticMessage: + // when callback is injected by graph node, not the component implementation itself, + // the input is the input of Agentic Model interface, which is []*schema.AgenticMessage + return &AgenticCallbackInput{ + Messages: t, + } + default: + return nil + } +} + +// ConvAgenticCallbackOutput converts the callback output to the agentic model callback output. +func ConvAgenticCallbackOutput(src callbacks.CallbackOutput) *AgenticCallbackOutput { + switch t := src.(type) { + case *AgenticCallbackOutput: + // when callback is triggered within component implementation, + // the output is usually already a typed *model.AgenticCallbackOutput + return t + case *schema.AgenticMessage: + // when callback is injected by graph node, not the component implementation itself, + // the output is the output of Agentic Model interface, which is *schema.AgenticMessage + return &AgenticCallbackOutput{ + Message: t, + } + default: + return nil + } +} diff --git a/components/model/agentic_callback_extra_test.go b/components/model/agentic_callback_extra_test.go new file mode 100644 index 0000000..9373674 --- /dev/null +++ b/components/model/agentic_callback_extra_test.go @@ -0,0 +1,35 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestConvAgenticModel(t *testing.T) { + assert.NotNil(t, ConvAgenticCallbackInput(&AgenticCallbackInput{})) + assert.NotNil(t, ConvAgenticCallbackInput([]*schema.AgenticMessage{})) + assert.Nil(t, ConvAgenticCallbackInput("asd")) + + assert.NotNil(t, ConvAgenticCallbackOutput(&AgenticCallbackOutput{})) + assert.NotNil(t, ConvAgenticCallbackOutput(&schema.AgenticMessage{})) + assert.Nil(t, ConvAgenticCallbackOutput("asd")) +} diff --git a/components/model/callback_extra.go b/components/model/callback_extra.go new file mode 100644 index 0000000..8591c43 --- /dev/null +++ b/components/model/callback_extra.go @@ -0,0 +1,117 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// TokenUsage is the token usage for the model. +type TokenUsage struct { + // PromptTokens is the number of prompt tokens, including all the input tokens of this request. + PromptTokens int + // PromptTokenDetails is a breakdown of the prompt tokens. + PromptTokenDetails PromptTokenDetails + // CompletionTokens is the number of completion tokens. + CompletionTokens int + // TotalTokens is the total number of tokens. + TotalTokens int + // CompletionTokensDetails is breakdown of completion tokens. + CompletionTokensDetails CompletionTokensDetails `json:"completion_token_details"` +} + +type CompletionTokensDetails struct { + // ReasoningTokens tokens generated by the model for reasoning. + // This is currently supported by OpenAI, Gemini, ARK and Qwen chat models. + // For other models, this field will be 0. + ReasoningTokens int `json:"reasoning_tokens,omitempty"` +} + +// PromptTokenDetails provides a breakdown of prompt token usage. +type PromptTokenDetails struct { + // Cached tokens present in the prompt. + CachedTokens int +} + +// Config is the config for the model. +type Config struct { + // Model is the model name. + Model string + // MaxTokens is the max number of tokens, if reached the max tokens, the model will stop generating, and mostly return an finish reason of "length". + MaxTokens int + // Temperature is the temperature, which controls the randomness of the model. + Temperature float32 + // TopP is the top p, which controls the diversity of the model. + TopP float32 + // Stop is the stop words, which controls the stopping condition of the model. + Stop []string +} + +// CallbackInput is the input for the model callback. +type CallbackInput struct { + // Messages is the messages to be sent to the model. + Messages []*schema.Message + // Tools is the tools to be used in the model. + Tools []*schema.ToolInfo + // ToolChoice is the tool choice, which controls the tool to be used in the model. + ToolChoice *schema.ToolChoice + // Config is the config for the model. + Config *Config + // Extra is the extra information for the callback. + Extra map[string]any +} + +// CallbackOutput is the output for the model callback. +type CallbackOutput struct { + // Message is the message generated by the model. + Message *schema.Message + // Config is the config for the model. + Config *Config + // TokenUsage is the token usage of this request. + TokenUsage *TokenUsage + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvCallbackInput converts the callback input to the model callback input. +func ConvCallbackInput(src callbacks.CallbackInput) *CallbackInput { + switch t := src.(type) { + case *CallbackInput: // when callback is triggered within component implementation, the input is usually already a typed *model.CallbackInput + return t + case []*schema.Message: // when callback is injected by graph node, not the component implementation itself, the input is the input of Chat Model interface, which is []*schema.Message + return &CallbackInput{ + Messages: t, + } + default: + return nil + } +} + +// ConvCallbackOutput converts the callback output to the model callback output. +func ConvCallbackOutput(src callbacks.CallbackOutput) *CallbackOutput { + switch t := src.(type) { + case *CallbackOutput: // when callback is triggered within component implementation, the output is usually already a typed *model.CallbackOutput + return t + case *schema.Message: // when callback is injected by graph node, not the component implementation itself, the output is the output of Chat Model interface, which is *schema.Message + return &CallbackOutput{ + Message: t, + } + default: + return nil + } +} diff --git a/components/model/callback_extra_test.go b/components/model/callback_extra_test.go new file mode 100644 index 0000000..2fe8443 --- /dev/null +++ b/components/model/callback_extra_test.go @@ -0,0 +1,35 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestConvModel(t *testing.T) { + assert.NotNil(t, ConvCallbackInput(&CallbackInput{})) + assert.NotNil(t, ConvCallbackInput([]*schema.Message{})) + assert.Nil(t, ConvCallbackInput("asd")) + + assert.NotNil(t, ConvCallbackOutput(&CallbackOutput{})) + assert.NotNil(t, ConvCallbackOutput(&schema.Message{})) + assert.Nil(t, ConvCallbackOutput("asd")) +} diff --git a/components/model/doc.go b/components/model/doc.go new file mode 100644 index 0000000..4c91d85 --- /dev/null +++ b/components/model/doc.go @@ -0,0 +1,59 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package model defines the ChatModel component interface for interacting with +// large language models (LLMs). +// +// # Overview +// +// A ChatModel takes a slice of [schema.Message] as input and returns a response +// message — either in full ([BaseChatModel.Generate]) or incrementally as a +// stream ([BaseChatModel.Stream]). It is the most fundamental building block in +// an eino pipeline: every application that talks to an LLM goes through this +// interface. +// +// Concrete implementations (OpenAI, Ark, Ollama, …) live in eino-ext: +// +// github.com/cloudwego/eino-ext/components/model/ +// +// # Interface Hierarchy +// +// BaseChatModel — Generate + Stream (all implementations) +// ├── ToolCallingChatModel — preferred; WithTools returns a new instance (concurrency-safe) +// └── ChatModel — deprecated; BindTools mutates state (avoid in new code) +// +// # Choosing Generate vs Stream +// +// Use [BaseChatModel.Generate] when the full response is needed before +// proceeding (e.g. structured extraction, classification). +// Use [BaseChatModel.Stream] when output should be forwarded to the caller +// incrementally (e.g. chat UI, long-form generation). Always close the +// [schema.StreamReader] returned by Stream — failing to do so leaks the +// underlying connection: +// +// reader, err := model.Stream(ctx, messages) +// if err != nil { ... } +// defer reader.Close() +// +// # Implementing a ChatModel +// +// Implementations must call [GetCommonOptions] to extract standard options and +// [GetImplSpecificOptions] to extract their own options from the Option list. +// Expose implementation-specific options via [WrapImplSpecificOptFn]. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/chat_model_guide/ +// for the full component guide. +package model diff --git a/components/model/interface.go b/components/model/interface.go new file mode 100644 index 0000000..d326e66 --- /dev/null +++ b/components/model/interface.go @@ -0,0 +1,109 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// messageType is the sealed type constraint for message types used in BaseModel. +// Only *schema.Message and *schema.AgenticMessage satisfy this constraint. +type messageType interface { + *schema.Message | *schema.AgenticMessage +} + +// BaseModel is the generic base model interface parameterized by message type M. +// It exposes two modes of interaction: +// - [BaseModel.Generate]: blocks until the model returns a complete response. +// - [BaseModel.Stream]: returns a [schema.StreamReader] that yields message +// chunks incrementally as the model generates them. +type BaseModel[M messageType] interface { + Generate(ctx context.Context, input []M, opts ...Option) (M, error) + Stream(ctx context.Context, input []M, opts ...Option) (*schema.StreamReader[M], error) +} + +// BaseChatModel is a backward-compatible type alias for BaseModel specialized +// with *schema.Message. All existing code using model.BaseChatModel continues +// to work without modification. +// +// It exposes two modes of interaction: +// - [BaseChatModel.Generate]: blocks until the model returns a complete response. +// - [BaseChatModel.Stream]: returns a [schema.StreamReader] that yields message +// chunks incrementally as the model generates them. +// +// The input is a slice of [schema.Message] representing the conversation history. +// Messages carry a role (system, user, assistant, tool) and support multimodal +// content (text, images, audio, video). Tool messages must include a ToolCallID +// that correlates them with a prior assistant tool-call message. +// +// Stream usage — the caller is responsible for closing the reader: +// +// reader, err := m.Stream(ctx, messages) +// if err != nil { ... } +// defer reader.Close() +// for { +// chunk, err := reader.Recv() +// if errors.Is(err, io.EOF) { break } +// if err != nil { ... } +// // handle chunk +// } +// +// Note: a [schema.StreamReader] can only be read once. If multiple consumers +// need the stream, it must be copied before reading. +// +//go:generate mockgen -destination ../../internal/mock/components/model/ChatModel_mock.go --package model github.com/cloudwego/eino/components/model BaseChatModel,ChatModel,ToolCallingChatModel +type BaseChatModel = BaseModel[*schema.Message] + +// Deprecated: Use [ToolCallingChatModel] instead. +// +// ChatModel extends [BaseChatModel] with tool binding via [ChatModel.BindTools]. +// BindTools mutates the instance in place, which causes a race condition when +// the same instance is used concurrently: one goroutine's tool list can +// overwrite another's. Prefer [ToolCallingChatModel.WithTools], which returns +// a new immutable instance and is safe for concurrent use. +type ChatModel interface { + BaseChatModel + + // BindTools bind tools to the model. + // BindTools before requesting ChatModel generally. + // notice the non-atomic problem of BindTools and Generate. + BindTools(tools []*schema.ToolInfo) error +} + +// ToolCallingChatModel extends [BaseChatModel] with safe tool binding. +// +// Unlike the deprecated [ChatModel.BindTools], [ToolCallingChatModel.WithTools] +// does not mutate the receiver — it returns a new instance with the given tools +// attached. This makes it safe to share a base model instance across goroutines +// and derive per-request variants with different tool sets: +// +// base, _ := openai.NewChatModel(ctx, cfg) // shared, no tools +// withSearch, _ := base.WithTools([]*schema.ToolInfo{searchTool}) +// withCalc, _ := base.WithTools([]*schema.ToolInfo{calcTool}) +type ToolCallingChatModel interface { + BaseChatModel + + WithTools(tools []*schema.ToolInfo) (ToolCallingChatModel, error) +} + +// AgenticModel is a type alias for BaseModel specialized with +// *schema.AgenticMessage. Unlike ToolCallingChatModel, agentic models do NOT +// expose a WithTools method; tools are passed at request time via the +// model.WithTools option, consistent with how ChatModelAgent binds tools. +type AgenticModel = BaseModel[*schema.AgenticMessage] diff --git a/components/model/option.go b/components/model/option.go new file mode 100644 index 0000000..2222e14 --- /dev/null +++ b/components/model/option.go @@ -0,0 +1,255 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import "github.com/cloudwego/eino/schema" + +// Options is the common options for the model. +type Options struct { + // Temperature is the temperature for the model, which controls the randomness of the model. + Temperature *float32 + // Model is the model name. + Model *string + // TopP is the top p for the model, which controls the diversity of the model. + TopP *float32 + // Tools is a list of tools the model may call. + Tools []*schema.ToolInfo + // DeferredTools is a list of tools to be registered with defer_loading=true + // for the model's built-in (server-side) tool search capability. + // These tools are sent to the model API but not loaded into context upfront — + // only their names and descriptions are visible to the model. The model's + // built-in tool search tool searches through them and loads matching ones + // on demand. + DeferredTools []*schema.ToolInfo + + ToolSearchTool *schema.ToolInfo + + // MaxTokens is the max number of tokens, if reached the max tokens, the model will stop generating, and mostly return a finish reason of "length". + MaxTokens *int + // Stop is the stop words for the model, which controls the stopping condition of the model. + Stop []string + + // Options only available for chat model. + + // ToolChoice controls which tool is called by the model. + ToolChoice *schema.ToolChoice + // AllowedToolNames specifies a list of tool names that the model is allowed to call. + // This allows for constraining the model to a specific subset of the available tools. + AllowedToolNames []string + + // Options only available for agentic model. + + // AgenticToolChoice controls how the agentic model calls tools. + AgenticToolChoice *schema.AgenticToolChoice +} + +// Option is a call-time option for a ChatModel. Options are immutable and +// composable: each Option carries either a common-option setter (applied via +// [GetCommonOptions]) or an implementation-specific setter (applied via +// [GetImplSpecificOptions]), never both. +type Option struct { + apply func(opts *Options) + + implSpecificOptFn any +} + +// WithTemperature is the option to set the temperature for the model. +func WithTemperature(temperature float32) Option { + return Option{ + apply: func(opts *Options) { + opts.Temperature = &temperature + }, + } +} + +// WithMaxTokens is the option to set the max tokens for the model. +func WithMaxTokens(maxTokens int) Option { + return Option{ + apply: func(opts *Options) { + opts.MaxTokens = &maxTokens + }, + } +} + +// WithModel is the option to set the model name. +func WithModel(name string) Option { + return Option{ + apply: func(opts *Options) { + opts.Model = &name + }, + } +} + +// WithTopP is the option to set the top p for the model. +func WithTopP(topP float32) Option { + return Option{ + apply: func(opts *Options) { + opts.TopP = &topP + }, + } +} + +// WithStop is the option to set the stop words for the model. +func WithStop(stop []string) Option { + return Option{ + apply: func(opts *Options) { + opts.Stop = stop + }, + } +} + +// WithTools is the option to set tools for the model. +func WithTools(tools []*schema.ToolInfo) Option { + if tools == nil { + tools = []*schema.ToolInfo{} + } + return Option{ + apply: func(opts *Options) { + opts.Tools = tools + }, + } +} + +// WithToolSearchTool is the option to register a tool search tool with the model. +// When set, the model uses this tool to discover and load deferred tools on demand. +// Note: The tool search tool should NOT be included in WithTools. +func WithToolSearchTool(tool *schema.ToolInfo) Option { + return Option{ + apply: func(opts *Options) { + opts.ToolSearchTool = tool + }, + } +} + +// WithDeferredTools is the option to set deferred tools for the model's +// built-in (server-side) tool search. These tools are registered with +// defer_loading=true so the model can discover and load them on demand +// via its native tool search capability. +// Note: Deferred tools should NOT be included in WithTools. +func WithDeferredTools(tools []*schema.ToolInfo) Option { + if tools == nil { + tools = []*schema.ToolInfo{} + } + return Option{ + apply: func(opts *Options) { + opts.DeferredTools = tools + }, + } +} + +// WithToolChoice sets the tool choice for the model. It also allows for providing a list of +// tool names to constrain the model to a specific subset of the available tools. +// Only available for ChatModel. +func WithToolChoice(toolChoice schema.ToolChoice, allowedToolNames ...string) Option { + return Option{ + apply: func(opts *Options) { + opts.ToolChoice = &toolChoice + opts.AllowedToolNames = allowedToolNames + }, + } +} + +// WithAgenticToolChoice is the option to set tool choice for the agentic model. +// Only available for AgenticModel. +func WithAgenticToolChoice(toolChoice *schema.AgenticToolChoice) Option { + return Option{ + apply: func(opts *Options) { + opts.AgenticToolChoice = toolChoice + }, + } +} + +// WrapImplSpecificOptFn is the option to wrap the implementation specific option function. +// WrapImplSpecificOptFn wraps an implementation-specific option function into +// an [Option] so it can be passed alongside standard options. +// +// This is intended for ChatModel implementors, not callers. Define a typed +// setter for your own config struct and expose it as an Option: +// +// // In your implementation package: +// func WithMyParam(v string) model.Option { +// return model.WrapImplSpecificOptFn(func(o *MyOptions) { +// o.MyParam = v +// }) +// } +// +// Callers can then mix standard and implementation-specific options freely: +// +// model.Generate(ctx, msgs, +// model.WithTemperature(0.7), +// mypkg.WithMyParam("value"), +// ) +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetCommonOptions extracts standard [Options] from an Option list, merging +// them onto base. If base is nil, a zero-value Options is used. +// +// Implementors must call this to honour options passed by callers: +// +// func (m *MyModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { +// options := model.GetCommonOptions(&model.Options{Temperature: &m.defaultTemp}, opts...) +// // use options.Temperature, options.Tools, etc. +// } +func GetCommonOptions(base *Options, opts ...Option) *Options { + if base == nil { + base = &Options{} + } + + for i := range opts { + opt := opts[i] + if opt.apply != nil { + opt.apply(base) + } + } + + return base +} + +// GetImplSpecificOptions extracts implementation-specific options from an +// Option list, merging them onto base. If base is nil, a zero-value T is used. +// +// Call this alongside [GetCommonOptions] to support both standard and custom +// options in your implementation: +// +// type MyOptions struct { MyParam string } +// +// func (m *MyModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { +// common := model.GetCommonOptions(nil, opts...) +// myOpts := model.GetImplSpecificOptions(&MyOptions{MyParam: "default"}, opts...) +// // use common.Temperature, myOpts.MyParam, etc. +// } +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} diff --git a/components/model/option_test.go b/components/model/option_test.go new file mode 100644 index 0000000..c836933 --- /dev/null +++ b/components/model/option_test.go @@ -0,0 +1,136 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" + + "github.com/cloudwego/eino/schema" +) + +func TestOptions(t *testing.T) { + convey.Convey("test options", t, func() { + var ( + modelName = "model" + temperature float32 = 0.9 + maxToken = 5000 + topP float32 = 0.8 + defaultModel = "default_model" + defaultTemperature float32 = 1.0 + defaultMaxTokens = 1000 + defaultTopP float32 = 0.5 + tools = []*schema.ToolInfo{{Name: "asd"}, {Name: "qwe"}} + toolChoice = schema.ToolChoiceForced + allowedToolNames = []string{"web_search"} + ) + + opts := GetCommonOptions( + &Options{ + Model: &defaultModel, + Temperature: &defaultTemperature, + MaxTokens: &defaultMaxTokens, + TopP: &defaultTopP, + }, + WithModel(modelName), + WithTemperature(temperature), + WithMaxTokens(maxToken), + WithTopP(topP), + WithStop([]string{"hello", "bye"}), + WithTools(tools), + WithToolChoice(toolChoice, allowedToolNames...), + ) + + convey.So(opts, convey.ShouldResemble, &Options{ + Model: &modelName, + Temperature: &temperature, + MaxTokens: &maxToken, + TopP: &topP, + Stop: []string{"hello", "bye"}, + Tools: tools, + ToolChoice: &toolChoice, + AllowedToolNames: allowedToolNames, + }) + }) + + convey.Convey("test nil tools option", t, func() { + opts := GetCommonOptions( + &Options{ + Tools: []*schema.ToolInfo{ + {Name: "asd"}, + {Name: "qwe"}, + }, + }, + WithTools(nil), + ) + + convey.So(opts.Tools, convey.ShouldNotBeNil) + convey.So(len(opts.Tools), convey.ShouldEqual, 0) + }) + + convey.Convey("test agentic tool choice option", t, func() { + var ( + toolChoice = schema.ToolChoiceForced + allowedTools = []*schema.AllowedTool{ + {FunctionName: "agentic_tool"}, + } + ) + opts := GetCommonOptions( + nil, + WithAgenticToolChoice(&schema.AgenticToolChoice{ + Type: toolChoice, + Forced: &schema.AgenticForcedToolChoice{ + Tools: allowedTools, + }, + }), + ) + + convey.So(opts.AgenticToolChoice, convey.ShouldNotBeNil) + convey.So(opts.AgenticToolChoice.Type, convey.ShouldEqual, toolChoice) + convey.So(opts.AgenticToolChoice.Forced, convey.ShouldNotBeNil) + convey.So(opts.AgenticToolChoice.Forced.Tools, convey.ShouldResemble, allowedTools) + }) +} + +type implOption struct { + userID int64 + name string +} + +func WithUserID(uid int64) Option { + return WrapImplSpecificOptFn[implOption](func(i *implOption) { + i.userID = uid + }) +} + +func WithName(n string) Option { + return WrapImplSpecificOptFn[implOption](func(i *implOption) { + i.name = n + }) +} + +func TestImplSpecificOption(t *testing.T) { + convey.Convey("impl_specific_option", t, func() { + opt := GetImplSpecificOptions(&implOption{}, WithUserID(101), WithName("Wang")) + + convey.So(opt, convey.ShouldEqual, &implOption{ + userID: 101, + name: "Wang", + }) + }) +} diff --git a/components/prompt/agentic_callback_extra.go b/components/prompt/agentic_callback_extra.go new file mode 100644 index 0000000..315d5a4 --- /dev/null +++ b/components/prompt/agentic_callback_extra.go @@ -0,0 +1,70 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// AgenticCallbackInput is the input for the callback. +type AgenticCallbackInput struct { + // Variables is the variables for the callback. + Variables map[string]any + // Templates is the agentic templates for the callback. + Templates []schema.AgenticMessagesTemplate + // Extra is the extra information for the callback. + Extra map[string]any +} + +// AgenticCallbackOutput is the output for the callback. +type AgenticCallbackOutput struct { + // Result is the agentic result for the callback. + Result []*schema.AgenticMessage + // Templates is the agentic templates for the callback. + Templates []schema.AgenticMessagesTemplate + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvAgenticCallbackInput converts the callback input to the agentic prompt callback input. +func ConvAgenticCallbackInput(src callbacks.CallbackInput) *AgenticCallbackInput { + switch t := src.(type) { + case *AgenticCallbackInput: + return t + case map[string]any: + return &AgenticCallbackInput{ + Variables: t, + } + default: + return nil + } +} + +// ConvAgenticCallbackOutput converts the callback output to the agentic prompt callback output. +func ConvAgenticCallbackOutput(src callbacks.CallbackOutput) *AgenticCallbackOutput { + switch t := src.(type) { + case *AgenticCallbackOutput: + return t + case []*schema.AgenticMessage: + return &AgenticCallbackOutput{ + Result: t, + } + default: + return nil + } +} diff --git a/components/prompt/agentic_callback_extra_test.go b/components/prompt/agentic_callback_extra_test.go new file mode 100644 index 0000000..67982be --- /dev/null +++ b/components/prompt/agentic_callback_extra_test.go @@ -0,0 +1,46 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestConvAgenticPrompt(t *testing.T) { + assert.NotNil(t, ConvAgenticCallbackInput(&AgenticCallbackInput{ + Variables: map[string]any{}, + Templates: []schema.AgenticMessagesTemplate{ + &schema.AgenticMessage{}, + }, + })) + assert.NotNil(t, ConvAgenticCallbackInput(map[string]any{})) + assert.Nil(t, ConvAgenticCallbackInput("asd")) + + assert.NotNil(t, ConvAgenticCallbackOutput(&AgenticCallbackOutput{ + Result: []*schema.AgenticMessage{ + {}, + }, + Templates: []schema.AgenticMessagesTemplate{ + &schema.AgenticMessage{}, + }, + })) + assert.NotNil(t, ConvAgenticCallbackOutput([]*schema.AgenticMessage{})) +} diff --git a/components/prompt/agentic_chat_template.go b/components/prompt/agentic_chat_template.go new file mode 100644 index 0000000..41d2910 --- /dev/null +++ b/components/prompt/agentic_chat_template.go @@ -0,0 +1,84 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "context" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/schema" +) + +// FromAgenticMessages creates a new DefaultAgenticChatTemplate from the given templates and format type. +// eg. +// +// template := prompt.FromAgenticMessages(schema.FString, &schema.AgenticMessage{}) +// // in chain, or graph +// chain := compose.NewChain[map[string]any, []*schema.AgenticMessage]() +// chain.AppendAgenticChatTemplate(template) +func FromAgenticMessages(formatType schema.FormatType, templates ...schema.AgenticMessagesTemplate) *DefaultAgenticChatTemplate { + return &DefaultAgenticChatTemplate{ + templates: templates, + formatType: formatType, + } +} + +type DefaultAgenticChatTemplate struct { + templates []schema.AgenticMessagesTemplate + formatType schema.FormatType +} + +func (t *DefaultAgenticChatTemplate) Format(ctx context.Context, vs map[string]any, opts ...Option) (result []*schema.AgenticMessage, err error) { + ctx = callbacks.EnsureRunInfo(ctx, t.GetType(), components.ComponentOfAgenticPrompt) + ctx = callbacks.OnStart(ctx, &AgenticCallbackInput{ + Variables: vs, + Templates: t.templates, + }) + defer func() { + if err != nil { + _ = callbacks.OnError(ctx, err) + } + }() + + result = make([]*schema.AgenticMessage, 0, len(t.templates)) + for _, template := range t.templates { + msgs, err := template.Format(ctx, vs, t.formatType) + if err != nil { + return nil, err + } + + result = append(result, msgs...) + } + + _ = callbacks.OnEnd(ctx, &AgenticCallbackOutput{ + Result: result, + Templates: t.templates, + }) + + return result, nil +} + +// GetType returns the type of the agentic template (DefaultAgentic). +func (t *DefaultAgenticChatTemplate) GetType() string { + return "Default" +} + +// IsCallbacksEnabled checks if the callbacks are enabled for the chat template. +func (t *DefaultAgenticChatTemplate) IsCallbacksEnabled() bool { + return true +} diff --git a/components/prompt/agentic_chat_template_test.go b/components/prompt/agentic_chat_template_test.go new file mode 100644 index 0000000..f47020a --- /dev/null +++ b/components/prompt/agentic_chat_template_test.go @@ -0,0 +1,125 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +type mockAgenticTemplate struct { + err error +} + +func (m *mockAgenticTemplate) Format(ctx context.Context, vs map[string]any, formatType schema.FormatType) ([]*schema.AgenticMessage, error) { + if m.err != nil { + return nil, m.err + } + return []*schema.AgenticMessage{schema.UserAgenticMessage("mocked")}, nil +} + +func TestFromAgenticMessages(t *testing.T) { + t.Run("create template", func(t *testing.T) { + tpl := schema.UserAgenticMessage("hello") + ft := schema.FString + at := FromAgenticMessages(ft, tpl) + + assert.NotNil(t, at) + assert.Equal(t, ft, at.formatType) + assert.Len(t, at.templates, 1) + assert.Same(t, tpl, at.templates[0]) + }) +} + +func TestDefaultAgenticTemplate_GetType(t *testing.T) { + t.Run("get type", func(t *testing.T) { + at := &DefaultAgenticChatTemplate{} + assert.Equal(t, "Default", at.GetType()) + }) +} + +func TestDefaultAgenticTemplate_IsCallbacksEnabled(t *testing.T) { + t.Run("callbacks enabled", func(t *testing.T) { + at := &DefaultAgenticChatTemplate{} + assert.True(t, at.IsCallbacksEnabled()) + }) +} + +func TestDefaultAgenticTemplate_Format(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Mock callback handler + cb := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + assert.Equal(t, "Default", info.Type) + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + assert.Equal(t, "Default", info.Type) + return ctx + }). + OnErrorFn(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + assert.Fail(t, "unexpected error callback") + return ctx + }). + Build() + + tpl := schema.UserAgenticMessage("hello {val}") + at := FromAgenticMessages(schema.FString, tpl) + + ctx := context.Background() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{ + Type: "Default", + Component: "agentic_prompt", + }, cb) + + res, err := at.Format(ctx, map[string]any{"val": "world"}) + assert.NoError(t, err) + assert.Len(t, res, 1) + assert.Equal(t, "hello world", res[0].ContentBlocks[0].UserInputText.Text) + }) + + t.Run("template format error", func(t *testing.T) { + mockErr := errors.New("mock error") + mockTpl := &mockAgenticTemplate{err: mockErr} + at := FromAgenticMessages(schema.FString, mockTpl) + + // Mock callback handler to verify OnError + cb := callbacks.NewHandlerBuilder(). + OnErrorFn(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + assert.Equal(t, mockErr, err) + return ctx + }). + Build() + + ctx := context.Background() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{ + Type: "Default", + Component: "agentic_prompt", + }, cb) + + res, err := at.Format(ctx, map[string]any{}) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, mockErr, err) + }) +} diff --git a/components/prompt/callback_extra.go b/components/prompt/callback_extra.go new file mode 100644 index 0000000..324a418 --- /dev/null +++ b/components/prompt/callback_extra.go @@ -0,0 +1,70 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// CallbackInput is the input for the callback. +type CallbackInput struct { + // Variables is the variables for the callback. + Variables map[string]any + // Templates is the templates for the callback. + Templates []schema.MessagesTemplate + // Extra is the extra information for the callback. + Extra map[string]any +} + +// CallbackOutput is the output for the callback. +type CallbackOutput struct { + // Result is the result for the callback. + Result []*schema.Message + // Templates is the templates for the callback. + Templates []schema.MessagesTemplate + // Extra is the extra information for the callback. + Extra map[string]any +} + +// ConvCallbackInput converts the callback input to the prompt callback input. +func ConvCallbackInput(src callbacks.CallbackInput) *CallbackInput { + switch t := src.(type) { + case *CallbackInput: + return t + case map[string]any: + return &CallbackInput{ + Variables: t, + } + default: + return nil + } +} + +// ConvCallbackOutput converts the callback output to the prompt callback output. +func ConvCallbackOutput(src callbacks.CallbackOutput) *CallbackOutput { + switch t := src.(type) { + case *CallbackOutput: + return t + case []*schema.Message: + return &CallbackOutput{ + Result: t, + } + default: + return nil + } +} diff --git a/components/prompt/callback_extra_test.go b/components/prompt/callback_extra_test.go new file mode 100644 index 0000000..ad8a3c0 --- /dev/null +++ b/components/prompt/callback_extra_test.go @@ -0,0 +1,45 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestConvPrompt(t *testing.T) { + assert.NotNil(t, ConvCallbackInput(&CallbackInput{ + Templates: []schema.MessagesTemplate{ + &schema.Message{}, + }, + })) + assert.NotNil(t, ConvCallbackInput(map[string]any{})) + assert.Nil(t, ConvCallbackInput("asd")) + + assert.NotNil(t, ConvCallbackOutput(&CallbackOutput{ + Result: []*schema.Message{ + {}, + }, + Templates: []schema.MessagesTemplate{ + &schema.Message{}, + }, + })) + assert.NotNil(t, ConvCallbackOutput([]*schema.Message{})) +} diff --git a/components/prompt/chat_template.go b/components/prompt/chat_template.go new file mode 100644 index 0000000..2648058 --- /dev/null +++ b/components/prompt/chat_template.go @@ -0,0 +1,89 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "context" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/schema" +) + +// DefaultChatTemplate is the default chat template implementation. +type DefaultChatTemplate struct { + // templates is the templates for the chat template. + templates []schema.MessagesTemplate + // formatType is the format type for the chat template. + formatType schema.FormatType +} + +// FromMessages creates a new DefaultChatTemplate from the given templates and format type. +// eg. +// +// template := prompt.FromMessages(schema.FString, &schema.Message{Content: "Hello, {name}!"}, &schema.Message{Content: "how are you?"}) +// // in chain, or graph +// chain := compose.NewChain[map[string]any, []*schema.Message]() +// chain.AppendChatTemplate(template) +func FromMessages(formatType schema.FormatType, templates ...schema.MessagesTemplate) *DefaultChatTemplate { + return &DefaultChatTemplate{ + templates: templates, + formatType: formatType, + } +} + +// Format formats the chat template with the given context and variables. +func (t *DefaultChatTemplate) Format(ctx context.Context, + vs map[string]any, _ ...Option) (result []*schema.Message, err error) { + ctx = callbacks.EnsureRunInfo(ctx, t.GetType(), components.ComponentOfPrompt) + ctx = callbacks.OnStart(ctx, &CallbackInput{ + Variables: vs, + Templates: t.templates, + }) + defer func() { + if err != nil { + _ = callbacks.OnError(ctx, err) + } + }() + + result = make([]*schema.Message, 0, len(t.templates)) + for _, template := range t.templates { + msgs, err := template.Format(ctx, vs, t.formatType) + if err != nil { + return nil, err + } + + result = append(result, msgs...) + } + + _ = callbacks.OnEnd(ctx, &CallbackOutput{ + Result: result, + Templates: t.templates, + }) + + return result, nil +} + +// GetType returns the type of the chat template (Default). +func (t *DefaultChatTemplate) GetType() string { + return "Default" +} + +// IsCallbacksEnabled checks if the callbacks are enabled for the chat template. +func (t *DefaultChatTemplate) IsCallbacksEnabled() bool { + return true +} diff --git a/components/prompt/chat_template_test.go b/components/prompt/chat_template_test.go new file mode 100644 index 0000000..25f5ce5 --- /dev/null +++ b/components/prompt/chat_template_test.go @@ -0,0 +1,201 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestFormat(t *testing.T) { + pyFmtTestTemplate := []schema.MessagesTemplate{ + schema.SystemMessage( + "you are a helpful assistant.\n" + + "here is the context: {context}"), + schema.MessagesPlaceholder("chat_history", true), + schema.UserMessage("question: {question}"), + } + jinja2TestTemplate := []schema.MessagesTemplate{ + schema.SystemMessage( + "you are a helpful assistant.\n" + + "here is the context: {{context}}"), + schema.MessagesPlaceholder("chat_history", true), + schema.UserMessage("question: {{question}}"), + } + goFmtTestTemplate := []schema.MessagesTemplate{ + schema.SystemMessage( + "you are a helpful assistant.\n" + + "here is the context: {{.context}}"), + schema.MessagesPlaceholder("chat_history", true), + schema.UserMessage("question: {{.question}}"), + } + testValues := map[string]any{ + "context": "it's beautiful day", + "question": "how is the day today", + "chat_history": []*schema.Message{ + schema.UserMessage("who are you"), + schema.AssistantMessage("I'm a helpful assistant", nil), + }, + } + expected := []*schema.Message{ + schema.SystemMessage( + "you are a helpful assistant.\n" + + "here is the context: it's beautiful day"), + schema.UserMessage("who are you"), + schema.AssistantMessage("I'm a helpful assistant", nil), + schema.UserMessage("question: how is the day today"), + } + + // FString + chatTemplate := FromMessages(schema.FString, pyFmtTestTemplate...) + msgs, err := chatTemplate.Format(context.Background(), testValues) + assert.Nil(t, err) + assert.Equal(t, expected, msgs) + + // Jinja2 + chatTemplate = FromMessages(schema.Jinja2, jinja2TestTemplate...) + msgs, err = chatTemplate.Format(context.Background(), testValues) + assert.Nil(t, err) + assert.Equal(t, expected, msgs) + + // GoTemplate + chatTemplate = FromMessages(schema.GoTemplate, goFmtTestTemplate...) + msgs, err = chatTemplate.Format(context.Background(), testValues) + assert.Nil(t, err) + assert.Equal(t, expected, msgs) +} + +func TestDocumentFormat(t *testing.T) { + docs := []*schema.Document{ + { + ID: "1", + Content: "qwe", + MetaData: map[string]any{ + "hello": 888, + }, + }, + { + ID: "2", + Content: "asd", + MetaData: map[string]any{ + "bye": 111, + }, + }, + } + + template := FromMessages(schema.FString, + schema.SystemMessage("all:{all_docs}\nsingle:{single_doc}"), + ) + + msgs, err := template.Format(context.Background(), map[string]any{ + "all_docs": docs, + "single_doc": docs[0], + }) + + assert.Nil(t, err) + t.Log(msgs) +} + +func TestMultiContentFormat(t *testing.T) { + mtpl := []schema.MessagesTemplate{ + &schema.Message{ + Content: "{a}", + MultiContent: []schema.ChatMessagePart{ + { + Type: schema.ChatMessagePartTypeText, + Text: "{b}", + }, + { + Type: schema.ChatMessagePartTypeImageURL, + ImageURL: &schema.ChatMessageImageURL{ + URL: "{c}", + }, + }, + { + Type: schema.ChatMessagePartTypeAudioURL, + AudioURL: &schema.ChatMessageAudioURL{ + URL: "{d}", + }, + }, + { + Type: schema.ChatMessagePartTypeVideoURL, + VideoURL: &schema.ChatMessageVideoURL{ + URL: "{e}", + }, + }, + { + Type: schema.ChatMessagePartTypeFileURL, + FileURL: &schema.ChatMessageFileURL{ + URL: "{f}", + }, + }, + }, + }, + } + input := map[string]any{ + "a": "content", + "b": "text", + "c": "image url", + "d": "audio url", + "e": "video url", + "f": "file url", + } + expected := []*schema.Message{ + { + Content: "content", + MultiContent: []schema.ChatMessagePart{ + { + Type: schema.ChatMessagePartTypeText, + Text: "text", + }, + { + Type: schema.ChatMessagePartTypeImageURL, + ImageURL: &schema.ChatMessageImageURL{ + URL: "image url", + }, + }, + { + Type: schema.ChatMessagePartTypeAudioURL, + AudioURL: &schema.ChatMessageAudioURL{ + URL: "audio url", + }, + }, + { + Type: schema.ChatMessagePartTypeVideoURL, + VideoURL: &schema.ChatMessageVideoURL{ + URL: "video url", + }, + }, + { + Type: schema.ChatMessagePartTypeFileURL, + FileURL: &schema.ChatMessageFileURL{ + URL: "file url", + }, + }, + }, + }, + } + + tpl := FromMessages(schema.FString, mtpl...) + result, err := tpl.Format(context.Background(), input) + assert.Nil(t, err) + assert.Equal(t, expected, result) +} diff --git a/components/prompt/doc.go b/components/prompt/doc.go new file mode 100644 index 0000000..898aa5c --- /dev/null +++ b/components/prompt/doc.go @@ -0,0 +1,56 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package prompt defines the ChatTemplate component interface for building +// structured message lists from templates and runtime variables. +// +// # Overview +// +// A ChatTemplate takes a variables map and produces a []*schema.Message slice +// ready to pass to a [model.BaseChatModel]. It is typically the first node in +// a pipeline, sitting before the ChatModel. +// +// The built-in [DefaultChatTemplate] supports three template syntaxes: +// - FString: {variable} substitution +// - GoTemplate: Go's text/template with conditionals and loops +// - Jinja2: Jinja2 template syntax +// +// # Construction +// +// Use [FromMessages] to build a template from a list of message templates: +// +// tmpl := prompt.FromMessages(schema.FString, +// schema.SystemMessage("You are a helpful assistant."), +// schema.UserMessage("Answer this: {question}"), +// ) +// msgs, err := tmpl.Format(ctx, map[string]any{"question": "What is eino?"}) +// +// Use [schema.MessagesPlaceholder] to insert a dynamic list of messages +// (e.g. conversation history) at a fixed position in the template: +// +// tmpl := prompt.FromMessages(schema.FString, +// schema.SystemMessage("You are a helpful assistant."), +// schema.MessagesPlaceholder("history", true), +// schema.UserMessage("{question}"), +// ) +// +// # Common Pitfall +// +// Variable mismatches (a key present in the template but missing from the +// variables map) produce a runtime error — there is no compile-time check. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/chat_template_guide/ +package prompt diff --git a/components/prompt/interface.go b/components/prompt/interface.go new file mode 100644 index 0000000..2d5a2cb --- /dev/null +++ b/components/prompt/interface.go @@ -0,0 +1,50 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +var _ ChatTemplate = &DefaultChatTemplate{} +var _ AgenticChatTemplate = &DefaultAgenticChatTemplate{} + +// ChatTemplate formats a variables map into a list of messages for a ChatModel. +// +// Format substitutes the values from vs into the template's message list and +// returns the resulting []*schema.Message. The exact substitution syntax +// (FString, GoTemplate, Jinja2) is determined at construction time. +// +// Variable keys present in the template but absent from vs produce a runtime +// error — there is no compile-time safety. Prefer consistent variable naming +// across templates and callers. +// +// In a Graph or Chain, ChatTemplate typically precedes ChatModel. Use +// compose.WithOutputKey to convert the prior node's output into the map[string]any +// that Format expects. +// +// See [FromMessages] and [schema.MessagesPlaceholder] for construction helpers. +type ChatTemplate interface { + Format(ctx context.Context, vs map[string]any, opts ...Option) ([]*schema.Message, error) +} + +// AgenticChatTemplate formats variables into a list of agentic messages according to a prompt schema. +type AgenticChatTemplate interface { + Format(ctx context.Context, vs map[string]any, opts ...Option) ([]*schema.AgenticMessage, error) +} diff --git a/components/prompt/option.go b/components/prompt/option.go new file mode 100644 index 0000000..3b93c42 --- /dev/null +++ b/components/prompt/option.go @@ -0,0 +1,52 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +// Option is a call-time option for a ChatTemplate. The built-in +// [DefaultChatTemplate] has no common options — this type exists primarily for +// custom ChatTemplate implementations that need per-call configuration. +type Option struct { + implSpecificOptFn any +} + +// WrapImplSpecificOptFn wraps an implementation-specific option function so it +// can be passed alongside any future standard options. For use by custom +// ChatTemplate implementors. +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions extracts the implementation specific options from Option list, optionally providing a base options with default values. +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + s, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + s(base) + } + } + } + + return base +} diff --git a/components/prompt/option_test.go b/components/prompt/option_test.go new file mode 100644 index 0000000..851a013 --- /dev/null +++ b/components/prompt/option_test.go @@ -0,0 +1,51 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package prompt + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" +) + +type implOption struct { + userID int64 + name string +} + +func WithUserID(uid int64) Option { + return WrapImplSpecificOptFn[implOption](func(i *implOption) { + i.userID = uid + }) +} + +func WithName(n string) Option { + return WrapImplSpecificOptFn[implOption](func(i *implOption) { + i.name = n + }) +} + +func TestImplSpecificOption(t *testing.T) { + convey.Convey("impl_specific_option", t, func() { + opt := GetImplSpecificOptions(&implOption{}, WithUserID(101), WithName("Wang")) + + convey.So(opt, convey.ShouldEqual, &implOption{ + userID: 101, + name: "Wang", + }) + }) +} diff --git a/components/retriever/callback_extra.go b/components/retriever/callback_extra.go new file mode 100644 index 0000000..49ea421 --- /dev/null +++ b/components/retriever/callback_extra.go @@ -0,0 +1,74 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package retriever + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// CallbackInput is the input for the retriever callback. +type CallbackInput struct { + // Query is the query for the retriever. + Query string + + // TopK is the top k for the retriever, which means the top number of documents to retrieve. + TopK int + // Filter is the filter for the retriever. + Filter string + // ScoreThreshold is the score threshold for the retriever, eg 0.5 means the score of the document must be greater than 0.5. + ScoreThreshold *float64 + + // Extra is the extra information for the retriever. + Extra map[string]any +} + +// CallbackOutput is the output for the retriever callback. +type CallbackOutput struct { + // Docs is the documents for the retriever. + Docs []*schema.Document + // Extra is the extra information for the retriever. + Extra map[string]any +} + +// ConvCallbackInput converts the callback input to the retriever callback input. +func ConvCallbackInput(src callbacks.CallbackInput) *CallbackInput { + switch t := src.(type) { + case *CallbackInput: + return t + case string: + return &CallbackInput{ + Query: t, + } + default: + return nil + } +} + +// ConvCallbackOutput converts the callback output to the retriever callback output. +func ConvCallbackOutput(src callbacks.CallbackOutput) *CallbackOutput { + switch t := src.(type) { + case *CallbackOutput: + return t + case []*schema.Document: + return &CallbackOutput{ + Docs: t, + } + default: + return nil + } +} diff --git a/components/retriever/callback_extra_test.go b/components/retriever/callback_extra_test.go new file mode 100644 index 0000000..83cb9b5 --- /dev/null +++ b/components/retriever/callback_extra_test.go @@ -0,0 +1,35 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package retriever + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestConvRetriever(t *testing.T) { + assert.NotNil(t, ConvCallbackInput(&CallbackInput{})) + assert.NotNil(t, ConvCallbackInput("asd")) + assert.Nil(t, ConvCallbackInput([]string{})) + + assert.NotNil(t, ConvCallbackOutput(&CallbackOutput{})) + assert.NotNil(t, ConvCallbackOutput([]*schema.Document{})) + assert.Nil(t, ConvCallbackOutput("asd")) +} diff --git a/components/retriever/doc.go b/components/retriever/doc.go new file mode 100644 index 0000000..243bef3 --- /dev/null +++ b/components/retriever/doc.go @@ -0,0 +1,46 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package retriever defines the Retriever component interface for fetching +// relevant documents from a document store given a query. +// +// # Overview +// +// A Retriever is the read path of a RAG (Retrieval-Augmented Generation) +// pipeline. Given a query string it returns the most relevant [schema.Document] +// values from an underlying store (vector DB, keyword index, etc.). +// +// Concrete implementations (VikingDB, Milvus, Elasticsearch, …) live in +// eino-ext: +// +// github.com/cloudwego/eino-ext/components/retriever/ +// +// # Relationship to Indexer +// +// [Indexer] and Retriever are complementary: +// - Indexer writes documents (and their vectors) to the store +// - Retriever reads them back +// +// When both use an [embedding.Embedder], it must be the same model — vector +// dimensions must match or similarity scores will be meaningless. +// +// # Result Ordering +// +// Results are ordered by relevance score (descending). Scores and other +// backend metadata are available via [schema.Document].MetaData. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/retriever_guide/ +package retriever diff --git a/components/retriever/interface.go b/components/retriever/interface.go new file mode 100644 index 0000000..7b27e48 --- /dev/null +++ b/components/retriever/interface.go @@ -0,0 +1,50 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package retriever + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +//go:generate mockgen -destination ../../internal/mock/components/retriever/retriever_mock.go --package retriever -source interface.go + +// Retriever fetches the most relevant documents from a store for a given query. +// +// Retrieve accepts a natural-language query string and returns matching +// [schema.Document] values ordered by relevance (most relevant first). +// Relevance scores and backend-specific metadata are available in +// [schema.Document].MetaData. +// +// When [Options.Embedding] is set, the implementation converts the query to a +// vector before searching. The embedder must be the same model used at index +// time — see [indexer.Options.Embedding]. +// +// [Options.ScoreThreshold] is a filter, not a sort: documents scoring below +// the threshold are excluded entirely. [Options.TopK] caps the number of +// results returned. +// +// Retrieve can be used standalone or added to a Graph via AddRetrieverNode: +// +// retriever, _ := redis.NewRetriever(ctx, cfg) +// docs, _ := retriever.Retrieve(ctx, "what is eino?", retriever.WithTopK(5)) +// +// graph.AddRetrieverNode("retriever", retriever) +type Retriever interface { + Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error) +} diff --git a/components/retriever/option.go b/components/retriever/option.go new file mode 100644 index 0000000..a6bbef4 --- /dev/null +++ b/components/retriever/option.go @@ -0,0 +1,147 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package retriever + +import "github.com/cloudwego/eino/components/embedding" + +// Options is the options for the retriever. +type Options struct { + // Index is the index for the retriever, index in different retriever may be different. + Index *string + // SubIndex is the sub index for the retriever, sub index in different retriever may be different. + SubIndex *string + // TopK is the top k for the retriever, which means the top number of documents to retrieve. + TopK *int + // ScoreThreshold is the score threshold for the retriever, eg 0.5 means the score of the document must be greater than 0.5. + ScoreThreshold *float64 + // Embedding is the embedder for the retriever, which is used to embed the query for retrieval . + Embedding embedding.Embedder + + // DSLInfo carries backend-specific filter/query expressions. The structure and + // semantics are defined by the underlying store implementation. + DSLInfo map[string]any +} + +// WithIndex wraps the index option. +func WithIndex(index string) Option { + return Option{ + apply: func(opts *Options) { + opts.Index = &index + }, + } +} + +// WithSubIndex wraps the sub index option. +func WithSubIndex(subIndex string) Option { + return Option{ + apply: func(opts *Options) { + opts.SubIndex = &subIndex + }, + } +} + +// WithTopK wraps the top k option. +func WithTopK(topK int) Option { + return Option{ + apply: func(opts *Options) { + opts.TopK = &topK + }, + } +} + +// WithScoreThreshold wraps the score threshold option. +func WithScoreThreshold(threshold float64) Option { + return Option{ + apply: func(opts *Options) { + opts.ScoreThreshold = &threshold + }, + } +} + +// WithEmbedding wraps the embedder option. +func WithEmbedding(emb embedding.Embedder) Option { + return Option{ + apply: func(opts *Options) { + opts.Embedding = emb + }, + } +} + +// WithDSLInfo wraps the dsl info option. +func WithDSLInfo(dsl map[string]any) Option { + return Option{ + apply: func(opts *Options) { + opts.DSLInfo = dsl + }, + } +} + +// Option is a call-time option for a Retriever. +type Option struct { + apply func(opts *Options) + + implSpecificOptFn any +} + +// GetCommonOptions extracts standard [Options] from opts, merging onto base. +// Implementors must call this to honour caller-provided options: +// +// func (r *MyRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { +// options := retriever.GetCommonOptions(&retriever.Options{TopK: &r.defaultTopK}, opts...) +// // use options.TopK, options.ScoreThreshold, options.Embedding, etc. +// } +func GetCommonOptions(base *Options, opts ...Option) *Options { + if base == nil { + base = &Options{} + } + + for i := range opts { + if opts[i].apply != nil { + opts[i].apply(base) + } + } + + return base +} + +// WrapImplSpecificOptFn wraps an implementation-specific option function so it +// can be passed alongside standard options. For use by Retriever implementors. +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions extracts implementation-specific options from opts, +// merging onto base. Call alongside [GetCommonOptions] inside Retrieve. +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} diff --git a/components/retriever/option_test.go b/components/retriever/option_test.go new file mode 100644 index 0000000..b332dc7 --- /dev/null +++ b/components/retriever/option_test.go @@ -0,0 +1,60 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package retriever + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" + + "github.com/cloudwego/eino/internal/mock/components/embedding" +) + +func TestOptions(t *testing.T) { + convey.Convey("test options", t, func() { + var ( + index = "index" + topK = 2 + scoreThreshold = 4.0 + subIndex = "sub_index" + dslInfo = map[string]any{"dsl": "dsl"} + e = &embedding.MockEmbedder{} + defaultTopK = 1 + ) + + opts := GetCommonOptions( + &Options{ + TopK: &defaultTopK, + }, + WithIndex(index), + WithTopK(topK), + WithScoreThreshold(scoreThreshold), + WithSubIndex(subIndex), + WithDSLInfo(dslInfo), + WithEmbedding(e), + ) + + convey.So(opts, convey.ShouldResemble, &Options{ + Index: &index, + TopK: &topK, + ScoreThreshold: &scoreThreshold, + SubIndex: &subIndex, + DSLInfo: dslInfo, + Embedding: e, + }) + }) +} diff --git a/components/tool/callback_extra.go b/components/tool/callback_extra.go new file mode 100644 index 0000000..407f043 --- /dev/null +++ b/components/tool/callback_extra.go @@ -0,0 +1,68 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +import ( + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/schema" +) + +// CallbackInput is the input for the tool callback. +type CallbackInput struct { + // ArgumentsInJSON is the arguments in json format for the tool. + ArgumentsInJSON string + // Extra is the extra information for the tool. + Extra map[string]any +} + +// CallbackOutput is the output for the tool callback. +type CallbackOutput struct { + // Response is the response for the tool. + Response string + // ToolOutput is the multimodal output for the tool. Used when the tool returns structured data. + ToolOutput *schema.ToolResult + // Extra is the extra information for the tool. + Extra map[string]any +} + +// ConvCallbackInput converts the callback input to the tool callback input. +func ConvCallbackInput(src callbacks.CallbackInput) *CallbackInput { + switch t := src.(type) { + case *CallbackInput: + return t + case string: + return &CallbackInput{ArgumentsInJSON: t} + case *schema.ToolArgument: + return &CallbackInput{ArgumentsInJSON: t.Text} + default: + return nil + } +} + +// ConvCallbackOutput converts the callback output to the tool callback output. +func ConvCallbackOutput(src callbacks.CallbackOutput) *CallbackOutput { + switch t := src.(type) { + case *CallbackOutput: + return t + case string: + return &CallbackOutput{Response: t} + case *schema.ToolResult: + return &CallbackOutput{ToolOutput: t} + default: + return nil + } +} diff --git a/components/tool/callback_extra_test.go b/components/tool/callback_extra_test.go new file mode 100644 index 0000000..dc51edf --- /dev/null +++ b/components/tool/callback_extra_test.go @@ -0,0 +1,37 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConvCallbackInput(t *testing.T) { + assert.NotNil(t, ConvCallbackInput(&CallbackInput{})) + assert.NotNil(t, ConvCallbackInput("asd")) + assert.Nil(t, ConvCallbackInput(123)) + assert.Nil(t, ConvCallbackInput(nil)) +} + +func TestConvCallbackOutput(t *testing.T) { + assert.NotNil(t, ConvCallbackOutput(&CallbackOutput{})) + assert.NotNil(t, ConvCallbackOutput("asd")) + assert.Nil(t, ConvCallbackOutput(123)) + assert.Nil(t, ConvCallbackOutput(nil)) +} diff --git a/components/tool/doc.go b/components/tool/doc.go new file mode 100644 index 0000000..a9753d2 --- /dev/null +++ b/components/tool/doc.go @@ -0,0 +1,53 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package tool defines the tool component interfaces that allow language models +// to invoke external capabilities, and helpers for interrupt/resume within tools. +// +// # Interface Hierarchy +// +// BaseTool — Info() only; for passing tool metadata to a ChatModel +// ├── InvokableTool — standard: args as JSON string, returns string +// ├── StreamableTool — standard streaming: args as JSON string, returns StreamReader[string] +// ├── EnhancedInvokableTool — multimodal: args as *schema.ToolArgument, returns *schema.ToolResult +// └── EnhancedStreamableTool— multimodal streaming +// +// # Choosing an Interface +// +// Implement [InvokableTool] for most tools — arguments arrive as a JSON string +// automatically decoded from the model's tool call, and the result is a string +// sent back to the model. +// +// Implement [EnhancedInvokableTool] when the tool needs to return structured +// multimodal content (images, audio, files) rather than plain text. When a +// tool implements both a standard and an enhanced interface, ToolsNode +// prioritises the enhanced interface. +// +// # Creating Tools +// +// The [utils] sub-package provides constructors that eliminate boilerplate: +// - [utils.InferTool] / [utils.InferStreamTool] — infer parameter schema from Go struct tags +// - [utils.NewTool] / [utils.NewStreamTool] — manual ToolInfo + typed function +// +// # Interrupt / Resume +// +// Tools can pause execution and wait for external input using [Interrupt], +// [StatefulInterrupt], and [CompositeInterrupt]. Use [GetInterruptState] and +// [GetResumeContext] inside the tool to distinguish first-run from resumed-run. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/tools_node_guide/ +// See https://www.cloudwego.io/docs/eino/core_modules/components/tools_node_guide/how_to_create_a_tool/ +package tool diff --git a/components/tool/interface.go b/components/tool/interface.go new file mode 100644 index 0000000..257c1fc --- /dev/null +++ b/components/tool/interface.go @@ -0,0 +1,79 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// BaseTool provides the metadata that a ChatModel uses to decide whether and +// how to call a tool. Info returns a [schema.ToolInfo] containing the tool +// name, description, and parameter JSON schema. +// +// BaseTool alone is sufficient when passing tool definitions to a ChatModel +// via WithTools — the model only needs the schema to generate tool calls. +// To also execute the tool, implement [InvokableTool] or [StreamableTool]. +type BaseTool interface { + Info(ctx context.Context) (*schema.ToolInfo, error) +} + +// InvokableTool is a tool that can be executed by ToolsNode. +// +// InvokableRun receives the model's tool call arguments as a JSON-encoded +// string and returns a plain string result that is sent back to the model as +// a tool message. The framework handles JSON decoding automatically when using +// the [utils.InferTool] or [utils.NewTool] constructors. +type InvokableTool interface { + BaseTool + + // InvokableRun executes the tool with arguments encoded as a JSON string. + InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error) +} + +// StreamableTool is a streaming variant of [InvokableTool]. +// +// StreamableRun returns a [schema.StreamReader] that yields string chunks +// incrementally. The caller (ToolsNode) is responsible for closing the reader. +type StreamableTool interface { + BaseTool + + StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*schema.StreamReader[string], error) +} + +// EnhancedInvokableTool is a tool that returns structured multimodal results. +// +// Unlike [InvokableTool], arguments arrive as a [schema.ToolArgument] (not a +// raw JSON string) and the result is a [schema.ToolResult] which can carry +// text, images, audio, video, and file content. +// +// When a tool implements both a standard and an enhanced interface, ToolsNode +// prioritises the enhanced interface. +type EnhancedInvokableTool interface { + BaseTool + InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...Option) (*schema.ToolResult, error) +} + +// EnhancedStreamableTool is the streaming variant of [EnhancedInvokableTool]. +// +// It streams [schema.ToolResult] chunks, enabling incremental multimodal +// output. The caller is responsible for closing the returned [schema.StreamReader]. +type EnhancedStreamableTool interface { + BaseTool + StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...Option) (*schema.StreamReader[*schema.ToolResult], error) +} diff --git a/components/tool/interrupt.go b/components/tool/interrupt.go new file mode 100644 index 0000000..1640097 --- /dev/null +++ b/components/tool/interrupt.go @@ -0,0 +1,185 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +import ( + "context" + "errors" + "fmt" + + "github.com/cloudwego/eino/internal/core" +) + +// Interrupt pauses tool execution and signals the orchestration layer to checkpoint. +// The tool can be resumed later with optional data. +// +// Parameters: +// - ctx: The context passed to InvokableRun/StreamableRun +// - info: User-facing information about why the tool is interrupting (e.g., "needs user confirmation") +// +// Returns an error that should be returned from InvokableRun/StreamableRun. +// +// Example: +// +// func (t *MyTool) InvokableRun(ctx context.Context, args string, opts ...Option) (string, error) { +// if needsConfirmation(args) { +// return "", tool.Interrupt(ctx, "Please confirm this action") +// } +// return doWork(args), nil +// } +func Interrupt(ctx context.Context, info any) error { + is, err := core.Interrupt(ctx, info, nil, nil) + if err != nil { + return err + } + return is +} + +// StatefulInterrupt pauses tool execution with state preservation. +// Use this when the tool has internal state that must be restored on resume. +// +// Parameters: +// - ctx: The context passed to InvokableRun/StreamableRun +// - info: User-facing information about the interrupt +// - state: Internal state to persist (must be gob-serializable) +// +// Example: +// +// func (t *MyTool) InvokableRun(ctx context.Context, args string, opts ...Option) (string, error) { +// wasInterrupted, hasState, state := tool.GetInterruptState[MyState](ctx) +// if !wasInterrupted { +// // First run - interrupt with state +// return "", tool.StatefulInterrupt(ctx, "processing", MyState{Step: 1}) +// } +// // Resumed - continue from saved state +// return continueFrom(state), nil +// } +func StatefulInterrupt(ctx context.Context, info any, state any) error { + is, err := core.Interrupt(ctx, info, state, nil) + if err != nil { + return err + } + return is +} + +// CompositeInterrupt creates an interrupt that aggregates multiple sub-interrupts. +// Use this when a tool internally executes a graph or other interruptible components. +// +// Parameters: +// - ctx: The context passed to InvokableRun/StreamableRun +// - info: User-facing information for this tool's interrupt +// - state: Internal state to persist for this tool +// - errs: Interrupt errors from sub-components (graphs, other tools, etc.) +// +// Example: +// +// func (t *MyTool) InvokableRun(ctx context.Context, args string, opts ...Option) (string, error) { +// result, err := t.internalGraph.Invoke(ctx, input) +// if err != nil { +// if _, ok := tool.IsInterruptError(err); ok { +// return "", tool.CompositeInterrupt(ctx, "graph interrupted", myState, err) +// } +// return "", err +// } +// return result, nil +// } +func CompositeInterrupt(ctx context.Context, info any, state any, errs ...error) error { + if len(errs) == 0 { + return StatefulInterrupt(ctx, info, state) + } + + var cErrs []*core.InterruptSignal + for _, err := range errs { + ire := &core.InterruptSignal{} + if errors.As(err, &ire) { + cErrs = append(cErrs, ire) + continue + } + + var provider core.InterruptContextsProvider + if errors.As(err, &provider) { + is := core.FromInterruptContexts(provider.GetInterruptContexts()) + if is != nil { + cErrs = append(cErrs, is) + } + continue + } + + return fmt.Errorf("composite interrupt but one of the sub error is not interrupt error: %w", err) + } + + is, err := core.Interrupt(ctx, info, state, cErrs) + if err != nil { + return err + } + return is +} + +// GetInterruptState checks if the tool was previously interrupted and retrieves saved state. +// +// Returns: +// - wasInterrupted: true if this tool was part of a previous interruption +// - hasState: true if state was saved and successfully cast to type T +// - state: the saved state (zero value if hasState is false) +// +// Example: +// +// func (t *MyTool) InvokableRun(ctx context.Context, args string, opts ...Option) (string, error) { +// wasInterrupted, hasState, state := tool.GetInterruptState[MyState](ctx) +// if wasInterrupted && hasState { +// // Continue from saved state +// return continueFrom(state), nil +// } +// // First run +// return "", tool.StatefulInterrupt(ctx, "need input", MyState{Step: 1}) +// } +func GetInterruptState[T any](ctx context.Context) (wasInterrupted bool, hasState bool, state T) { + return core.GetInterruptState[T](ctx) +} + +// GetResumeContext checks if this tool is the explicit target of a resume operation. +// +// Returns: +// - isResumeTarget: true if this tool was explicitly targeted for resume +// - hasData: true if resume data was provided +// - data: the resume data (zero value if hasData is false) +// +// Use this to differentiate between: +// - Being resumed as the target (should proceed with work) +// - Being re-executed because a sibling was resumed (should re-interrupt) +// +// Example: +// +// func (t *MyTool) InvokableRun(ctx context.Context, args string, opts ...Option) (string, error) { +// wasInterrupted, _, _ := tool.GetInterruptState[any](ctx) +// if !wasInterrupted { +// return "", tool.Interrupt(ctx, "need confirmation") +// } +// +// isTarget, hasData, data := tool.GetResumeContext[string](ctx) +// if !isTarget { +// // Not our turn - re-interrupt +// return "", tool.Interrupt(ctx, nil) +// } +// if hasData { +// return data, nil +// } +// return "default result", nil +// } +func GetResumeContext[T any](ctx context.Context) (isResumeTarget bool, hasData bool, data T) { + return core.GetResumeContext[T](ctx) +} diff --git a/components/tool/interrupt_test.go b/components/tool/interrupt_test.go new file mode 100644 index 0000000..2f20807 --- /dev/null +++ b/components/tool/interrupt_test.go @@ -0,0 +1,134 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/core" +) + +func TestInterrupt(t *testing.T) { + ctx := context.Background() + + t.Run("basic interrupt", func(t *testing.T) { + err := Interrupt(ctx, "test info") + assert.Error(t, err) + + var signal *core.InterruptSignal + assert.True(t, errors.As(err, &signal)) + assert.Equal(t, "test info", signal.Info) + assert.True(t, signal.IsRootCause) + }) +} + +func TestStatefulInterrupt(t *testing.T) { + ctx := context.Background() + + t.Run("stateful interrupt", func(t *testing.T) { + type myState struct { + Value int + } + state := &myState{Value: 42} + + err := StatefulInterrupt(ctx, "test info", state) + assert.Error(t, err) + + var signal *core.InterruptSignal + assert.True(t, errors.As(err, &signal)) + assert.Equal(t, "test info", signal.Info) + assert.Equal(t, state, signal.State) + assert.True(t, signal.IsRootCause) + }) +} + +func TestCompositeInterrupt(t *testing.T) { + ctx := context.Background() + + t.Run("no sub errors falls back to StatefulInterrupt", func(t *testing.T) { + err := CompositeInterrupt(ctx, "composite info", "my state") + assert.Error(t, err) + + var signal *core.InterruptSignal + assert.True(t, errors.As(err, &signal)) + assert.Equal(t, "composite info", signal.Info) + assert.Equal(t, "my state", signal.State) + assert.True(t, signal.IsRootCause) + assert.Empty(t, signal.Subs) + }) + + t.Run("with InterruptSignal sub error", func(t *testing.T) { + subSignal, _ := core.Interrupt(ctx, "sub info", "sub state", nil) + + err := CompositeInterrupt(ctx, "composite info", "my state", subSignal) + assert.Error(t, err) + + var signal *core.InterruptSignal + assert.True(t, errors.As(err, &signal)) + assert.Equal(t, "composite info", signal.Info) + assert.Equal(t, "my state", signal.State) + assert.Len(t, signal.Subs, 1) + assert.Equal(t, "sub info", signal.Subs[0].Info) + }) + + t.Run("with non-interrupt error returns error", func(t *testing.T) { + nonInterruptErr := errors.New("regular error") + + err := CompositeInterrupt(ctx, "composite info", "my state", nonInterruptErr) + assert.Error(t, err) + assert.Contains(t, err.Error(), "composite interrupt but one of the sub error is not interrupt error") + + var signal *core.InterruptSignal + assert.False(t, errors.As(err, &signal)) + }) + + t.Run("with multiple sub errors", func(t *testing.T) { + subSignal1, _ := core.Interrupt(ctx, "sub1 info", nil, nil) + subSignal2, _ := core.Interrupt(ctx, "sub2 info", nil, nil) + + err := CompositeInterrupt(ctx, "composite info", nil, subSignal1, subSignal2) + assert.Error(t, err) + + var signal *core.InterruptSignal + assert.True(t, errors.As(err, &signal)) + assert.Len(t, signal.Subs, 2) + }) +} + +func TestGetInterruptState(t *testing.T) { + t.Run("not interrupted returns false", func(t *testing.T) { + ctx := context.Background() + wasInterrupted, hasState, state := GetInterruptState[string](ctx) + assert.False(t, wasInterrupted) + assert.False(t, hasState) + assert.Empty(t, state) + }) +} + +func TestGetResumeContext(t *testing.T) { + t.Run("not resume target returns false", func(t *testing.T) { + ctx := context.Background() + isResumeTarget, hasData, data := GetResumeContext[string](ctx) + assert.False(t, isResumeTarget) + assert.False(t, hasData) + assert.Empty(t, data) + }) +} diff --git a/components/tool/option.go b/components/tool/option.go new file mode 100644 index 0000000..1458448 --- /dev/null +++ b/components/tool/option.go @@ -0,0 +1,78 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +// Option defines call option for InvokableTool or StreamableTool component, which is part of component interface signature. +// Each tool implementation could define its own options struct and option funcs within its own package, +// then wrap the impl specific option funcs into this type, before passing to InvokableRun or StreamableRun. +type Option struct { + implSpecificOptFn any +} + +// WrapImplSpecificOptFn wraps the impl specific option functions into Option type. +// T: the type of the impl specific options struct. +// Tool implementations are required to use this function to convert its own option functions into the unified Option type. +// For example, if the tool defines its own options struct: +// +// type customOptions struct { +// conf string +// } +// +// Then the tool needs to provide an option function as such: +// +// func WithConf(conf string) Option { +// return WrapImplSpecificOptFn(func(o *customOptions) { +// o.conf = conf +// } +// } +// +// . +func WrapImplSpecificOptFn[T any](optFn func(*T)) Option { + return Option{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions provides tool author the ability to extract their own custom options from the unified Option type. +// T: the type of the impl specific options struct. +// This function should be used within the tool implementation's InvokableRun or StreamableRun functions. +// It is recommended to provide a base T as the first argument, within which the tool author can provide default values for the impl specific options. +// eg. +// +// type customOptions struct { +// conf string +// } +// defaultOptions := &customOptions{} +// +// customOptions := tool.GetImplSpecificOptions(defaultOptions, opts...) +func GetImplSpecificOptions[T any](base *T, opts ...Option) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} diff --git a/components/tool/option_test.go b/components/tool/option_test.go new file mode 100644 index 0000000..2a27604 --- /dev/null +++ b/components/tool/option_test.go @@ -0,0 +1,54 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tool + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" +) + +func TestImplSpecificOpts(t *testing.T) { + convey.Convey("TestImplSpecificOpts", t, func() { + type implSpecificOptions struct { + conf string + index int + } + + withConf := func(conf string) func(o *implSpecificOptions) { + return func(o *implSpecificOptions) { + o.conf = conf + } + } + + withIndex := func(index int) func(o *implSpecificOptions) { + return func(o *implSpecificOptions) { + o.index = index + } + } + + toolOption1 := WrapImplSpecificOptFn(withConf("test_conf")) + toolOption2 := WrapImplSpecificOptFn(withIndex(1)) + + implSpecificOpts := GetImplSpecificOptions(&implSpecificOptions{}, toolOption1, toolOption2) + + convey.So(implSpecificOpts, convey.ShouldResemble, &implSpecificOptions{ + conf: "test_conf", + index: 1, + }) + }) +} diff --git a/components/tool/utils/common.go b/components/tool/utils/common.go new file mode 100644 index 0000000..6228708 --- /dev/null +++ b/components/tool/utils/common.go @@ -0,0 +1,28 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "github.com/bytedance/sonic" +) + +func marshalString(resp any) (string, error) { + if rs, ok := resp.(string); ok { + return rs, nil + } + return sonic.MarshalString(resp) +} diff --git a/components/tool/utils/common_test.go b/components/tool/utils/common_test.go new file mode 100644 index 0000000..e015dd8 --- /dev/null +++ b/components/tool/utils/common_test.go @@ -0,0 +1,187 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalString(t *testing.T) { + tests := []struct { + name string + input interface{} + expected string + hasError bool + }{ + { + name: "string input should return as-is", + input: "hello world", + expected: "hello world", + hasError: false, + }, + { + name: "empty string should return empty", + input: "", + expected: "", + hasError: false, + }, + { + name: "string with special characters", + input: "hello\nworld\t\"test\"", + expected: "hello\nworld\t\"test\"", + hasError: false, + }, + { + name: "string with unicode", + input: "你好世界", + expected: "你好世界", + hasError: false, + }, + { + name: "integer should be marshaled to JSON", + input: 42, + expected: "42", + hasError: false, + }, + { + name: "float should be marshaled to JSON", + input: 3.14, + expected: "3.14", + hasError: false, + }, + { + name: "boolean true should be marshaled to JSON", + input: true, + expected: "true", + hasError: false, + }, + { + name: "boolean false should be marshaled to JSON", + input: false, + expected: "false", + hasError: false, + }, + { + name: "nil should be marshaled to JSON null", + input: nil, + expected: "null", + hasError: false, + }, + { + name: "slice should be marshaled to JSON array", + input: []int{1, 2, 3}, + expected: "[1,2,3]", + hasError: false, + }, + { + name: "empty slice should be marshaled to JSON empty array", + input: []int{}, + expected: "[]", + hasError: false, + }, + { + name: "empty map should be marshaled to JSON empty object", + input: map[string]int{}, + expected: "{}", + hasError: false, + }, + { + name: "struct should be marshaled to JSON", + input: struct{ Name string }{Name: "test"}, + expected: `{"Name":"test"}`, + hasError: false, + }, + { + name: "pointer to string should be handled as non-string", + input: func() *string { s := "test"; return &s }(), + expected: `"test"`, + hasError: false, + }, + { + name: "interface{} containing string should return as-is", + input: interface{}("test string"), + expected: "test string", + hasError: false, + }, + { + name: "interface{} containing int should be marshaled", + input: interface{}(123), + expected: "123", + hasError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := marshalString(tt.input) + + if tt.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestMarshalStringEdgeCases(t *testing.T) { + t.Run("complex nested structure", func(t *testing.T) { + complex := map[string]interface{}{ + "string": "value", + "number": 42, + "nested": map[string]interface{}{ + "array": []string{"a", "b", "c"}, + "bool": true, + }, + } + + result, err := marshalString(complex) + assert.NoError(t, err) + assert.Contains(t, result, `"string":"value"`) + assert.Contains(t, result, `"number":42`) + assert.Contains(t, result, `"nested"`) + }) + + t.Run("string type assertion priority", func(t *testing.T) { + // Test that string type assertion has priority over JSON marshaling + var input interface{} = "direct string" + result, err := marshalString(input) + assert.NoError(t, err) + assert.Equal(t, "direct string", result) + + // Verify it's not JSON encoded + assert.NotEqual(t, `"direct string"`, result) + }) +} + +func TestMarshalStringConsistency(t *testing.T) { + t.Run("string vs JSON marshaling difference", func(t *testing.T) { + input := `{"key": "value"}` + + // Direct string should return as-is + result, err := marshalString(input) + assert.NoError(t, err) + assert.Equal(t, input, result) + + // Should not be double-encoded + assert.NotEqual(t, fmt.Sprintf(`"%s"`, input), result) + }) +} diff --git a/components/tool/utils/create_options.go b/components/tool/utils/create_options.go new file mode 100644 index 0000000..ea09e33 --- /dev/null +++ b/components/tool/utils/create_options.go @@ -0,0 +1,82 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "reflect" + + "github.com/eino-contrib/jsonschema" +) + +// UnmarshalArguments is the function type for unmarshalling the arguments. +type UnmarshalArguments func(ctx context.Context, arguments string) (any, error) + +// MarshalOutput is the function type for marshalling the output. +type MarshalOutput func(ctx context.Context, output any) (string, error) + +type toolOptions struct { + um UnmarshalArguments + m MarshalOutput + scModifier SchemaModifierFn +} + +// Option is the option func for the tool. +type Option func(o *toolOptions) + +// WithUnmarshalArguments wraps the unmarshal arguments option. +// when you want to unmarshal the arguments by yourself, you can use this option. +func WithUnmarshalArguments(um UnmarshalArguments) Option { + return func(o *toolOptions) { + o.um = um + } +} + +// WithMarshalOutput wraps the marshal output option. +// when you want to marshal the output by yourself, you can use this option. +func WithMarshalOutput(m MarshalOutput) Option { + return func(o *toolOptions) { + o.m = m + } +} + +// SchemaModifierFn is the schema modifier function for inferring tool parameter from tagged go struct. +// Within this function, end-user can parse custom go struct tags into corresponding json schema field. +// Parameters: +// 1. jsonTagName: the name defined in the json tag. Specifically, the last 'jsonTagName' visited is fixed to be '_root', which represents the entire go struct. Also, for array field, both the field itself and the element within the array will trigger this function. +// 2. t: the type of current schema, usually the field type of the go struct. +// 3. tag: the struct tag of current schema, usually the field tag of the go struct. Note that the element within an array field will use the same go struct tag as the array field itself. +// 4. schema: the current json schema object to be modified. +type SchemaModifierFn func(jsonTagName string, t reflect.Type, tag reflect.StructTag, schema *jsonschema.Schema) + +// WithSchemaModifier sets a user-defined schema modifier for inferring tool parameter from tagged go struct. +func WithSchemaModifier(modifier SchemaModifierFn) Option { + return func(o *toolOptions) { + o.scModifier = modifier + } +} + +func getToolOptions(opt ...Option) *toolOptions { + opts := &toolOptions{ + um: nil, + m: nil, + } + for _, o := range opt { + o(opts) + } + return opts +} diff --git a/components/tool/utils/doc.go b/components/tool/utils/doc.go new file mode 100644 index 0000000..654e7ea --- /dev/null +++ b/components/tool/utils/doc.go @@ -0,0 +1,57 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package utils provides constructors for building tool implementations without +// writing boilerplate JSON serialization code. +// +// # Choosing a Constructor +// +// There are two main strategies: +// +// 1. Infer from struct tags (recommended): [InferTool], [InferStreamTool], +// [InferEnhancedTool], [InferEnhancedStreamTool]. +// The parameter JSON schema is derived automatically from the input struct's +// field names and tags. Requires a typed input struct. +// +// 2. Manual ToolInfo: [NewTool], [NewStreamTool], [NewEnhancedTool], +// [NewEnhancedStreamTool]. +// You supply a [schema.ToolInfo] directly. Useful when the schema cannot +// be expressed as a Go struct, or must be dynamically constructed. +// +// # Struct Tag Convention +// +// InferTool and friends use the following tags on the input struct fields: +// +// type Input struct { +// Query string `json:"query" jsonschema:"required" jsonschema_description:"The search query"` +// MaxItems int `json:"max_items" jsonschema_description:"Maximum results to return"` +// } +// +// Key rules: +// - Use a separate jsonschema_description tag for field descriptions — +// embedding descriptions inside the jsonschema tag causes comma-parsing +// issues. +// - Use jsonschema:"required" to mark mandatory parameters. +// - The json tag controls the parameter name visible to the model. +// +// # Schema Utilities +// +// [GoStruct2ToolInfo] and [GoStruct2ParamsOneOf] convert a Go struct to schema +// types without creating a tool — useful for ChatModel structured output via +// ResponseFormat or BindTools. +// +// See https://www.cloudwego.io/docs/eino/core_modules/components/tools_node_guide/how_to_create_a_tool/ +package utils diff --git a/components/tool/utils/error_handler.go b/components/tool/utils/error_handler.go new file mode 100644 index 0000000..a53d463 --- /dev/null +++ b/components/tool/utils/error_handler.go @@ -0,0 +1,166 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// ErrorHandler converts a tool error into a string response. +type ErrorHandler func(context.Context, error) string + +// WrapToolWithErrorHandler wraps any BaseTool with custom error handling. +// This function detects the tool type (InvokableTool, StreamableTool, or both) +// and applies the appropriate error handling wrapper. +// When the wrapped tool returns an error, the error handler function 'h' will be called +// to convert the error into a string result, and no error will be returned from the wrapper. +// +// Parameters: +// - t: The original BaseTool to be wrapped +// - h: A function that converts an error to a string +// +// Returns: +// - A wrapped BaseTool that handles errors internally based on its capabilities +func WrapToolWithErrorHandler(t tool.BaseTool, h ErrorHandler) tool.BaseTool { + ih := &infoHelper{info: t.Info} + var s tool.StreamableTool + if st, ok := t.(tool.StreamableTool); ok { + s = st + } + if it, ok := t.(tool.InvokableTool); ok { + if s == nil { + return WrapInvokableToolWithErrorHandler(it, h) + } else { + return &combinedErrorWrapper{ + infoHelper: ih, + errorHelper: &errorHelper{ + i: it.InvokableRun, + h: h, + }, + streamErrorHelper: &streamErrorHelper{ + s: s.StreamableRun, + h: h, + }, + } + } + } + if s != nil { + return WrapStreamableToolWithErrorHandler(s, h) + } + return t +} + +// WrapInvokableToolWithErrorHandler wraps an InvokableTool with custom error handling. +// When the wrapped tool returns an error, the error handler function 'h' will be called +// to convert the error into a string result, and no error will be returned from the wrapper. +// +// Parameters: +// - tool: The original InvokableTool to be wrapped +// - h: A function that converts an error to a string +// +// Returns: +// - A wrapped InvokableTool that handles errors internally +func WrapInvokableToolWithErrorHandler(t tool.InvokableTool, h ErrorHandler) tool.InvokableTool { + return &errorWrapper{ + infoHelper: &infoHelper{info: t.Info}, + errorHelper: &errorHelper{ + i: t.InvokableRun, + h: h, + }, + } +} + +// WrapStreamableToolWithErrorHandler wraps a StreamableTool with custom error handling. +// When the wrapped tool returns an error, the error handler function 'h' will be called +// to convert the error into a string result, which will be returned as a single-item stream, +// and no error will be returned from the wrapper. +// +// Parameters: +// - tool: The original StreamableTool to be wrapped +// - h: A function that converts an error to a string +// +// Returns: +// - A wrapped StreamableTool that handles errors internally +func WrapStreamableToolWithErrorHandler(t tool.StreamableTool, h ErrorHandler) tool.StreamableTool { + return &streamErrorWrapper{ + infoHelper: &infoHelper{info: t.Info}, + streamErrorHelper: &streamErrorHelper{ + s: t.StreamableRun, + h: h, + }, + } +} + +type errorWrapper struct { + *infoHelper + *errorHelper +} + +type streamErrorWrapper struct { + *infoHelper + *streamErrorHelper +} + +type combinedErrorWrapper struct { + *infoHelper + *errorHelper + *streamErrorHelper +} + +type infoHelper struct { + info func(ctx context.Context) (*schema.ToolInfo, error) +} + +func (i *infoHelper) Info(ctx context.Context) (*schema.ToolInfo, error) { + return i.info(ctx) +} + +type errorHelper struct { + i func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) + h ErrorHandler +} + +func (s *errorHelper) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + result, err := s.i(ctx, argumentsInJSON, opts...) + if _, ok := compose.IsInterruptRerunError(err); ok { + return result, err + } + if err != nil { + return s.h(ctx, err), nil + } + return result, nil +} + +type streamErrorHelper struct { + s func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) + h ErrorHandler +} + +func (s *streamErrorHelper) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + result, err := s.s(ctx, argumentsInJSON, opts...) + if _, ok := compose.IsInterruptRerunError(err); ok { + return result, err + } + if err != nil { + return schema.StreamReaderFromArray([]string{s.h(ctx, err)}), nil + } + return result, nil +} diff --git a/components/tool/utils/error_handler_test.go b/components/tool/utils/error_handler_test.go new file mode 100644 index 0000000..06648de --- /dev/null +++ b/components/tool/utils/error_handler_test.go @@ -0,0 +1,81 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +type testErrorTool struct{} + +func (t *testErrorTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return nil, nil +} + +func (t *testErrorTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + return "", errors.New("test error") +} + +func (t *testErrorTool) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + return nil, errors.New("test stream error") +} + +func TestErrorWrapper(t *testing.T) { + ctx := context.Background() + + nt := WrapToolWithErrorHandler(&testErrorTool{}, func(_ context.Context, err error) string { + return err.Error() + }) + result, err := nt.(tool.InvokableTool).InvokableRun(ctx, "") + assert.NoError(t, err) + assert.Equal(t, "test error", result) + streamResult, err := nt.(tool.StreamableTool).StreamableRun(ctx, "") + assert.NoError(t, err) + chunk, err := streamResult.Recv() + assert.NoError(t, err) + assert.Equal(t, "test stream error", chunk) + _, err = streamResult.Recv() + assert.True(t, errors.Is(err, io.EOF)) + + wrappedTool := WrapInvokableToolWithErrorHandler(&testErrorTool{}, func(_ context.Context, err error) string { + return err.Error() + }) + result, err = wrappedTool.InvokableRun(ctx, "") + assert.NoError(t, err) + assert.Equal(t, "test error", result) + + wrappedStreamTool := WrapStreamableToolWithErrorHandler(&testErrorTool{}, func(_ context.Context, err error) string { + return err.Error() + }) + + streamResult, err = wrappedStreamTool.StreamableRun(ctx, "") + assert.NoError(t, err) + + chunk, err = streamResult.Recv() + assert.NoError(t, err) + assert.Equal(t, "test stream error", chunk) + _, err = streamResult.Recv() + assert.True(t, errors.Is(err, io.EOF)) +} diff --git a/components/tool/utils/invokable_func.go b/components/tool/utils/invokable_func.go new file mode 100644 index 0000000..d5c9b3a --- /dev/null +++ b/components/tool/utils/invokable_func.go @@ -0,0 +1,318 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "fmt" + "strings" + + "github.com/bytedance/sonic" + "github.com/eino-contrib/jsonschema" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +// InvokeFunc is the function type for the tool. +type InvokeFunc[T, D any] func(ctx context.Context, input T) (output D, err error) + +// OptionableInvokeFunc is the function type for the tool with tool option. +type OptionableInvokeFunc[T, D any] func(ctx context.Context, input T, opts ...tool.Option) (output D, err error) + +// InferTool creates an [tool.InvokableTool] by inferring the parameter JSON +// schema from the fields and tags of the input type T. +// +// The tool automatically JSON-decodes the model's argument string into T before +// calling fn, and JSON-encodes the D return value into the result string. +// +// Use [WithSchemaModifier] in opts to customise how struct tags are mapped to +// JSON schema fields. +func InferTool[T, D any](toolName, toolDesc string, i InvokeFunc[T, D], opts ...Option) (tool.InvokableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return NewTool(ti, i, opts...), nil +} + +// InferOptionableTool is like [InferTool] but the function also receives +// [tool.Option] values passed by ToolsNode at call time. +func InferOptionableTool[T, D any](toolName, toolDesc string, i OptionableInvokeFunc[T, D], opts ...Option) (tool.InvokableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return newOptionableTool(ti, i, opts...), nil +} + +// EnhancedInvokeFunc is the function type for the enhanced tool. +type EnhancedInvokeFunc[T any] func(ctx context.Context, input T) (output *schema.ToolResult, err error) + +// OptionableEnhancedInvokeFunc is the function type for the enhanced tool with tool option. +type OptionableEnhancedInvokeFunc[T any] func(ctx context.Context, input T, opts ...tool.Option) (output *schema.ToolResult, err error) + +// InferEnhancedTool creates an [tool.EnhancedInvokableTool] by inferring the +// parameter JSON schema from type T. The function returns a [schema.ToolResult] +// for multimodal output (text, images, audio, video, files). +func InferEnhancedTool[T any](toolName, toolDesc string, i EnhancedInvokeFunc[T], opts ...Option) (tool.EnhancedInvokableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return NewEnhancedTool(ti, i, opts...), nil +} + +// InferOptionableEnhancedTool creates an EnhancedInvokableTool from a given function by inferring the ToolInfo from the function's request parameters, with tool option. +func InferOptionableEnhancedTool[T any](toolName, toolDesc string, i OptionableEnhancedInvokeFunc[T], opts ...Option) (tool.EnhancedInvokableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return newOptionableEnhancedTool(ti, i, opts...), nil +} + +// GoStruct2ParamsOneOf converts a Go struct's fields and tags into a +// [schema.ParamsOneOf] (JSON Schema 2020-12). Useful for ChatModel structured +// output via ResponseFormat without creating a full tool. +func GoStruct2ParamsOneOf[T any](opts ...Option) (*schema.ParamsOneOf, error) { + return goStruct2ParamsOneOf[T](opts...) +} + +// GoStruct2ToolInfo converts a Go struct into a [schema.ToolInfo]. Useful for +// binding a typed schema to a ChatModel via BindTools for structured output, +// when you do not need a full executable tool. +func GoStruct2ToolInfo[T any](toolName, toolDesc string, opts ...Option) (*schema.ToolInfo, error) { + return goStruct2ToolInfo[T](toolName, toolDesc, opts...) +} + +func goStruct2ToolInfo[T any](toolName, toolDesc string, opts ...Option) (*schema.ToolInfo, error) { + paramsOneOf, err := goStruct2ParamsOneOf[T](opts...) + if err != nil { + return nil, err + } + return &schema.ToolInfo{ + Name: toolName, + Desc: toolDesc, + ParamsOneOf: paramsOneOf, + }, nil +} + +func goStruct2ParamsOneOf[T any](opts ...Option) (*schema.ParamsOneOf, error) { + options := getToolOptions(opts...) + + r := &jsonschema.Reflector{ + Anonymous: true, + DoNotReference: true, + SchemaModifier: jsonschema.SchemaModifierFn(options.scModifier), + } + + js := r.Reflect(generic.NewInstance[T]()) + js.Version = "" + + paramsOneOf := schema.NewParamsOneOfByJSONSchema(js) + + return paramsOneOf, nil +} + +// NewTool creates an [tool.InvokableTool] from an explicit [schema.ToolInfo] +// and a typed function. Use this when the schema cannot be inferred from struct +// tags (e.g. dynamic or complex parameter schemas). +// +// Note: you are responsible for keeping desc.ParamsOneOf consistent with the +// actual fields of T — there is no compile-time check. +func NewTool[T, D any](desc *schema.ToolInfo, i InvokeFunc[T, D], opts ...Option) tool.InvokableTool { + return newOptionableTool(desc, func(ctx context.Context, input T, _ ...tool.Option) (D, error) { + return i(ctx, input) + }, opts...) +} + +func newOptionableTool[T, D any](desc *schema.ToolInfo, i OptionableInvokeFunc[T, D], opts ...Option) tool.InvokableTool { + to := getToolOptions(opts...) + + return &invokableTool[T, D]{ + info: desc, + um: to.um, + m: to.m, + Fn: i, + } +} + +type invokableTool[T, D any] struct { + info *schema.ToolInfo + + um UnmarshalArguments + m MarshalOutput + + Fn OptionableInvokeFunc[T, D] +} + +func (i *invokableTool[T, D]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return i.info, nil +} + +// InvokableRun invokes the tool with the given arguments. +func (i *invokableTool[T, D]) InvokableRun(ctx context.Context, arguments string, opts ...tool.Option) (output string, err error) { + + var inst T + if i.um != nil { + var val any + val, err = i.um(ctx, arguments) + if err != nil { + return "", fmt.Errorf("[LocalFunc] failed to unmarshal arguments, toolName=%s, err=%w", i.getToolName(), err) + } + gt, ok := val.(T) + if !ok { + return "", fmt.Errorf("[LocalFunc] invalid type, toolName=%s, expected=%T, given=%T", i.getToolName(), inst, val) + } + inst = gt + } else { + inst = generic.NewInstance[T]() + + err = sonic.UnmarshalString(arguments, &inst) + if err != nil { + return "", fmt.Errorf("[LocalFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", i.getToolName(), err) + } + } + + resp, err := i.Fn(ctx, inst, opts...) + if err != nil { + return "", fmt.Errorf("[LocalFunc] failed to invoke tool, toolName=%s, err=%w", i.getToolName(), err) + } + + if i.m != nil { + output, err = i.m(ctx, resp) + if err != nil { + return "", fmt.Errorf("[LocalFunc] failed to marshal output, toolName=%s, err=%w", i.getToolName(), err) + } + } else { + output, err = marshalString(resp) + if err != nil { + return "", fmt.Errorf("[LocalFunc] failed to marshal output in json, toolName=%s, err=%w", i.getToolName(), err) + } + } + + return output, nil +} + +func (i *invokableTool[T, D]) GetType() string { + return snakeToCamel(i.getToolName()) +} + +func (i *invokableTool[T, D]) getToolName() string { + if i.info == nil { + return "" + } + + return i.info.Name +} + +// snakeToCamel converts a snake_case string to CamelCase. +func snakeToCamel(s string) string { + if s == "" { + return "" + } + + parts := strings.Split(s, "_") + + for i := 0; i < len(parts); i++ { + if len(parts[i]) > 0 { + parts[i] = strings.ToUpper(string(parts[i][0])) + strings.ToLower(parts[i][1:]) + } + } + + return strings.Join(parts, "") +} + +// NewEnhancedTool creates an [tool.EnhancedInvokableTool] from an explicit +// [schema.ToolInfo] and a function that returns [schema.ToolResult]. +func NewEnhancedTool[T any](desc *schema.ToolInfo, i EnhancedInvokeFunc[T], opts ...Option) tool.EnhancedInvokableTool { + return newOptionableEnhancedTool(desc, func(ctx context.Context, input T, _ ...tool.Option) (*schema.ToolResult, error) { + return i(ctx, input) + }, opts...) +} + +func newOptionableEnhancedTool[T any](desc *schema.ToolInfo, i OptionableEnhancedInvokeFunc[T], opts ...Option) tool.EnhancedInvokableTool { + to := getToolOptions(opts...) + + return &enhancedInvokableTool[T]{ + info: desc, + um: to.um, + Fn: i, + } +} + +type enhancedInvokableTool[T any] struct { + info *schema.ToolInfo + + um UnmarshalArguments + + Fn OptionableEnhancedInvokeFunc[T] +} + +func (e *enhancedInvokableTool[T]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return e.info, nil +} + +func (e *enhancedInvokableTool[T]) InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + var inst T + var err error + + if e.um != nil { + var val any + val, err = e.um(ctx, toolArgument.Text) + if err != nil { + return nil, fmt.Errorf("[EnhancedLocalFunc] failed to unmarshal arguments, toolName=%s, err=%w", e.getToolName(), err) + } + gt, ok := val.(T) + if !ok { + return nil, fmt.Errorf("[EnhancedLocalFunc] invalid type, toolName=%s, expected=%T, given=%T", e.getToolName(), inst, val) + } + inst = gt + } else { + inst = generic.NewInstance[T]() + + err = sonic.UnmarshalString(toolArgument.Text, &inst) + if err != nil { + return nil, fmt.Errorf("[EnhancedLocalFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", e.getToolName(), err) + } + } + + resp, err := e.Fn(ctx, inst, opts...) + if err != nil { + return nil, fmt.Errorf("[EnhancedLocalFunc] failed to invoke tool, toolName=%s, err=%w", e.getToolName(), err) + } + + return resp, nil +} + +func (e *enhancedInvokableTool[T]) GetType() string { + return snakeToCamel(e.getToolName()) +} + +func (e *enhancedInvokableTool[T]) getToolName() string { + if e.info == nil { + return "" + } + + return e.info.Name +} diff --git a/components/tool/utils/invokable_func_test.go b/components/tool/utils/invokable_func_test.go new file mode 100644 index 0000000..1330f17 --- /dev/null +++ b/components/tool/utils/invokable_func_test.go @@ -0,0 +1,405 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/eino-contrib/jsonschema" + "github.com/stretchr/testify/assert" + orderedmap "github.com/wk8/go-ordered-map/v2" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +type Job struct { + Company string `json:"company" jsonschema:"description=the company where the user works"` + Position string `json:"position,omitempty" jsonschema:"description=the position of the user's job"` + ServiceLength float32 `json:"service_length,omitempty" jsonschema:"description=the year of user's service"` // 司龄,年 +} + +type Income struct { + Source string `json:"source" jsonschema:"description=the source of income"` + Amount int `json:"amount" jsonschema:"description=the amount of income"` + HasPayTax bool `json:"has_pay_tax" jsonschema:"description=whether the user has paid tax"` + Job *Job `json:"job,omitempty" jsonschema:"description=the job of the user when earning this income"` +} + +type User struct { + Name string `json:"name" jsonschema:"required,description=the name of the user"` + Age int `json:"age" jsonschema:"required,description=the age of the user"` + + Job *Job `json:"job,omitempty" jsonschema:"description=the job of the user"` + + Incomes []*Income `json:"incomes" jsonschema:"description=the incomes of the user"` +} + +type UserResult struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +var toolInfo = &schema.ToolInfo{ + Name: "update_user_info", + Desc: "full update user info", + ParamsOneOf: schema.NewParamsOneOfByJSONSchema( + &jsonschema.Schema{ + Type: "object", + Required: []string{"name", "age", "incomes"}, + AdditionalProperties: jsonschema.FalseSchema, + Properties: orderedmap.New[string, *jsonschema.Schema]( + orderedmap.WithInitialData( + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "name", + Value: &jsonschema.Schema{ + Type: "string", + Description: "the name of the user", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "age", + Value: &jsonschema.Schema{ + Type: "integer", + Description: "the age of the user", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "job", + Value: &jsonschema.Schema{ + Type: "object", + Required: []string{"company"}, + AdditionalProperties: jsonschema.FalseSchema, + Description: "the job of the user", + Properties: orderedmap.New[string, *jsonschema.Schema]( + orderedmap.WithInitialData( + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "company", + Value: &jsonschema.Schema{ + Type: "string", + Description: "the company where the user works", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "position", + Value: &jsonschema.Schema{ + Type: "string", + Description: "the position of the user's job", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "service_length", + Value: &jsonschema.Schema{ + Type: "number", + Description: "the year of user's service", + }, + }, + ), + ), + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "incomes", + Value: &jsonschema.Schema{ + Type: "array", + Description: "the incomes of the user", + Items: &jsonschema.Schema{ + Type: "object", + AdditionalProperties: jsonschema.FalseSchema, + Required: []string{"source", "amount", "has_pay_tax"}, + Properties: orderedmap.New[string, *jsonschema.Schema]( + orderedmap.WithInitialData( + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "source", + Value: &jsonschema.Schema{ + Type: "string", + Description: "the source of income", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "amount", + Value: &jsonschema.Schema{ + Type: "integer", + Description: "the amount of income", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "has_pay_tax", + Value: &jsonschema.Schema{ + Type: "boolean", + Description: "whether the user has paid tax", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "job", + Value: &jsonschema.Schema{ + Type: "object", + AdditionalProperties: jsonschema.FalseSchema, + Required: []string{"company"}, + Description: "the job of the user when earning this income", + Properties: orderedmap.New[string, *jsonschema.Schema]( + orderedmap.WithInitialData( + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "company", + Value: &jsonschema.Schema{ + Type: "string", + Description: "the company where the user works", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "position", + Value: &jsonschema.Schema{ + Type: "string", + Description: "the position of the user's job", + }, + }, + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "service_length", + Value: &jsonschema.Schema{ + Type: "number", + Description: "the year of user's service", + }, + }, + ), + ), + }, + }, + ), + ), + }, + }, + }, + ), + ), + }), +} + +func updateUserInfo(ctx context.Context, input *User) (output *UserResult, err error) { + return &UserResult{ + Code: 200, + Msg: fmt.Sprintf("update %v success", input.Name), + }, nil +} + +type UserInfoOption struct { + Field1 string +} + +func WithUserInfoOption(s string) tool.Option { + return tool.WrapImplSpecificOptFn(func(t *UserInfoOption) { + t.Field1 = s + }) +} + +func updateUserInfoWithOption(_ context.Context, input *User, opts ...tool.Option) (output *UserResult, err error) { + baseOption := &UserInfoOption{ + Field1: "test_origin", + } + + option := tool.GetImplSpecificOptions(baseOption, opts...) + return &UserResult{ + Code: 200, + Msg: option.Field1, + }, nil +} + +func TestInferTool(t *testing.T) { + t.Run("invoke_infer_tool", func(t *testing.T) { + ctx := context.Background() + + tl, err := InferTool("update_user_info", "full update user info", updateUserInfo) + assert.NoError(t, err) + + info, err := tl.Info(context.Background()) + assert.NoError(t, err) + + actual, err := info.ToJSONSchema() + assert.NoError(t, err) + actualStr, err := json.Marshal(actual) + assert.NoError(t, err) + + expect, err := toolInfo.ToJSONSchema() + assert.NoError(t, err) + expectStr, err := json.Marshal(expect) + assert.NoError(t, err) + + assert.Equal(t, string(expectStr), string(actualStr)) + + content, err := tl.InvokableRun(ctx, `{"name": "bruce lee"}`) + assert.NoError(t, err) + assert.JSONEq(t, `{"code":200,"msg":"update bruce lee success"}`, content) + }) +} + +func TestInferOptionableTool(t *testing.T) { + ctx := context.Background() + + t.Run("invoke_infer_optionable_tool", func(t *testing.T) { + + tl, err := InferOptionableTool("invoke_infer_optionable_tool", "full update user info", updateUserInfoWithOption) + assert.NoError(t, err) + + content, err := tl.InvokableRun(ctx, `{"name": "bruce lee"}`, WithUserInfoOption("hello world")) + assert.NoError(t, err) + assert.JSONEq(t, `{"code":200,"msg":"hello world"}`, content) + }) +} + +func TestNewTool(t *testing.T) { + ctx := context.Background() + type Input struct { + Name string `json:"name"` + } + type Output struct { + Name string `json:"name"` + } + + t.Run("struct_input_struct_output", func(t *testing.T) { + + tl := NewTool[Input, Output](nil, func(ctx context.Context, input Input) (output Output, err error) { + return Output{ + Name: input.Name, + }, nil + }) + + _, err := tl.InvokableRun(ctx, `{"name":"test"}`) + assert.Nil(t, err) + }) + + t.Run("pointer_input_pointer_output", func(t *testing.T) { + tl := NewTool[*Input, *Output](nil, func(ctx context.Context, input *Input) (output *Output, err error) { + return &Output{ + Name: input.Name, + }, nil + }) + + content, err := tl.InvokableRun(ctx, `{"name":"test"}`) + assert.NoError(t, err) + assert.Equal(t, `{"name":"test"}`, content) + }) + + t.Run("string_input_int64_output", func(t *testing.T) { + tl := NewTool(nil, func(ctx context.Context, input string) (output int64, err error) { + return 10, nil + }) + + content, err := tl.InvokableRun(ctx, `100`) // json unmarshal must contains double quote if is not json string. + assert.Error(t, err) + assert.Equal(t, "", content) + }) + + t.Run("string_pointer_input_int64_pointer_output", func(t *testing.T) { + tl := NewTool[*string, *int64](nil, func(ctx context.Context, input *string) (output *int64, err error) { + n := int64(10) + return &n, nil + }) + + content, err := tl.InvokableRun(ctx, `"100"`) + assert.NoError(t, err) + assert.Equal(t, `10`, content) + }) +} + +func TestSnakeToCamel(t *testing.T) { + t.Run("normal_case", func(t *testing.T) { + assert.Equal(t, "GoogleSearch3", snakeToCamel("google_search_3")) + }) + + t.Run("empty_case", func(t *testing.T) { + assert.Equal(t, "", snakeToCamel("")) + }) + + t.Run("single_word_case", func(t *testing.T) { + assert.Equal(t, "Google", snakeToCamel("google")) + }) + + t.Run("upper_case", func(t *testing.T) { + assert.Equal(t, "HttpHost", snakeToCamel("_HTTP_HOST_")) + }) + + t.Run("underscore_case", func(t *testing.T) { + assert.Equal(t, "", snakeToCamel("_")) + }) +} + +type stringAlias string +type integerAlias uint32 +type floatAlias float64 +type boolAlias bool + +type testEnumStruct struct { + Field1 string `json:"field1" jsonschema:"enum=a,enum=b"` + Field2 int `json:"field2" jsonschema:"enum=1,enum=2"` + Field3 float32 `json:"field3" jsonschema:"enum=1.1,enum=2.2"` + Field4 bool `json:"field4" jsonschema:"default=true"` + Field5 stringAlias `json:"field5" jsonschema:"enum=a,enum=c"` + Field6 integerAlias `json:"field6" jsonschema:"enum=3,enum=4"` + Field7 floatAlias `json:"field7" jsonschema:"enum=3.3,enum=4.4"` + Field8 boolAlias `json:"field8" jsonschema:"enum=false"` +} + +type testEnumStruct2 struct { + Field1 int8 `json:"field1" jsonschema:"enum=1.1"` +} + +type testEnumStruct3 struct { + Field1 float64 `json:"field1" jsonschema:"enum=a"` +} + +func TestEnumTag(t *testing.T) { + info, err := goStruct2ParamsOneOf[testEnumStruct]() + assert.NoError(t, err) + s, err := info.ToJSONSchema() + assert.NoError(t, err) + + enum, ok := s.Properties.Get("field1") + assert.True(t, ok) + assert.Equal(t, []any{"a", "b"}, enum.Enum) + + enum, ok = s.Properties.Get("field2") + assert.True(t, ok) + assert.Equal(t, []any{json.Number("1"), json.Number("2")}, enum.Enum) + + enum, ok = s.Properties.Get("field3") + assert.True(t, ok) + assert.Equal(t, []any{json.Number("1.1"), json.Number("2.2")}, enum.Enum) + + enum, ok = s.Properties.Get("field4") + assert.True(t, ok) + assert.Equal(t, true, enum.Default) + + enum, ok = s.Properties.Get("field5") + assert.True(t, ok) + assert.Equal(t, []any{"a", "c"}, enum.Enum) + + enum, ok = s.Properties.Get("field6") + assert.True(t, ok) + assert.Equal(t, []any{json.Number("3"), json.Number("4")}, enum.Enum) + + enum, ok = s.Properties.Get("field7") + assert.True(t, ok) + assert.Equal(t, []any{json.Number("3.3"), json.Number("4.4")}, enum.Enum) + + _, err = goStruct2ParamsOneOf[testEnumStruct2]() + assert.NoError(t, err) + + _, err = goStruct2ParamsOneOf[testEnumStruct3]() + assert.NoError(t, err) +} diff --git a/components/tool/utils/streamable_func.go b/components/tool/utils/streamable_func.go new file mode 100644 index 0000000..c6601e8 --- /dev/null +++ b/components/tool/utils/streamable_func.go @@ -0,0 +1,259 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "fmt" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +// StreamFunc is the function type for the streamable tool. +type StreamFunc[T, D any] func(ctx context.Context, input T) (output *schema.StreamReader[D], err error) + +// OptionableStreamFunc is the function type for the streamable tool with tool option. +type OptionableStreamFunc[T, D any] func(ctx context.Context, input T, opts ...tool.Option) (output *schema.StreamReader[D], err error) + +// InferStreamTool creates a [tool.StreamableTool] by inferring the parameter +// JSON schema from type T. The function returns a [schema.StreamReader] of D +// values which the framework serialises to a string stream. +func InferStreamTool[T, D any](toolName, toolDesc string, s StreamFunc[T, D], opts ...Option) (tool.StreamableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return NewStreamTool(ti, s, opts...), nil +} + +// InferOptionableStreamTool is like [InferStreamTool] but the function also +// receives [tool.Option] values passed by ToolsNode at call time. +func InferOptionableStreamTool[T, D any](toolName, toolDesc string, s OptionableStreamFunc[T, D], opts ...Option) (tool.StreamableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return newOptionableStreamTool(ti, s, opts...), nil +} + +// NewStreamTool creates a [tool.StreamableTool] from an explicit [schema.ToolInfo] +// and a typed streaming function. +func NewStreamTool[T, D any](desc *schema.ToolInfo, s StreamFunc[T, D], opts ...Option) tool.StreamableTool { + return newOptionableStreamTool(desc, + func(ctx context.Context, input T, _ ...tool.Option) (output *schema.StreamReader[D], err error) { + return s(ctx, input) + }, + opts...) +} + +func newOptionableStreamTool[T, D any](desc *schema.ToolInfo, s OptionableStreamFunc[T, D], opts ...Option) tool.StreamableTool { + + to := getToolOptions(opts...) + + return &streamableTool[T, D]{ + info: desc, + + um: to.um, + m: to.m, + Fn: s, + } +} + +type streamableTool[T, D any] struct { + info *schema.ToolInfo + + um UnmarshalArguments + m MarshalOutput + + Fn OptionableStreamFunc[T, D] +} + +// Info returns the tool info, implement the BaseTool interface. +func (s *streamableTool[T, D]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return s.info, nil +} + +// StreamableRun invokes the tool with the given arguments, implement the StreamableTool interface. +func (s *streamableTool[T, D]) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) ( + outStream *schema.StreamReader[string], err error) { + + var inst T + if s.um != nil { + var val any + val, err = s.um(ctx, argumentsInJSON) + if err != nil { + return nil, fmt.Errorf("[LocalStreamFunc] failed to unmarshal arguments, toolName=%s, err=%w", s.getToolName(), err) + } + + gt, ok := val.(T) + if !ok { + return nil, fmt.Errorf("[LocalStreamFunc] type err, toolName=%s, expected=%T, given=%T", s.getToolName(), inst, val) + } + inst = gt + } else { + + inst = generic.NewInstance[T]() + + err = sonic.UnmarshalString(argumentsInJSON, &inst) + if err != nil { + return nil, fmt.Errorf("[LocalStreamFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", s.getToolName(), err) + } + } + + streamD, err := s.Fn(ctx, inst, opts...) + if err != nil { + return nil, err + } + + outStream = schema.StreamReaderWithConvert(streamD, func(d D) (string, error) { + var out string + var e error + if s.m != nil { + out, e = s.m(ctx, d) + if e != nil { + return "", fmt.Errorf("[LocalStreamFunc] failed to marshal output, toolName=%s, err=%w", s.getToolName(), e) + } + } else { + out, e = marshalString(d) + if e != nil { + return "", fmt.Errorf("[LocalStreamFunc] failed to marshal output in json, toolName=%s, err=%w", s.getToolName(), e) + } + } + + return out, nil + }) + + return outStream, nil +} + +func (s *streamableTool[T, D]) GetType() string { + return snakeToCamel(s.getToolName()) +} + +func (s *streamableTool[T, D]) getToolName() string { + if s.info == nil { + return "" + } + + return s.info.Name +} + +// EnhancedStreamFunc is the function type for the enhanced streamable tool. +type EnhancedStreamFunc[T any] func(ctx context.Context, input T) (output *schema.StreamReader[*schema.ToolResult], err error) + +// OptionableEnhancedStreamFunc is the function type for the enhanced streamable tool with tool option. +type OptionableEnhancedStreamFunc[T any] func(ctx context.Context, input T, opts ...tool.Option) (output *schema.StreamReader[*schema.ToolResult], err error) + +// InferEnhancedStreamTool creates an [tool.EnhancedStreamableTool] by inferring +// the parameter JSON schema from type T. The function streams [schema.ToolResult] +// values for multimodal output. +func InferEnhancedStreamTool[T any](toolName, toolDesc string, s EnhancedStreamFunc[T], opts ...Option) (tool.EnhancedStreamableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return NewEnhancedStreamTool(ti, s, opts...), nil +} + +// InferOptionableEnhancedStreamTool creates an EnhancedStreamableTool from a given function by inferring the ToolInfo from the function's request parameters, with tool option. +func InferOptionableEnhancedStreamTool[T any](toolName, toolDesc string, s OptionableEnhancedStreamFunc[T], opts ...Option) (tool.EnhancedStreamableTool, error) { + ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...) + if err != nil { + return nil, err + } + + return newOptionableEnhancedStreamTool(ti, s, opts...), nil +} + +// NewEnhancedStreamTool creates an [tool.EnhancedStreamableTool] from an +// explicit [schema.ToolInfo] and a typed streaming function. +func NewEnhancedStreamTool[T any](desc *schema.ToolInfo, s EnhancedStreamFunc[T], opts ...Option) tool.EnhancedStreamableTool { + return newOptionableEnhancedStreamTool(desc, + func(ctx context.Context, input T, _ ...tool.Option) (output *schema.StreamReader[*schema.ToolResult], err error) { + return s(ctx, input) + }, + opts...) +} + +func newOptionableEnhancedStreamTool[T any](desc *schema.ToolInfo, s OptionableEnhancedStreamFunc[T], opts ...Option) tool.EnhancedStreamableTool { + to := getToolOptions(opts...) + + return &enhancedStreamableTool[T]{ + info: desc, + um: to.um, + Fn: s, + } +} + +type enhancedStreamableTool[T any] struct { + info *schema.ToolInfo + + um UnmarshalArguments + + Fn OptionableEnhancedStreamFunc[T] +} + +func (s *enhancedStreamableTool[T]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return s.info, nil +} + +func (s *enhancedStreamableTool[T]) StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) ( + outStream *schema.StreamReader[*schema.ToolResult], err error) { + + var inst T + if s.um != nil { + var val any + val, err = s.um(ctx, toolArgument.Text) + if err != nil { + return nil, fmt.Errorf("[EnhancedLocalStreamFunc] failed to unmarshal arguments, toolName=%s, err=%w", s.getToolName(), err) + } + + gt, ok := val.(T) + if !ok { + return nil, fmt.Errorf("[EnhancedLocalStreamFunc] type err, toolName=%s, expected=%T, given=%T", s.getToolName(), inst, val) + } + inst = gt + } else { + inst = generic.NewInstance[T]() + + err = sonic.UnmarshalString(toolArgument.Text, &inst) + if err != nil { + return nil, fmt.Errorf("[EnhancedLocalStreamFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", s.getToolName(), err) + } + } + + return s.Fn(ctx, inst, opts...) +} + +func (s *enhancedStreamableTool[T]) GetType() string { + return snakeToCamel(s.getToolName()) +} + +func (s *enhancedStreamableTool[T]) getToolName() string { + if s.info == nil { + return "" + } + + return s.info.Name +} diff --git a/components/tool/utils/streamable_func_test.go b/components/tool/utils/streamable_func_test.go new file mode 100644 index 0000000..56632a0 --- /dev/null +++ b/components/tool/utils/streamable_func_test.go @@ -0,0 +1,329 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "errors" + "io" + "testing" + + "github.com/eino-contrib/jsonschema" + "github.com/stretchr/testify/assert" + orderedmap "github.com/wk8/go-ordered-map/v2" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +func TestNewStreamableTool(t *testing.T) { + ctx := context.Background() + type Input struct { + Name string `json:"name"` + } + type Output struct { + Name string `json:"name"` + } + + t.Run("simple_case", func(t *testing.T) { + tl := NewStreamTool[*Input, *Output]( + &schema.ToolInfo{ + Name: "search_user", + Desc: "search user info", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Type: "string", + Desc: "user name", + }, + }), + }, + func(ctx context.Context, input *Input) (output *schema.StreamReader[*Output], err error) { + sr, sw := schema.Pipe[*Output](2) + sw.Send(&Output{ + Name: input.Name, + }, nil) + sw.Send(&Output{ + Name: "lee", + }, nil) + sw.Close() + + return sr, nil + }, + ) + + info, err := tl.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "search_user", info.Name) + + js, err := info.ToJSONSchema() + assert.NoError(t, err) + + assert.Equal(t, &jsonschema.Schema{ + Type: "object", + Properties: orderedmap.New[string, *jsonschema.Schema]( + orderedmap.WithInitialData[string, *jsonschema.Schema]( + orderedmap.Pair[string, *jsonschema.Schema]{ + Key: "name", + Value: &jsonschema.Schema{ + Type: "string", + Description: "user name", + }, + }, + ), + ), + Required: make([]string, 0), + }, js) + + sr, err := tl.StreamableRun(ctx, `{"name":"xxx"}`) + assert.NoError(t, err) + + defer sr.Close() + + idx := 0 + for { + m, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + assert.NoError(t, err) + + if idx == 0 { + assert.Equal(t, `{"name":"xxx"}`, m) + } else { + assert.Equal(t, `{"name":"lee"}`, m) + } + idx++ + } + + assert.Equal(t, 2, idx) + }) +} + +type FakeStreamOption struct { + Field string +} + +type FakeStreamInferToolInput struct { + Field string `json:"field"` +} + +type FakeStreamInferToolOutput struct { + Field string `json:"field"` +} + +func FakeWithToolOption(s string) tool.Option { + return tool.WrapImplSpecificOptFn(func(t *FakeStreamOption) { + t.Field = s + }) +} + +func fakeStreamFunc(ctx context.Context, input FakeStreamInferToolInput, opts ...tool.Option) (output *schema.StreamReader[*FakeStreamInferToolOutput], err error) { + baseOpt := &FakeStreamOption{ + Field: "default_field_value", + } + option := tool.GetImplSpecificOptions(baseOpt, opts...) + + return schema.StreamReaderFromArray([]*FakeStreamInferToolOutput{ + { + Field: option.Field, + }, + }), nil +} + +func TestInferStreamTool(t *testing.T) { + st, err := InferOptionableStreamTool("infer_optionable_stream_tool", "test infer stream tool with option", fakeStreamFunc) + assert.Nil(t, err) + + sr, err := st.StreamableRun(context.Background(), `{"field": "value"}`, FakeWithToolOption("hello world")) + assert.Nil(t, err) + + defer sr.Close() + + idx := 0 + for { + m, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + assert.NoError(t, err) + + if idx == 0 { + assert.JSONEq(t, `{"field":"hello world"}`, m) + } + } +} + +type EnhancedStreamInput struct { + Query string `json:"query" jsonschema:"description=the search query"` +} + +func TestNewEnhancedStreamTool(t *testing.T) { + ctx := context.Background() + + t.Run("simple_case", func(t *testing.T) { + tl := NewEnhancedStreamTool[*EnhancedStreamInput]( + &schema.ToolInfo{ + Name: "enhanced_stream_search", + Desc: "search with enhanced stream output", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "query": { + Type: "string", + Desc: "the search query", + }, + }), + }, + func(ctx context.Context, input *EnhancedStreamInput) (*schema.StreamReader[*schema.ToolResult], error) { + sr, sw := schema.Pipe[*schema.ToolResult](2) + sw.Send(&schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "result for: " + input.Query}, + }, + }, nil) + sw.Send(&schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "more results"}, + }, + }, nil) + sw.Close() + return sr, nil + }, + ) + + info, err := tl.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "enhanced_stream_search", info.Name) + + sr, err := tl.StreamableRun(ctx, &schema.ToolArgument{Text: `{"query":"test"}`}) + assert.NoError(t, err) + defer sr.Close() + + idx := 0 + for { + m, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + assert.NoError(t, err) + + if idx == 0 { + assert.Len(t, m.Parts, 1) + assert.Equal(t, schema.ToolPartTypeText, m.Parts[0].Type) + assert.Equal(t, "result for: test", m.Parts[0].Text) + } else { + assert.Len(t, m.Parts, 1) + assert.Equal(t, "more results", m.Parts[0].Text) + } + idx++ + } + assert.Equal(t, 2, idx) + }) +} + +type FakeEnhancedStreamOption struct { + Prefix string +} + +func FakeWithEnhancedStreamOption(prefix string) tool.Option { + return tool.WrapImplSpecificOptFn(func(t *FakeEnhancedStreamOption) { + t.Prefix = prefix + }) +} + +func fakeEnhancedStreamFunc(ctx context.Context, input EnhancedStreamInput) (*schema.StreamReader[*schema.ToolResult], error) { + return schema.StreamReaderFromArray([]*schema.ToolResult{ + { + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "result: " + input.Query}, + }, + }, + }), nil +} + +func fakeOptionableEnhancedStreamFunc(ctx context.Context, input EnhancedStreamInput, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + baseOpt := &FakeEnhancedStreamOption{ + Prefix: "default", + } + option := tool.GetImplSpecificOptions(baseOpt, opts...) + + return schema.StreamReaderFromArray([]*schema.ToolResult{ + { + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: option.Prefix + ": " + input.Query}, + }, + }, + }), nil +} + +func TestInferEnhancedStreamTool(t *testing.T) { + ctx := context.Background() + + t.Run("infer_enhanced_stream_tool", func(t *testing.T) { + tl, err := InferEnhancedStreamTool("infer_enhanced_stream", "test infer enhanced stream tool", fakeEnhancedStreamFunc) + assert.NoError(t, err) + + info, err := tl.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "infer_enhanced_stream", info.Name) + + sr, err := tl.StreamableRun(ctx, &schema.ToolArgument{Text: `{"query":"hello"}`}) + assert.NoError(t, err) + defer sr.Close() + + m, err := sr.Recv() + assert.NoError(t, err) + assert.Len(t, m.Parts, 1) + assert.Equal(t, "result: hello", m.Parts[0].Text) + }) +} + +func TestInferOptionableEnhancedStreamTool(t *testing.T) { + ctx := context.Background() + + t.Run("infer_optionable_enhanced_stream_tool", func(t *testing.T) { + tl, err := InferOptionableEnhancedStreamTool("infer_optionable_enhanced_stream", "test infer optionable enhanced stream tool", fakeOptionableEnhancedStreamFunc) + assert.NoError(t, err) + + info, err := tl.Info(ctx) + assert.NoError(t, err) + assert.Equal(t, "infer_optionable_enhanced_stream", info.Name) + + sr, err := tl.StreamableRun(ctx, &schema.ToolArgument{Text: `{"query":"world"}`}, FakeWithEnhancedStreamOption("custom")) + assert.NoError(t, err) + defer sr.Close() + + m, err := sr.Recv() + assert.NoError(t, err) + assert.Len(t, m.Parts, 1) + assert.Equal(t, "custom: world", m.Parts[0].Text) + }) + + t.Run("infer_optionable_enhanced_stream_tool_default_option", func(t *testing.T) { + tl, err := InferOptionableEnhancedStreamTool("infer_optionable_enhanced_stream", "test infer optionable enhanced stream tool", fakeOptionableEnhancedStreamFunc) + assert.NoError(t, err) + + sr, err := tl.StreamableRun(ctx, &schema.ToolArgument{Text: `{"query":"test"}`}) + assert.NoError(t, err) + defer sr.Close() + + m, err := sr.Recv() + assert.NoError(t, err) + assert.Len(t, m.Parts, 1) + assert.Equal(t, "default: test", m.Parts[0].Text) + }) +} diff --git a/components/types.go b/components/types.go new file mode 100644 index 0000000..2b0ad8f --- /dev/null +++ b/components/types.go @@ -0,0 +1,87 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package components defines common interfaces that describe component +// types and callback capabilities used across Eino. +package components + +// Typer provides a human-readable type name for a component implementation. +// +// When implemented, the component's full display name in DevOps tooling +// (visual debugger, IDE plugin, dashboards) becomes "{GetType()}{ComponentKind}" +// — e.g. "OpenAIChatModel". Use CamelCase naming. +// +// Also used by [utils.InferTool] and similar constructors to set the display +// name of tool instances. +type Typer interface { + GetType() string +} + +// GetType returns the type name for a component that implements Typer. +func GetType(component any) (string, bool) { + if typer, ok := component.(Typer); ok { + return typer.GetType(), true + } + + return "", false +} + +// Checker controls whether the framework's automatic callback instrumentation +// is active for a component. +// +// When IsCallbacksEnabled returns true, the framework skips its default +// OnStart/OnEnd wrapping and trusts the component to invoke callbacks itself +// at the correct points. Implement this when your component needs precise +// control over callback timing or content — for example, when streaming +// requires callbacks to fire mid-stream rather than only at completion. +type Checker interface { + IsCallbacksEnabled() bool +} + +// IsCallbacksEnabled reports whether a component implements Checker and enables callbacks. +func IsCallbacksEnabled(i any) bool { + if checker, ok := i.(Checker); ok { + return checker.IsCallbacksEnabled() + } + + return false +} + +// Component names representing the different categories of components. +type Component string + +const ( + // ComponentOfPrompt identifies chat template components. + ComponentOfPrompt Component = "ChatTemplate" + // ComponentOfAgenticPrompt identifies agentic template components. + ComponentOfAgenticPrompt Component = "AgenticChatTemplate" + // ComponentOfChatModel identifies chat model components. + ComponentOfChatModel Component = "ChatModel" + // ComponentOfAgenticModel identifies agentic model components. + ComponentOfAgenticModel Component = "AgenticModel" + // ComponentOfEmbedding identifies embedding components. + ComponentOfEmbedding Component = "Embedding" + // ComponentOfIndexer identifies indexer components. + ComponentOfIndexer Component = "Indexer" + // ComponentOfRetriever identifies retriever components. + ComponentOfRetriever Component = "Retriever" + // ComponentOfLoader identifies loader components. + ComponentOfLoader Component = "Loader" + // ComponentOfTransformer identifies document transformer components. + ComponentOfTransformer Component = "DocumentTransformer" + // ComponentOfTool identifies tool components. + ComponentOfTool Component = "Tool" +) diff --git a/compose/agentic_tools_node.go b/compose/agentic_tools_node.go new file mode 100644 index 0000000..10e430a --- /dev/null +++ b/compose/agentic_tools_node.go @@ -0,0 +1,260 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + + "github.com/cloudwego/eino/schema" +) + +// NewAgenticToolsNode creates a new AgenticToolsNode. +// e.g. +// +// conf := &ToolsNodeConfig{ +// Tools: []tool.BaseTool{invokableTool1, streamableTool2}, +// } +// toolsNode, err := NewAgenticToolsNode(ctx, conf) +func NewAgenticToolsNode(ctx context.Context, conf *ToolsNodeConfig) (*AgenticToolsNode, error) { + tn, err := NewToolNode(ctx, conf) + if err != nil { + return nil, err + } + return &AgenticToolsNode{inner: tn}, nil +} + +type AgenticToolsNode struct { + inner *ToolsNode +} + +func (a *AgenticToolsNode) Invoke(ctx context.Context, input *schema.AgenticMessage, opts ...ToolsNodeOption) ([]*schema.AgenticMessage, error) { + result, err := a.inner.Invoke(ctx, agenticMessageToToolCallMessage(input), opts...) + if err != nil { + return nil, err + } + return toolMessageToAgenticMessage(result), nil +} + +func (a *AgenticToolsNode) Stream(ctx context.Context, input *schema.AgenticMessage, + opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.AgenticMessage], error) { + result, err := a.inner.Stream(ctx, agenticMessageToToolCallMessage(input), opts...) + if err != nil { + return nil, err + } + return streamToolMessageToAgenticMessage(result), nil +} + +func agenticMessageToToolCallMessage(input *schema.AgenticMessage) *schema.Message { + var tc []schema.ToolCall + for _, block := range input.ContentBlocks { + if block.Type != schema.ContentBlockTypeFunctionToolCall || block.FunctionToolCall == nil { + continue + } + tc = append(tc, schema.ToolCall{ + ID: block.FunctionToolCall.CallID, + Function: schema.FunctionCall{ + Name: block.FunctionToolCall.Name, + Arguments: block.FunctionToolCall.Arguments, + }, + Extra: block.Extra, + }) + } + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: tc, + } +} + +func toolMessageToAgenticMessage(input []*schema.Message) []*schema.AgenticMessage { + results := make([]*schema.AgenticMessage, len(input)) + for i, m := range input { + if msg, ok := toolSearchResultMessageToAgenticMessage(m, nil); ok { + results[i] = msg + continue + } + + ftr := &schema.FunctionToolResult{ + CallID: m.ToolCallID, + Name: m.ToolName, + } + if len(m.UserInputMultiContent) > 0 { + ftr.Content = messageInputPartsToFunctionToolBlocks(m.UserInputMultiContent) + } else if m.Content != "" { + ftr.Content = []*schema.FunctionToolResultContentBlock{ + newFuncToolResultContentBlock(&schema.UserInputText{Text: m.Content}), + } + } + results[i] = &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{{ + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: ftr, + Extra: m.Extra, + }}, + Extra: m.Extra, + } + } + return results +} + +func streamToolMessageToAgenticMessage(input *schema.StreamReader[[]*schema.Message]) *schema.StreamReader[[]*schema.AgenticMessage] { + return schema.StreamReaderWithConvert(input, func(t []*schema.Message) ([]*schema.AgenticMessage, error) { + results := make([]*schema.AgenticMessage, len(t)) + for i, m := range t { + if m == nil { + continue + } + if msg, ok := toolSearchResultMessageToAgenticMessage(m, &schema.StreamingMeta{Index: i}); ok { + results[i] = msg + continue + } + + ftr := &schema.FunctionToolResult{ + CallID: m.ToolCallID, + Name: m.ToolName, + } + if len(m.UserInputMultiContent) > 0 { + ftr.Content = messageInputPartsToFunctionToolBlocks(m.UserInputMultiContent) + } else if m.Content != "" { + ftr.Content = []*schema.FunctionToolResultContentBlock{ + newFuncToolResultContentBlock(&schema.UserInputText{Text: m.Content}), + } + } + results[i] = &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{{ + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: ftr, + StreamingMeta: &schema.StreamingMeta{Index: i}, + Extra: m.Extra, + }}, + Extra: m.Extra, + } + } + return results, nil + }) +} + +func toolSearchResultMessageToAgenticMessage(m *schema.Message, meta *schema.StreamingMeta) (*schema.AgenticMessage, bool) { + if m == nil || len(m.UserInputMultiContent) != 1 { + return nil, false + } + + part := m.UserInputMultiContent[0] + if part.Type != schema.ChatMessagePartTypeToolSearchResult || part.ToolSearchResult == nil { + return nil, false + } + + block := schema.NewContentBlock(&schema.ToolSearchFunctionToolResult{ + CallID: m.ToolCallID, + Name: m.ToolName, + Result: part.ToolSearchResult, + }) + block.StreamingMeta = meta + block.Extra = m.Extra + + return &schema.AgenticMessage{ + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{block}, + Extra: m.Extra, + }, true +} + +func messageInputPartsToFunctionToolBlocks(parts []schema.MessageInputPart) []*schema.FunctionToolResultContentBlock { + blocks := make([]*schema.FunctionToolResultContentBlock, 0, len(parts)) + for _, p := range parts { + var block *schema.FunctionToolResultContentBlock + switch p.Type { + case schema.ChatMessagePartTypeText: + block = newFuncToolResultContentBlock(&schema.UserInputText{Text: p.Text}) + block.Extra = p.Extra + case schema.ChatMessagePartTypeImageURL: + if p.Image != nil { + block = newFuncToolResultContentBlock(&schema.UserInputImage{ + URL: derefString(p.Image.URL), + Base64Data: derefString(p.Image.Base64Data), + MIMEType: p.Image.MIMEType, + Detail: p.Image.Detail, + }) + block.Extra = p.Extra + } + case schema.ChatMessagePartTypeAudioURL: + if p.Audio != nil { + block = newFuncToolResultContentBlock(&schema.UserInputAudio{ + URL: derefString(p.Audio.URL), + Base64Data: derefString(p.Audio.Base64Data), + MIMEType: p.Audio.MIMEType, + }) + block.Extra = p.Extra + } + case schema.ChatMessagePartTypeVideoURL: + if p.Video != nil { + block = newFuncToolResultContentBlock(&schema.UserInputVideo{ + URL: derefString(p.Video.URL), + Base64Data: derefString(p.Video.Base64Data), + MIMEType: p.Video.MIMEType, + }) + block.Extra = p.Extra + } + case schema.ChatMessagePartTypeFileURL: + if p.File != nil { + block = newFuncToolResultContentBlock(&schema.UserInputFile{ + URL: derefString(p.File.URL), + Base64Data: derefString(p.File.Base64Data), + Name: p.File.Name, + MIMEType: p.File.MIMEType, + }) + block.Extra = p.Extra + } + } + if block != nil { + blocks = append(blocks, block) + } + } + return blocks +} + +type userInputVariant interface { + schema.UserInputText | schema.UserInputImage | schema.UserInputAudio | schema.UserInputVideo | schema.UserInputFile +} + +// newFuncToolResultContentBlock creates a FunctionToolResultContentBlock from a typed content pointer. +func newFuncToolResultContentBlock[T userInputVariant](content *T) *schema.FunctionToolResultContentBlock { + switch c := any(content).(type) { + case *schema.UserInputText: + return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeText, Text: c} + case *schema.UserInputImage: + return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeImage, Image: c} + case *schema.UserInputAudio: + return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeAudio, Audio: c} + case *schema.UserInputVideo: + return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeVideo, Video: c} + case *schema.UserInputFile: + return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeFile, File: c} + default: + return nil + } +} + +func derefString(s *string) string { + if s == nil { + return "" + } + return *s +} + +func (a *AgenticToolsNode) GetType() string { return "" } diff --git a/compose/agentic_tools_node_test.go b/compose/agentic_tools_node_test.go new file mode 100644 index 0000000..1f57963 --- /dev/null +++ b/compose/agentic_tools_node_test.go @@ -0,0 +1,467 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "io" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/schema" +) + +func TestAgenticMessageToToolCallMessage(t *testing.T) { + input := &schema.AgenticMessage{ + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolCall, + FunctionToolCall: &schema.FunctionToolCall{ + CallID: "1", + Name: "name1", + Arguments: "arg1", + }, + }, + { + Type: schema.ContentBlockTypeFunctionToolCall, + FunctionToolCall: &schema.FunctionToolCall{ + CallID: "2", + Name: "name2", + Arguments: "arg2", + }, + }, + { + Type: schema.ContentBlockTypeFunctionToolCall, + FunctionToolCall: &schema.FunctionToolCall{ + CallID: "3", + Name: "name3", + Arguments: "arg3", + }, + }, + }, + } + ret := agenticMessageToToolCallMessage(input) + assert.Equal(t, schema.Assistant, ret.Role) + assert.Equal(t, []schema.ToolCall{ + { + ID: "1", + Function: schema.FunctionCall{ + Name: "name1", + Arguments: "arg1", + }, + }, + { + ID: "2", + Function: schema.FunctionCall{ + Name: "name2", + Arguments: "arg2", + }, + }, + { + ID: "3", + Function: schema.FunctionCall{ + Name: "name3", + Arguments: "arg3", + }, + }, + }, ret.ToolCalls) +} + +func TestToolMessageToAgenticMessage(t *testing.T) { + t.Run("text only", func(t *testing.T) { + input := []*schema.Message{ + { + Role: schema.Tool, + Content: "content1", + ToolCallID: "1", + ToolName: "name1", + }, + { + Role: schema.Tool, + Content: "content2", + ToolCallID: "2", + ToolName: "name2", + }, + { + Role: schema.Tool, + Content: "content3", + ToolCallID: "3", + ToolName: "name3", + }, + } + ret := toolMessageToAgenticMessage(input) + assert.Equal(t, 3, len(ret)) + for i, msg := range ret { + assert.Equal(t, schema.AgenticRoleTypeUser, msg.Role) + assert.Equal(t, 1, len(msg.ContentBlocks)) + assert.Equal(t, schema.ContentBlockTypeFunctionToolResult, msg.ContentBlocks[0].Type) + ftr := msg.ContentBlocks[0].FunctionToolResult + assert.Equal(t, input[i].ToolCallID, ftr.CallID) + assert.Equal(t, input[i].ToolName, ftr.Name) + assert.Equal(t, 1, len(ftr.Content)) + assert.Equal(t, input[i].Content, ftr.Content[0].Text.Text) + } + }) + + t.Run("with multimodal content", func(t *testing.T) { + imageURL := "https://example.com/image.png" + audioBase64 := "YXVkaW9kYXRh" + videoURL := "https://example.com/video.mp4" + fileURL := "https://example.com/file.pdf" + + input := []*schema.Message{ + { + Role: schema.Tool, + Content: "text result", + ToolCallID: "1", + ToolName: "tool1", + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "hello"}, + {Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageInputImage{ + MessagePartCommon: schema.MessagePartCommon{URL: &imageURL, MIMEType: "image/png"}, + Detail: schema.ImageURLDetailHigh, + }}, + {Type: schema.ChatMessagePartTypeAudioURL, Audio: &schema.MessageInputAudio{ + MessagePartCommon: schema.MessagePartCommon{Base64Data: &audioBase64, MIMEType: "audio/wav"}, + }}, + {Type: schema.ChatMessagePartTypeVideoURL, Video: &schema.MessageInputVideo{ + MessagePartCommon: schema.MessagePartCommon{URL: &videoURL, MIMEType: "video/mp4"}, + }}, + {Type: schema.ChatMessagePartTypeFileURL, File: &schema.MessageInputFile{ + MessagePartCommon: schema.MessagePartCommon{URL: &fileURL, MIMEType: "application/pdf"}, + }}, + }, + }, + { + Role: schema.Tool, + Content: "plain result", + ToolCallID: "2", + ToolName: "tool2", + }, + } + + ret := toolMessageToAgenticMessage(input) + assert.Equal(t, 2, len(ret)) + + // first message: multimodal tool result + assert.Equal(t, schema.AgenticRoleTypeUser, ret[0].Role) + assert.Equal(t, 1, len(ret[0].ContentBlocks)) + ftr1 := ret[0].ContentBlocks[0].FunctionToolResult + assert.Equal(t, "1", ftr1.CallID) + assert.Equal(t, 5, len(ftr1.Content)) + + assert.Equal(t, "hello", ftr1.Content[0].Text.Text) + + assert.Equal(t, imageURL, ftr1.Content[1].Image.URL) + assert.Equal(t, schema.ImageURLDetailHigh, ftr1.Content[1].Image.Detail) + + assert.Equal(t, audioBase64, ftr1.Content[2].Audio.Base64Data) + + assert.Equal(t, videoURL, ftr1.Content[3].Video.URL) + + assert.Equal(t, fileURL, ftr1.Content[4].File.URL) + + // second message: text-only tool result + assert.Equal(t, schema.AgenticRoleTypeUser, ret[1].Role) + assert.Equal(t, 1, len(ret[1].ContentBlocks)) + ftr2 := ret[1].ContentBlocks[0].FunctionToolResult + assert.Equal(t, "2", ftr2.CallID) + assert.Equal(t, 1, len(ftr2.Content)) + assert.Equal(t, "plain result", ftr2.Content[0].Text.Text) + }) + + t.Run("nil media fields are skipped", func(t *testing.T) { + input := []*schema.Message{ + { + Role: schema.Tool, + Content: "result", + ToolCallID: "1", + ToolName: "tool1", + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeImageURL, Image: nil}, + {Type: schema.ChatMessagePartTypeAudioURL, Audio: nil}, + {Type: schema.ChatMessagePartTypeVideoURL, Video: nil}, + {Type: schema.ChatMessagePartTypeFileURL, File: nil}, + {Type: schema.ChatMessagePartTypeText, Text: "only text"}, + }, + }, + } + ret := toolMessageToAgenticMessage(input) + assert.Equal(t, 1, len(ret)) + ftr := ret[0].ContentBlocks[0].FunctionToolResult + assert.Equal(t, 1, len(ftr.Content)) + assert.Equal(t, "only text", ftr.Content[0].Text.Text) + }) + + t.Run("tool search result", func(t *testing.T) { + input := []*schema.Message{ + { + Role: schema.Tool, + ToolCallID: "call_1", + ToolName: "tool_search", + UserInputMultiContent: []schema.MessageInputPart{ + { + Type: schema.ChatMessagePartTypeToolSearchResult, + ToolSearchResult: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{ + {Name: "dynamic_tool", Desc: "dynamic tool"}, + }}, + }, + }, + }, + } + + ret := toolMessageToAgenticMessage(input) + require.Len(t, ret, 1) + require.Len(t, ret[0].ContentBlocks, 1) + block := ret[0].ContentBlocks[0] + assert.Equal(t, schema.ContentBlockTypeToolSearchResult, block.Type) + require.NotNil(t, block.ToolSearchFunctionToolResult) + assert.Equal(t, "call_1", block.ToolSearchFunctionToolResult.CallID) + assert.Equal(t, "tool_search", block.ToolSearchFunctionToolResult.Name) + require.NotNil(t, block.ToolSearchFunctionToolResult.Result) + require.Len(t, block.ToolSearchFunctionToolResult.Result.Tools, 1) + assert.Equal(t, "dynamic_tool", block.ToolSearchFunctionToolResult.Result.Tools[0].Name) + }) +} + +func TestStreamToolMessageToAgenticMessage(t *testing.T) { + t.Run("text only", func(t *testing.T) { + testStreamToolMessageTextOnly(t) + }) + + t.Run("with multimodal content", func(t *testing.T) { + imageURL := "https://example.com/image.png" + input := schema.StreamReaderFromArray([][]*schema.Message{ + { + { + Role: schema.Tool, + Content: "result1", + ToolName: "tool1", + ToolCallID: "1", + UserInputMultiContent: []schema.MessageInputPart{ + {Type: schema.ChatMessagePartTypeText, Text: "text part"}, + {Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageInputImage{ + MessagePartCommon: schema.MessagePartCommon{URL: &imageURL}, + }}, + }, + }, + nil, + }, + { + nil, + { + Role: schema.Tool, + Content: "result2", + ToolName: "tool2", + ToolCallID: "2", + }, + }, + }) + ret := streamToolMessageToAgenticMessage(input) + var chunks [][]*schema.AgenticMessage + for { + chunk, err := ret.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + chunks = append(chunks, chunk) + } + result, err := schema.ConcatAgenticMessagesArray(chunks) + assert.NoError(t, err) + + assert.Equal(t, 2, len(result)) + + // first message: multimodal tool result (single chunk → StreamingMeta preserved) + assert.Equal(t, schema.AgenticRoleTypeUser, result[0].Role) + assert.Equal(t, 1, len(result[0].ContentBlocks)) + ftr1 := result[0].ContentBlocks[0].FunctionToolResult + assert.Equal(t, "1", ftr1.CallID) + assert.Equal(t, 2, len(ftr1.Content)) + assert.NotNil(t, ftr1.Content[0].Text) + assert.NotNil(t, ftr1.Content[1].Image) + assert.Equal(t, imageURL, ftr1.Content[1].Image.URL) + + // second message: text-only tool result (single chunk → StreamingMeta preserved) + assert.Equal(t, schema.AgenticRoleTypeUser, result[1].Role) + assert.Equal(t, 1, len(result[1].ContentBlocks)) + ftr2 := result[1].ContentBlocks[0].FunctionToolResult + assert.Equal(t, "2", ftr2.CallID) + assert.Equal(t, 1, len(ftr2.Content)) + assert.Equal(t, "result2", ftr2.Content[0].Text.Text) + }) + + t.Run("tool search result", func(t *testing.T) { + input := schema.StreamReaderFromArray([][]*schema.Message{ + { + { + Role: schema.Tool, + ToolName: "tool_search", + ToolCallID: "call_1", + UserInputMultiContent: []schema.MessageInputPart{ + { + Type: schema.ChatMessagePartTypeToolSearchResult, + ToolSearchResult: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{ + {Name: "dynamic_tool", Desc: "dynamic tool"}, + }}, + }, + }, + }, + }, + }) + + ret := streamToolMessageToAgenticMessage(input) + chunk, err := ret.Recv() + require.NoError(t, err) + require.Len(t, chunk, 1) + require.Len(t, chunk[0].ContentBlocks, 1) + block := chunk[0].ContentBlocks[0] + assert.Equal(t, schema.ContentBlockTypeToolSearchResult, block.Type) + assert.Equal(t, &schema.StreamingMeta{Index: 0}, block.StreamingMeta) + require.NotNil(t, block.ToolSearchFunctionToolResult) + assert.Equal(t, "call_1", block.ToolSearchFunctionToolResult.CallID) + assert.Equal(t, "tool_search", block.ToolSearchFunctionToolResult.Name) + require.NotNil(t, block.ToolSearchFunctionToolResult.Result) + require.Len(t, block.ToolSearchFunctionToolResult.Result.Tools, 1) + assert.Equal(t, "dynamic_tool", block.ToolSearchFunctionToolResult.Result.Tools[0].Name) + }) +} + +func testStreamToolMessageTextOnly(t *testing.T) { + input := schema.StreamReaderFromArray([][]*schema.Message{ + { + { + Role: schema.Tool, + Content: "content1-1", + ToolName: "name1", + ToolCallID: "1", + }, + nil, nil, + }, + { + nil, + { + Role: schema.Tool, + Content: "content2-1", + ToolName: "name2", + ToolCallID: "2", + }, + nil, + }, + { + nil, + { + Role: schema.Tool, + Content: "content2-2", + ToolName: "name2", + ToolCallID: "2", + }, + nil, + }, + { + nil, nil, + { + Role: schema.Tool, + Content: "content3-1", + ToolName: "name3", + ToolCallID: "3", + }, + }, + { + nil, nil, + { + Role: schema.Tool, + Content: "content3-2", + ToolName: "name3", + ToolCallID: "3", + }, + }, + }) + ret := streamToolMessageToAgenticMessage(input) + var chunks [][]*schema.AgenticMessage + for { + chunk, err := ret.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + chunks = append(chunks, chunk) + } + result, err := schema.ConcatAgenticMessagesArray(chunks) + assert.NoError(t, err) + + actualStr, err := sonic.MarshalString(result) + assert.NoError(t, err) + + expected := []*schema.AgenticMessage{ + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "1", + Name: "name1", + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content1-1"}}, + }, + }, + StreamingMeta: &schema.StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "2", + Name: "name2", + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content2-1"}}, + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content2-2"}}, + }, + }, + }, + }, + }, + { + Role: schema.AgenticRoleTypeUser, + ContentBlocks: []*schema.ContentBlock{ + { + Type: schema.ContentBlockTypeFunctionToolResult, + FunctionToolResult: &schema.FunctionToolResult{ + CallID: "3", + Name: "name3", + Content: []*schema.FunctionToolResultContentBlock{ + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content3-1"}}, + {Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content3-2"}}, + }, + }, + }, + }, + }, + } + + expectedStr, err := sonic.MarshalString(expected) + assert.NoError(t, err) + + assert.Equal(t, expectedStr, actualStr) +} diff --git a/compose/branch.go b/compose/branch.go new file mode 100644 index 0000000..5507d10 --- /dev/null +++ b/compose/branch.go @@ -0,0 +1,176 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +// GraphBranchCondition is the condition type for the branch. +type GraphBranchCondition[T any] func(ctx context.Context, in T) (endNode string, err error) + +// StreamGraphBranchCondition is the condition type for the stream branch. +type StreamGraphBranchCondition[T any] func(ctx context.Context, in *schema.StreamReader[T]) (endNode string, err error) + +// GraphMultiBranchCondition is the condition type for the multi choice branch. +type GraphMultiBranchCondition[T any] func(ctx context.Context, in T) (endNode map[string]bool, err error) + +// StreamGraphMultiBranchCondition is the condition type for the stream multi choice branch. +type StreamGraphMultiBranchCondition[T any] func(ctx context.Context, in *schema.StreamReader[T]) (endNodes map[string]bool, err error) + +// GraphBranch is the branch type for the graph. +// It is used to determine the next node based on the condition. +type GraphBranch struct { + invoke func(ctx context.Context, input any) (output []string, err error) + collect func(ctx context.Context, input streamReader) (output []string, err error) + inputType reflect.Type + *genericHelper + endNodes map[string]bool + idx int // used to distinguish branches in parallel + noDataFlow bool +} + +// GetEndNode returns the all end nodes of the branch. +func (gb *GraphBranch) GetEndNode() map[string]bool { + return gb.endNodes +} + +func newGraphBranch[T any](r *runnablePacker[T, []string, any], endNodes map[string]bool) *GraphBranch { + return &GraphBranch{ + invoke: func(ctx context.Context, input any) (output []string, err error) { + in, ok := input.(T) + if !ok { + // When a nil is passed as an 'any' type, its original type information is lost, + // becoming an untyped nil. This would cause type assertions to fail. + // So if the input is nil and the target type T is an interface, we need to explicitly create a nil of type T. + if input == nil && generic.TypeOf[T]().Kind() == reflect.Interface { + var i T + in = i + } else { + panic(newUnexpectedInputTypeErr(generic.TypeOf[T](), reflect.TypeOf(input))) + } + } + return r.Invoke(ctx, in) + }, + collect: func(ctx context.Context, input streamReader) (output []string, err error) { + in, ok := unpackStreamReader[T](input) + if !ok { + panic(newUnexpectedInputTypeErr(generic.TypeOf[T](), input.getType())) + } + return r.Collect(ctx, in) + }, + inputType: generic.TypeOf[T](), + genericHelper: newGenericHelper[T, T](), + endNodes: endNodes, + } +} + +// NewGraphMultiBranch creates a branch for graphs where a condition selects +// multiple end nodes; only keys present in endNodes are allowed. +func NewGraphMultiBranch[T any](condition GraphMultiBranchCondition[T], endNodes map[string]bool) *GraphBranch { + condRun := func(ctx context.Context, in T, opts ...any) ([]string, error) { + ends, err := condition(ctx, in) + if err != nil { + return nil, err + } + ret := make([]string, 0, len(ends)) + for end := range ends { + if !endNodes[end] { + return nil, fmt.Errorf("branch invocation returns unintended end node: %s", end) + } + ret = append(ret, end) + } + + return ret, nil + } + + return newGraphBranch(newRunnablePacker(condRun, nil, nil, nil, false), endNodes) +} + +// NewStreamGraphMultiBranch creates a streaming branch where a condition on +// the input stream selects multiple end nodes. +func NewStreamGraphMultiBranch[T any](condition StreamGraphMultiBranchCondition[T], + endNodes map[string]bool) *GraphBranch { + + condRun := func(ctx context.Context, in *schema.StreamReader[T], opts ...any) ([]string, error) { + ends, err := condition(ctx, in) + if err != nil { + return nil, err + } + + ret := make([]string, 0, len(ends)) + for end := range ends { + if !endNodes[end] { + return nil, fmt.Errorf("branch invocation returns unintended end node: %s", end) + } + ret = append(ret, end) + } + return ret, nil + } + + return newGraphBranch(newRunnablePacker(nil, nil, condRun, nil, false), endNodes) +} + +// NewGraphBranch creates a new graph branch. +// It is used to determine the next node based on the condition. +// e.g. +// +// condition := func(ctx context.Context, in string) (string, error) { +// // logic to determine the next node +// return "next_node_key", nil +// } +// endNodes := map[string]bool{"path01": true, "path02": true} +// branch := compose.NewGraphBranch(condition, endNodes) +// +// graph.AddBranch("key_of_node_before_branch", branch) +func NewGraphBranch[T any](condition GraphBranchCondition[T], endNodes map[string]bool) *GraphBranch { + return NewGraphMultiBranch(func(ctx context.Context, in T) (endNode map[string]bool, err error) { + ret, err := condition(ctx, in) + if err != nil { + return nil, err + } + return map[string]bool{ret: true}, nil + }, endNodes) +} + +// NewStreamGraphBranch creates a new stream graph branch. +// It is used to determine the next node based on the condition of stream input. +// e.g. +// +// condition := func(ctx context.Context, in *schema.StreamReader[T]) (string, error) { +// // logic to determine the next node. +// // to use the feature of stream, you can use the first chunk to determine the next node. +// return "next_node_key", nil +// } +// endNodes := map[string]bool{"path01": true, "path02": true} +// branch := compose.NewStreamGraphBranch(condition, endNodes) +// +// graph.AddBranch("key_of_node_before_branch", branch) +func NewStreamGraphBranch[T any](condition StreamGraphBranchCondition[T], endNodes map[string]bool) *GraphBranch { + return NewStreamGraphMultiBranch(func(ctx context.Context, in *schema.StreamReader[T]) (endNode map[string]bool, err error) { + ret, err := condition(ctx, in) + if err != nil { + return nil, err + } + return map[string]bool{ret: true}, nil + }, endNodes) +} diff --git a/compose/branch_test.go b/compose/branch_test.go new file mode 100644 index 0000000..0c13059 --- /dev/null +++ b/compose/branch_test.go @@ -0,0 +1,132 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestMultiBranch(t *testing.T) { + g := NewGraph[string, map[string]any]() + emptyLambda := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil }) + err := g.AddLambdaNode("1", emptyLambda, WithOutputKey("1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", emptyLambda, WithOutputKey("2")) + assert.NoError(t, err) + err = g.AddLambdaNode("3", emptyLambda, WithOutputKey("3")) + assert.NoError(t, err) + + err = g.AddBranch(START, NewGraphMultiBranch(func(ctx context.Context, in string) (endNode map[string]bool, err error) { + return map[string]bool{"1": true, "2": true}, nil + }, map[string]bool{"1": true, "2": true, "3": true})) + assert.NoError(t, err) + + err = g.AddEdge("1", END) + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx) + assert.NoError(t, err) + + result, err := r.Invoke(ctx, "start") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) + + streamResult, err := r.Stream(ctx, "start") + assert.NoError(t, err) + result = map[string]any{} + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + for k, v := range chunk { + result[k] = v + } + } + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) +} + +func TestStreamMultiBranch(t *testing.T) { + g := NewGraph[string, map[string]any]() + emptyLambda := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil }) + err := g.AddLambdaNode("1", emptyLambda, WithOutputKey("1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", emptyLambda, WithOutputKey("2")) + assert.NoError(t, err) + err = g.AddLambdaNode("3", emptyLambda, WithOutputKey("3")) + assert.NoError(t, err) + + err = g.AddBranch(START, NewStreamGraphMultiBranch(func(ctx context.Context, in *schema.StreamReader[string]) (endNode map[string]bool, err error) { + in.Close() + return map[string]bool{"1": true, "2": true}, nil + }, map[string]bool{"1": true, "2": true, "3": true})) + assert.NoError(t, err) + + err = g.AddEdge("1", END) + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx) + assert.NoError(t, err) + + result, err := r.Invoke(ctx, "start") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) + + streamResult, err := r.Stream(ctx, "start") + assert.NoError(t, err) + result = map[string]any{} + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + for k, v := range chunk { + result[k] = v + } + } + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) +} diff --git a/compose/chain.go b/compose/chain.go new file mode 100644 index 0000000..abfa6bf --- /dev/null +++ b/compose/chain.go @@ -0,0 +1,600 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/internal/gmap" + "github.com/cloudwego/eino/internal/gslice" +) + +// NewChain create a chain with input/output type. +func NewChain[I, O any](opts ...NewGraphOption) *Chain[I, O] { + ch := &Chain[I, O]{ + gg: NewGraph[I, O](opts...), + } + + ch.gg.cmp = ComponentOfChain + + return ch +} + +// Chain is a chain of components. +// Chain nodes can be parallel / branch / sequence components. +// Chain is designed to be used in a builder pattern (should Compile() before use). +// And the interface is `Chain style`, you can use it like: `chain.AppendXX(...).AppendXX(...)` +// +// Normal usage: +// 1. create a chain with input/output type: `chain := NewChain[inputType, outputType]()` +// 2. add components to chainable list: +// 2.1 add components: `chain.AppendChatTemplate(...).AppendChatModel(...).AppendToolsNode(...)` +// 2.2 add parallel or branch node if needed: `chain.AppendParallel()`, `chain.AppendBranch()` +// 3. compile: `r, err := c.Compile()` +// 4. run: +// 4.1 `one input & one output` use `r.Invoke(ctx, input)` +// 4.2 `one input & multi output chunk` use `r.Stream(ctx, input)` +// 4.3 `multi input chunk & one output` use `r.Collect(ctx, inputReader)` +// 4.4 `multi input chunk & multi output chunk` use `r.Transform(ctx, inputReader)` +// +// Using in graph or other chain: +// chain1 := NewChain[inputType, outputType]() +// graph := NewGraph[](runTypePregel) +// graph.AddGraph("key", chain1) // chain is an AnyGraph implementation +// +// // or in another chain: +// chain2 := NewChain[inputType, outputType]() +// chain2.AppendGraph(chain1) +type Chain[I, O any] struct { + err error + + gg *Graph[I, O] + + nodeIdx int + + preNodeKeys []string + + hasEnd bool +} + +// ErrChainCompiled is returned when attempting to modify a chain after it has been compiled +var ErrChainCompiled = errors.New("chain has been compiled, cannot be modified") + +// implements AnyGraph. +func (c *Chain[I, O]) compile(ctx context.Context, option *graphCompileOptions) (*composableRunnable, error) { + if err := c.addEndIfNeeded(); err != nil { + return nil, err + } + + return c.gg.compile(ctx, option) +} + +// addEndIfNeeded add END edge of the chain/graph. +// only run once when compiling. +func (c *Chain[I, O]) addEndIfNeeded() error { + if c.hasEnd { + return nil + } + + if c.err != nil { + return c.err + } + + if len(c.preNodeKeys) == 0 { + return fmt.Errorf("pre node keys not set, number of nodes in chain= %d", len(c.gg.nodes)) + } + + for _, nodeKey := range c.preNodeKeys { + err := c.gg.AddEdge(nodeKey, END) + if err != nil { + return err + } + } + + c.hasEnd = true + + return nil +} + +func (c *Chain[I, O]) getGenericHelper() *genericHelper { + return newGenericHelper[I, O]() +} + +// inputType returns the input type of the chain. +// implements AnyGraph. +func (c *Chain[I, O]) inputType() reflect.Type { + return generic.TypeOf[I]() +} + +// outputType returns the output type of the chain. +// implements AnyGraph. +func (c *Chain[I, O]) outputType() reflect.Type { + return generic.TypeOf[O]() +} + +// compositeType returns the composite type of the chain. +// implements AnyGraph. +func (c *Chain[I, O]) component() component { + return c.gg.component() +} + +// Compile to a Runnable. +// Runnable can be used directly. +// e.g. +// +// chain := NewChain[string, string]() +// r, err := chain.Compile() +// if err != nil {} +// +// r.Invoke(ctx, input) // ping => pong +// r.Stream(ctx, input) // ping => stream out +// r.Collect(ctx, inputReader) // stream in => pong +// r.Transform(ctx, inputReader) // stream in => stream out +func (c *Chain[I, O]) Compile(ctx context.Context, opts ...GraphCompileOption) (Runnable[I, O], error) { + if err := c.addEndIfNeeded(); err != nil { + return nil, err + } + + return c.gg.Compile(ctx, opts...) +} + +// AppendChatModel add a ChatModel node to the chain. +// e.g. +// +// model, err := openai.NewChatModel(ctx, config) +// if err != nil {...} +// chain.AppendChatModel(model) +func (c *Chain[I, O]) AppendChatModel(node model.BaseChatModel, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toChatModelNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendAgenticModel add a agentic.Model node to the chain. +// e.g. +// +// model, err := openai.NewAgenticModel(ctx, config) +// if err != nil {...} +// chain.AppendAgenticModel(model) +func (c *Chain[I, O]) AppendAgenticModel(node model.AgenticModel, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toAgenticModelNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendChatTemplate add a ChatTemplate node to the chain. +// eg. +// +// chatTemplate, err := prompt.FromMessages(schema.FString, &schema.Message{ +// Role: schema.System, +// Content: "You are acting as a {role}.", +// }) +// +// chain.AppendChatTemplate(chatTemplate) +func (c *Chain[I, O]) AppendChatTemplate(node prompt.ChatTemplate, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toChatTemplateNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendAgenticChatTemplate add a prompt.AgenticChatTemplate node to the chain. +// eg. +// +// chatTemplate, err := prompt.FromAgenticMessages(schema.FString, &schema.AgenticMessage{}) +// +// chain.AppendAgenticChatTemplate(chatTemplate) +func (c *Chain[I, O]) AppendAgenticChatTemplate(node prompt.AgenticChatTemplate, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toAgenticChatTemplateNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendToolsNode add a ToolsNode node to the chain. +// e.g. +// +// toolsNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{ +// Tools: []tools.BaseTool{...}, +// }) +// +// chain.AppendToolsNode(toolsNode) +func (c *Chain[I, O]) AppendToolsNode(node *ToolsNode, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toToolsNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendAgenticToolsNode add a AgenticToolsNode node to the chain. +// e.g. +// +// toolsNode, err := compose.NewAgenticToolsNode(ctx, &compose.ToolsNodeConfig{ +// Tools: []tools.BaseTool{...}, +// }) +// +// chain.AppendAgenticToolsNode(toolsNode) +func (c *Chain[I, O]) AppendAgenticToolsNode(node *AgenticToolsNode, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toAgenticToolsNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendDocumentTransformer add a DocumentTransformer node to the chain. +// e.g. +// +// markdownSplitter, err := markdown.NewHeaderSplitter(ctx, &markdown.HeaderSplitterConfig{}) +// +// chain.AppendDocumentTransformer(markdownSplitter) +func (c *Chain[I, O]) AppendDocumentTransformer(node document.Transformer, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toDocumentTransformerNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendLambda add a Lambda node to the chain. +// Lambda is a node that can be used to implement custom logic. +// e.g. +// +// lambdaNode := compose.InvokableLambda(func(ctx context.Context, docs []*schema.Document) (string, error) {...}) +// chain.AppendLambda(lambdaNode) +// +// Note: +// to create a Lambda node, you need to use `compose.AnyLambda` or `compose.InvokableLambda` or `compose.StreamableLambda` or `compose.TransformableLambda`. +// if you want this node has real stream output, you need to use `compose.StreamableLambda` or `compose.TransformableLambda`, for example. +func (c *Chain[I, O]) AppendLambda(node *Lambda, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toLambdaNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendEmbedding add a Embedding node to the chain. +// e.g. +// +// embedder, err := openai.NewEmbedder(ctx, config) +// if err != nil {...} +// chain.AppendEmbedding(embedder) +func (c *Chain[I, O]) AppendEmbedding(node embedding.Embedder, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toEmbeddingNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendRetriever add a Retriever node to the chain. +// e.g. +// +// retriever, err := vectorstore.NewRetriever(ctx, config) +// if err != nil {...} +// chain.AppendRetriever(retriever) +// +// or using fornax knowledge as retriever: +// +// config := fornaxknowledge.Config{...} +// retriever, err := fornaxknowledge.NewKnowledgeRetriever(ctx, config) +// if err != nil {...} +// chain.AppendRetriever(retriever) +func (c *Chain[I, O]) AppendRetriever(node retriever.Retriever, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toRetrieverNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendLoader adds a Loader node to the chain. +// e.g. +// +// loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{}) +// if err != nil {...} +// chain.AppendLoader(loader) +func (c *Chain[I, O]) AppendLoader(node document.Loader, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toLoaderNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendIndexer add an Indexer node to the chain. +// Indexer is a node that can store documents. +// e.g. +// +// vectorStoreImpl, err := vikingdb.NewVectorStorer(ctx, vikingdbConfig) // in components/vectorstore/vikingdb/vectorstore.go +// if err != nil {...} +// +// config := vectorstore.IndexerConfig{VectorStore: vectorStoreImpl} +// indexer, err := vectorstore.NewIndexer(ctx, config) +// if err != nil {...} +// +// chain.AppendIndexer(indexer) +func (c *Chain[I, O]) AppendIndexer(node indexer.Indexer, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toIndexerNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendBranch add a conditional branch to chain. +// Each branch within the ChainBranch can be an AnyGraph. +// All branches should either lead to END, or converge to another node within the Chain. +// e.g. +// +// cb := compose.NewChainBranch(conditionFunc) +// cb.AddChatTemplate("chat_template_key_01", chatTemplate) +// cb.AddChatTemplate("chat_template_key_02", chatTemplate2) +// chain.AppendBranch(cb) +func (c *Chain[I, O]) AppendBranch(b *ChainBranch) *Chain[I, O] { + if b == nil { + c.reportError(fmt.Errorf("append branch invalid, branch is nil")) + return c + } + + if b.err != nil { + c.reportError(fmt.Errorf("append branch error: %w", b.err)) + return c + } + + if len(b.key2BranchNode) == 0 { + c.reportError(fmt.Errorf("append branch invalid, nodeList is empty")) + return c + } + + if len(b.key2BranchNode) == 1 { + c.reportError(fmt.Errorf("append branch invalid, nodeList length = 1")) + return c + } + + var startNode string + if len(c.preNodeKeys) == 0 { // branch appended directly to START + startNode = START + } else if len(c.preNodeKeys) == 1 { + startNode = c.preNodeKeys[0] + } else { + c.reportError(fmt.Errorf("append branch invalid, multiple previous nodes: %v ", c.preNodeKeys)) + return c + } + + prefix := c.nextNodeKey() + key2NodeKey := make(map[string]string, len(b.key2BranchNode)) + + for key := range b.key2BranchNode { + node := b.key2BranchNode[key] + + var nodeKey string + + if node.Second != nil && node.Second.nodeOptions != nil && node.Second.nodeOptions.nodeKey != "" { + nodeKey = node.Second.nodeOptions.nodeKey + } else { + nodeKey = fmt.Sprintf("%s_branch_%s", prefix, key) + } + + if err := c.gg.addNode(nodeKey, node.First, node.Second); err != nil { + c.reportError(fmt.Errorf("add branch node[%s] to chain failed: %w", nodeKey, err)) + return c + } + + key2NodeKey[key] = nodeKey + } + + gBranch := *b.internalBranch + + invokeCon := func(ctx context.Context, in any) (endNode []string, err error) { + ends, err := b.internalBranch.invoke(ctx, in) + if err != nil { + return nil, err + } + + nodeKeyEnds := make([]string, 0, len(ends)) + for _, end := range ends { + if nodeKey, ok := key2NodeKey[end]; !ok { + return nil, fmt.Errorf("branch invocation returns unintended end node: %s", end) + } else { + nodeKeyEnds = append(nodeKeyEnds, nodeKey) + } + } + + return nodeKeyEnds, nil + } + gBranch.invoke = invokeCon + + collectCon := func(ctx context.Context, sr streamReader) ([]string, error) { + ends, err := b.internalBranch.collect(ctx, sr) + if err != nil { + return nil, err + } + + nodeKeyEnds := make([]string, 0, len(ends)) + for _, end := range ends { + if nodeKey, ok := key2NodeKey[end]; !ok { + return nil, fmt.Errorf("branch invocation returns unintended end node: %s", end) + } else { + nodeKeyEnds = append(nodeKeyEnds, nodeKey) + } + } + + return nodeKeyEnds, nil + } + gBranch.collect = collectCon + + gBranch.endNodes = gslice.ToMap(gmap.Values(key2NodeKey), func(k string) (string, bool) { + return k, true + }) + + if err := c.gg.AddBranch(startNode, &gBranch); err != nil { + c.reportError(fmt.Errorf("chain append branch failed: %w", err)) + return c + } + + c.preNodeKeys = gmap.Values(key2NodeKey) + + return c +} + +// AppendParallel add a Parallel structure (multiple concurrent nodes) to the chain. +// e.g. +// +// parallel := compose.NewParallel() +// parallel.AddChatModel("openai", model1) // => "openai": *schema.Message{} +// parallel.AddChatModel("maas", model2) // => "maas": *schema.Message{} +// +// chain.AppendParallel(parallel) // => multiple concurrent nodes are added to the Chain +// +// The next node in the chain is either an END, or a node which accepts a map[string]any, where keys are `openai` `maas` as specified above. +func (c *Chain[I, O]) AppendParallel(p *Parallel) *Chain[I, O] { + if p == nil { + c.reportError(fmt.Errorf("append parallel invalid, parallel is nil")) + return c + } + + if p.err != nil { + c.reportError(fmt.Errorf("append parallel invalid, parallel error: %w", p.err)) + return c + } + + if len(p.nodes) <= 1 { + c.reportError(fmt.Errorf("append parallel invalid, not enough nodes, count = %d", len(p.nodes))) + return c + } + + var startNode string + if len(c.preNodeKeys) == 0 { // parallel appended directly to START + startNode = START + } else if len(c.preNodeKeys) == 1 { + startNode = c.preNodeKeys[0] + } else { + c.reportError(fmt.Errorf("append parallel invalid, multiple previous nodes: %v ", c.preNodeKeys)) + return c + } + + prefix := c.nextNodeKey() + var nodeKeys []string + + for i := range p.nodes { + node := p.nodes[i] + + var nodeKey string + if node.Second != nil && node.Second.nodeOptions != nil && node.Second.nodeOptions.nodeKey != "" { + nodeKey = node.Second.nodeOptions.nodeKey + } else { + nodeKey = fmt.Sprintf("%s_parallel_%d", prefix, i) + } + + if err := c.gg.addNode(nodeKey, node.First, node.Second); err != nil { + c.reportError(fmt.Errorf("add parallel node to chain failed, key=%s, err: %w", nodeKey, err)) + return c + } + + if err := c.gg.AddEdge(startNode, nodeKey); err != nil { + c.reportError(fmt.Errorf("add parallel edge failed, from=%s, to=%s, err: %w", startNode, nodeKey, err)) + return c + } + + nodeKeys = append(nodeKeys, nodeKey) + } + + c.preNodeKeys = nodeKeys + + return c +} + +// AppendGraph add a AnyGraph node to the chain. +// AnyGraph can be a chain or a graph. +// e.g. +// +// graph := compose.NewGraph[string, string]() +// chain.AppendGraph(graph) +func (c *Chain[I, O]) AppendGraph(node AnyGraph, opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toAnyGraphNode(node, opts...) + c.addNode(gNode, options) + return c +} + +// AppendPassthrough add a Passthrough node to the chain. +// Could be used to connect multiple ChainBranch or Parallel. +// e.g. +// +// chain.AppendPassthrough() +func (c *Chain[I, O]) AppendPassthrough(opts ...GraphAddNodeOpt) *Chain[I, O] { + gNode, options := toPassthroughNode(opts...) + c.addNode(gNode, options) + return c +} + +// nextIdx. +// get the next idx for the chain. +// chain key is: node_idx => eg: node_0 => represent the first node of the chain (idx start from 0) +// if has parallel: node_idx_parallel_idx => eg: node_0_parallel_1 => represent the first node of the chain, and is a parallel node, and the second node of the parallel +// if has branch: node_idx_branch_key => eg: node_1_branch_customkey => represent the second node of the chain, and is a branch node, and the 'customkey' is the key of the branch +func (c *Chain[I, O]) nextNodeKey() string { + idx := c.nodeIdx + c.nodeIdx++ + return fmt.Sprintf("node_%d", idx) +} + +// reportError. +// save the first error in the chain. +func (c *Chain[I, O]) reportError(err error) { + if c.err == nil { + c.err = err + } +} + +// addNode. +// add a node to the chain. +func (c *Chain[I, O]) addNode(node *graphNode, options *graphAddNodeOpts) { + if c.err != nil { + return + } + + if c.gg.compiled { + c.reportError(ErrChainCompiled) + return + } + + if node == nil { + c.reportError(fmt.Errorf("chain add node invalid, node is nil")) + return + } + + nodeKey := options.nodeOptions.nodeKey + defaultNodeKey := c.nextNodeKey() + if nodeKey == "" { + nodeKey = defaultNodeKey + } + + err := c.gg.addNode(nodeKey, node, options) + if err != nil { + c.reportError(err) + return + } + + if len(c.preNodeKeys) == 0 { + c.preNodeKeys = append(c.preNodeKeys, START) + } + + for _, preNodeKey := range c.preNodeKeys { + e := c.gg.AddEdge(preNodeKey, nodeKey) + if e != nil { + c.reportError(e) + return + } + } + + c.preNodeKeys = []string{nodeKey} +} diff --git a/compose/chain_branch.go b/compose/chain_branch.go new file mode 100644 index 0000000..84fb110 --- /dev/null +++ b/compose/chain_branch.go @@ -0,0 +1,343 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +type nodeOptionsPair generic.Pair[*graphNode, *graphAddNodeOpts] + +// ChainBranch represents a conditional branch in a chain of operations. +// It allows for dynamic routing of execution based on a condition. +// All branches within ChainBranch are expected to either end the Chain, or converge to another node in the Chain. +type ChainBranch struct { + internalBranch *GraphBranch + key2BranchNode map[string]nodeOptionsPair + err error +} + +// NewChainMultiBranch creates a chain branch where a condition selects +// multiple end nodes to route execution. +func NewChainMultiBranch[T any](cond GraphMultiBranchCondition[T]) *ChainBranch { + invokeCond := func(ctx context.Context, in T, opts ...any) (endNodes []string, err error) { + ends, err := cond(ctx, in) + if err != nil { + return nil, err + } + endNodes = make([]string, 0, len(ends)) + for end := range ends { + endNodes = append(endNodes, end) + } + return endNodes, nil + } + + return &ChainBranch{ + key2BranchNode: make(map[string]nodeOptionsPair), + internalBranch: newGraphBranch(newRunnablePacker(invokeCond, nil, nil, nil, false), nil), + } +} + +// NewStreamChainMultiBranch creates a chain branch that selects multiple end +// nodes based on a condition evaluated on the input stream. +func NewStreamChainMultiBranch[T any](cond StreamGraphMultiBranchCondition[T]) *ChainBranch { + collectCon := func(ctx context.Context, in *schema.StreamReader[T], opts ...any) (endNodes []string, err error) { + ends, err := cond(ctx, in) + if err != nil { + return nil, err + } + endNodes = make([]string, 0, len(ends)) + for end := range ends { + endNodes = append(endNodes, end) + } + return endNodes, nil + } + + return &ChainBranch{ + key2BranchNode: make(map[string]nodeOptionsPair), + internalBranch: newGraphBranch(newRunnablePacker(nil, nil, collectCon, nil, false), nil), + } +} + +// NewChainBranch creates a new ChainBranch instance based on a given condition. +// It takes a generic type T and a GraphBranchCondition function for that type. +// The returned ChainBranch will have an empty key2BranchNode map and a condition function +// that wraps the provided cond to handle type assertions and error checking. +// eg. +// +// condition := func(ctx context.Context, in string, opts ...any) (endNode string, err error) { +// // logic to determine the next node +// return "some_next_node_key", nil +// } +// +// cb := NewChainBranch[string](condition) +// cb.AddPassthrough("next_node_key_01", xxx) // node in branch, represent one path of branch +// cb.AddPassthrough("next_node_key_02", xxx) // node in branch +func NewChainBranch[T any](cond GraphBranchCondition[T]) *ChainBranch { + return NewChainMultiBranch(func(ctx context.Context, in T) (endNode map[string]bool, err error) { + ret, err := cond(ctx, in) + if err != nil { + return nil, err + } + return map[string]bool{ret: true}, nil + }) +} + +// NewStreamChainBranch creates a new ChainBranch instance based on a given stream condition. +// It takes a generic type T and a StreamGraphBranchCondition function for that type. +// The returned ChainBranch will have an empty key2BranchNode map and a condition function +// that wraps the provided cond to handle type assertions and error checking. +// eg. +// +// condition := func(ctx context.Context, in *schema.StreamReader[string], opts ...any) (endNode string, err error) { +// // logic to determine the next node, you can read the stream and make a decision. +// // to save time, usually read the first chunk of stream, then make a decision which path to go. +// return "some_next_node_key", nil +// } +// +// cb := NewStreamChainBranch[string](condition) +func NewStreamChainBranch[T any](cond StreamGraphBranchCondition[T]) *ChainBranch { + return NewStreamChainMultiBranch(func(ctx context.Context, in *schema.StreamReader[T]) (endNodes map[string]bool, err error) { + ret, err := cond(ctx, in) + if err != nil { + return nil, err + } + return map[string]bool{ret: true}, nil + }) +} + +// AddChatModel adds a ChatModel node to the branch. +// eg. +// +// chatModel01, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ +// Model: "gpt-4o", +// }) +// chatModel02, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ +// Model: "gpt-4o-mini", +// }) +// cb.AddChatModel("chat_model_key_01", chatModel01) +// cb.AddChatModel("chat_model_key_02", chatModel02) +func (cb *ChainBranch) AddChatModel(key string, node model.BaseChatModel, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toChatModelNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddAgenticModel adds a agentic.Model node to the branch. +// eg. +// +// model1, err := openai.NewAgenticModel(ctx, &openai.AgenticModelConfig{ +// Model: "gpt-4o", +// }) +// model2, err := openai.NewAgenticModel(ctx, &openai.AgenticModelConfig{ +// Model: "gpt-4o-mini", +// }) +// cb.AddAgenticModel("agentic_model_key_1", model1) +// cb.AddAgenticModel("agentic_model_key_2", model2) +func (cb *ChainBranch) AddAgenticModel(key string, node model.AgenticModel, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toAgenticModelNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddChatTemplate adds a ChatTemplate node to the branch. +// eg. +// +// chatTemplate, err := prompt.FromMessages(schema.FString, &schema.Message{ +// Role: schema.System, +// Content: "You are acting as a {role}.", +// }) +// +// cb.AddChatTemplate("chat_template_key_01", chatTemplate) +// +// chatTemplate2, err := prompt.FromMessages(schema.FString, &schema.Message{ +// Role: schema.System, +// Content: "You are acting as a {role}, you are not allowed to chat in other topics.", +// }) +// +// cb.AddChatTemplate("chat_template_key_02", chatTemplate2) +func (cb *ChainBranch) AddChatTemplate(key string, node prompt.ChatTemplate, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toChatTemplateNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddAgenticChatTemplate adds a prompt.AgenticChatTemplate node to the branch. +// eg. +// +// chatTemplate, err := prompt.FromAgenticMessages(schema.FString, &schema.AgenticMessage{}) +// +// cb.AddAgenticChatTemplate("chat_template_key_01", chatTemplate) +// +// chatTemplate2, err := prompt.FromAgenticMessages(schema.FString, &schema.AgenticMessage{}) +// +// cb.AddAgenticChatTemplate("chat_template_key_02", chatTemplate2) +func (cb *ChainBranch) AddAgenticChatTemplate(key string, node prompt.AgenticChatTemplate, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toAgenticChatTemplateNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddToolsNode adds a ToolsNode to the branch. +// eg. +// +// toolsNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{ +// Tools: []tools.BaseTool{...}, +// }) +// +// cb.AddToolsNode("tools_node_key", toolsNode) +func (cb *ChainBranch) AddToolsNode(key string, node *ToolsNode, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toToolsNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddAgenticToolsNode adds a AgenticToolsNode to the branch. +// eg. +// +// toolsNode, err := compose.NewAgenticToolsNode(ctx, &compose.ToolsNodeConfig{ +// Tools: []tools.BaseTool{...}, +// }) +// +// cb.AddAgenticToolsNode("tools_node_key", toolsNode) +func (cb *ChainBranch) AddAgenticToolsNode(key string, node *AgenticToolsNode, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toAgenticToolsNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddLambda adds a Lambda node to the branch. +// eg. +// +// lambdaFunc := func(ctx context.Context, in string, opts ...any) (out string, err error) { +// // logic to process the input +// return "processed_output", nil +// } +// +// cb.AddLambda("lambda_node_key", compose.InvokeLambda(lambdaFunc)) +func (cb *ChainBranch) AddLambda(key string, node *Lambda, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toLambdaNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddEmbedding adds an Embedding node to the branch. +// eg. +// +// embeddingNode, err := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{ +// Model: "text-embedding-3-small", +// }) +// +// cb.AddEmbedding("embedding_node_key", embeddingNode) +func (cb *ChainBranch) AddEmbedding(key string, node embedding.Embedder, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toEmbeddingNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddRetriever adds a Retriever node to the branch. +// eg. +// +// retriever, err := volc_vikingdb.NewRetriever(ctx, &volc_vikingdb.RetrieverConfig{ +// Collection: "my_collection", +// }) +// +// cb.AddRetriever("retriever_node_key", retriever) +func (cb *ChainBranch) AddRetriever(key string, node retriever.Retriever, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toRetrieverNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddLoader adds a Loader node to the branch. +// eg. +// +// pdfParser, err := pdf.NewPDFParser() +// loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{ +// Parser: pdfParser, +// }) +// +// cb.AddLoader("loader_node_key", loader) +func (cb *ChainBranch) AddLoader(key string, node document.Loader, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toLoaderNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddIndexer adds an Indexer node to the branch. +// eg. +// +// indexer, err := volc_vikingdb.NewIndexer(ctx, &volc_vikingdb.IndexerConfig{ +// Collection: "my_collection", +// }) +// +// cb.AddIndexer("indexer_node_key", indexer) +func (cb *ChainBranch) AddIndexer(key string, node indexer.Indexer, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toIndexerNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddDocumentTransformer adds an Document Transformer node to the branch. +// eg. +// +// markdownSplitter, err := markdown.NewHeaderSplitter(ctx, &markdown.HeaderSplitterConfig{}) +// +// cb.AddDocumentTransformer("document_transformer_node_key", markdownSplitter) +func (cb *ChainBranch) AddDocumentTransformer(key string, node document.Transformer, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toDocumentTransformerNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddGraph adds a generic Graph node to the branch. +// eg. +// +// graph, err := compose.NewGraph[string, string]() +// +// cb.AddGraph("graph_node_key", graph) +func (cb *ChainBranch) AddGraph(key string, node AnyGraph, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toAnyGraphNode(node, opts...) + return cb.addNode(key, gNode, options) +} + +// AddPassthrough adds a Passthrough node to the branch. +// eg. +// +// cb.AddPassthrough("passthrough_node_key") +func (cb *ChainBranch) AddPassthrough(key string, opts ...GraphAddNodeOpt) *ChainBranch { + gNode, options := toPassthroughNode(opts...) + return cb.addNode(key, gNode, options) +} + +func (cb *ChainBranch) addNode(key string, node *graphNode, options *graphAddNodeOpts) *ChainBranch { + if cb.err != nil { + return cb + } + + if cb.key2BranchNode == nil { + cb.key2BranchNode = make(map[string]nodeOptionsPair) + } + + _, ok := cb.key2BranchNode[key] + if ok { + cb.err = fmt.Errorf("chain branch add node, duplicate branch node key= %s", key) + return cb + } + + cb.key2BranchNode[key] = nodeOptionsPair{node, options} + + return cb +} diff --git a/compose/chain_branch_test.go b/compose/chain_branch_test.go new file mode 100644 index 0000000..6b3b038 --- /dev/null +++ b/compose/chain_branch_test.go @@ -0,0 +1,348 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + "io" + "strconv" + "strings" + "testing" + "unicode/utf8" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/schema" +) + +func TestChainBranch(t *testing.T) { + cond := func(ctx context.Context, input string) (key string, err error) { + switch input { + case "one": + return "one_key", nil + case "two": + return "two_key", nil + case "three": + return "three_key", nil + default: + return "", fmt.Errorf("invalid input= %s", input) + } + } + + t.Run("nested chain", func(t *testing.T) { + inner := NewChain[string, string]() + inner.AppendBranch(NewChainBranch(cond). + AddLambda("one_key", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in, nil + })). + AddLambda("two_key", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in + in, nil + }))) + inner.AppendParallel(NewParallel(). + AddLambda("one_key", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in, nil + })). + AddLambda("two_key", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in + in, nil + }))) + + outer := NewChain[string, string]() + outer.AppendGraph(inner) + _, err := outer.Compile(context.Background()) + assert.Error(t, err) + }) + + t.Run("bad param", func(t *testing.T) { + c := NewChain[string, string]() + c.AppendBranch(nil) + assert.NotNil(t, c.err) + + c = NewChain[string, string]() + c.AppendBranch(NewChainBranch[string](nil)) + assert.NotNil(t, c.err) + + c = NewChain[string, string]() + c.AppendBranch(NewChainBranch(cond).AddChatTemplate("template", prompt.FromMessages(schema.FString, schema.SystemMessage("hello")))) + assert.NotNil(t, c.err) + + c = NewChain[string, string]() + c.AppendBranch(NewChainBranch(cond).AddChatTemplate("1", prompt.FromMessages(schema.FString)).AddChatTemplate("1", prompt.FromMessages(schema.FString))) + assert.NotNil(t, c.err) + }) + + t.Run("different Node types in branch", func(t *testing.T) { + c := NewChain[string, string]() + c.AppendBranch(NewChainBranch(cond). + AddChatTemplate("t", prompt.FromMessages(schema.FString)). + AddGraph("c", NewChain[string, string]())) + assert.NotNil(t, c.err) + }) + + t.Run("type mismatch", func(t *testing.T) { + c := NewChain[int, string]() + c.AppendBranch(NewChainBranch(cond). + AddLambda("one_key", InvokableLambda(func(ctx context.Context, in int) (output string, err error) { + return strconv.Itoa(in), nil + })). + AddLambda("two_key", InvokableLambda(func(ctx context.Context, in int) (output string, err error) { + return strconv.Itoa(in), nil + }))) + _, err := c.Compile(context.Background()) + assert.NotNil(t, err) + }) + + t.Run("invoke", func(t *testing.T) { + c := NewChain[string, string]() + c.AppendBranch(NewChainBranch(cond). + AddLambda("one_key", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in, nil + })). + AddLambda("two_key", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in + in, nil + }))) + c.AppendLambda(InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + in, nil + })) + assert.Nil(t, c.err) + compiledChain, err := c.Compile(context.Background()) + assert.Nil(t, err) + + out, err := compiledChain.Invoke(context.Background(), "two") + assert.Nil(t, err) + assert.Equal(t, "twotwotwotwotwotwo", out) + + _, err = compiledChain.Invoke(context.Background(), "three") + assert.NotNil(t, err) + + _, err = compiledChain.Invoke(context.Background(), "four") + assert.NotNil(t, err) + }) + + t.Run("fake stream", func(t *testing.T) { + c := NewChain[string, string]() + c.AppendLambda(StreamableLambda(func(ctx context.Context, in string) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](utf8.RuneCountInString(in)) + + go func() { + for _, field := range strings.Fields(in) { + sw.Send(field, nil) + } + sw.Close() + }() + + return sr, nil + })) + c.AppendBranch(NewChainBranch[string](cond).AddLambda("one_key", CollectableLambda(func(ctx context.Context, in *schema.StreamReader[string]) (output string, err error) { + defer in.Close() + for { + v, err := in.Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return "", err + } + + output += v + } + + return output + output, nil + })). + AddLambda("two_key", CollectableLambda(func(ctx context.Context, in *schema.StreamReader[string]) (output string, err error) { + defer in.Close() + for { + v, err := in.Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return "", err + } + + output += v + } + + return output + output + output, nil + }))) + + assert.Nil(t, c.err) + compiledChain, err := c.Compile(context.Background()) + assert.Nil(t, err) + + out, err := compiledChain.Invoke(context.Background(), "one") + assert.Nil(t, err) + assert.Equal(t, "oneone", out) + }) + + t.Run("real stream", func(t *testing.T) { + streamCon := func(ctx context.Context, sr *schema.StreamReader[string]) (key string, err error) { + msg, err := sr.Recv() + if err != nil { + return "", err + } + defer sr.Close() + + switch msg { + case "one": + return "one_key", nil + case "two": + return "two_key", nil + case "three": + return "three_key", nil + default: + return "", fmt.Errorf("invalid input= %s", msg) + } + } + + c := NewChain[string, string]() + c.AppendLambda(StreamableLambda(func(ctx context.Context, in string) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](utf8.RuneCountInString(in)) + + go func() { + for _, field := range strings.Fields(in) { + sw.Send(field, nil) + } + sw.Close() + }() + + return sr, nil + })) + c.AppendBranch(NewStreamChainBranch(streamCon).AddLambda("one_key", CollectableLambda(func(ctx context.Context, in *schema.StreamReader[string]) (output string, err error) { + defer in.Close() + for { + v, err := in.Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return "", err + } + + output += v + } + + return output + output, nil + })). + AddLambda("two_key", CollectableLambda(func(ctx context.Context, in *schema.StreamReader[string]) (output string, err error) { + defer in.Close() + for { + v, err := in.Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return "", err + } + + output += v + } + + return output + output + output, nil + }))) + + assert.Nil(t, c.err) + compiledChain, err := c.Compile(context.Background()) + assert.Nil(t, err) + + out, err := compiledChain.Stream(context.Background(), "one size fit all") + assert.Nil(t, err) + concat, err := concatStreamReader(out) + assert.Nil(t, err) + assert.Equal(t, "onesizefitallonesizefitall", concat) + }) +} + +func TestChainMultiBranch(t *testing.T) { + emptyLambda := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil }) + + ctx := context.Background() + r, err := NewChain[string, map[string]any](). + AppendBranch(NewChainMultiBranch(func(ctx context.Context, in string) (endNode map[string]bool, err error) { + return map[string]bool{"1": true, "2": true}, nil + }).AddLambda("1", emptyLambda, WithOutputKey("1")).AddLambda("2", emptyLambda, WithOutputKey("2")).AddLambda("3", emptyLambda, WithOutputKey("3"))). + Compile(ctx) + assert.Nil(t, err) + + result, err := r.Invoke(ctx, "start") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) + + streamResult, err := r.Stream(ctx, "start") + assert.NoError(t, err) + result = map[string]any{} + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + for k, v := range chunk { + result[k] = v + } + } + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) +} + +func TestStreamChainMultiBranch(t *testing.T) { + emptyLambda := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil }) + + ctx := context.Background() + r, err := NewChain[string, map[string]any](). + AppendBranch(NewStreamChainMultiBranch(func(ctx context.Context, in *schema.StreamReader[string]) (endNode map[string]bool, err error) { + return map[string]bool{"1": true, "2": true}, nil + }).AddLambda("1", emptyLambda, WithOutputKey("1")).AddLambda("2", emptyLambda, WithOutputKey("2")).AddLambda("3", emptyLambda, WithOutputKey("3"))). + Compile(ctx) + assert.Nil(t, err) + + result, err := r.Invoke(ctx, "start") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) + + streamResult, err := r.Stream(ctx, "start") + assert.NoError(t, err) + result = map[string]any{} + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + for k, v := range chunk { + result[k] = v + } + } + assert.Equal(t, map[string]any{ + "1": "start", + "2": "start", + }, result) +} diff --git a/compose/chain_parallel.go b/compose/chain_parallel.go new file mode 100644 index 0000000..463140b --- /dev/null +++ b/compose/chain_parallel.go @@ -0,0 +1,263 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "fmt" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" +) + +// NewParallel creates a new parallel type. +// it is useful when you want to run multiple nodes in parallel in a chain. +func NewParallel() *Parallel { + return &Parallel{ + outputKeys: make(map[string]bool), + } +} + +// Parallel run multiple nodes in parallel +// +// use `NewParallel()` to create a new parallel type +// Example: +// +// parallel := NewParallel() +// parallel.AddChatModel("output_key01", chat01) +// parallel.AddChatModel("output_key01", chat02) +// +// chain := NewChain[any,any]() +// chain.AppendParallel(parallel) +type Parallel struct { + nodes []nodeOptionsPair + outputKeys map[string]bool + err error +} + +// AddChatModel adds a chat model to the parallel. +// eg. +// +// chatModel01, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ +// Model: "gpt-4o", +// }) +// +// chatModel02, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ +// Model: "gpt-4o", +// }) +// +// p.AddChatModel("output_key01", chatModel01) +// p.AddChatModel("output_key02", chatModel02) +func (p *Parallel) AddChatModel(outputKey string, node model.BaseChatModel, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toChatModelNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddAgenticModel adds a agentic.Model to the parallel. +// eg. +// +// model1, err := openai.NewAgenticModel(ctx, &openai.AgenticModelConfig{ +// Model: "gpt-4o", +// }) +// +// model2, err := openai.NewAgenticModel(ctx, &openai.AgenticModelConfig{ +// Model: "gpt-4o", +// }) +// +// p.AddAgenticModel("output_key1", model1) +// p.AddAgenticModel("output_key2", model2) +func (p *Parallel) AddAgenticModel(outputKey string, node model.AgenticModel, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toAgenticModelNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddChatTemplate adds a chat template to the parallel. +// eg. +// +// chatTemplate01, err := prompt.FromMessages(schema.FString, &schema.Message{ +// Role: schema.System, +// Content: "You are acting as a {role}.", +// }) +// +// p.AddChatTemplate("output_key01", chatTemplate01) +func (p *Parallel) AddChatTemplate(outputKey string, node prompt.ChatTemplate, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toChatTemplateNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddAgenticChatTemplate adds a prompt.AgenticChatTemplate to the parallel. +// eg. +// +// chatTemplate01, err := prompt.FromAgenticMessages(schema.FString, &schema.AgenticMessage{}) +// +// p.AddAgenticChatTemplate("output_key01", chatTemplate01) +func (p *Parallel) AddAgenticChatTemplate(outputKey string, node prompt.AgenticChatTemplate, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toAgenticChatTemplateNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddToolsNode adds a tools node to the parallel. +// eg. +// +// toolsNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{ +// Tools: []tool.BaseTool{...}, +// }) +// +// p.AddToolsNode("output_key01", toolsNode) +func (p *Parallel) AddToolsNode(outputKey string, node *ToolsNode, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toToolsNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddAgenticToolsNode adds a tools node to the parallel. +// eg. +// +// toolsNode, err := compose.NewAgenticToolsNode(ctx, &compose.ToolsNodeConfig{ +// Tools: []tool.BaseTool{...}, +// }) +// +// p.AddAgenticToolsNode("output_key01", toolsNode) +func (p *Parallel) AddAgenticToolsNode(outputKey string, node *AgenticToolsNode, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toAgenticToolsNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddLambda adds a lambda node to the parallel. +// eg. +// +// lambdaFunc := func(ctx context.Context, input *schema.Message) ([]*schema.Message, error) { +// return []*schema.Message{input}, nil +// } +// +// p.AddLambda("output_key01", compose.InvokeLambda(lambdaFunc)) +func (p *Parallel) AddLambda(outputKey string, node *Lambda, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toLambdaNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddEmbedding adds an embedding node to the parallel. +// eg. +// +// embeddingNode, err := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{ +// Model: "text-embedding-3-small", +// }) +// +// p.AddEmbedding("output_key01", embeddingNode) +func (p *Parallel) AddEmbedding(outputKey string, node embedding.Embedder, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toEmbeddingNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddRetriever adds a retriever node to the parallel. +// eg. +// +// retriever, err := vikingdb.NewRetriever(ctx, &vikingdb.RetrieverConfig{}) +// +// p.AddRetriever("output_key01", retriever) +func (p *Parallel) AddRetriever(outputKey string, node retriever.Retriever, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toRetrieverNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddLoader adds a loader node to the parallel. +// eg. +// +// loader, err := file.NewLoader(ctx, &file.LoaderConfig{}) +// +// p.AddLoader("output_key01", loader) +func (p *Parallel) AddLoader(outputKey string, node document.Loader, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toLoaderNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddIndexer adds an indexer node to the parallel. +// eg. +// +// indexer, err := volc_vikingdb.NewIndexer(ctx, &volc_vikingdb.IndexerConfig{ +// Collection: "my_collection", +// }) +// +// p.AddIndexer("output_key01", indexer) +func (p *Parallel) AddIndexer(outputKey string, node indexer.Indexer, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toIndexerNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddDocumentTransformer adds an Document Transformer node to the parallel. +// eg. +// +// markdownSplitter, err := markdown.NewHeaderSplitter(ctx, &markdown.HeaderSplitterConfig{}) +// +// p.AddDocumentTransformer("output_key01", markdownSplitter) +func (p *Parallel) AddDocumentTransformer(outputKey string, node document.Transformer, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toDocumentTransformerNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddGraph adds a graph node to the parallel. +// It is useful when you want to use a graph or a chain as a node in the parallel. +// eg. +// +// graph, err := compose.NewChain[any,any]() +// +// p.AddGraph("output_key01", graph) +func (p *Parallel) AddGraph(outputKey string, node AnyGraph, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toAnyGraphNode(node, append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +// AddPassthrough adds a passthrough node to the parallel. +// eg. +// +// p.AddPassthrough("output_key01") +func (p *Parallel) AddPassthrough(outputKey string, opts ...GraphAddNodeOpt) *Parallel { + gNode, options := toPassthroughNode(append(opts, WithOutputKey(outputKey))...) + return p.addNode(outputKey, gNode, options) +} + +func (p *Parallel) addNode(outputKey string, node *graphNode, options *graphAddNodeOpts) *Parallel { + if p.err != nil { + return p + } + + if node == nil { + p.err = fmt.Errorf("chain parallel add node invalid, node is nil") + return p + } + + if p.outputKeys == nil { + p.outputKeys = make(map[string]bool) + } + + if _, ok := p.outputKeys[outputKey]; ok { + p.err = fmt.Errorf("parallel add node err, duplicate output key= %s", outputKey) + return p + } + + if node.nodeInfo == nil { + p.err = fmt.Errorf("chain parallel add node invalid, nodeInfo is nil") + return p + } + + node.nodeInfo.outputKey = outputKey + p.nodes = append(p.nodes, nodeOptionsPair{node, options}) + p.outputKeys[outputKey] = true + return p +} diff --git a/compose/chain_test.go b/compose/chain_test.go new file mode 100644 index 0000000..27f17a8 --- /dev/null +++ b/compose/chain_test.go @@ -0,0 +1,873 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/internal/mock/components/document" + "github.com/cloudwego/eino/internal/mock/components/embedding" + "github.com/cloudwego/eino/internal/mock/components/indexer" + "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/internal/mock/components/retriever" + "github.com/cloudwego/eino/schema" +) + +func TestChain(t *testing.T) { + + cm := &mockIntentChatModel{} + + // 构建 branch + branchCond := func(ctx context.Context, input map[string]any) (string, error) { + if rand.Intn(2) == 1 { + return "b1", nil + } + return "b2", nil + } + + b1 := InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("hello in branch lambda 01") + kvs["role"] = "cat" + return kvs, nil + }) + b2 := InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("hello in branch lambda 02") + kvs["role"] = "dog" + return kvs, nil + }) + + // 并发节点 + parallel := NewParallel() + parallel. + AddLambda("role", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + // may be change role to others by input kvs, for example (dentist/doctor...) + role := kvs["role"] + if role.(string) == "" { + role = "bird" + } + return role.(string), nil + })). + AddLambda("input", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + return "你的叫声是怎样的?", nil + })) + + // 顺序节点 + rolePlayChain := NewChain[map[string]any, *schema.Message]() + rolePlayChain. + AppendChatTemplate(prompt.FromMessages(schema.FString, schema.SystemMessage(`You are a {role}.`), schema.UserMessage(`{input}`))). + AppendChatModel(cm) + + // 构建 chain + + chain := NewChain[map[string]any, string]() + chain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + // do some logic to prepare kv as variables for next Node + // just pass through + t.Log("in view lambda: ", kvs) + return kvs, nil + })). + AppendBranch(NewChainBranch[map[string]any](branchCond).AddLambda("b1", b1).AddLambda("b2", b2)). + AppendPassthrough(). + AppendParallel(parallel). + AppendGraph(rolePlayChain). + AppendLambda(InvokableLambda(func(ctx context.Context, m *schema.Message) (string, error) { + // do some logic to check the output or something + t.Log("in view of messages: ", m.Content) + + return m.Content, nil + })) + + r, err := chain.Compile(context.Background()) + assert.Nil(t, err) + + out, err := r.Invoke(context.Background(), map[string]any{}) + assert.Nil(t, err) + t.Log(err) + + t.Log("out is : ", out) +} + +func TestChainWithException(t *testing.T) { + chain := NewChain[map[string]any, string]() + chain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + // do some logic to prepare kv as variables for next Node + // just pass through + t.Log("in view lambda: ", kvs) + return kvs, nil + })). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in view lambda 02: ", kvs) + return kvs, nil + }), WithNodeKey("xlam")) + + // items with parallels + parallel := NewParallel() + parallel. + AddLambda("hello", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + t.Log("in parallel item 01") + return "world", nil + })). + AddLambda("world", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + t.Log("in parallel item 02") + return "hello", nil + })) + + // sequence items + nchain := NewChain[map[string]any, map[string]any]() + nchain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence item 01") + return kvs, nil + })). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence item 02") + return kvs, nil + })) + + branchCond := func(ctx context.Context, input map[string]any) (string, error) { + if rand.Intn(2) == 1 { + return "b1", nil + } + return "b2", nil + } + + b1 := InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("hello in branch lambda 01") + kvs["role"] = "cat" + return kvs, nil + }) + b2 := InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }) + + // sequence with branch + chain.AppendBranch(NewChainBranch[map[string]any](branchCond).AddLambda("b1", b1).AddLambda("b2", b2)) + + // parallel with sequence + parallel.AddGraph("test_sequence", nchain) + + // parallel with parallel + npara := NewParallel(). + AddLambda("test_parallel1", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })). + AddLambda("test_parallel2", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + + // parallel with graph + ngraph := NewChain[map[string]any, map[string]any](). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in graph item 01") + return kvs, nil + })). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in graph item 02") + return kvs, nil + })) + nc := NewChain[map[string]any, map[string]any]() + nc.AppendGraph(ngraph) + parallel.AddGraph("test_graph", nc) + + chain.AppendPassthrough() + + // sequence with parallel + chain.AppendParallel(npara) + + // 构建 chain + chain. + AppendGraph(nchain). + AppendParallel(parallel). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + t.Log("in last view lambda: ", kvs) + return "hello last", nil + })) + + ctx := context.Background() + + r, err := chain.Compile(ctx) + assert.Nil(t, err) + + out, err := r.Invoke(ctx, map[string]any{"test": "test"}) + assert.Nil(t, err) + t.Log("out is : ", out) +} + +func TestEmptyList(t *testing.T) { + ctx := context.Background() + + // no nodes in chain + chain := NewChain[map[string]any, map[string]any]() + _, err := chain.Compile(ctx) + assert.Error(t, err) + + // no nodes in parallel + parallel := NewParallel() + chain = NewChain[map[string]any, map[string]any]() + chain.AppendParallel(parallel) + + _, err = chain.Compile(ctx) + assert.Error(t, err) + + // no nodes in sequence + emptyChain := NewChain[map[string]any, map[string]any]() + chain = NewChain[map[string]any, map[string]any]() + + chain. + AppendParallel(parallel). + AppendGraph(emptyChain). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + + _, err = chain.Compile(ctx) + assert.Error(t, err) +} + +func TestChainList(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in view lambda: ", kvs) + return kvs, nil + })) + + // parallel + parallel := NewParallel() + parallel. + AddLambda("test_parallel1", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in parallel item 01") + return kvs, nil + })) + + // seq in parallel + nchain := NewChain[map[string]any, map[string]any]() + nchain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence in parallel item 01") + return kvs, nil + })). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence in parallel item 02") + return kvs, nil + })) + + // seq in seq + nchainInChain := NewChain[map[string]any, map[string]any]() + nchainInChain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence in sequence item 01") + return kvs, nil + })). + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence in sequence item 02") + return kvs, nil + })) + + nchain.AppendGraph(nchainInChain) + + parallel.AddGraph("test_seq_in_parallel", nchain) + + chain.AppendParallel(parallel) + + r, err := chain.Compile(context.Background()) + assert.Nil(t, err) + out, err := r.Invoke(context.Background(), map[string]any{"test": "test"}) + assert.Nil(t, err) + t.Log("out is : ", out) +} + +func TestChainSingleNode(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in view lambda: ", kvs) + return kvs, nil + })) + + // single Node in chain (prepare for parallel) + singleNodeChain := NewChain[map[string]any, map[string]any]() + singleNodeChain. + AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in sequence item 01") + return kvs, nil + })) + + // add parallel + parallel := NewParallel() + parallel. + AddLambda("test_parallel1_lambda", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in parallel item 01") + return kvs, nil + })) + + parallel.AddGraph("test_parallel2_chain", singleNodeChain) + + ctx := context.Background() + + chain.AppendParallel(parallel) + r, err := chain.Compile(ctx) + assert.Nil(t, err) + + out, err := r.Invoke(ctx, map[string]any{"test": "test"}) + assert.Nil(t, err) + t.Log("out is : ", out) +} + +func TestParallelModels(t *testing.T) { + cm := &mockIntentChatModel{} + chain := NewChain[map[string]any, map[string]any]() + chatSuite := NewChain[map[string]any, string]() + chatSuite. + AppendChatTemplate(prompt.FromMessages(schema.FString, schema.SystemMessage(`You are a {role}.`), schema.UserMessage(`{input}`))). + AppendChatModel(cm). + AppendLambda(InvokableLambda(func(ctx context.Context, msg *schema.Message) (string, error) { + t.Log("in parallel item 01") + return msg.Content, nil + })) + + parallel := NewParallel() + parallel. + AddGraph("time001", chatSuite). + AddGraph("time002", chatSuite). + AddGraph("time003", chatSuite) + + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + t.Log("in view lambda: ", kvs) + return kvs, nil + })) + + chain.AppendParallel(parallel) + + ctx := context.Background() + + r, err := chain.Compile(ctx) + assert.Nil(t, err) + + out, err := r.Invoke(ctx, map[string]any{"role": "cat", "input": "你怎么叫的?"}) + assert.Nil(t, err) + + t.Log("out is : ", out) +} + +func TestChainMultiNodes(t *testing.T) { + ctx := context.Background() + + t.Run("test embedding Node", func(t *testing.T) { + chain := NewChain[[]string, [][]float64]() + + mockCtrl := gomock.NewController(t) + eb := embedding.NewMockEmbedder(mockCtrl) + chain.AppendEmbedding(eb) + + r, err := chain.Compile(ctx) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test retriever Node", func(t *testing.T) { + chain := NewChain[string, []*schema.Document]() + + chain.AppendRetriever(retriever.NewMockRetriever(gomock.NewController(t))) + + r, err := chain.Compile(ctx) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test chat model", func(t *testing.T) { + chain := NewChain[[]*schema.Message, *schema.Message]() + + cm := &mockIntentChatModel{} + chain.AppendChatModel(cm) + + r, err := chain.Compile(ctx) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test chat template", func(t *testing.T) { + chain := NewChain[map[string]any, []*schema.Message]() + + chatTemplate := prompt.FromMessages(schema.FString) + chain.AppendChatTemplate(chatTemplate) + + r, err := chain.Compile(ctx) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test lambda", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + + r, err := chain.Compile(ctx) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test indexer", func(t *testing.T) { + chain := NewChain[[]*schema.Document, []string]() + + chain.AppendIndexer(indexer.NewMockIndexer(gomock.NewController(t))) + + r, err := chain.Compile(ctx) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test parallel", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + parallel := NewParallel() + parallel.AddLambda("test_parallel", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + chain.AppendParallel(parallel) + _, err := chain.Compile(ctx) + assert.Error(t, err) + + chain = NewChain[map[string]any, map[string]any]() + parallel = NewParallel() + parallel.AddLambda("test_parallel", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + parallel.AddLambda("test_parallel", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + chain.AppendParallel(parallel) + _, err = chain.Compile(ctx) + assert.Error(t, err) + + chain = NewChain[map[string]any, map[string]any]() + parallel = NewParallel() + parallel.AddLambda("test_parallel", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + parallel.AddLambda("test_parallel1", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + chain.AppendParallel(parallel) + _, err = chain.Compile(ctx) + assert.NoError(t, err) + + chain = NewChain[map[string]any, map[string]any]() + parallel = NewParallel() + parallel.AddLambda("test_parallel", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + parallel.AddLambda("test_parallel1", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + chain.AppendParallel(parallel) + + parallel1 := NewParallel() + parallel1.AddLambda("test_parallel", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + parallel1.AddLambda("test_parallel1", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + chain.AppendParallel(parallel1) + + _, err = chain.Compile(ctx) + assert.Error(t, err) + }) + + t.Run("test tools Node", func(t *testing.T) { + ctx := context.Background() + chain := NewChain[map[string]any, map[string]any]() + toolsNode, err := NewToolNode(ctx, &ToolsNodeConfig{}) + assert.NoError(t, err) + chain.AppendToolsNode(toolsNode) + }) + + t.Run("test chain with compile option", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + r, err := chain.Compile(ctx, WithMaxRunSteps(10)) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("test chain return type", func(t *testing.T) { + t.Run("test chain any output type", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (any, error) { + return 1, nil + })) + _, err := chain.Compile(ctx) + assert.Nil(t, err) + }) + + t.Run("test chain error output type", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + return "123", nil + })) + _, err := chain.Compile(ctx) + assert.Error(t, err) + }) + + t.Run("test chain error input type", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, input string) (map[string]any, error) { + return nil, nil + })) + _, err := chain.Compile(ctx) + assert.Error(t, err) + }) + }) + +} + +func TestParallelMultiNodes(t *testing.T) { + ctx := context.Background() + p := NewParallel() + p.AddLambda("lambda", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + p.AddGraph("graph", NewChain[map[string]any, map[string]any]()) + p.AddIndexer("indexer", indexer.NewMockIndexer(gomock.NewController(t))) + p.AddLoader("loader", document.NewMockLoader(gomock.NewController(t))) + p.AddDocumentTransformer("document transformer", document.NewMockTransformer(gomock.NewController(t))) + p.AddRetriever("retriever", retriever.NewMockRetriever(gomock.NewController(t))) + p.AddChatModel("chatmodel", model.NewMockChatModel(gomock.NewController(t))) + p.AddChatTemplate("chatTemplate", prompt.FromMessages(schema.FString, schema.SystemMessage("hello"))) + p.AddEmbedding("embedding", embedding.NewMockEmbedder(gomock.NewController(t))) + p.AddPassthrough("passthrough") + toolsNode, err := NewToolNode(ctx, &ToolsNodeConfig{}) + assert.NoError(t, err) + p.AddToolsNode("tools", toolsNode) + + assert.Greater(t, len(p.nodes), 6) + + ctrl := gomock.NewController(t) + p = NewParallel() + p.AddIndexer("key", indexer.NewMockIndexer(ctrl)) + p.AddLoader("key", document.NewMockLoader(ctrl)) + p.AddRetriever("r", retriever.NewMockRetriever(ctrl)) + assert.NotNil(t, p.err) + + p = NewParallel() + p.addNode("k", nil, nil) + assert.NotNil(t, p.err) + + p = &Parallel{ + outputKeys: nil, + } + p.addNode("k", &graphNode{}, nil) + assert.NotNil(t, p.err) +} + +type FakeLambdaOptions struct { + Info string +} + +type FakeLambdaOption func(opt *FakeLambdaOptions) + +func FakeWithLambdaInfo(info string) FakeLambdaOption { + return func(opt *FakeLambdaOptions) { + opt.Info = info + } +} + +func TestChainWithNodeKey(t *testing.T) { + ctx := context.Background() + + t.Run("test normal chain with node key option", func(t *testing.T) { + + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_01")) + + b := NewChainBranch(func(ctx context.Context, input map[string]any) (string, error) { + return "lambda_02", nil + }) + + b.AddLambda("lambda_02", InvokableLambdaWithOption(func(ctx context.Context, kvs map[string]any, opts ...FakeLambdaOption) (map[string]any, error) { + opt := &FakeLambdaOptions{} + for _, optFn := range opts { + optFn(opt) + } + kvs["lambda_02"] = opt.Info + return kvs, nil + }), WithNodeKey("lambda_02")) + + b.AddLambda("lambda_03", InvokableLambdaWithOption(func(ctx context.Context, kvs map[string]any, opts ...FakeLambdaOption) (map[string]any, error) { + opt := &FakeLambdaOptions{} + for _, optFn := range opts { + optFn(opt) + } + kvs["lambda_03"] = opt.Info + return kvs, nil + }), WithNodeKey("lambda_03")) + + chain.AppendBranch(b) + + chain.AppendPassthrough() + + p := NewParallel() + p.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + return kvs["lambda_02"].(string), nil + })) + p.AddLambda("lambda_04", InvokableLambdaWithOption(func(ctx context.Context, kvs map[string]any, opts ...FakeLambdaOption) (string, error) { + opt := &FakeLambdaOptions{} + for _, optFn := range opts { + optFn(opt) + } + return opt.Info, nil + }), WithNodeKey("lambda_04")) + + p.AddLambda("lambda_05", InvokableLambdaWithOption(func(ctx context.Context, kvs map[string]any, opts ...FakeLambdaOption) (string, error) { + opt := &FakeLambdaOptions{} + for _, optFn := range opts { + optFn(opt) + } + return opt.Info, nil + }), WithNodeKey("lambda_05")) + chain.AppendParallel(p) + + chain.AppendLambda(InvokableLambdaWithOption(func(ctx context.Context, kvs map[string]any, opts ...FakeLambdaOption) (map[string]any, error) { + opt := &FakeLambdaOptions{} + for _, optFn := range opts { + optFn(opt) + } + kvs["lambda_06"] = opt.Info + return kvs, nil + }), WithNodeKey("lambda_06")) + + r, err := chain.Compile(ctx) + assert.Nil(t, err) + + res, err := r.Invoke(ctx, map[string]any{}, + WithLambdaOption(FakeWithLambdaInfo("normal")), + WithLambdaOption(FakeWithLambdaInfo("info_lambda_02")).DesignateNode("lambda_02"), // branch + WithLambdaOption(FakeWithLambdaInfo("info_lambda_03")).DesignateNode("lambda_03"), // branch (wont run) + WithLambdaOption(FakeWithLambdaInfo("info_lambda_05")).DesignateNode("lambda_05"), // parallel + ) + assert.Nil(t, err) + + assert.Equal(t, "info_lambda_02", res["lambda_02"]) // transmit option with DesigateNode + assert.Equal(t, "info_lambda_05", res["lambda_05"]) // transmit option with DesigateNode + assert.Equal(t, "normal", res["lambda_06"]) // without DesigateNode, using default option + }) + + t.Run("test chain with node key option and error with correct error info", func(t *testing.T) { + + t.Run("compile error of chain", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + return "123", nil + }), WithNodeKey("lambda_01")) + + c, err := chain.Compile(ctx) + assert.Nil(t, c) + fmt.Printf("%+v\n", err) + + assert.Contains(t, err.Error(), "edge[lambda_01]") + }) + + t.Run("compile error of branch", func(t *testing.T) { + t.Run("without node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_01")) + b := NewChainBranch(func(ctx context.Context, input map[string]any) (string, error) { + return "lambda_02", nil + }) + b.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + b.AddLambda("lambda_03", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + return "", nil + })) + chain.AppendBranch(b) + c, err := chain.Compile(ctx) + assert.Nil(t, c) + fmt.Printf("%+v\n", err) + assert.Contains(t, err.Error(), "edge[node_1_branch_lambda_03]") // with no node key option, will use default node key + }) + + t.Run("with node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_01")) + + b := NewChainBranch(func(ctx context.Context, input map[string]any) (string, error) { + return "lambda_02", nil + }) + b.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_02")) + + b.AddLambda("lambda_03", InvokableLambda(func(ctx context.Context, kvs map[string]any) (string, error) { + return "123", nil + }), WithNodeKey("key_of_lambda_03")) + + chain.AppendBranch(b) + c, err := chain.Compile(ctx) + assert.Nil(t, c) + fmt.Printf("%+v\n", err) + assert.Contains(t, err.Error(), "edge[key_of_lambda_03]") + }) + }) + + t.Run("compile error of parallel", func(t *testing.T) { + t.Run("without node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_01")) + p := NewParallel() + p.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + })) + p.AddLambda("lambda_03", InvokableLambda(func(ctx context.Context, v string) (string, error) { + return "", nil + })) + + chain.AppendParallel(p) + + c, err := chain.Compile(ctx) + assert.Nil(t, c) + fmt.Printf("%+v\n", err) + assert.Contains(t, err.Error(), "to=node_1_parallel_1") // with no node key option, will use default node key + }) + + t.Run("with node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + chain.AppendLambda(InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_01")) + p := NewParallel() + p.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return kvs, nil + }), WithNodeKey("lambda_02")) + p.AddLambda("lambda_03", InvokableLambda(func(ctx context.Context, v string) (string, error) { + return "", nil + }), WithNodeKey("key_of_lambda_03")) + chain.AppendParallel(p) + c, err := chain.Compile(ctx) + assert.Nil(t, c) + fmt.Printf("%+v\n", err) + assert.Contains(t, err.Error(), "to=key_of_lambda_03") + }) + }) + + t.Run("invoke error", func(t *testing.T) { + t.Run("branch with out node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + + b := NewChainBranch(func(ctx context.Context, input map[string]any) (string, error) { + return "lambda_01", nil + }) + + b.AddLambda("lambda_01", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, fmt.Errorf("fake error") + })) + b.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, nil + })) + + chain.AppendBranch(b) + c, err := chain.Compile(ctx) + assert.Nil(t, err) + + _, err = c.Invoke(ctx, map[string]any{}) + fmt.Printf("%+v\n", err) + assert.Error(t, err) + assert.Contains(t, err.Error(), "node_0_branch_lambda_01") // with no node key option, will use default node key + }) + + t.Run("branch with node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + b := NewChainBranch(func(ctx context.Context, input map[string]any) (string, error) { + return "lambda_01", nil + }) + b.AddLambda("lambda_01", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, fmt.Errorf("fake error") + }), WithNodeKey("key_of_lambda_01")) + b.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, nil + })) + + chain.AppendBranch(b) + c, err := chain.Compile(ctx) + assert.Nil(t, err) + _, err = c.Invoke(ctx, map[string]any{}) + fmt.Printf("%+v\n", err) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key_of_lambda_01") + }) + + t.Run("parallel with out node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + p := NewParallel() + p.AddLambda("lambda_01", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, fmt.Errorf("fake error") + })) + p.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, nil + })) + chain.AppendParallel(p) + c, err := chain.Compile(ctx) + assert.Nil(t, err) + _, err = c.Invoke(ctx, map[string]any{}) + fmt.Printf("%+v\n", err) + assert.Error(t, err) + assert.Contains(t, err.Error(), "node_0_parallel_0") // with no node key option, will use default node key + }) + + t.Run("parallel with node key", func(t *testing.T) { + chain := NewChain[map[string]any, map[string]any]() + p := NewParallel() + p.AddLambda("lambda_01", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, fmt.Errorf("fake error") + }), WithNodeKey("key_of_lambda_01")) + p.AddLambda("lambda_02", InvokableLambda(func(ctx context.Context, kvs map[string]any) (map[string]any, error) { + return nil, nil + })) + chain.AppendParallel(p) + c, err := chain.Compile(ctx) + assert.Nil(t, err) + _, err = c.Invoke(ctx, map[string]any{}) + fmt.Printf("%+v\n", err) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key_of_lambda_01") + }) + }) + + }) + +} diff --git a/compose/checkpoint.go b/compose/checkpoint.go new file mode 100644 index 0000000..c174994 --- /dev/null +++ b/compose/checkpoint.go @@ -0,0 +1,404 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/internal/serialization" + "github.com/cloudwego/eino/schema" +) + +func init() { + schema.RegisterName[*checkpoint]("_eino_checkpoint") + schema.RegisterName[*dagChannel]("_eino_dag_channel") + schema.RegisterName[*pregelChannel]("_eino_pregel_channel") + schema.RegisterName[dependencyState]("_eino_dependency_state") + _ = serialization.GenericRegister[channel]("_eino_channel") +} + +// RegisterSerializableType registers a custom type for eino serialization. +// This allows eino to properly serialize and deserialize custom types. +// Both custom interfaces and structs need to be registered using this function. +// Types only need to be registered once - pointers and other references will be handled automatically. +// All built-in eino types are already registered. +// Parameters: +// - name: A unique identifier for the type being registered (should not start with "_eino") +// - T: The generic type parameter representing the type to register +// Returns: +// - error: An error if registration fails (e.g., if the type is already registered) +// Deprecated: RegisterSerializableType is deprecated. Use schema.RegisterName[T](name) instead. +func RegisterSerializableType[T any](name string) (err error) { + return serialization.GenericRegister[T](name) +} + +type CheckPointStore = core.CheckPointStore + +type Serializer interface { + Marshal(v any) ([]byte, error) + Unmarshal(data []byte, v any) error +} + +// WithCheckPointStore sets the checkpoint store implementation for a graph. +func WithCheckPointStore(store CheckPointStore) GraphCompileOption { + return func(o *graphCompileOptions) { + o.checkPointStore = store + } +} + +// WithSerializer sets the serializer used to persist checkpoint state. +func WithSerializer(serializer Serializer) GraphCompileOption { + return func(o *graphCompileOptions) { + o.serializer = serializer + } +} + +// WithCheckPointID sets the checkpoint ID to load from and write to by default. +func WithCheckPointID(checkPointID string) Option { + return Option{ + checkPointID: &checkPointID, + } +} + +// WithWriteToCheckPointID specifies a different checkpoint ID to write to. +// If not provided, the checkpoint ID from WithCheckPointID will be used for writing. +// This is useful for scenarios where you want to load from an existed checkpoint +// but save the progress to a new, separate checkpoint. +func WithWriteToCheckPointID(checkPointID string) Option { + return Option{ + writeToCheckPointID: &checkPointID, + } +} + +// WithForceNewRun forces the graph to run from the beginning, ignoring any checkpoints. +func WithForceNewRun() Option { + return Option{ + forceNewRun: true, + } +} + +// StateModifier modifies state during checkpoint operations for a given node path. +type StateModifier func(ctx context.Context, path NodePath, state any) error + +// WithStateModifier installs a state modifier invoked during checkpoint read/write. +func WithStateModifier(sm StateModifier) Option { + return Option{ + stateModifier: sm, + } +} + +type checkpoint struct { + Channels map[string]channel + Inputs map[string] /*node key*/ any /*input*/ + State any + SkipPreHandler map[string]bool + RerunNodes []string + + SubGraphs map[string]*checkpoint + + InterruptID2Addr map[string]Address + InterruptID2State map[string]core.InterruptState +} + +type stateModifierKey struct{} +type checkPointKey struct{} // *checkpoint + +func getStateModifier(ctx context.Context) StateModifier { + if sm, ok := ctx.Value(stateModifierKey{}).(StateModifier); ok { + return sm + } + return nil +} + +func setStateModifier(ctx context.Context, modifier StateModifier) context.Context { + return context.WithValue(ctx, stateModifierKey{}, modifier) +} + +func getCheckPointFromStore(ctx context.Context, id string, cpr *checkPointer) (cp *checkpoint, err error) { + cp, existed, err := cpr.get(ctx, id) + if err != nil { + return nil, err + } + if !existed { + return nil, nil + } + + return cp, nil +} + +func setCheckPointToCtx(ctx context.Context, cp *checkpoint) context.Context { + ctx = core.PopulateInterruptState(ctx, cp.InterruptID2Addr, cp.InterruptID2State) + return context.WithValue(ctx, checkPointKey{}, cp) +} + +func getCheckPointFromCtx(ctx context.Context) *checkpoint { + if cp, ok := ctx.Value(checkPointKey{}).(*checkpoint); ok { + return cp + } + return nil +} + +func forwardCheckPoint(ctx context.Context, nodeKey string) context.Context { + cp := getCheckPointFromCtx(ctx) + if cp == nil { + return ctx + } + + if subCP, ok := cp.SubGraphs[nodeKey]; ok { + delete(cp.SubGraphs, nodeKey) // only forward once + return context.WithValue(ctx, checkPointKey{}, subCP) + } + return context.WithValue(ctx, checkPointKey{}, (*checkpoint)(nil)) +} + +func newCheckPointer( + inputPairs, outputPairs map[string]streamConvertPair, + store CheckPointStore, + serializer Serializer, +) *checkPointer { + if serializer == nil { + serializer = &serialization.InternalSerializer{} + } + return &checkPointer{ + sc: newStreamConverter(inputPairs, outputPairs), + store: store, + serializer: serializer, + } +} + +type checkPointer struct { + sc *streamConverter + store CheckPointStore + serializer Serializer +} + +func (c *checkPointer) get(ctx context.Context, id string) (*checkpoint, bool, error) { + data, existed, err := c.store.Get(ctx, id) + if err != nil || existed == false { + return nil, existed, err + } + + cp := &checkpoint{} + err = c.serializer.Unmarshal(data, cp) + if err != nil { + return nil, false, err + } + + return cp, true, nil +} + +func (c *checkPointer) set(ctx context.Context, id string, cp *checkpoint) error { + normalizeCheckpointTypedNilInputs(cp) + + data, err := c.serializer.Marshal(cp) + if err != nil { + return err + } + + return c.store.Set(ctx, id, data) +} + +func normalizeCheckpointTypedNilInputs(cp *checkpoint) { + if cp == nil { + return + } + for key, input := range cp.Inputs { + if isTypedNil(input) { + cp.Inputs[key] = nil + } + } + for _, sub := range cp.SubGraphs { + normalizeCheckpointTypedNilInputs(sub) + } +} + +func isTypedNil(v any) bool { + if v == nil { + return false + } + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return rv.IsNil() + default: + return false + } +} + +// MigrateCheckpointState is an advanced compatibility utility for checkpoint upgrades. +// +// It decodes checkpoint bytes using the given serializer, applies migrate to checkpoint.State and +// all nested SubGraphs' states, then re-encodes the checkpoint. +// +// Typical use cases: +// - Resume-time migration when you changed your graph state type/schema and need to load old +// checkpoints without discarding them. +// - Framework-level backward compatibility (e.g. ADK upgrading checkpoints across versions). +// +// Migrate callback contract: +// - Returns (newState, changed, error). +// - If changed is false, the state is left as-is. +// - If error is non-nil, migration stops and the error is returned to the caller. +// +// The original bytes are returned only if no state was changed anywhere in the checkpoint tree. +func MigrateCheckpointState(data []byte, serializer Serializer, migrate func(state any) (any, bool, error)) ([]byte, error) { + cp := &checkpoint{} + if err := serializer.Unmarshal(data, cp); err != nil { + return nil, err + } + changed, err := migrateCheckpoint(cp, migrate) + if err != nil { + return nil, err + } + if !changed { + return data, nil + } + return serializer.Marshal(cp) +} + +// migrateCheckpoint recursively applies migrate to cp.State and all SubGraphs. +func migrateCheckpoint(cp *checkpoint, migrate func(state any) (any, bool, error)) (bool, error) { + anyChanged := false + if cp.State != nil { + newState, changed, err := migrate(cp.State) + if err != nil { + return false, err + } + if changed { + cp.State = newState + anyChanged = true + } + } + for _, sub := range cp.SubGraphs { + changed, err := migrateCheckpoint(sub, migrate) + if err != nil { + return false, err + } + if changed { + anyChanged = true + } + } + return anyChanged, nil +} + +// convertCheckPoint if value in checkpoint is streamReader, convert it to non-stream +func (c *checkPointer) convertCheckPoint(cp *checkpoint, isStream bool) (err error) { + for _, ch := range cp.Channels { + err = ch.convertValues(func(m map[string]any) error { + return c.sc.convertOutputs(isStream, m) + }) + if err != nil { + return err + } + } + + err = c.sc.convertInputs(isStream, cp.Inputs) + if err != nil { + return err + } + + return nil +} + +// convertCheckPoint convert values in checkpoint to streamReader if needed +func (c *checkPointer) restoreCheckPoint(cp *checkpoint, isStream bool) (err error) { + for _, ch := range cp.Channels { + err = ch.convertValues(func(m map[string]any) error { + return c.sc.restoreOutputs(isStream, m) + }) + if err != nil { + return err + } + } + + err = c.sc.restoreInputs(isStream, cp.Inputs) + if err != nil { + return err + } + + return nil +} + +func newStreamConverter(inputPairs, outputPairs map[string]streamConvertPair) *streamConverter { + return &streamConverter{ + inputPairs: inputPairs, + outputPairs: outputPairs, + } +} + +type streamConverter struct { + inputPairs, outputPairs map[string]streamConvertPair +} + +func (s *streamConverter) convertInputs(isStream bool, values map[string]any) error { + return convert(values, s.inputPairs, isStream) +} + +func (s *streamConverter) restoreInputs(isStream bool, values map[string]any) error { + return restore(values, s.inputPairs, isStream) +} + +func (s *streamConverter) convertOutputs(isStream bool, values map[string]any) error { + return convert(values, s.outputPairs, isStream) +} + +func (s *streamConverter) restoreOutputs(isStream bool, values map[string]any) error { + return restore(values, s.outputPairs, isStream) +} + +func convert(values map[string]any, convPairs map[string]streamConvertPair, isStream bool) error { + if !isStream { + return nil + } + for key, v := range values { + convPair, ok := convPairs[key] + if !ok { + return fmt.Errorf("checkpoint conv stream fail, node[%s] have not been registered", key) + } + sr, ok := v.(streamReader) + if !ok { + return fmt.Errorf("checkpoint conv stream fail, value of [%s] isn't stream", key) + } + nValue, err := convPair.concatStream(sr) + if err != nil { + return err + } + values[key] = nValue + } + return nil +} + +func restore(values map[string]any, convPairs map[string]streamConvertPair, isStream bool) error { + if !isStream { + return nil + } + for key, v := range values { + convPair, ok := convPairs[key] + if !ok { + return fmt.Errorf("checkpoint restore stream fail, node[%s] have not been registered", key) + } + sr, err := convPair.restoreStream(v) + if err != nil { + return err + } + values[key] = sr + } + return nil +} diff --git a/compose/checkpoint_migrate_test.go b/compose/checkpoint_migrate_test.go new file mode 100644 index 0000000..6b64b60 --- /dev/null +++ b/compose/checkpoint_migrate_test.go @@ -0,0 +1,116 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +type stubSerializer struct { + unmarshal func(data []byte, v any) error + marshal func(v any) ([]byte, error) +} + +func (s stubSerializer) Marshal(v any) ([]byte, error) { + return s.marshal(v) +} + +func (s stubSerializer) Unmarshal(data []byte, v any) error { + return s.unmarshal(data, v) +} + +func TestMigrateCheckpointState_UnmarshalError(t *testing.T) { + in := []byte("in") + codec := stubSerializer{ + unmarshal: func(_ []byte, _ any) error { return errors.New("bad") }, + marshal: func(_ any) ([]byte, error) { return []byte("unused"), nil }, + } + _, err := MigrateCheckpointState(in, codec, func(state any) (any, bool, error) { + return state, false, nil + }) + assert.Error(t, err) +} + +func TestMigrateCheckpointState_NoChangeReturnsOriginalBytes(t *testing.T) { + in := []byte("in") + cp := &checkpoint{State: "s"} + codec := stubSerializer{ + unmarshal: func(_ []byte, v any) error { + *(v.(*checkpoint)) = *cp + return nil + }, + marshal: func(_ any) ([]byte, error) { + return []byte("marshaled"), nil + }, + } + out, err := MigrateCheckpointState(in, codec, func(state any) (any, bool, error) { + return state, false, nil + }) + assert.NoError(t, err) + assert.Equal(t, in, out) +} + +func TestMigrateCheckpointState_ChangeTriggersMarshal(t *testing.T) { + in := []byte("in") + cp := &checkpoint{State: "s"} + var sawState any + codec := stubSerializer{ + unmarshal: func(_ []byte, v any) error { + *(v.(*checkpoint)) = *cp + return nil + }, + marshal: func(v any) ([]byte, error) { + sawState = v.(*checkpoint).State + return []byte("marshaled"), nil + }, + } + out, err := MigrateCheckpointState(in, codec, func(state any) (any, bool, error) { + return "s2", true, nil + }) + assert.NoError(t, err) + assert.Equal(t, []byte("marshaled"), out) + assert.Equal(t, "s2", sawState) +} + +func TestMigrateCheckpointState_MigrateErrorStops(t *testing.T) { + in := []byte("in") + cp := &checkpoint{ + State: "root", + SubGraphs: map[string]*checkpoint{ + "sub": {State: "sub"}, + }, + } + codec := stubSerializer{ + unmarshal: func(_ []byte, v any) error { + *(v.(*checkpoint)) = *cp + return nil + }, + marshal: func(_ any) ([]byte, error) { + return []byte("marshaled"), nil + }, + } + _, err := MigrateCheckpointState(in, codec, func(state any) (any, bool, error) { + if state == "sub" { + return nil, false, errors.New("boom") + } + return state, false, nil + }) + assert.Error(t, err) +} diff --git a/compose/checkpoint_test.go b/compose/checkpoint_test.go new file mode 100644 index 0000000..ccba397 --- /dev/null +++ b/compose/checkpoint_test.go @@ -0,0 +1,2059 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "io" + "sync" + "testing" + "time" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/internal/callbacks" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/internal/serialization" + "github.com/cloudwego/eino/schema" +) + +type inMemoryStore struct { + m map[string][]byte +} + +func (i *inMemoryStore) Get(_ context.Context, checkPointID string) ([]byte, bool, error) { + v, ok := i.m[checkPointID] + return v, ok, nil +} + +func (i *inMemoryStore) Set(_ context.Context, checkPointID string, checkPoint []byte) error { + i.m[checkPointID] = checkPoint + return nil +} + +func newInMemoryStore() *inMemoryStore { + return &inMemoryStore{ + m: make(map[string][]byte), + } +} + +type checkpointTypedNilInput struct { + Value string +} + +type checkpointGobSerializer struct{} + +func (g *checkpointGobSerializer) Marshal(v any) ([]byte, error) { + buf := new(bytes.Buffer) + err := gob.NewEncoder(buf).Encode(v) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (g *checkpointGobSerializer) Unmarshal(data []byte, v any) error { + return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v) +} + +func TestCheckpointGobSerializerNormalizesTypedNilInput(t *testing.T) { + store := newInMemoryStore() + cpr := newCheckPointer(nil, nil, store, &checkpointGobSerializer{}) + cp := &checkpoint{ + Channels: map[string]channel{}, + Inputs: map[string]any{"node": (*checkpointTypedNilInput)(nil)}, + SkipPreHandler: map[string]bool{}, + } + + err := cpr.set(context.Background(), "cp", cp) + assert.NoError(t, err) + if err != nil { + return + } + + got, existed, err := cpr.get(context.Background(), "cp") + assert.NoError(t, err) + assert.True(t, existed) + assert.Contains(t, got.Inputs, "node") + assert.Nil(t, got.Inputs["node"]) +} + +func TestRunnableInvokeAcceptsNilForPointerInput(t *testing.T) { + lambda := InvokableLambda(func(ctx context.Context, input *checkpointTypedNilInput) (string, error) { + assert.Nil(t, input) + return "ok", nil + }) + + out, err := lambda.executor.i(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, "ok", out) +} + +type testStruct struct { + A string +} + +func init() { + schema.Register[testStruct]() + schema.RegisterName[*checkpointTypedNilInput]("compose_checkpoint_typed_nil_input") +} + +func TestSimpleCheckPoint(t *testing.T) { + store := newInMemoryStore() + + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testStruct) { + return &testStruct{A: ""} + })) + + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + assert.NoError(t, err) + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "2", nil + }), WithStatePreHandler(func(ctx context.Context, in string, state *testStruct) (string, error) { + return in + state.A, nil + })) + assert.NoError(t, err) + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", "2") + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor), WithCheckPointStore(store), WithInterruptAfterNodes([]string{"1"}), WithInterruptBeforeNodes([]string{"2"}), WithGraphName("root")) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "start", WithCheckPointID("1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, &testStruct{A: ""}, info.State) + assert.Equal(t, []string{"2"}, info.BeforeNodes) + assert.Equal(t, []string{"1"}, info.AfterNodes) + assert.Empty(t, info.RerunNodesExtra) + assert.Empty(t, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + })) + + rCtx := ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + result, err := r.Invoke(rCtx, "start", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "start1state2", result) + + /* _, err = r.Stream(ctx, "start", WithCheckPointID("2")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, &testStruct{A: ""}, info.State) + assert.Equal(t, []string{"2"}, info.BeforeNodes) + assert.Equal(t, []string{"1"}, info.AfterNodes) + assert.Empty(t, info.RerunNodesExtra) + assert.Empty(t, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + })) + + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + streamResult, err := r.Stream(rCtx, "start", WithCheckPointID("2")) + assert.NoError(t, err) + result = "" + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + result += chunk + } + + assert.Equal(t, "start1state2", result)*/ +} + +func TestCustomStructInAn2y(t *testing.T) { + store := newInMemoryStore() + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testStruct) { + return &testStruct{A: ""} + })) + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output *testStruct, err error) { + return &testStruct{A: input + "1"}, nil + }), WithOutputKey("1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input map[string]any) (output string, err error) { + return input["1"].(*testStruct).A + "2", nil + }), WithStatePreHandler(func(ctx context.Context, in map[string]any, state *testStruct) (map[string]any, error) { + in["1"].(*testStruct).A += state.A + return in, nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", "2") + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(store), WithInterruptAfterNodes([]string{"1"}), + WithGraphName("root")) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "start", WithCheckPointID("1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, &testStruct{A: ""}, info.State) + assert.Equal(t, []string{"1"}, info.AfterNodes) + assert.Empty(t, info.RerunNodesExtra) + assert.Empty(t, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + })) + rCtx := ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + result, err := r.Invoke(rCtx, "start", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "start1state2", result) + + _, err = r.Stream(ctx, "start", WithCheckPointID("2")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, &testStruct{A: ""}, info.State) + assert.Equal(t, []string{"1"}, info.AfterNodes) + assert.Empty(t, info.RerunNodesExtra) + assert.Empty(t, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + })) + + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + streamResult, err := r.Stream(rCtx, "start", WithCheckPointID("2")) + assert.NoError(t, err) + result = "" + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + result += chunk + } + + assert.Equal(t, "start1state2", result) +} + +func TestSubGraph(t *testing.T) { + subG := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testStruct) { + return &testStruct{A: ""} + })) + err := subG.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + assert.NoError(t, err) + err = subG.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "2", nil + }), WithStatePreHandler(func(ctx context.Context, in string, state *testStruct) (string, error) { + return in + state.A, nil + })) + assert.NoError(t, err) + + err = subG.AddEdge(START, "1") + assert.NoError(t, err) + err = subG.AddEdge("1", "2") + assert.NoError(t, err) + err = subG.AddEdge("2", END) + assert.NoError(t, err) + + g := NewGraph[string, string]() + err = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + assert.NoError(t, err) + err = g.AddGraphNode("2", subG, WithGraphCompileOptions(WithInterruptAfterNodes([]string{"1"}))) + assert.NoError(t, err) + err = g.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "3", nil + })) + assert.NoError(t, err) + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", "2") + assert.NoError(t, err) + err = g.AddEdge("2", "3") + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore()), WithGraphName("root")) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "start", WithCheckPointID("1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + + rCtx := ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + result, err := r.Invoke(rCtx, "start", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "start11state23", result) + + _, err = r.Stream(ctx, "start", WithCheckPointID("2")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]any), + SubGraphs: map[string]*InterruptInfo{}, + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + streamResult, err := r.Stream(rCtx, "start", WithCheckPointID("2")) + assert.NoError(t, err) + result = "" + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + result += chunk + } + + assert.Equal(t, "start11state23", result) +} + +type testGraphCallback struct { + onStartTimes int + onEndTimes int + onStreamStartTimes int + onStreamEndTimes int + onErrorTimes int +} + +func (t *testGraphCallback) OnStart(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackInput) context.Context { + if info.Component == ComponentOfGraph { + t.onStartTimes++ + } + return ctx +} + +func (t *testGraphCallback) OnEnd(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackOutput) context.Context { + if info.Component == ComponentOfGraph { + t.onEndTimes++ + } + return ctx +} + +func (t *testGraphCallback) OnError(ctx context.Context, info *callbacks.RunInfo, _ error) context.Context { + if info.Component == ComponentOfGraph { + t.onErrorTimes++ + } + return ctx +} + +func (t *testGraphCallback) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + input.Close() + if info.Component == ComponentOfGraph { + t.onStreamStartTimes++ + } + return ctx +} + +func (t *testGraphCallback) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context { + output.Close() + if info.Component == ComponentOfGraph { + t.onStreamEndTimes++ + } + return ctx +} + +func TestNestedSubGraph(t *testing.T) { + sSubG := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testStruct) { + return &testStruct{A: ""} + })) + err := sSubG.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + assert.NoError(t, err) + err = sSubG.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "2", nil + }), WithStatePreHandler(func(ctx context.Context, in string, state *testStruct) (string, error) { + return in + state.A, nil + })) + assert.NoError(t, err) + + err = sSubG.AddEdge(START, "1") + assert.NoError(t, err) + err = sSubG.AddEdge("1", "2") + assert.NoError(t, err) + err = sSubG.AddEdge("2", END) + assert.NoError(t, err) + + subG := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testStruct) { + return &testStruct{A: ""} + })) + err = subG.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + assert.NoError(t, err) + err = subG.AddGraphNode("2", sSubG, WithGraphCompileOptions(WithInterruptAfterNodes([]string{"1"})), WithStatePreHandler(func(ctx context.Context, in string, state *testStruct) (string, error) { + return in + state.A, nil + }), WithOutputKey("2")) + assert.NoError(t, err) + err = subG.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "3", nil + }), WithOutputKey("3")) + assert.NoError(t, err) + err = subG.AddLambdaNode("4", InvokableLambda(func(ctx context.Context, input map[string]any) (output string, err error) { + return input["2"].(string) + "4\n" + input["3"].(string) + "4\n" + input["state"].(string) + "4\n", nil + }), WithStatePreHandler(func(ctx context.Context, in map[string]any, state *testStruct) (map[string]any, error) { + in["state"] = state.A + return in, nil + })) + assert.NoError(t, err) + err = subG.AddEdge(START, "1") + assert.NoError(t, err) + err = subG.AddEdge("1", "2") + assert.NoError(t, err) + err = subG.AddEdge("1", "3") + assert.NoError(t, err) + err = subG.AddEdge("3", "4") + assert.NoError(t, err) + err = subG.AddEdge("2", "4") + assert.NoError(t, err) + err = subG.AddEdge("4", END) + assert.NoError(t, err) + + g := NewGraph[string, string]() + err = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + assert.NoError(t, err) + err = g.AddGraphNode("2", subG, WithGraphCompileOptions(WithInterruptAfterNodes([]string{"1", "3"}), WithInterruptBeforeNodes([]string{"4"}))) + assert.NoError(t, err) + err = g.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "3", nil + })) + assert.NoError(t, err) + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", "2") + assert.NoError(t, err) + err = g.AddEdge("2", "3") + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore()), WithGraphName("root")) + assert.NoError(t, err) + + tGCB := &testGraphCallback{} + _, err = r.Invoke(ctx, "start", WithCheckPointID("1"), WithCallbacks(tGCB)) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + + rCtx := ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Invoke(rCtx, "start", WithCheckPointID("1"), WithCallbacks(tGCB)) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + AfterNodes: []string{"3"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + Parent: &InterruptCtx{ + ID: "runnable:root", + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Invoke(rCtx, "start", WithCheckPointID("1"), WithCallbacks(tGCB)) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + BeforeNodes: []string{"4"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state2"}) + result, err := r.Invoke(rCtx, "start", WithCheckPointID("1"), WithCallbacks(tGCB)) + assert.NoError(t, err) + assert.Equal(t, `start11state1state24 +start1134 +state24 +3`, result) + + _, err = r.Stream(ctx, "start", WithCheckPointID("2"), WithCallbacks(tGCB)) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Stream(rCtx, "start", WithCheckPointID("2"), WithCallbacks(tGCB)) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + AfterNodes: []string{"3"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Stream(rCtx, "start", WithCheckPointID("2"), WithCallbacks(tGCB)) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + BeforeNodes: []string{"4"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state2"}) + streamResult, err := r.Stream(rCtx, "start", WithCheckPointID("2"), WithCallbacks(tGCB)) + assert.NoError(t, err) + result = "" + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + result += chunk + } + assert.Equal(t, `start11state1state24 +start1134 +state24 +3`, result) + + assert.Equal(t, 10, tGCB.onStartTimes) // 3+sSubG*1*3+subG*2*2+g*0 + assert.Equal(t, 3, tGCB.onEndTimes) // success*3 + assert.Equal(t, 10, tGCB.onStreamStartTimes) // 3+sSubG*1*3+subG*2*2+g*0 + assert.Equal(t, 3, tGCB.onStreamEndTimes) // success*3 + assert.Equal(t, 14, tGCB.onErrorTimes) // 2*(sSubG*1*3+subG*2*2+g*0) + + // dag + r, err = g.Compile(ctx, WithCheckPointStore(newInMemoryStore()), WithNodeTriggerMode(AllPredecessor), + WithGraphName("root")) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "start", WithCheckPointID("1")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Invoke(rCtx, "start", WithCheckPointID("1")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + AfterNodes: []string{"3"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + ID: "runnable:root;node:2;node:2", + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Invoke(rCtx, "start", WithCheckPointID("1")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + BeforeNodes: []string{"4"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state2"}) + result, err = r.Invoke(rCtx, "start", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, `start11state1state24 +start1134 +state24 +3`, result) + + _, err = r.Stream(ctx, "start", WithCheckPointID("2")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Stream(rCtx, "start", WithCheckPointID("2")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + AfterNodes: []string{"3"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: ""}, + AfterNodes: []string{"1"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state"}) + _, err = r.Stream(rCtx, "start", WithCheckPointID("2")) + assert.NotNil(t, err) + info, ok = ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, map[string]*InterruptInfo{ + "2": { + State: &testStruct{A: "state"}, + BeforeNodes: []string{"4"}, + RerunNodesExtra: make(map[string]interface{}), + SubGraphs: make(map[string]*InterruptInfo), + }, + }, info.SubGraphs) + assert.True(t, info.InterruptContexts[0].EqualsWithoutID(&InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + { + Type: AddressSegmentNode, + ID: "2", + }, + }, + Info: &testStruct{ + A: "state", + }, + IsRootCause: true, + Parent: &InterruptCtx{ + Address: Address{ + { + Type: AddressSegmentRunnable, + ID: "root", + }, + }, + }, + })) + rCtx = ResumeWithData(ctx, info.InterruptContexts[0].ID, &testStruct{A: "state2"}) + streamResult, err = r.Stream(rCtx, "start", WithCheckPointID("2")) + assert.NoError(t, err) + result = "" + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + result += chunk + } + assert.Equal(t, `start11state1state24 +start1134 +state24 +3`, result) +} + +func TestDAGInterrupt(t *testing.T) { + g := NewGraph[string, map[string]any]() + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + time.Sleep(time.Millisecond * 100) + return input, nil + }), WithOutputKey("1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + time.Sleep(time.Millisecond * 200) + return input, nil + }), WithOutputKey("2")) + assert.NoError(t, err) + err = g.AddPassthroughNode("3") + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge(START, "2") + assert.NoError(t, err) + err = g.AddEdge("1", "3") + assert.NoError(t, err) + err = g.AddEdge("2", "3") + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore()), WithInterruptAfterNodes([]string{"1", "2"})) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "input", WithCheckPointID("1")) + info, existed := ExtractInterruptInfo(err) + assert.True(t, existed) + assert.Equal(t, []string{"1", "2"}, info.AfterNodes) + + result, err := r.Invoke(ctx, "", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, map[string]any{"1": "input", "2": "input"}, result) +} + +func TestRerunNodeInterrupt(t *testing.T) { + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testStruct) { + return &testStruct{} + })) + + times := 0 + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + defer func() { times++ }() + if times%2 == 0 { + return "", NewInterruptAndRerunErr("test extra") + } + return input, nil + }), WithStatePreHandler(func(ctx context.Context, in string, state *testStruct) (string, error) { + return state.A, nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "input", WithCheckPointID("1")) + info, existed := ExtractInterruptInfo(err) + assert.True(t, existed) + assert.Equal(t, []string{"1"}, info.RerunNodes) + + result, err := r.Invoke(ctx, "", WithCheckPointID("1"), WithStateModifier(func(ctx context.Context, path NodePath, state any) error { + state.(*testStruct).A = "state" + return nil + })) + assert.NoError(t, err) + assert.Equal(t, "state", result) + + _, err = r.Stream(ctx, "input", WithCheckPointID("2")) + info, existed = ExtractInterruptInfo(err) + assert.True(t, existed) + assert.Equal(t, []string{"1"}, info.RerunNodes) + assert.Equal(t, "test extra", info.RerunNodesExtra["1"].(string)) + + streamResult, err := r.Stream(ctx, "", WithCheckPointID("2"), WithStateModifier(func(ctx context.Context, path NodePath, state any) error { + state.(*testStruct).A = "state" + return nil + })) + assert.NoError(t, err) + chunk, err := streamResult.Recv() + assert.NoError(t, err) + assert.Equal(t, "state", chunk) + _, err = streamResult.Recv() + assert.Equal(t, io.EOF, err) +} + +type myInterface interface { + A() +} + +func TestInterfaceResume(t *testing.T) { + g := NewGraph[myInterface, string]() + times := 0 + assert.NoError(t, g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input myInterface) (output string, err error) { + if times == 0 { + times++ + return "", NewInterruptAndRerunErr("test extra") + } + return "success", nil + }))) + assert.NoError(t, g.AddEdge(START, "1")) + assert.NoError(t, g.AddEdge("1", END)) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, nil, WithCheckPointID("1")) + info, existed := ExtractInterruptInfo(err) + assert.True(t, existed) + assert.Equal(t, []string{"1"}, info.RerunNodes) + result, err := r.Invoke(ctx, nil, WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "success", result) +} + +func TestEarlyFailCallback(t *testing.T) { + g := NewGraph[string, string]() + assert.NoError(t, g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }))) + assert.NoError(t, g.AddEdge(START, "1")) + assert.NoError(t, g.AddEdge("1", END)) + + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + tGCB := &testGraphCallback{} + _, _ = r.Invoke(ctx, "", WithCallbacks(tGCB), WithRuntimeMaxSteps(1)) + assert.Equal(t, 1, tGCB.onStartTimes) + assert.Equal(t, 1, tGCB.onErrorTimes) + assert.Equal(t, 0, tGCB.onEndTimes) +} + +func TestGraphStartInterrupt(t *testing.T) { + subG := NewGraph[string, string]() + _ = subG.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "sub1", nil + })) + _ = subG.AddEdge(START, "1") + _ = subG.AddEdge("1", END) + + g := NewGraph[string, string]() + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + _ = g.AddGraphNode("2", subG, WithGraphCompileOptions(WithInterruptBeforeNodes([]string{"1"}))) + _ = g.AddEdge(START, "1") + _ = g.AddEdge("1", "2") + _ = g.AddEdge("2", END) + + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "input", WithCheckPointID("1")) + info, existed := ExtractInterruptInfo(err) + assert.True(t, existed) + assert.Equal(t, []string{"1"}, info.SubGraphs["2"].BeforeNodes) + result, err := r.Invoke(ctx, "", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "input1sub1", result) +} + +func TestWithForceNewRun(t *testing.T) { + g := NewGraph[string, string]() + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + _ = g.AddEdge(START, "1") + _ = g.AddEdge("1", END) + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(&failStore{t: t})) + assert.NoError(t, err) + result, err := r.Invoke(ctx, "input", WithCheckPointID("1"), WithForceNewRun()) + assert.NoError(t, err) + assert.Equal(t, "input1", result) +} + +type failStore struct { + t *testing.T +} + +func (f *failStore) Get(_ context.Context, _ string) ([]byte, bool, error) { + f.t.Fatalf("cannot call store") + return nil, false, errors.New("fail") +} + +func (f *failStore) Set(_ context.Context, _ string, _ []byte) error { + f.t.Fatalf("cannot call store") + return errors.New("fail") +} + +func TestPreHandlerInterrupt(t *testing.T) { + type state struct{} + assert.NoError(t, serialization.GenericRegister[state]("_eino_TestPreHandlerInterrupt_state")) + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) state { + return state{} + })) + times := 0 + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + }), WithStatePreHandler(func(ctx context.Context, in string, state state) (string, error) { + if times == 0 { + times++ + return "", NewInterruptAndRerunErr("") + } + return in, nil + })) + _ = g.AddEdge(START, "1") + _ = g.AddEdge("1", END) + ctx := context.Background() + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + _, err = r.Invoke(ctx, "input", WithCheckPointID("1")) + info, existed := ExtractInterruptInfo(err) + assert.True(t, existed) + assert.Equal(t, []string{"1"}, info.RerunNodes) + result, err := r.Invoke(ctx, "", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "1", result) +} + +func TestCancelInterrupt(t *testing.T) { + g := NewGraph[string, string]() + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + time.Sleep(3 * time.Second) + return input + "1", nil + })) + _ = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "2", nil + })) + _ = g.AddEdge(START, "1") + _ = g.AddEdge("1", "2") + _ = g.AddEdge("2", END) + ctx := context.Background() + + // pregel + r, err := g.Compile(ctx, WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + // interrupt after nodes + canceledCtx, cancel := WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel(WithGraphInterruptTimeout(time.Hour)) + }() + _, err = r.Invoke(canceledCtx, "input", WithCheckPointID("1")) + assert.Error(t, err) + info, success := ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, []string{"1"}, info.AfterNodes) + result, err := r.Invoke(ctx, "input", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "input12", result) + // infinite timeout + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel() + }() + _, err = r.Invoke(canceledCtx, "input", WithCheckPointID("2")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, []string{"1"}, info.AfterNodes) + result, err = r.Invoke(ctx, "input", WithCheckPointID("2")) + assert.NoError(t, err) + assert.Equal(t, "input12", result) + + // interrupt rerun nodes - with auto-enabled PersistRerunInput, input is preserved + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + _, err = r.Invoke(canceledCtx, "input", WithCheckPointID("3")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, []string{"1"}, info.RerunNodes) + result, err = r.Invoke(ctx, "input", WithCheckPointID("3")) + assert.NoError(t, err) + assert.Equal(t, "input12", result) + + // dag + g = NewGraph[string, string]() + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + time.Sleep(3 * time.Second) + return input + "1", nil + })) + _ = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "2", nil + })) + _ = g.AddEdge(START, "1") + _ = g.AddEdge("1", "2") + _ = g.AddEdge("2", END) + r, err = g.Compile(ctx, WithNodeTriggerMode(AllPredecessor), WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + // interrupt after nodes + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel(WithGraphInterruptTimeout(time.Hour)) + }() + _, err = r.Invoke(canceledCtx, "input", WithCheckPointID("1")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, []string{"1"}, info.AfterNodes) + result, err = r.Invoke(ctx, "input", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, "input12", result) + // infinite timeout + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel() + }() + _, err = r.Invoke(canceledCtx, "input", WithCheckPointID("2")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, []string{"1"}, info.AfterNodes) + result, err = r.Invoke(ctx, "input", WithCheckPointID("2")) + assert.NoError(t, err) + assert.Equal(t, "input12", result) + + // interrupt rerun nodes - with auto-enabled PersistRerunInput, input is preserved + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(300 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + _, err = r.Invoke(canceledCtx, "input", WithCheckPointID("3")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, []string{"1"}, info.RerunNodes) + result, err = r.Invoke(ctx, "input", WithCheckPointID("3")) + assert.NoError(t, err) + assert.Equal(t, "input12", result) + + // dag multi canceled nodes + gg := NewGraph[string, map[string]any]() + _ = gg.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "1", nil + })) + _ = gg.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + time.Sleep(3 * time.Second) + return input + "2", nil + }), WithOutputKey("2")) + _ = gg.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + time.Sleep(3 * time.Second) + return input + "3", nil + }), WithOutputKey("3")) + _ = gg.AddLambdaNode("4", InvokableLambda(func(ctx context.Context, input map[string]any) (output map[string]any, err error) { + return input, nil + })) + _ = gg.AddEdge(START, "1") + _ = gg.AddEdge("1", "2") + _ = gg.AddEdge("1", "3") + _ = gg.AddEdge("2", "4") + _ = gg.AddEdge("3", "4") + _ = gg.AddEdge("4", END) + ctx = context.Background() + rr, err := gg.Compile(ctx, WithNodeTriggerMode(AllPredecessor), WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + // interrupt after nodes + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel(WithGraphInterruptTimeout(time.Hour)) + }() + _, err = rr.Invoke(canceledCtx, "input", WithCheckPointID("1")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, 2, len(info.AfterNodes)) + result2, err := rr.Invoke(ctx, "input", WithCheckPointID("1")) + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "2": "input12", + "3": "input13", + }, result2) + + // interrupt rerun nodes - with auto-enabled PersistRerunInput, input is preserved + canceledCtx, cancel = WithGraphInterrupt(ctx) + go func() { + time.Sleep(500 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + _, err = rr.Invoke(canceledCtx, "input", WithCheckPointID("2")) + assert.Error(t, err) + info, success = ExtractInterruptInfo(err) + assert.True(t, success) + assert.Equal(t, 2, len(info.RerunNodes)) + result2, err = rr.Invoke(ctx, "input", WithCheckPointID("2")) + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "2": "input12", + "3": "input13", + }, result2) +} + +func TestPersistRerunInputNonStream(t *testing.T) { + store := newInMemoryStore() + + var mu sync.Mutex + var receivedInput string + var callCount int + + g := NewGraph[string, string]() + + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + mu.Lock() + callCount++ + currentCount := callCount + receivedInput = input + mu.Unlock() + + if currentCount == 1 { + time.Sleep(2 * time.Second) + } + return input + "_processed", nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, + WithNodeTriggerMode(AllPredecessor), + WithCheckPointStore(store), + ) + assert.NoError(t, err) + + canceledCtx, cancel := WithGraphInterrupt(ctx) + go func() { + time.Sleep(100 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + + _, err = r.Invoke(canceledCtx, "test_input", WithCheckPointID("cp1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, []string{"1"}, info.RerunNodes) + + mu.Lock() + assert.Equal(t, "test_input", receivedInput) + mu.Unlock() + + result, err := r.Invoke(ctx, "", WithCheckPointID("cp1")) + assert.NoError(t, err) + assert.Equal(t, "test_input_processed", result) + + mu.Lock() + assert.Equal(t, "test_input", receivedInput) + assert.Equal(t, 2, callCount) + mu.Unlock() +} + +func TestPersistRerunInputStream(t *testing.T) { + store := newInMemoryStore() + + var mu sync.Mutex + var receivedInput string + var callCount int + + g := NewGraph[string, string]() + + err := g.AddLambdaNode("1", TransformableLambda(func(ctx context.Context, input *schema.StreamReader[string]) (output *schema.StreamReader[string], err error) { + mu.Lock() + callCount++ + currentCount := callCount + mu.Unlock() + + var sb string + for { + chunk, err := input.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + sb += chunk + } + + mu.Lock() + receivedInput = sb + mu.Unlock() + + if currentCount == 1 { + time.Sleep(2 * time.Second) + } + + return schema.StreamReaderFromArray([]string{sb + "_processed"}), nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, + WithNodeTriggerMode(AllPredecessor), + WithCheckPointStore(store), + ) + assert.NoError(t, err) + + inputStream := schema.StreamReaderFromArray([]string{"chunk1", "chunk2", "chunk3"}) + + canceledCtx, cancel := WithGraphInterrupt(ctx) + go func() { + time.Sleep(100 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + + _, err = r.Transform(canceledCtx, inputStream, WithCheckPointID("cp1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, []string{"1"}, info.RerunNodes) + + mu.Lock() + assert.Equal(t, "chunk1chunk2chunk3", receivedInput) + mu.Unlock() + + emptyInputStream := schema.StreamReaderFromArray([]string{}) + + resultStream, err := r.Transform(ctx, emptyInputStream, WithCheckPointID("cp1")) + assert.NoError(t, err) + + var result string + for { + chunk, err := resultStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + result += chunk + } + + assert.Equal(t, "chunk1chunk2chunk3_processed", result) + + mu.Lock() + assert.Equal(t, "chunk1chunk2chunk3", receivedInput) + assert.Equal(t, 2, callCount) + mu.Unlock() +} + +type testPersistRerunInputState struct { + Prefix string +} + +func TestPersistRerunInputWithPreHandler(t *testing.T) { + store := newInMemoryStore() + + var mu sync.Mutex + var receivedInput string + var callCount int + + schema.Register[testPersistRerunInputState]() + + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) *testPersistRerunInputState { + return &testPersistRerunInputState{Prefix: "prefix_"} + })) + + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + mu.Lock() + callCount++ + currentCount := callCount + receivedInput = input + mu.Unlock() + + if currentCount == 1 { + time.Sleep(2 * time.Second) + } + return input + "_processed", nil + }), WithStatePreHandler(func(ctx context.Context, in string, s *testPersistRerunInputState) (string, error) { + return s.Prefix + in, nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, + WithNodeTriggerMode(AllPredecessor), + WithCheckPointStore(store), + ) + assert.NoError(t, err) + + canceledCtx, cancel := WithGraphInterrupt(ctx) + go func() { + time.Sleep(100 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + + _, err = r.Invoke(canceledCtx, "test_input", WithCheckPointID("cp1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + if ok { + assert.Equal(t, []string{"1"}, info.RerunNodes) + } + + mu.Lock() + assert.Equal(t, "prefix_test_input", receivedInput) + mu.Unlock() + + result, err := r.Invoke(ctx, "", WithCheckPointID("cp1")) + assert.NoError(t, err) + assert.Equal(t, "prefix_test_input_processed", result) + + mu.Lock() + assert.Equal(t, "prefix_test_input", receivedInput) + assert.Equal(t, 2, callCount) + mu.Unlock() +} + +func TestPersistRerunInputBackwardCompatibility(t *testing.T) { + store := newInMemoryStore() + + var receivedInput string + var callCount int + + g := NewGraph[string, string]() + + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + callCount++ + receivedInput = input + if len(input) > 0 { + return "", StatefulInterrupt(ctx, "interrupt", input) + } + + _, _, restoredInput := GetInterruptState[string](ctx) + return restoredInput + "_processed", nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, + WithNodeTriggerMode(AllPredecessor), + WithCheckPointStore(store), + ) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "test_input", WithCheckPointID("cp1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, []string{"1"}, info.RerunNodes) + + assert.Equal(t, "test_input", receivedInput) + + result, err := r.Invoke(ctx, "", WithCheckPointID("cp1")) + assert.NoError(t, err) + assert.Equal(t, "test_input_processed", result) + assert.Equal(t, "", receivedInput) + assert.Equal(t, 2, callCount) +} + +func TestPersistRerunInputSubGraph(t *testing.T) { + store := newInMemoryStore() + + var mu sync.Mutex + var receivedInput string + var callCount int + + subG := NewGraph[string, string]() + err := subG.AddLambdaNode("sub1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + mu.Lock() + callCount++ + currentCount := callCount + receivedInput = input + mu.Unlock() + + if currentCount == 1 { + time.Sleep(2 * time.Second) + } + return input + "_sub_processed", nil + })) + assert.NoError(t, err) + err = subG.AddEdge(START, "sub1") + assert.NoError(t, err) + err = subG.AddEdge("sub1", END) + assert.NoError(t, err) + + g := NewGraph[string, string]() + err = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input + "_main", nil + })) + assert.NoError(t, err) + err = g.AddGraphNode("2", subG) + assert.NoError(t, err) + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", "2") + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, + WithNodeTriggerMode(AllPredecessor), + WithCheckPointStore(store), + ) + assert.NoError(t, err) + + canceledCtx, cancel := WithGraphInterrupt(ctx) + go func() { + time.Sleep(100 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + + _, err = r.Invoke(canceledCtx, "test", WithCheckPointID("cp1")) + assert.NotNil(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok, "Expected interrupt error, got: %v", err) + if len(info.SubGraphs) > 0 { + assert.Contains(t, info.SubGraphs, "2") + subInfo := info.SubGraphs["2"] + assert.Equal(t, []string{"sub1"}, subInfo.RerunNodes) + } else { + assert.Equal(t, []string{"2"}, info.RerunNodes) + } + + mu.Lock() + assert.Equal(t, "test_main", receivedInput) + mu.Unlock() + + result, err := r.Invoke(ctx, "", WithCheckPointID("cp1")) + assert.NoError(t, err) + assert.Equal(t, "test_main_sub_processed", result) + + mu.Lock() + assert.Equal(t, "test_main", receivedInput) + assert.Equal(t, 2, callCount) + mu.Unlock() +} + +type longRunningToolInput struct { + Input string `json:"input"` +} + +func TestToolsNodeWithExternalGraphInterrupt(t *testing.T) { + store := newInMemoryStore() + ctx := context.Background() + + var mu sync.Mutex + var callCount int + + longRunningToolInfo := &schema.ToolInfo{ + Name: "long_running_tool", + Desc: "A tool that takes a long time to run", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: "string", Desc: "input"}, + }), + } + + longRunningTool := newCheckpointTestTool(longRunningToolInfo, func(ctx context.Context, in *longRunningToolInput) (string, error) { + mu.Lock() + callCount++ + currentCount := callCount + mu.Unlock() + + if currentCount == 1 { + time.Sleep(2 * time.Second) + } + return "result_" + in.Input, nil + }) + + toolsNode, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{longRunningTool}, + }) + assert.NoError(t, err) + + g := NewGraph[*schema.Message, []*schema.Message]() + err = g.AddToolsNode("tools", toolsNode) + assert.NoError(t, err) + err = g.AddEdge(START, "tools") + assert.NoError(t, err) + err = g.AddEdge("tools", END) + assert.NoError(t, err) + + r, err := g.Compile(ctx, + WithNodeTriggerMode(AllPredecessor), + WithCheckPointStore(store), + ) + assert.NoError(t, err) + + inputMsg := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{{ + ID: "call_1", + Type: "function", + Function: schema.FunctionCall{ + Name: "long_running_tool", + Arguments: `{"input": "test"}`, + }, + }}, + } + + canceledCtx, cancel := WithGraphInterrupt(ctx) + go func() { + time.Sleep(100 * time.Millisecond) + cancel(WithGraphInterruptTimeout(0)) + }() + + _, err = r.Invoke(canceledCtx, inputMsg, WithCheckPointID("cp1")) + assert.Error(t, err) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok, "Expected interrupt error, got: %v", err) + if ok { + assert.Equal(t, []string{"tools"}, info.RerunNodes) + } + + result, err := r.Invoke(ctx, &schema.Message{}, WithCheckPointID("cp1")) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, `"result_test"`, result[0].Content) + + mu.Lock() + assert.Equal(t, 2, callCount) + mu.Unlock() +} + +type checkpointTestTool[I, O any] struct { + info *schema.ToolInfo + fn func(ctx context.Context, in I) (O, error) +} + +func newCheckpointTestTool[I, O any](info *schema.ToolInfo, f func(ctx context.Context, in I) (O, error)) tool.InvokableTool { + return &checkpointTestTool[I, O]{ + info: info, + fn: f, + } +} + +func (f *checkpointTestTool[I, O]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return f.info, nil +} + +func (f *checkpointTestTool[I, O]) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + t := generic.NewInstance[I]() + err := sonic.UnmarshalString(argumentsInJSON, t) + if err != nil { + return "", err + } + o, err := f.fn(ctx, t) + if err != nil { + return "", err + } + return sonic.MarshalString(o) +} diff --git a/compose/component_to_graph_node.go b/compose/component_to_graph_node.go new file mode 100644 index 0000000..4bd27fe --- /dev/null +++ b/compose/component_to_graph_node.go @@ -0,0 +1,212 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" +) + +func toComponentNode[I, O, TOption any]( + node any, + componentType component, + invoke Invoke[I, O, TOption], + stream Stream[I, O, TOption], + collect Collect[I, O, TOption], + transform Transform[I, O, TOption], + opts ...GraphAddNodeOpt, +) (*graphNode, *graphAddNodeOpts) { + meta := parseExecutorInfoFromComponent(componentType, node) + info, options := getNodeInfo(opts...) + run := runnableLambda(invoke, stream, collect, transform, + !meta.isComponentCallbackEnabled, + ) + + gn := toNode(info, run, nil, meta, node, opts...) + + return gn, options +} + +func toEmbeddingNode(node embedding.Embedder, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfEmbedding, + node.EmbedStrings, + nil, + nil, + nil, + opts...) +} + +func toRetrieverNode(node retriever.Retriever, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfRetriever, + node.Retrieve, + nil, + nil, + nil, + opts...) +} + +func toLoaderNode(node document.Loader, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfLoader, + node.Load, + nil, + nil, + nil, + opts...) +} + +func toIndexerNode(node indexer.Indexer, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfIndexer, + node.Store, + nil, + nil, + nil, + opts...) +} + +func toChatModelNode(node model.BaseChatModel, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfChatModel, + node.Generate, + node.Stream, + nil, + nil, + opts...) +} + +func toAgenticModelNode(node model.AgenticModel, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfAgenticModel, + node.Generate, + node.Stream, + nil, nil, + opts..., + ) +} + +func toChatTemplateNode(node prompt.ChatTemplate, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfPrompt, + node.Format, + nil, + nil, + nil, + opts...) +} + +func toAgenticChatTemplateNode(node prompt.AgenticChatTemplate, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfAgenticPrompt, + node.Format, + nil, nil, nil, + opts..., + ) +} + +func toDocumentTransformerNode(node document.Transformer, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + components.ComponentOfTransformer, + node.Transform, + nil, + nil, + nil, + opts...) +} + +func toToolsNode(node *ToolsNode, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + ComponentOfToolsNode, + node.Invoke, + node.Stream, + nil, + nil, + opts...) +} + +func toAgenticToolsNode(node *AgenticToolsNode, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + return toComponentNode( + node, + ComponentOfAgenticToolsNode, + node.Invoke, + node.Stream, + nil, nil, + opts..., + ) +} + +func toLambdaNode(node *Lambda, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + info, options := getNodeInfo(opts...) + + gn := toNode(info, node.executor, nil, node.executor.meta, node, opts...) + + return gn, options +} + +func toAnyGraphNode(node AnyGraph, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + meta := parseExecutorInfoFromComponent(node.component(), node) + info, options := getNodeInfo(opts...) + + gn := toNode(info, nil, node, meta, node, opts...) + + return gn, options +} + +func toPassthroughNode(opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) { + node := composablePassthrough() + info, options := getNodeInfo(opts...) + gn := toNode(info, node, nil, node.meta, node, opts...) + return gn, options +} + +func toNode(nodeInfo *nodeInfo, executor *composableRunnable, graph AnyGraph, + meta *executorMeta, instance any, opts ...GraphAddNodeOpt) *graphNode { + + if meta == nil { + meta = &executorMeta{} + } + + gn := &graphNode{ + nodeInfo: nodeInfo, + + cr: executor, + g: graph, + executorMeta: meta, + + instance: instance, + opts: opts, + } + + return gn +} diff --git a/compose/dag.go b/compose/dag.go new file mode 100644 index 0000000..98b85ae --- /dev/null +++ b/compose/dag.go @@ -0,0 +1,195 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "fmt" +) + +func dagChannelBuilder(controlDependencies []string, dataDependencies []string, zeroValue func() any, emptyStream func() streamReader) channel { + deps := make(map[string]dependencyState, len(controlDependencies)) + for _, dep := range controlDependencies { + deps[dep] = dependencyStateWaiting + } + indirect := make(map[string]bool, len(dataDependencies)) + for _, dep := range dataDependencies { + indirect[dep] = false + } + + return &dagChannel{ + Values: make(map[string]any), + ControlPredecessors: deps, + DataPredecessors: indirect, + zeroValue: zeroValue, + emptyStream: emptyStream, + } +} + +type dependencyState uint8 + +const ( + dependencyStateWaiting dependencyState = iota + dependencyStateReady + dependencyStateSkipped +) + +type dagChannel struct { + zeroValue func() any + emptyStream func() streamReader + + ControlPredecessors map[string]dependencyState + Values map[string]any + DataPredecessors map[string]bool // if all dependencies have been skipped, indirect dependencies won't effect. + Skipped bool + + mergeConfig FanInMergeConfig +} + +func (ch *dagChannel) setMergeConfig(cfg FanInMergeConfig) { + ch.mergeConfig.StreamMergeWithSourceEOF = cfg.StreamMergeWithSourceEOF +} + +func (ch *dagChannel) load(c channel) error { + dc, ok := c.(*dagChannel) + if !ok { + return fmt.Errorf("load dag channel fail, got %T, want *dagChannel", c) + } + ch.ControlPredecessors = dc.ControlPredecessors + ch.DataPredecessors = dc.DataPredecessors + ch.Skipped = dc.Skipped + ch.Values = dc.Values + return nil +} + +func (ch *dagChannel) reportValues(ins map[string]any) error { + if ch.Skipped { + return nil + } + + for k, v := range ins { + if _, ok := ch.DataPredecessors[k]; !ok { + continue + } + ch.DataPredecessors[k] = true + ch.Values[k] = v + } + return nil +} + +func (ch *dagChannel) reportDependencies(dependencies []string) { + if ch.Skipped { + return + } + + for _, dep := range dependencies { + if _, ok := ch.ControlPredecessors[dep]; ok { + ch.ControlPredecessors[dep] = dependencyStateReady + } + } + return +} + +func (ch *dagChannel) reportSkip(keys []string) bool { + for _, k := range keys { + if _, ok := ch.ControlPredecessors[k]; ok { + ch.ControlPredecessors[k] = dependencyStateSkipped + } + if _, ok := ch.DataPredecessors[k]; ok { + ch.DataPredecessors[k] = true + } + } + + allSkipped := true + for _, state := range ch.ControlPredecessors { + if state != dependencyStateSkipped { + allSkipped = false + break + } + } + ch.Skipped = allSkipped + + return allSkipped +} + +func (ch *dagChannel) get(isStream bool, name string, edgeHandler *edgeHandlerManager) ( + any, bool, error) { + if ch.Skipped { + return nil, false, nil + } + + if len(ch.ControlPredecessors)+len(ch.DataPredecessors) == 0 { + return nil, false, nil + } + + for _, state := range ch.ControlPredecessors { + if state == dependencyStateWaiting { + return nil, false, nil + } + } + for _, ready := range ch.DataPredecessors { + if !ready { + return nil, false, nil + } + } + + defer func() { + ch.Values = make(map[string]any) + for k := range ch.ControlPredecessors { + ch.ControlPredecessors[k] = dependencyStateWaiting + } + for k := range ch.DataPredecessors { + ch.DataPredecessors[k] = false + } + }() + + valueList := make([]any, len(ch.Values)) + names := make([]string, len(ch.Values)) + i := 0 + for k, value := range ch.Values { + resolvedV, err := edgeHandler.handle(k, name, value, isStream) + if err != nil { + return nil, false, err + } + valueList[i] = resolvedV + names[i] = k + i++ + } + + if len(valueList) == 0 { + if isStream { + return ch.emptyStream(), true, nil + } + return ch.zeroValue(), true, nil + } + if len(valueList) == 1 { + return valueList[0], true, nil + } + + mergeOpts := &mergeOptions{ + streamMergeWithSourceEOF: ch.mergeConfig.StreamMergeWithSourceEOF, + names: names, + } + v, err := mergeValues(valueList, mergeOpts) + if err != nil { + return nil, false, err + } + return v, true, nil +} + +func (ch *dagChannel) convertValues(fn func(map[string]any) error) error { + return fn(ch.Values) +} diff --git a/compose/dag_test.go b/compose/dag_test.go new file mode 100644 index 0000000..6185ec4 --- /dev/null +++ b/compose/dag_test.go @@ -0,0 +1,223 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "io" + "testing" +) + +func TestDAG(t *testing.T) { + var err error + + g := NewGraph[string, string]() + err = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithOutputKey("1")) + if err != nil { + t.Fatal(err) + } + + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithOutputKey("2")) + if err != nil { + t.Fatal(err) + } + + err = g.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input map[string]any) (output string, err error) { + if _, ok := input["1"]; !ok { + return "", fmt.Errorf("node 1 output fail: %+v", input) + } + if _, ok := input["2"]; !ok { + return "", fmt.Errorf("node 2 output fail: %+v", input) + } + return input["1"].(string) + input["2"].(string), nil + })) + if err != nil { + t.Fatal(err) + } + + err = g.AddLambdaNode("4", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + if err != nil { + t.Fatal(err) + } + + err = g.AddLambdaNode("5", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + if err != nil { + t.Fatal(err) + } + + err = g.AddLambdaNode("6", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithOutputKey("6")) + if err != nil { + t.Fatal(err) + } + + err = g.AddLambdaNode("7", InvokableLambda(func(ctx context.Context, input map[string]any) (output string, err error) { + if _, ok := input["1"]; !ok { + return "", fmt.Errorf("7:node 1 output fail: %+v", input) + } + if _, ok := input["6"]; !ok { + return "", fmt.Errorf("7:node 6 output fail: %+v", input) + } + return input["1"].(string) + input["6"].(string), nil + })) + if err != nil { + t.Fatal(err) + } + + err = g.AddEdge("1", "3") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("2", "3") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("3", "4") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("4", "5") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("4", "6") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("6", "7") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", "7") + if err != nil { + t.Fatal(err) + } + + err = g.AddEdge(START, "1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "2") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("7", END) + if err != nil { + t.Fatal(err) + } + + runner, err := g.Compile(context.Background(), WithNodeTriggerMode(AllPredecessor)) + if err != nil { + t.Fatal(err) + } + + // success + ctx := context.Background() + out, err := runner.Invoke(ctx, "hello") + if err != nil { + t.Fatal(err) + } + if out != "hellohellohello" { + t.Fatalf("node7 fail") + } + + result, err := runner.Invoke(ctx, "1") + if err != nil { + t.Fatal(err) + } + if result != "111" { + t.Fatalf("runner invoke fail, output: %s", result) + } + streamResult, err := runner.Stream(ctx, "1") + if err != nil { + t.Fatal(err) + } + defer streamResult.Close() + ret := "" + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + ret += chunk + } + if ret != "111" { + t.Fatalf("runner stream fail, output: %s", ret) + } + + // loop + gg := NewGraph[string, map[string]any]() + err = gg.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithOutputKey("1")) + if err != nil { + t.Fatal(err) + } + + err = gg.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input map[string]any) (output string, err error) { + return input["1"].(string), nil + })) + if err != nil { + t.Fatal(err) + } + + err = gg.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithOutputKey("3")) + if err != nil { + t.Fatal(err) + } + + err = gg.AddEdge("1", "2") + if err != nil { + t.Fatal(err) + } + err = gg.AddEdge("2", "3") + if err != nil { + t.Fatal(err) + } + err = gg.AddEdge("3", "2") + if err != nil { + t.Fatal(err) + } + err = gg.AddEdge(START, "1") + if err != nil { + t.Fatal(err) + } + err = gg.AddEdge("3", END) + if err != nil { + t.Fatal(err) + } + + _, err = gg.compile(ctx, &graphCompileOptions{nodeTriggerMode: AllPredecessor}) + if err == nil { + t.Fatal("cannot validate loop") + } +} diff --git a/compose/doc.go b/compose/doc.go new file mode 100644 index 0000000..5057f46 --- /dev/null +++ b/compose/doc.go @@ -0,0 +1,19 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package compose provides graph and workflow primitives to build +// composable, interruptible execution pipelines with callback support. +package compose diff --git a/compose/error.go b/compose/error.go new file mode 100644 index 0000000..18d7078 --- /dev/null +++ b/compose/error.go @@ -0,0 +1,111 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +// ErrExceedMaxSteps graph will throw this error when the number of steps exceeds the maximum number of steps. +var ErrExceedMaxSteps = errors.New("exceeds max steps") + +func newUnexpectedInputTypeErr(expected reflect.Type, got reflect.Type) error { + return fmt.Errorf("unexpected input type. expected: %v, got: %v", expected, got) +} + +type defaultImplAction string + +const ( + actionInvokeByStream defaultImplAction = "InvokeByStream" + actionInvokeByCollect defaultImplAction = "InvokeByCollect" + actionInvokeByTransform defaultImplAction = "InvokeByTransform" + actionStreamByInvoke defaultImplAction = "StreamByInvoke" + actionStreamByTransform defaultImplAction = "StreamByTransform" + actionStreamByCollect defaultImplAction = "StreamByCollect" + actionCollectByTransform defaultImplAction = "CollectByTransform" + actionCollectByInvoke defaultImplAction = "CollectByInvoke" + actionCollectByStream defaultImplAction = "CollectByStream" + actionTransformByStream defaultImplAction = "TransformByStream" + actionTransformByCollect defaultImplAction = "TransformByCollect" + actionTransformByInvoke defaultImplAction = "TransformByInvoke" +) + +func newStreamReadError(err error) error { + return fmt.Errorf("failed to read from stream. error: %w", err) +} + +func newGraphRunError(err error) error { + return &internalError{ + typ: internalErrorTypeGraphRun, + nodePath: NodePath{}, + origError: err, + } +} + +func wrapGraphNodeError(nodeKey string, err error) error { + if ok := isInterruptError(err); ok { + return err + } + var ie *internalError + ok := errors.As(err, &ie) + if !ok { + return &internalError{ + typ: internalErrorTypeNodeRun, + nodePath: NodePath{path: []string{nodeKey}}, + origError: err, + } + } + ie.nodePath.path = append([]string{nodeKey}, ie.nodePath.path...) + return ie +} + +type internalErrorType string + +const ( + internalErrorTypeNodeRun = "NodeRunError" + internalErrorTypeGraphRun = "GraphRunError" +) + +type internalError struct { + typ internalErrorType + nodePath NodePath + origError error +} + +func (i *internalError) Error() string { + sb := strings.Builder{} + sb.WriteString(string("[" + i.typ + "] ")) + sb.WriteString(i.origError.Error()) + if len(i.nodePath.path) > 0 { + sb.WriteString("\n------------------------\n") + sb.WriteString("node path: [") + for j := 0; j < len(i.nodePath.path)-1; j++ { + sb.WriteString(i.nodePath.path[j] + ", ") + } + sb.WriteString(i.nodePath.path[len(i.nodePath.path)-1]) + sb.WriteString("]") + } + sb.WriteString("") + return sb.String() +} + +func (i *internalError) Unwrap() error { + return i.origError +} diff --git a/compose/error_test.go b/compose/error_test.go new file mode 100644 index 0000000..e7f140b --- /dev/null +++ b/compose/error_test.go @@ -0,0 +1,137 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestCommonError(t *testing.T) { + g := NewGraph[string, string]() + assert.NoError(t, g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", errors.New("my error") + }))) + assert.NoError(t, g.AddEdge(START, "1")) + assert.NoError(t, g.AddEdge("1", END)) + + ctx := context.Background() + r, err := g.Compile(ctx) + assert.NoError(t, err) + + // node error + _, err = r.Invoke(ctx, "input") + var ie *internalError + + assert.True(t, errors.As(err, &ie)) + assert.Equal(t, "my error", ie.origError.Error()) + + // wrapper error + sr, sw := schema.Pipe[string](0) + sw.Close() + _, err = r.Transform(ctx, sr) + assert.True(t, errors.As(err, &ie)) + assert.ErrorContains(t, ie.origError, "stream reader is empty, concat fail") + assert.Equal(t, []string{"1"}, ie.nodePath.path) + println(err.Error()) +} + +func TestSubGraphNodeError(t *testing.T) { + subG := NewGraph[string, string]() + assert.NoError(t, subG.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", errors.New("my error") + }))) + assert.NoError(t, subG.AddEdge(START, "1")) + assert.NoError(t, subG.AddEdge("1", END)) + + g := NewGraph[string, string]() + assert.NoError(t, g.AddGraphNode("a", subG)) + assert.NoError(t, g.AddEdge(START, "a")) + assert.NoError(t, g.AddEdge("a", END)) + + ctx := context.Background() + r, err := g.Compile(ctx) + assert.NoError(t, err) + _, err = r.Invoke(ctx, "input") + var ie *internalError + assert.True(t, errors.As(err, &ie)) + assert.Equal(t, "my error", ie.origError.Error()) + assert.Equal(t, []string{"a", "1"}, ie.nodePath.path) +} + +func TestContextCancelDuringRun(t *testing.T) { + // Create a graph with a long-running node to test context cancellation + g := NewGraph[string, string]() + + // Add a node that waits for some time (long enough to be cancelled) + assert.NoError(t, g.AddLambdaNode("slow_node", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + select { + case <-ctx.Done(): + // Return context's error when cancelled + return "", ctx.Err() + case <-time.After(200 * time.Millisecond): + return input + "_processed", nil + } + }))) + + assert.NoError(t, g.AddEdge(START, "slow_node")) + assert.NoError(t, g.AddEdge("slow_node", END)) + + // Create a context that we can cancel + ctx, cancel := context.WithCancel(context.Background()) + + // Compile the graph + r, err := g.Compile(ctx) + assert.NoError(t, err) + + // Run the invoke in a goroutine + resultCh := make(chan error) + go func() { + _, err := r.Invoke(ctx, "input") + resultCh <- err + }() + + // Cancel the context after a short delay + time.Sleep(50 * time.Millisecond) + cancel() + + // Get the result + err = <-resultCh + + // Verify the error is related to context cancellation + assert.Error(t, err) + + // Check error type and content + var ie *internalError + assert.True(t, errors.As(err, &ie)) + + // Error path should contain the node + assert.Equal(t, []string{"slow_node"}, ie.nodePath.path) + + // Original error should be context.Canceled + assert.ErrorIs(t, ie.origError, context.Canceled) + + // Test unwrap capability + unwrappedErr := ie.Unwrap() + assert.ErrorIs(t, unwrappedErr, context.Canceled) +} diff --git a/compose/field_mapping.go b/compose/field_mapping.go new file mode 100644 index 0000000..aaf8de7 --- /dev/null +++ b/compose/field_mapping.go @@ -0,0 +1,774 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "errors" + "fmt" + "reflect" + "runtime/debug" + "strings" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +type FieldMapping struct { + fromNodeKey string + from string + to string + + customExtractor func(input any) (any, error) +} + +// String returns the string representation of the FieldMapping. +func (m *FieldMapping) String() string { + var sb strings.Builder + sb.WriteString("[from ") + + if m.from != "" { + sb.WriteString(m.from) + sb.WriteString("(field) of ") + } + + sb.WriteString(m.fromNodeKey) + + if m.to != "" { + sb.WriteString(" to ") + sb.WriteString(m.to) + sb.WriteString("(field)") + } + + sb.WriteString("]") + return sb.String() +} + +// FromField creates a FieldMapping that maps a single predecessor field to the entire successor input. +// This is an exclusive mapping - once set, no other field mappings can be added since the successor input +// has already been fully mapped. +// Field: either the field of a struct, or the key of a map. +func FromField(from string) *FieldMapping { + return &FieldMapping{ + from: from, + } +} + +// ToField creates a FieldMapping that maps the entire predecessor output to a single successor field. +// Field: either the field of a struct, or the key of a map. +func ToField(to string, opts ...FieldMappingOption) *FieldMapping { + fm := &FieldMapping{ + to: to, + } + for _, opt := range opts { + opt(fm) + } + return fm +} + +// MapFields creates a FieldMapping that maps a single predecessor field to a single successor field. +// Field: either the field of a struct, or the key of a map. +func MapFields(from, to string) *FieldMapping { + return &FieldMapping{ + from: from, + to: to, + } +} + +func (m *FieldMapping) FromNodeKey() string { + return m.fromNodeKey +} + +func (m *FieldMapping) FromPath() FieldPath { + return splitFieldPath(m.from) +} + +func (m *FieldMapping) ToPath() FieldPath { + return splitFieldPath(m.to) +} + +func (m *FieldMapping) Equals(o *FieldMapping) bool { + if m == nil { + return o == nil + } + + if o == nil || m.customExtractor != nil || o.customExtractor != nil { + return false + } + + return m.from == o.from && m.to == o.to && m.fromNodeKey == o.fromNodeKey +} + +// FieldPath represents a path to a nested field in a struct or map. +// Each element in the path is either: +// - a struct field name +// - a map key +// +// Example paths: +// - []string{"user"} // top-level field +// - []string{"user", "name"} // nested struct field +// - []string{"users", "admin"} // map key access +type FieldPath []string + +func (fp *FieldPath) join() string { + return strings.Join(*fp, pathSeparator) +} + +func splitFieldPath(path string) FieldPath { + p := strings.Split(path, pathSeparator) + if len(p) == 1 && p[0] == "" { + return FieldPath{} + } + + return p +} + +// pathSeparator is a special character (Unit Separator) used internally to join path elements. +// This character is chosen because it's extremely unlikely to appear in user-defined field names or map keys. +const pathSeparator = "\x1F" + +// FromFieldPath creates a FieldMapping that maps a single predecessor field path to the entire successor input. +// This is an exclusive mapping - once set, no other field mappings can be added since the successor input +// has already been fully mapped. +// +// Example: +// +// // Maps the 'name' field from nested 'user.profile' to the entire successor input +// FromFieldPath(FieldPath{"user", "profile", "name"}) +// +// Note: The field path elements must not contain the internal path separator character ('\x1F'). +func FromFieldPath(fromFieldPath FieldPath) *FieldMapping { + return &FieldMapping{ + from: fromFieldPath.join(), + } +} + +// ToFieldPath creates a FieldMapping that maps the entire predecessor output to a single successor field path. +// +// Example: +// +// // Maps the entire predecessor output to response.data.userName +// ToFieldPath(FieldPath{"response", "data", "userName"}) +// +// Note: The field path elements must not contain the internal path separator character ('\x1F'). +func ToFieldPath(toFieldPath FieldPath, opts ...FieldMappingOption) *FieldMapping { + fm := &FieldMapping{ + to: toFieldPath.join(), + } + for _, opt := range opts { + opt(fm) + } + return fm +} + +// MapFieldPaths creates a FieldMapping that maps a single predecessor field path to a single successor field path. +// +// Example: +// +// // Maps user.profile.name to response.userName +// MapFieldPaths( +// FieldPath{"user", "profile", "name"}, +// FieldPath{"response", "userName"}, +// ) +// +// Note: The field path elements must not contain the internal path separator character ('\x1F'). +func MapFieldPaths(fromFieldPath, toFieldPath FieldPath) *FieldMapping { + return &FieldMapping{ + from: fromFieldPath.join(), + to: toFieldPath.join(), + } +} + +// FieldMappingOption is a functional option for configuring a FieldMapping. +type FieldMappingOption func(*FieldMapping) + +// WithCustomExtractor sets a custom extractor function for the FieldMapping. +// The extractor function is used to extract a value from the 'source' of the FieldMapping. +// NOTE: if specified in this way, Eino can only check the validity of the field mapping at request time.. +func WithCustomExtractor(extractor func(input any) (any, error)) FieldMappingOption { + return func(m *FieldMapping) { + m.customExtractor = extractor + } +} + +func (m *FieldMapping) targetPath() FieldPath { + return splitFieldPath(m.to) +} + +func buildFieldMappingConverter[I any]() func(input any) (any, error) { + return func(input any) (any, error) { + in, ok := input.(map[string]any) + if !ok { + panic(newUnexpectedInputTypeErr(reflect.TypeOf(map[string]any{}), reflect.TypeOf(input))) + } + + return convertTo(in, generic.TypeOf[I]()), nil + } +} + +func buildStreamFieldMappingConverter[I any]() func(input streamReader) streamReader { + return func(input streamReader) streamReader { + s, ok := unpackStreamReader[map[string]any](input) + if !ok { + panic("mappingStreamAssign incoming streamReader chunk type not map[string]any") + } + + return packStreamReader(schema.StreamReaderWithConvert(s, func(v map[string]any) (I, error) { + t := convertTo(v, generic.TypeOf[I]()) + return t.(I), nil + })) + } +} + +func convertTo(mappings map[string]any, typ reflect.Type) any { + tValue := newInstanceByType(typ) + if !tValue.CanAddr() { + tValue = newInstanceByType(reflect.PointerTo(typ)).Elem() + } + + for mapping, taken := range mappings { + tValue = assignOne(tValue, taken, mapping) + } + + return tValue.Interface() +} + +func assignOne(destValue reflect.Value, taken any, to string) reflect.Value { + if len(to) == 0 { // assign to output directly + destValue.Set(reflect.ValueOf(taken)) + return destValue + } + + var ( + toPaths = splitFieldPath(to) + originalDestValue = destValue + parentMap reflect.Value + parentKey string + ) + + for { + path := toPaths[0] + toPaths = toPaths[1:] + if len(toPaths) == 0 { + toSet := reflect.ValueOf(taken) + + if destValue.Type() == reflect.TypeOf((*any)(nil)).Elem() { + existingMap, ok := destValue.Interface().(map[string]any) + if ok { + destValue = reflect.ValueOf(existingMap) + } else { + mapValue := reflect.MakeMap(reflect.TypeOf(map[string]any{})) + destValue.Set(mapValue) + destValue = mapValue + } + } + + if destValue.Kind() == reflect.Map { + key := reflect.ValueOf(path) + keyType := destValue.Type().Key() + if keyType != strType { + key = key.Convert(keyType) + } + + if !toSet.IsValid() { + toSet = reflect.Zero(destValue.Type().Elem()) + } + destValue.SetMapIndex(key, toSet) + + if parentMap.IsValid() { + parentMap.SetMapIndex(reflect.ValueOf(parentKey), destValue) + } + + return originalDestValue + } + + ptrValue := destValue + for destValue.Kind() == reflect.Ptr { + destValue = destValue.Elem() + } + + if !toSet.IsValid() { + // just skip it, because this 'nil' is the zero value of the corresponding struct field + } else { + field := destValue.FieldByName(path) + field.Set(toSet) + } + + if parentMap.IsValid() { + parentMap.SetMapIndex(reflect.ValueOf(parentKey), ptrValue) + } + + return originalDestValue + } + + if destValue.Type() == reflect.TypeOf((*any)(nil)).Elem() { + existingMap, ok := destValue.Interface().(map[string]any) + if ok { + destValue = reflect.ValueOf(existingMap) + } else { + mapValue := reflect.MakeMap(reflect.TypeOf(map[string]any{})) + destValue.Set(mapValue) + destValue = mapValue + } + } + + if destValue.Kind() == reflect.Map { + keyValue := reflect.ValueOf(path) + valueValue := destValue.MapIndex(keyValue) + if !valueValue.IsValid() { + valueValue = newInstanceByType(destValue.Type().Elem()) + destValue.SetMapIndex(keyValue, valueValue) + } + + if parentMap.IsValid() { + parentMap.SetMapIndex(reflect.ValueOf(parentKey), destValue) + } + + parentMap = destValue + parentKey = path + destValue = valueValue + + continue + } + + ptrValue := destValue + for destValue.Kind() == reflect.Ptr { + destValue = destValue.Elem() + } + + field := destValue.FieldByName(path) + instantiateIfNeeded(field) + + if parentMap.IsValid() { + parentMap.SetMapIndex(reflect.ValueOf(parentKey), ptrValue) + parentMap = reflect.Value{} + parentKey = "" + } + + destValue = field + } +} + +func instantiateIfNeeded(field reflect.Value) { + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.Kind() == reflect.Map { + if field.IsNil() { + field.Set(reflect.MakeMap(field.Type())) + } + } +} + +func newInstanceByType(typ reflect.Type) reflect.Value { + switch typ.Kind() { + case reflect.Map: + return reflect.MakeMap(typ) + case reflect.Slice, reflect.Array: + slice := reflect.New(typ).Elem() + slice.Set(reflect.MakeSlice(typ, 0, 0)) + return slice + case reflect.Ptr: + typ = typ.Elem() + origin := reflect.New(typ) + nested := newInstanceByType(typ) + origin.Elem().Set(nested) + + return origin + default: + return reflect.New(typ).Elem() + } +} + +func checkAndExtractFromField(fromField string, input reflect.Value) (reflect.Value, error) { + f := input.FieldByName(fromField) + if !f.IsValid() { + return reflect.Value{}, fmt.Errorf("field mapping from a struct field, but field not found. field=%v, inputType=%v", fromField, input.Type()) + } + + if !f.CanInterface() { + return reflect.Value{}, fmt.Errorf("field mapping from a struct field, but field not exported. field= %v, inputType=%v", fromField, input.Type()) + } + + return f, nil +} + +type errMapKeyNotFound struct { + mapKey string +} + +func (e *errMapKeyNotFound) Error() string { + return fmt.Sprintf("key=%s", e.mapKey) +} + +type errInterfaceNotValidForFieldMapping struct { + interfaceType reflect.Type + actualType reflect.Type +} + +func (e *errInterfaceNotValidForFieldMapping) Error() string { + return fmt.Sprintf("field mapping from an interface type, but actual type is not struct, struct ptr or map. InterfaceType= %v, ActualType= %v", e.interfaceType, e.actualType) +} + +func checkAndExtractFromMapKey(fromMapKey string, input reflect.Value) (reflect.Value, error) { + key := reflect.ValueOf(fromMapKey) + if input.Type().Key() != strType { + key = key.Convert(input.Type().Key()) + } + + v := input.MapIndex(key) + if !v.IsValid() { + return reflect.Value{}, fmt.Errorf("field mapping from a map key, but key not found in input. %w", &errMapKeyNotFound{mapKey: fromMapKey}) + } + + return v, nil +} + +func checkAndExtractFieldType(paths []string, typ reflect.Type) (extracted reflect.Type, remainingPaths FieldPath, err error) { + extracted = typ + for i, field := range paths { + for extracted.Kind() == reflect.Ptr { + extracted = extracted.Elem() + } + + if extracted.Kind() == reflect.Map { + if !strType.ConvertibleTo(extracted.Key()) { + return nil, nil, fmt.Errorf("type[%v] is not a map with string or string alias key", extracted) + } + + extracted = extracted.Elem() + continue + } + + if extracted.Kind() == reflect.Struct { + f, ok := extracted.FieldByName(field) + if !ok { + return nil, nil, fmt.Errorf("type[%v] has no field[%s]", extracted, field) + } + + if !f.IsExported() { + return nil, nil, fmt.Errorf("type[%v] has an unexported field[%s]", extracted.String(), field) + } + + extracted = f.Type + continue + } + + if extracted.Kind() == reflect.Interface { + return extracted, paths[i:], nil + } + + return nil, nil, fmt.Errorf("intermediate type[%v] is not valid", extracted) + } + + return extracted, nil, nil +} + +var strType = reflect.TypeOf("") + +func fieldMap(mappings []*FieldMapping, allowMapKeyNotFound bool, uncheckedSourcePaths map[string]FieldPath) func(any) (map[string]any, error) { + return func(input any) (result map[string]any, err error) { + result = make(map[string]any, len(mappings)) + var inputValue reflect.Value + loop: + for _, mapping := range mappings { + if mapping.customExtractor != nil { + result[mapping.to], err = mapping.customExtractor(input) + if err != nil { + return nil, err + } + continue + } + + if len(mapping.from) == 0 { + result[mapping.to] = input + continue + } + + fromPath := splitFieldPath(mapping.from) + + if !inputValue.IsValid() { + inputValue = reflect.ValueOf(input) + } + + var ( + pathInputValue = inputValue + pathInputType = inputValue.Type() + taken = input + ) + + for i, path := range fromPath { + for pathInputValue.Kind() == reflect.Ptr { + pathInputValue = pathInputValue.Elem() + } + + if !pathInputValue.IsValid() { + return nil, fmt.Errorf("intermediate source value on path=%v is nil for type [%v]", fromPath[:i+1], pathInputType) + } + + if pathInputValue.Kind() == reflect.Map && pathInputValue.IsNil() { + return nil, fmt.Errorf("intermediate source value on path=%v is nil for map type [%v]", fromPath[:i+1], pathInputType) + } + + taken, pathInputType, err = takeOne(pathInputValue, pathInputType, path) + if err != nil { + // we deferred check from Compile time to request time for interface types, so we won't panic here + var interfaceNotValidErr *errInterfaceNotValidForFieldMapping + if errors.As(err, &interfaceNotValidErr) { + return nil, err + } + + // map key not found can only be a request time error, so we won't panic here + var mapKeyNotFoundErr *errMapKeyNotFound + if errors.As(err, &mapKeyNotFoundErr) { + if allowMapKeyNotFound { + continue loop + } + return nil, err + } + + if uncheckedSourcePaths != nil { + uncheckedPath, ok := uncheckedSourcePaths[mapping.from] + if ok && len(uncheckedPath) >= len(fromPath)-i { + // the err happens on the mapping source path which is unchecked at request time, so we won't panic here + return nil, err + } + } + + panic(safe.NewPanicErr(err, debug.Stack())) + } + + if i < len(fromPath)-1 { + pathInputValue = reflect.ValueOf(taken) + } + } + + result[mapping.to] = taken + } + + return result, nil + } +} + +func streamFieldMap(mappings []*FieldMapping, uncheckedSourcePaths map[string]FieldPath) func(streamReader) streamReader { + return func(input streamReader) streamReader { + return packStreamReader(schema.StreamReaderWithConvert(input.toAnyStreamReader(), fieldMap(mappings, true, uncheckedSourcePaths))) + } +} + +func takeOne(inputValue reflect.Value, inputType reflect.Type, from string) (taken any, takenType reflect.Type, err error) { + var f reflect.Value + switch k := inputValue.Kind(); k { + case reflect.Map: + f, err = checkAndExtractFromMapKey(from, inputValue) + if err != nil { + return nil, nil, err + } + + return f.Interface(), f.Type(), nil + case reflect.Struct: + f, err = checkAndExtractFromField(from, inputValue) + if err != nil { + return nil, nil, err + } + + return f.Interface(), f.Type(), nil + default: + if inputType.Kind() == reflect.Interface { + return nil, nil, &errInterfaceNotValidForFieldMapping{ + interfaceType: inputType, + actualType: inputValue.Type(), + } + } + + panic("when take one value from source, value not map or struct, and type not interface") + } +} + +func isFromAll(mappings []*FieldMapping) bool { + for _, mapping := range mappings { + if len(mapping.from) == 0 && mapping.customExtractor == nil { + return true + } + } + return false +} + +func fromFields(mappings []*FieldMapping) bool { + for _, mapping := range mappings { + if len(mapping.from) == 0 || mapping.customExtractor != nil { + return false + } + } + + return true +} + +func isToAll(mappings []*FieldMapping) bool { + for _, mapping := range mappings { + if len(mapping.to) == 0 { + return true + } + } + return false +} + +func validateStructOrMap(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return true + case reflect.Ptr: + t = t.Elem() + fallthrough + case reflect.Struct: + return true + default: + return false + } +} + +func validateFieldMapping(predecessorType reflect.Type, successorType reflect.Type, mappings []*FieldMapping) ( + // type checkers that are deferred to request-time + typeHandler *handlerPair, + // the remaining predecessor field paths that are not checked at compile time because of interface type found + uncheckedSourcePath map[string]FieldPath, + err error) { + // check if mapping is legal + if isFromAll(mappings) && isToAll(mappings) { + // unreachable + panic(fmt.Errorf("invalid field mappings: from all fields to all, use common edge instead")) + } else if !isToAll(mappings) && (!validateStructOrMap(successorType) && successorType != reflect.TypeOf((*any)(nil)).Elem()) { + // if user has not provided a specific struct type, graph cannot construct any struct in the runtime + return nil, nil, fmt.Errorf("static check fail: successor input type should be struct or map, actual: %v", successorType) + } else if fromFields(mappings) && !validateStructOrMap(predecessorType) { + return nil, nil, fmt.Errorf("static check fail: predecessor output type should be struct or map, actual: %v", predecessorType) + } + + var fieldCheckers map[string]handlerPair + + for i := range mappings { + mapping := mappings[i] + + successorFieldType, successorRemaining, err := checkAndExtractFieldType(splitFieldPath(mapping.to), successorType) + if err != nil { + return nil, nil, fmt.Errorf("static check failed for mapping %s: %w", mapping, err) + } + + if len(successorRemaining) > 0 { + if successorFieldType == reflect.TypeOf((*any)(nil)).Elem() { + continue // at request time expand this 'any' to 'map[string]any' + } + return nil, nil, fmt.Errorf("static check failed for mapping %s, the successor has intermediate interface type %v", mapping, successorFieldType) + } + + if mapping.customExtractor != nil { // custom extractor applies to request-time data, so skip compile-time check + continue + } + + predecessorFieldType, predecessorRemaining, err := checkAndExtractFieldType(splitFieldPath(mapping.from), predecessorType) + if err != nil { + return nil, nil, fmt.Errorf("static check failed for mapping %s: %w", mapping, err) + } + + if len(predecessorRemaining) > 0 { + if uncheckedSourcePath == nil { + uncheckedSourcePath = make(map[string]FieldPath) + } + uncheckedSourcePath[mapping.from] = predecessorRemaining + } + + checker := func(a any) (any, error) { + trueInType := reflect.TypeOf(a) + if trueInType == nil { + switch successorFieldType.Kind() { + case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Interface: + default: + return nil, fmt.Errorf("runtime check failed for mapping %s, field[%v]-[%v] is absolutely not assignable", mapping, trueInType, successorFieldType) + } + } else { + if !trueInType.AssignableTo(successorFieldType) { + return nil, fmt.Errorf("runtime check failed for mapping %s, field[%v]-[%v] is absolutely not assignable", mapping, trueInType, successorFieldType) + } + } + + return a, nil + } + + if len(predecessorRemaining) > 0 { + // can't check if types match at compile time, because there is interface type at some point along the source path. Defer to request time + if fieldCheckers == nil { + fieldCheckers = make(map[string]handlerPair) + } + fieldCheckers[mapping.to] = handlerPair{ + invoke: checker, + transform: func(input streamReader) streamReader { + return packStreamReader(schema.StreamReaderWithConvert(input.toAnyStreamReader(), checker)) + }, + } + } else { + at := checkAssignable(predecessorFieldType, successorFieldType) + if at == assignableTypeMustNot { + return nil, nil, fmt.Errorf("static check failed for mapping %s, field[%v]-[%v] is absolutely not assignable", mapping, predecessorFieldType, successorFieldType) + } else if at == assignableTypeMay { + // can't decide if types match, because the successorFieldType implements predecessorFieldType, which is an interface type + if fieldCheckers == nil { + fieldCheckers = make(map[string]handlerPair) + } + fieldCheckers[mapping.to] = handlerPair{ + invoke: checker, + transform: func(input streamReader) streamReader { + return packStreamReader(schema.StreamReaderWithConvert(input.toAnyStreamReader(), checker)) + }, + } + } + } + + } + + if len(fieldCheckers) == 0 { + return nil, uncheckedSourcePath, nil + } + + checker := func(value map[string]any) (map[string]any, error) { + var err error + for k, v := range fieldCheckers { + for mapping := range value { + if mapping == k { + value[mapping], err = v.invoke(value[mapping]) + if err != nil { + return nil, err + } + } + } + } + return value, nil + } + return &handlerPair{ + invoke: func(value any) (any, error) { + return checker(value.(map[string]any)) + }, + transform: func(input streamReader) streamReader { + s, ok := unpackStreamReader[map[string]any](input) + if !ok { + // impossible + panic("field mapping edge stream value isn't map[string]any") + } + return packStreamReader(schema.StreamReaderWithConvert(s, checker)) + }, + }, uncheckedSourcePath, nil +} diff --git a/compose/generic_graph.go b/compose/generic_graph.go new file mode 100644 index 0000000..c763811 --- /dev/null +++ b/compose/generic_graph.go @@ -0,0 +1,158 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "reflect" + + "github.com/cloudwego/eino/internal/generic" +) + +type newGraphOptions struct { + withState func(ctx context.Context) any + stateType reflect.Type +} + +// NewGraphOption configures behavior when creating a new graph, such as +// providing local state generation. +type NewGraphOption func(ngo *newGraphOptions) + +// WithGenLocalState registers a function to generate per-run local state +// that can be shared across nodes in the graph. +func WithGenLocalState[S any](gls GenLocalState[S]) NewGraphOption { + return func(ngo *newGraphOptions) { + ngo.withState = func(ctx context.Context) any { + return gls(ctx) + } + ngo.stateType = generic.TypeOf[S]() + } +} + +// NewGraph create a directed graph that can compose components, lambda, chain, parallel etc. +// simultaneously provide flexible and multi-granular aspect governance capabilities. +// I: the input type of graph compiled product +// O: the output type of graph compiled product +// +// To share state between nodes, use WithGenLocalState option: +// +// type testState struct { +// UserInfo *UserInfo +// KVs map[string]any +// } +// +// genStateFunc := func(ctx context.Context) *testState { +// return &testState{} +// } +// +// graph := compose.NewGraph[string, string](WithGenLocalState(genStateFunc)) +// +// // you can use WithStatePreHandler and WithStatePostHandler to do something with state +// graph.AddNode("node1", someNode, compose.WithPreHandler(func(ctx context.Context, in string, state *testState) (string, error) { +// // do something with state +// return in, nil +// }), compose.WithPostHandler(func(ctx context.Context, out string, state *testState) (string, error) { +// // do something with state +// return out, nil +// })) +func NewGraph[I, O any](opts ...NewGraphOption) *Graph[I, O] { + options := &newGraphOptions{} + for _, opt := range opts { + opt(options) + } + + g := &Graph[I, O]{ + newGraphFromGeneric[I, O]( + ComponentOfGraph, + options.withState, + options.stateType, + opts, + ), + } + + return g +} + +// Graph is a generic graph that can be used to compose components. +// I: the input type of graph compiled product +// O: the output type of graph compiled product +type Graph[I, O any] struct { + *graph +} + +// AddEdge adds an edge to the graph, edge means a data flow from startNode to endNode. +// the previous node's output type must be set to the next node's input type. +// NOTE: startNode and endNode must have been added to the graph before adding edge. +// e.g. +// +// graph.AddNode("start_node_key", compose.NewPassthroughNode()) +// graph.AddNode("end_node_key", compose.NewPassthroughNode()) +// +// err := graph.AddEdge("start_node_key", "end_node_key") +func (g *Graph[I, O]) AddEdge(startNode, endNode string) (err error) { + return g.graph.addEdgeWithMappings(startNode, endNode, false, false) +} + +// Compile take the raw graph and compile it into a form ready to be run. +// e.g. +// +// graph, err := compose.NewGraph[string, string]() +// if err != nil {...} +// +// runnable, err := graph.Compile(ctx, compose.WithGraphName("my_graph")) +// if err != nil {...} +// +// runnable.Invoke(ctx, "input") // invoke +// runnable.Stream(ctx, "input") // stream +// runnable.Collect(ctx, inputReader) // collect +// runnable.Transform(ctx, inputReader) // transform +func (g *Graph[I, O]) Compile(ctx context.Context, opts ...GraphCompileOption) (Runnable[I, O], error) { + return compileAnyGraph[I, O](ctx, g, opts...) +} + +func compileAnyGraph[I, O any](ctx context.Context, g AnyGraph, opts ...GraphCompileOption) (Runnable[I, O], error) { + if len(globalGraphCompileCallbacks) > 0 { + opts = append([]GraphCompileOption{WithGraphCompileCallbacks(globalGraphCompileCallbacks...)}, opts...) + } + option := newGraphCompileOptions(opts...) + + cr, err := g.compile(ctx, option) + if err != nil { + return nil, err + } + + cr.meta = &executorMeta{ + component: g.component(), + isComponentCallbackEnabled: true, + componentImplType: "", + } + + cr.nodeInfo = &nodeInfo{ + name: option.graphName, + } + + ctxWrapper := func(ctx context.Context, opts ...Option) context.Context { + return initGraphCallbacks(AppendAddressSegment(ctx, AddressSegmentRunnable, option.graphName), cr.nodeInfo, cr.meta, opts...) + } + + rp, err := toGenericRunnable[I, O](cr, ctxWrapper) + if err != nil { + return nil, err + } + + return rp, nil +} diff --git a/compose/generic_helper.go b/compose/generic_helper.go new file mode 100644 index 0000000..0534dc6 --- /dev/null +++ b/compose/generic_helper.go @@ -0,0 +1,256 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "errors" + "fmt" + "reflect" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +func newGenericHelper[I, O any]() *genericHelper { + return &genericHelper{ + inputStreamFilter: defaultStreamMapFilter[I], + outputStreamFilter: defaultStreamMapFilter[O], + inputConverter: handlerPair{ + invoke: defaultValueChecker[I], + transform: defaultStreamConverter[I], + }, + outputConverter: handlerPair{ + invoke: defaultValueChecker[O], + transform: defaultStreamConverter[O], + }, + inputFieldMappingConverter: handlerPair{ + invoke: buildFieldMappingConverter[I](), + transform: buildStreamFieldMappingConverter[I](), + }, + outputFieldMappingConverter: handlerPair{ + invoke: buildFieldMappingConverter[O](), + transform: buildStreamFieldMappingConverter[O](), + }, + inputStreamConvertPair: defaultStreamConvertPair[I](), + outputStreamConvertPair: defaultStreamConvertPair[O](), + inputZeroValue: zeroValueFromGeneric[I], + outputZeroValue: zeroValueFromGeneric[O], + inputEmptyStream: emptyStreamFromGeneric[I], + outputEmptyStream: emptyStreamFromGeneric[O], + } +} + +type genericHelper struct { + // when set input key, use this method to convert input from map[string]any to T + inputStreamFilter, outputStreamFilter streamMapFilter + // when predecessor's output is assignableTypeMay to current node's input, validate and convert(if needed) types using the following two methods + inputConverter, outputConverter handlerPair + // when current node enable field mapping, convert map input to expected struct using the following two methods + inputFieldMappingConverter, outputFieldMappingConverter handlerPair + // can convert input/output from stream to non-stream or non-stream to stream, used for checkpoint + inputStreamConvertPair, outputStreamConvertPair streamConvertPair + + inputZeroValue, outputZeroValue func() any + inputEmptyStream, outputEmptyStream func() streamReader +} + +func (g *genericHelper) forMapInput() *genericHelper { + return &genericHelper{ + outputStreamFilter: g.outputStreamFilter, + outputConverter: g.outputConverter, + outputFieldMappingConverter: g.outputFieldMappingConverter, + outputStreamConvertPair: g.outputStreamConvertPair, + outputZeroValue: g.outputZeroValue, + outputEmptyStream: g.outputEmptyStream, + + inputStreamFilter: defaultStreamMapFilter[map[string]any], + inputConverter: handlerPair{ + invoke: defaultValueChecker[map[string]any], + transform: defaultStreamConverter[map[string]any], + }, + inputFieldMappingConverter: handlerPair{ + invoke: buildFieldMappingConverter[map[string]any](), + transform: buildStreamFieldMappingConverter[map[string]any](), + }, + inputStreamConvertPair: defaultStreamConvertPair[map[string]any](), + inputZeroValue: zeroValueFromGeneric[map[string]any], + inputEmptyStream: emptyStreamFromGeneric[map[string]any], + } +} + +func (g *genericHelper) forMapOutput() *genericHelper { + return &genericHelper{ + inputStreamFilter: g.inputStreamFilter, + inputConverter: g.inputConverter, + inputFieldMappingConverter: g.inputFieldMappingConverter, + inputStreamConvertPair: g.inputStreamConvertPair, + inputZeroValue: g.inputZeroValue, + inputEmptyStream: g.inputEmptyStream, + + outputStreamFilter: defaultStreamMapFilter[map[string]any], + outputConverter: handlerPair{ + invoke: defaultValueChecker[map[string]any], + transform: defaultStreamConverter[map[string]any], + }, + outputFieldMappingConverter: handlerPair{ + invoke: buildFieldMappingConverter[map[string]any](), + transform: buildStreamFieldMappingConverter[map[string]any](), + }, + outputStreamConvertPair: defaultStreamConvertPair[map[string]any](), + outputZeroValue: zeroValueFromGeneric[map[string]any], + outputEmptyStream: emptyStreamFromGeneric[map[string]any], + } +} + +func (g *genericHelper) forPredecessorPassthrough() *genericHelper { + return &genericHelper{ + inputStreamFilter: g.inputStreamFilter, + outputStreamFilter: g.inputStreamFilter, + inputConverter: g.inputConverter, + outputConverter: g.inputConverter, + inputFieldMappingConverter: g.inputFieldMappingConverter, + outputFieldMappingConverter: g.inputFieldMappingConverter, + inputStreamConvertPair: g.inputStreamConvertPair, + outputStreamConvertPair: g.inputStreamConvertPair, + inputZeroValue: g.inputZeroValue, + outputZeroValue: g.inputZeroValue, + inputEmptyStream: g.inputEmptyStream, + outputEmptyStream: g.inputEmptyStream, + } +} + +func (g *genericHelper) forSuccessorPassthrough() *genericHelper { + return &genericHelper{ + inputStreamFilter: g.outputStreamFilter, + outputStreamFilter: g.outputStreamFilter, + inputConverter: g.outputConverter, + outputConverter: g.outputConverter, + inputFieldMappingConverter: g.outputFieldMappingConverter, + outputFieldMappingConverter: g.outputFieldMappingConverter, + inputStreamConvertPair: g.outputStreamConvertPair, + outputStreamConvertPair: g.outputStreamConvertPair, + inputZeroValue: g.outputZeroValue, + outputZeroValue: g.outputZeroValue, + inputEmptyStream: g.outputEmptyStream, + outputEmptyStream: g.outputEmptyStream, + } +} + +type streamMapFilter func(key string, isr streamReader) (streamReader, bool) + +type valueHandler func(value any) (any, error) +type streamHandler func(streamReader) streamReader + +type handlerPair struct { + invoke valueHandler + transform streamHandler +} + +type streamConvertPair struct { + concatStream func(sr streamReader) (any, error) + restoreStream func(any) (streamReader, error) +} + +func defaultStreamConvertPair[T any]() streamConvertPair { + var t T + return streamConvertPair{ + concatStream: func(sr streamReader) (any, error) { + tsr, ok := unpackStreamReader[T](sr) + if !ok { + return nil, fmt.Errorf("cannot convert sr to streamReader[%T]", t) + } + value, err := concatStreamReader(tsr) + if err != nil { + if errors.Is(err, emptyStreamConcatErr) { + return nil, nil + } + return nil, err + } + return value, nil + }, + restoreStream: func(a any) (streamReader, error) { + if a == nil { + return packStreamReader(schema.StreamReaderFromArray([]T{})), nil + } + value, ok := a.(T) + if !ok { + return nil, fmt.Errorf("cannot convert value[%T] to streamReader[%T]", a, t) + } + return packStreamReader(schema.StreamReaderFromArray([]T{value})), nil + }, + } +} + +func defaultStreamMapFilter[T any](key string, isr streamReader) (streamReader, bool) { + sr, ok := unpackStreamReader[map[string]any](isr) + if !ok { + return nil, false + } + + cvt := func(m map[string]any) (T, error) { + var t T + v, ok_ := m[key] + if !ok_ { + return t, schema.ErrNoValue + } + vv, ok_ := v.(T) + if !ok_ { + return t, fmt.Errorf( + "[defaultStreamMapFilter]fail, key[%s]'s value type[%s] isn't expected type[%s]", + key, reflect.TypeOf(v).String(), + generic.TypeOf[T]().String()) + } + return vv, nil + } + + ret := schema.StreamReaderWithConvert[map[string]any, T](sr, cvt) + + return packStreamReader(ret), true +} + +func defaultStreamConverter[T any](reader streamReader) streamReader { + return packStreamReader(schema.StreamReaderWithConvert(reader.toAnyStreamReader(), func(v any) (T, error) { + vv, ok := v.(T) + if !ok { + var t T + return t, fmt.Errorf("runtime type check fail, expected type: %T, actual type: %T", t, v) + } + return vv, nil + })) +} + +func defaultValueChecker[T any](v any) (any, error) { + nValue, ok := v.(T) + if !ok { + var t T + return nil, fmt.Errorf("runtime type check fail, expected type: %T, actual type: %T", t, v) + } + return nValue, nil +} + +func zeroValueFromGeneric[T any]() any { + var t T + return t +} + +func emptyStreamFromGeneric[T any]() streamReader { + var t T + sr, sw := schema.Pipe[T](1) + sw.Send(t, nil) + sw.Close() + return packStreamReader(sr) +} diff --git a/compose/graph.go b/compose/graph.go new file mode 100644 index 0000000..bcf5ae4 --- /dev/null +++ b/compose/graph.go @@ -0,0 +1,1219 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/internal/gmap" +) + +// START is the start node of the graph. You can add your first edge with START. +const START = "start" + +// END is the end node of the graph. You can add your last edge with END. +const END = "end" + +// graphRunType is a custom type used to control the running mode of the graph. +type graphRunType string + +const ( + // runTypePregel is a running mode of the graph that is suitable for large-scale graph processing tasks. Can have cycles in graph. Compatible with NodeTriggerType.AnyPredecessor. + runTypePregel graphRunType = "Pregel" + // runTypeDAG is a running mode of the graph that represents the graph as a directed acyclic graph, suitable for tasks that can be represented as a directed acyclic graph. Compatible with NodeTriggerType.AllPredecessor. + runTypeDAG graphRunType = "DAG" +) + +// String returns the string representation of the graph run type. +func (g graphRunType) String() string { + return string(g) +} + +type graph struct { + nodes map[string]*graphNode + controlEdges map[string][]string + dataEdges map[string][]string + branches map[string][]*GraphBranch + startNodes []string + endNodes []string + + toValidateMap map[string][]struct { + endNode string + mappings []*FieldMapping + } + + stateType reflect.Type + stateGenerator func(ctx context.Context) any + newOpts []NewGraphOption + + expectedInputType, expectedOutputType reflect.Type + + *genericHelper + + fieldMappingRecords map[string][]*FieldMapping + + buildError error + + cmp component + + compiled bool + + handlerOnEdges map[string]map[string][]handlerPair + handlerPreNode map[string][]handlerPair + handlerPreBranch map[string][][]handlerPair +} + +type newGraphConfig struct { + inputType, outputType reflect.Type + gh *genericHelper + cmp component + stateType reflect.Type + stateGenerator func(ctx context.Context) any + newOpts []NewGraphOption +} + +func newGraphFromGeneric[I, O any]( + cmp component, + stateGenerator func(ctx context.Context) any, + stateType reflect.Type, + opts []NewGraphOption, +) *graph { + return newGraph(&newGraphConfig{ + inputType: generic.TypeOf[I](), + outputType: generic.TypeOf[O](), + gh: newGenericHelper[I, O](), + cmp: cmp, + stateType: stateType, + stateGenerator: stateGenerator, + newOpts: opts, + }) +} + +func newGraph(cfg *newGraphConfig) *graph { + return &graph{ + nodes: make(map[string]*graphNode), + dataEdges: make(map[string][]string), + controlEdges: make(map[string][]string), + branches: make(map[string][]*GraphBranch), + + toValidateMap: make(map[string][]struct { + endNode string + mappings []*FieldMapping + }), + + expectedInputType: cfg.inputType, + expectedOutputType: cfg.outputType, + genericHelper: cfg.gh, + + fieldMappingRecords: make(map[string][]*FieldMapping), + + cmp: cfg.cmp, + + stateType: cfg.stateType, + stateGenerator: cfg.stateGenerator, + newOpts: cfg.newOpts, + + handlerOnEdges: make(map[string]map[string][]handlerPair), + handlerPreNode: make(map[string][]handlerPair), + handlerPreBranch: make(map[string][][]handlerPair), + } +} + +func (g *graph) component() component { + return g.cmp +} + +func isChain(cmp component) bool { + return cmp == ComponentOfChain +} + +func isWorkflow(cmp component) bool { + return cmp == ComponentOfWorkflow +} + +// ErrGraphCompiled is returned when attempting to modify a graph after it has been compiled +var ErrGraphCompiled = errors.New("graph has been compiled, cannot be modified") + +func (g *graph) addNode(key string, node *graphNode, options *graphAddNodeOpts) (err error) { + if g.buildError != nil { + return g.buildError + } + + if g.compiled { + return ErrGraphCompiled + } + + defer func() { + if err != nil { + g.buildError = err + } + }() + + if key == END || key == START { + return fmt.Errorf("node '%s' is reserved, cannot add manually", key) + } + + if _, ok := g.nodes[key]; ok { + return fmt.Errorf("node '%s' already present", key) + } + + // check options + if options.needState { + if g.stateGenerator == nil { + return fmt.Errorf("node '%s' needs state but graph state is not enabled", key) + } + } + + if options.nodeOptions.nodeKey != "" { + if !isChain(g.cmp) { + return errors.New("only chain support node key option") + } + } + // end: check options + + // check pre- / post-handler type + if options.processor != nil { + if options.processor.statePreHandler != nil { + // check state type + if g.stateType != options.processor.preStateType { + return fmt.Errorf("node[%s]'s pre handler state type[%v] is different from graph[%v]", key, options.processor.preStateType, g.stateType) + } + // check input type + if node.inputType() == nil && options.processor.statePreHandler.outputType != reflect.TypeOf((*any)(nil)).Elem() { + return fmt.Errorf("passthrough node[%s]'s pre handler type isn't any", key) + } else if node.inputType() != nil && node.inputType() != options.processor.statePreHandler.outputType { + return fmt.Errorf("node[%s]'s pre handler type[%v] is different from its input type[%v]", key, options.processor.statePreHandler.outputType, node.inputType()) + } + } + if options.processor.statePostHandler != nil { + // check state type + if g.stateType != options.processor.postStateType { + return fmt.Errorf("node[%s]'s post handler state type[%v] is different from graph[%v]", key, options.processor.postStateType, g.stateType) + } + // check input type + if node.outputType() == nil && options.processor.statePostHandler.inputType != reflect.TypeOf((*any)(nil)).Elem() { + return fmt.Errorf("passthrough node[%s]'s post handler type isn't any", key) + } else if node.outputType() != nil && node.outputType() != options.processor.statePostHandler.inputType { + return fmt.Errorf("node[%s]'s post handler type[%v] is different from its output type[%v]", key, options.processor.statePostHandler.inputType, node.outputType()) + } + } + } + + g.nodes[key] = node + + return nil +} + +func (g *graph) addEdgeWithMappings(startNode, endNode string, noControl bool, noData bool, mappings ...*FieldMapping) (err error) { + if g.buildError != nil { + return g.buildError + } + if g.compiled { + return ErrGraphCompiled + } + + if noControl && noData { + return fmt.Errorf("edge[%s]-[%s] cannot be both noDirectDependency and noDataFlow", startNode, endNode) + } + + defer func() { + if err != nil { + g.buildError = err + } + }() + if startNode == END { + return errors.New("END cannot be a start node") + } + if endNode == START { + return errors.New("START cannot be an end node") + } + + if _, ok := g.nodes[startNode]; !ok && startNode != START { + return fmt.Errorf("edge start node '%s' needs to be added to graph first", startNode) + } + if _, ok := g.nodes[endNode]; !ok && endNode != END { + return fmt.Errorf("edge end node '%s' needs to be added to graph first", endNode) + } + + if !noControl { + for i := range g.controlEdges[startNode] { + if g.controlEdges[startNode][i] == endNode { + return fmt.Errorf("control edge[%s]-[%s] have been added yet", startNode, endNode) + } + } + + g.controlEdges[startNode] = append(g.controlEdges[startNode], endNode) + if startNode == START { + g.startNodes = append(g.startNodes, endNode) + } + if endNode == END { + g.endNodes = append(g.endNodes, startNode) + } + } + if !noData { + for i := range g.dataEdges[startNode] { + if g.dataEdges[startNode][i] == endNode { + return fmt.Errorf("data edge[%s]-[%s] have been added yet", startNode, endNode) + } + } + + g.addToValidateMap(startNode, endNode, mappings) + err = g.updateToValidateMap() + if err != nil { + return err + } + g.dataEdges[startNode] = append(g.dataEdges[startNode], endNode) + } + + return nil +} + +// AddEmbeddingNode adds a node that implements embedding.Embedder. +// e.g. +// +// embeddingNode, err := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{ +// Model: "text-embedding-3-small", +// }) +// +// graph.AddEmbeddingNode("embedding_node_key", embeddingNode) +func (g *graph) AddEmbeddingNode(key string, node embedding.Embedder, opts ...GraphAddNodeOpt) error { + gNode, options := toEmbeddingNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddRetrieverNode adds a node that implements retriever.Retriever. +// e.g. +// +// retriever, err := vikingdb.NewRetriever(ctx, &vikingdb.RetrieverConfig{}) +// +// graph.AddRetrieverNode("retriever_node_key", retrieverNode) +func (g *graph) AddRetrieverNode(key string, node retriever.Retriever, opts ...GraphAddNodeOpt) error { + gNode, options := toRetrieverNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddLoaderNode adds a node that implements document.Loader. +// e.g. +// +// loader, err := file.NewLoader(ctx, &file.LoaderConfig{}) +// +// graph.AddLoaderNode("loader_node_key", loader) +func (g *graph) AddLoaderNode(key string, node document.Loader, opts ...GraphAddNodeOpt) error { + gNode, options := toLoaderNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddIndexerNode adds a node that implements indexer.Indexer. +// e.g. +// +// indexer, err := vikingdb.NewIndexer(ctx, &vikingdb.IndexerConfig{}) +// +// graph.AddIndexerNode("indexer_node_key", indexer) +func (g *graph) AddIndexerNode(key string, node indexer.Indexer, opts ...GraphAddNodeOpt) error { + gNode, options := toIndexerNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddChatModelNode add node that implements model.BaseChatModel. +// e.g. +// +// chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ +// Model: "gpt-4o", +// }) +// +// graph.AddChatModelNode("chat_model_node_key", chatModel) +func (g *graph) AddChatModelNode(key string, node model.BaseChatModel, opts ...GraphAddNodeOpt) error { + gNode, options := toChatModelNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddAgenticModelNode add node that implements agentic.Model. +// e.g. +// +// model, err := openai.NewAgenticModel(ctx, &openai.AgenticModelConfig{ +// Model: "gpt-4o", +// }) +// +// graph.AddAgenticModelNode("agentic_model_node_key", model) +func (g *graph) AddAgenticModelNode(key string, node model.AgenticModel, opts ...GraphAddNodeOpt) error { + gNode, options := toAgenticModelNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddChatTemplateNode add node that implements prompt.ChatTemplate. +// e.g. +// +// chatTemplate, err := prompt.FromMessages(schema.FString, &schema.Message{ +// Role: schema.System, +// Content: "You are acting as a {role}.", +// }) +// +// graph.AddChatTemplateNode("chat_template_node_key", chatTemplate) +func (g *graph) AddChatTemplateNode(key string, node prompt.ChatTemplate, opts ...GraphAddNodeOpt) error { + gNode, options := toChatTemplateNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddAgenticChatTemplateNode add node that implements prompt.AgenticChatTemplate. +// e.g. +// +// chatTemplate, err := prompt.FromAgenticMessages(schema.FString, &schema.AgenticMessage{}) +// +// graph.AddAgenticChatTemplateNode("chat_template_node_key", chatTemplate) +func (g *graph) AddAgenticChatTemplateNode(key string, node prompt.AgenticChatTemplate, opts ...GraphAddNodeOpt) error { + gNode, options := toAgenticChatTemplateNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddToolsNode adds a node that implements ToolsNode. +// e.g. +// +// toolsNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{}) +// +// graph.AddToolsNode("tools_node_key", toolsNode) +func (g *graph) AddToolsNode(key string, node *ToolsNode, opts ...GraphAddNodeOpt) error { + gNode, options := toToolsNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddAgenticToolsNode adds a node that implements AgenticToolsNode. +// e.g. +// +// toolsNode, err := compose.NewAgenticToolsNode(ctx, &compose.ToolsNodeConfig{}) +// +// graph.AddAgenticToolsNode("tools_node_key", toolsNode) +func (g *graph) AddAgenticToolsNode(key string, node *AgenticToolsNode, opts ...GraphAddNodeOpt) error { + gNode, options := toAgenticToolsNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddDocumentTransformerNode adds a node that implements document.Transformer. +// e.g. +// +// markdownSplitter, err := markdown.NewHeaderSplitter(ctx, &markdown.HeaderSplitterConfig{}) +// +// graph.AddDocumentTransformerNode("document_transformer_node_key", markdownSplitter) +func (g *graph) AddDocumentTransformerNode(key string, node document.Transformer, opts ...GraphAddNodeOpt) error { + gNode, options := toDocumentTransformerNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddLambdaNode add node that implements at least one of Invoke[I, O], Stream[I, O], Collect[I, O], Transform[I, O]. +// due to the lack of supporting method generics, we need to use function generics to generate Lambda run as Runnable[I, O]. +// for Invoke[I, O], use compose.InvokableLambda() +// for Stream[I, O], use compose.StreamableLambda() +// for Collect[I, O], use compose.CollectableLambda() +// for Transform[I, O], use compose.TransformableLambda() +// for arbitrary combinations of 4 kinds of lambda, use compose.AnyLambda() +func (g *graph) AddLambdaNode(key string, node *Lambda, opts ...GraphAddNodeOpt) error { + gNode, options := toLambdaNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddGraphNode add one kind of Graph[I, O]、Chain[I, O]、StateChain[I, O, S] as a node. +// for Graph[I, O], comes from NewGraph[I, O]() +// for Chain[I, O], comes from NewChain[I, O]() +func (g *graph) AddGraphNode(key string, node AnyGraph, opts ...GraphAddNodeOpt) error { + gNode, options := toAnyGraphNode(node, opts...) + return g.addNode(key, gNode, options) +} + +// AddPassthroughNode adds a passthrough node to the graph. +// mostly used in pregel mode of graph. +// e.g. +// +// graph.AddPassthroughNode("passthrough_node_key") +func (g *graph) AddPassthroughNode(key string, opts ...GraphAddNodeOpt) error { + gNode, options := toPassthroughNode(opts...) + return g.addNode(key, gNode, options) +} + +// AddBranch adds a branch to the graph. +// e.g. +// +// condition := func(ctx context.Context, in string) (string, error) { +// return "next_node_key", nil +// } +// endNodes := map[string]bool{"path01": true, "path02": true} +// branch := compose.NewGraphBranch(condition, endNodes) +// +// graph.AddBranch("start_node_key", branch) +func (g *graph) AddBranch(startNode string, branch *GraphBranch) (err error) { + return g.addBranch(startNode, branch, false) +} + +func (g *graph) addBranch(startNode string, branch *GraphBranch, skipData bool) (err error) { + if g.buildError != nil { + return g.buildError + } + + if g.compiled { + return ErrGraphCompiled + } + + defer func() { + if err != nil { + g.buildError = err + } + }() + + if startNode == END { + return errors.New("END cannot be a start node") + } + + if _, ok := g.nodes[startNode]; !ok && startNode != START { + return fmt.Errorf("branch start node '%s' needs to be added to graph first", startNode) + } + + if _, ok := g.handlerPreBranch[startNode]; !ok { + g.handlerPreBranch[startNode] = [][]handlerPair{} + } + branch.idx = len(g.handlerPreBranch[startNode]) + + if startNode != START && g.nodes[startNode].executorMeta.component == ComponentOfPassthrough { + g.nodes[startNode].cr.inputType = branch.inputType + g.nodes[startNode].cr.outputType = branch.inputType + g.nodes[startNode].cr.genericHelper = branch.genericHelper.forPredecessorPassthrough() + } + + // check branch condition type + result := checkAssignable(g.getNodeOutputType(startNode), branch.inputType) + if result == assignableTypeMustNot { + return fmt.Errorf("condition's input type[%s] and start node[%s]'s output type[%s] are mismatched", branch.inputType.String(), startNode, g.getNodeOutputType(startNode).String()) + } else if result == assignableTypeMay { + g.handlerPreBranch[startNode] = append(g.handlerPreBranch[startNode], []handlerPair{branch.inputConverter}) + } else { + g.handlerPreBranch[startNode] = append(g.handlerPreBranch[startNode], []handlerPair{}) + } + + if !skipData { + for endNode := range branch.endNodes { + if _, ok := g.nodes[endNode]; !ok { + if endNode != END { + return fmt.Errorf("branch end node '%s' needs to be added to graph first", endNode) + } + } + + g.addToValidateMap(startNode, endNode, nil) + e := g.updateToValidateMap() + if e != nil { + return e + } + + if startNode == START { + g.startNodes = append(g.startNodes, endNode) + } + if endNode == END { + g.endNodes = append(g.endNodes, startNode) + } + } + } else { + for endNode := range branch.endNodes { + if startNode == START { + g.startNodes = append(g.startNodes, endNode) + } + if endNode == END { + g.endNodes = append(g.endNodes, startNode) + } + } + branch.noDataFlow = true + } + + g.branches[startNode] = append(g.branches[startNode], branch) + + return nil +} + +func (g *graph) addToValidateMap(startNode, endNode string, mapping []*FieldMapping) { + g.toValidateMap[startNode] = append(g.toValidateMap[startNode], struct { + endNode string + mappings []*FieldMapping + }{endNode: endNode, mappings: mapping}) +} + +// updateToValidateMap after update node, check validate map +// check again if nodes in toValidateMap have been updated. because when there are multiple linked passthrough nodes, in the worst scenario, only one node can be updated at a time. +func (g *graph) updateToValidateMap() error { + var startNodeOutputType, endNodeInputType reflect.Type + for { + hasChanged := false + for startNode := range g.toValidateMap { + startNodeOutputType = g.getNodeOutputType(startNode) + + for i := 0; i < len(g.toValidateMap[startNode]); i++ { + endNode := g.toValidateMap[startNode][i] + + endNodeInputType = g.getNodeInputType(endNode.endNode) + if startNodeOutputType == nil && endNodeInputType == nil { + continue + } + + // update toValidateMap + g.toValidateMap[startNode] = append(g.toValidateMap[startNode][:i], g.toValidateMap[startNode][i+1:]...) + i-- + + hasChanged = true + // assume that START and END type isn't empty + if startNodeOutputType != nil && endNodeInputType == nil { + g.nodes[endNode.endNode].cr.inputType = startNodeOutputType + g.nodes[endNode.endNode].cr.outputType = g.nodes[endNode.endNode].cr.inputType + g.nodes[endNode.endNode].cr.genericHelper = g.getNodeGenericHelper(startNode).forSuccessorPassthrough() + } else if startNodeOutputType == nil /* redundant condition && endNodeInputType != nil */ { + g.nodes[startNode].cr.inputType = endNodeInputType + g.nodes[startNode].cr.outputType = g.nodes[startNode].cr.inputType + g.nodes[startNode].cr.genericHelper = g.getNodeGenericHelper(endNode.endNode).forPredecessorPassthrough() + } else if len(endNode.mappings) == 0 { + // common node check + result := checkAssignable(startNodeOutputType, endNodeInputType) + if result == assignableTypeMustNot { + return fmt.Errorf("graph edge[%s]-[%s]: start node's output type[%s] and end node's input type[%s] mismatch", + startNode, endNode.endNode, startNodeOutputType.String(), endNodeInputType.String()) + } else if result == assignableTypeMay { + // add runtime check edges + if _, ok := g.handlerOnEdges[startNode]; !ok { + g.handlerOnEdges[startNode] = make(map[string][]handlerPair) + } + g.handlerOnEdges[startNode][endNode.endNode] = append(g.handlerOnEdges[startNode][endNode.endNode], g.getNodeGenericHelper(endNode.endNode).inputConverter) + } + continue + } + + if len(endNode.mappings) > 0 { + if _, ok := g.handlerOnEdges[startNode]; !ok { + g.handlerOnEdges[startNode] = make(map[string][]handlerPair) + } + g.fieldMappingRecords[endNode.endNode] = append(g.fieldMappingRecords[endNode.endNode], endNode.mappings...) + + // field mapping check + checker, uncheckedSourcePaths, err := validateFieldMapping(g.getNodeOutputType(startNode), g.getNodeInputType(endNode.endNode), endNode.mappings) + if err != nil { + return err + } + + g.handlerOnEdges[startNode][endNode.endNode] = append(g.handlerOnEdges[startNode][endNode.endNode], handlerPair{ + invoke: func(value any) (any, error) { + return fieldMap(endNode.mappings, false, uncheckedSourcePaths)(value) + }, + transform: streamFieldMap(endNode.mappings, uncheckedSourcePaths), + }) + + if checker != nil { + g.handlerOnEdges[startNode][endNode.endNode] = append(g.handlerOnEdges[startNode][endNode.endNode], *checker) + } + } + } + } + if !hasChanged { + break + } + } + + return nil +} + +func (g *graph) getNodeGenericHelper(name string) *genericHelper { + if name == START { + return g.genericHelper.forPredecessorPassthrough() + } else if name == END { + return g.genericHelper.forSuccessorPassthrough() + } + return g.nodes[name].getGenericHelper() +} + +func (g *graph) getNodeInputType(name string) reflect.Type { + if name == START { + return g.inputType() + } else if name == END { + return g.outputType() + } + return g.nodes[name].inputType() +} + +func (g *graph) getNodeOutputType(name string) reflect.Type { + if name == START { + return g.inputType() + } else if name == END { + return g.outputType() + } + return g.nodes[name].outputType() +} + +func (g *graph) inputType() reflect.Type { + return g.expectedInputType +} + +func (g *graph) outputType() reflect.Type { + return g.expectedOutputType +} + +func (g *graph) compile(ctx context.Context, opt *graphCompileOptions) (*composableRunnable, error) { + if g.buildError != nil { + return nil, g.buildError + } + + // get run type + runType := runTypePregel + cb := pregelChannelBuilder + if isChain(g.cmp) || isWorkflow(g.cmp) { + if opt != nil && opt.nodeTriggerMode != "" { + return nil, errors.New(fmt.Sprintf("%s doesn't support node trigger mode option", g.cmp)) + } + } + if (opt != nil && opt.nodeTriggerMode == AllPredecessor) || isWorkflow(g.cmp) { + runType = runTypeDAG + cb = dagChannelBuilder + } + + // get eager type + eager := false + if isWorkflow(g.cmp) || runType == runTypeDAG { + eager = true + } + if opt != nil && opt.eagerDisabled { + eager = false + } + + if len(g.startNodes) == 0 { + return nil, errors.New("start node not set") + } + if len(g.endNodes) == 0 { + return nil, errors.New("end node not set") + } + + // toValidateMap isn't empty means there are nodes that cannot infer type + for _, v := range g.toValidateMap { + if len(v) > 0 { + return nil, fmt.Errorf("some node's input or output types cannot be inferred: %v", g.toValidateMap) + } + } + + for key := range g.fieldMappingRecords { + // not allowed to map multiple fields to the same field + toMap := make(map[string]bool) + for _, mapping := range g.fieldMappingRecords[key] { + if _, ok := toMap[mapping.to]; ok { + return nil, fmt.Errorf("duplicate mapping target field: %s of node[%s]", mapping.to, key) + } + toMap[mapping.to] = true + } + + // add map to input converter + g.handlerPreNode[key] = append(g.handlerPreNode[key], g.getNodeGenericHelper(key).inputFieldMappingConverter) + } + + key2SubGraphs := g.beforeChildGraphsCompile(opt) + chanSubscribeTo := make(map[string]*chanCall) + for name, node := range g.nodes { + node.beforeChildGraphCompile(name, key2SubGraphs) + + r, err := node.compileIfNeeded(ctx) + if err != nil { + return nil, err + } + + chCall := &chanCall{ + action: r, + writeTo: g.dataEdges[name], + controls: g.controlEdges[name], + + preProcessor: node.nodeInfo.preProcessor, + postProcessor: node.nodeInfo.postProcessor, + } + + branches := g.branches[name] + if len(branches) > 0 { + branchRuns := make([]*GraphBranch, 0, len(branches)) + branchRuns = append(branchRuns, branches...) + + chCall.writeToBranches = branchRuns + } + + chanSubscribeTo[name] = chCall + } + + dataPredecessors := make(map[string][]string) + controlPredecessors := make(map[string][]string) + for start, ends := range g.controlEdges { + for _, end := range ends { + if _, ok := controlPredecessors[end]; !ok { + controlPredecessors[end] = []string{start} + } else { + controlPredecessors[end] = append(controlPredecessors[end], start) + } + } + } + for start, ends := range g.dataEdges { + for _, end := range ends { + if _, ok := dataPredecessors[end]; !ok { + dataPredecessors[end] = []string{start} + } else { + dataPredecessors[end] = append(dataPredecessors[end], start) + } + } + } + for start, branches := range g.branches { + for _, branch := range branches { + for end := range branch.endNodes { + if _, ok := controlPredecessors[end]; !ok { + controlPredecessors[end] = []string{start} + } else { + controlPredecessors[end] = append(controlPredecessors[end], start) + } + + if !branch.noDataFlow { + if _, ok := dataPredecessors[end]; !ok { + dataPredecessors[end] = []string{start} + } else { + dataPredecessors[end] = append(dataPredecessors[end], start) + } + } + } + } + } + + inputChannels := &chanCall{ + writeTo: g.dataEdges[START], + controls: g.controlEdges[START], + writeToBranches: make([]*GraphBranch, len(g.branches[START])), + } + copy(inputChannels.writeToBranches, g.branches[START]) + + var mergeConfigs map[string]FanInMergeConfig + if opt != nil { + mergeConfigs = opt.mergeConfigs + } + if mergeConfigs == nil { + mergeConfigs = make(map[string]FanInMergeConfig) + } + + r := &runner{ + chanSubscribeTo: chanSubscribeTo, + controlPredecessors: controlPredecessors, + dataPredecessors: dataPredecessors, + + inputChannels: inputChannels, + + eager: eager, + + chanBuilder: cb, + + inputType: g.inputType(), + outputType: g.outputType(), + genericHelper: g.genericHelper, + + preBranchHandlerManager: &preBranchHandlerManager{h: g.handlerPreBranch}, + preNodeHandlerManager: &preNodeHandlerManager{h: g.handlerPreNode}, + edgeHandlerManager: &edgeHandlerManager{h: g.handlerOnEdges}, + + mergeConfigs: mergeConfigs, + } + + successors := make(map[string][]string) + for ch := range r.chanSubscribeTo { + successors[ch] = getSuccessors(r.chanSubscribeTo[ch]) + } + r.successors = successors + + if g.stateGenerator != nil { + r.runCtx = func(ctx context.Context) context.Context { + var parent *internalState + if p, ok := ctx.Value(stateKey{}).(*internalState); ok { + parent = p + } + + return context.WithValue(ctx, stateKey{}, &internalState{ + state: g.stateGenerator(ctx), + parent: parent, + }) + } + } + + if runType == runTypeDAG { + err := validateDAG(r.chanSubscribeTo, controlPredecessors) + if err != nil { + return nil, err + } + r.dag = true + } + + if opt != nil { + inputPairs := make(map[string]streamConvertPair) + outputPairs := make(map[string]streamConvertPair) + for key, c := range r.chanSubscribeTo { + inputPairs[key] = c.action.inputStreamConvertPair + outputPairs[key] = c.action.outputStreamConvertPair + } + inputPairs[END] = r.outputConvertStreamPair + outputPairs[START] = r.inputConvertStreamPair + r.checkPointer = newCheckPointer(inputPairs, outputPairs, opt.checkPointStore, opt.serializer) + + r.interruptBeforeNodes = opt.interruptBeforeNodes + r.interruptAfterNodes = opt.interruptAfterNodes + r.options = *opt + } + + // default options + if r.dag && r.options.maxRunSteps > 0 { + return nil, fmt.Errorf("cannot set max run steps in dag mode") + } else if !r.dag && r.options.maxRunSteps == 0 { + r.options.maxRunSteps = len(r.chanSubscribeTo) + 10 + } + + g.compiled = true + + g.onCompileFinish(ctx, opt, key2SubGraphs) + + return r.toComposableRunnable(), nil +} + +func getSuccessors(c *chanCall) []string { + ret := make([]string, len(c.writeTo)) + copy(ret, c.writeTo) + ret = append(ret, c.controls...) + for _, branch := range c.writeToBranches { + for node := range branch.endNodes { + ret = append(ret, node) + } + } + return uniqueSlice(ret) +} + +func uniqueSlice(s []string) []string { + seen := make(map[string]struct{}, len(s)) + cur := 0 + for i := range s { + if _, ok := seen[s[i]]; !ok { + seen[s[i]] = struct{}{} + s[cur] = s[i] + cur++ + } + } + return s[:cur] +} + +type subGraphCompileCallback struct { + closure func(ctx context.Context, info *GraphInfo) +} + +// OnFinish is called when the graph is compiled. +func (s *subGraphCompileCallback) OnFinish(ctx context.Context, info *GraphInfo) { + s.closure(ctx, info) +} + +func (g *graph) beforeChildGraphsCompile(opt *graphCompileOptions) map[string]*GraphInfo { + if opt == nil || len(opt.callbacks) == 0 { + return nil + } + + return make(map[string]*GraphInfo) +} + +func (gn *graphNode) beforeChildGraphCompile(nodeKey string, key2SubGraphs map[string]*GraphInfo) { + if gn.g == nil || key2SubGraphs == nil { + return + } + + subGraphCallback := func(ctx2 context.Context, subGraph *GraphInfo) { + key2SubGraphs[nodeKey] = subGraph + } + + gn.nodeInfo.compileOption.callbacks = append(gn.nodeInfo.compileOption.callbacks, &subGraphCompileCallback{closure: subGraphCallback}) +} + +func (g *graph) toGraphInfo(opt *graphCompileOptions, key2SubGraphs map[string]*GraphInfo) *GraphInfo { + gInfo := &GraphInfo{ + CompileOptions: opt.origOpts, + Nodes: make(map[string]GraphNodeInfo, len(g.nodes)), + Edges: gmap.Clone(g.controlEdges), + DataEdges: gmap.Clone(g.dataEdges), + Branches: gmap.Map(g.branches, func(startNode string, branches []*GraphBranch) (string, []GraphBranch) { + branchInfo := make([]GraphBranch, 0, len(branches)) + for _, b := range branches { + branchInfo = append(branchInfo, GraphBranch{ + invoke: b.invoke, + collect: b.collect, + inputType: b.inputType, + genericHelper: b.genericHelper, + endNodes: gmap.Clone(b.endNodes), + }) + } + return startNode, branchInfo + }), + InputType: g.expectedInputType, + OutputType: g.expectedOutputType, + Name: opt.graphName, + GenStateFn: g.stateGenerator, + NewGraphOptions: g.newOpts, + } + + for key := range g.nodes { + gNode := g.nodes[key] + if gNode.executorMeta.component == ComponentOfPassthrough { + gInfo.Nodes[key] = GraphNodeInfo{ + Component: gNode.executorMeta.component, + GraphAddNodeOpts: gNode.opts, + InputType: gNode.cr.inputType, + OutputType: gNode.cr.outputType, + Name: gNode.nodeInfo.name, + InputKey: gNode.cr.nodeInfo.inputKey, + OutputKey: gNode.cr.nodeInfo.outputKey, + } + continue + } + + gNodeInfo := &GraphNodeInfo{ + Component: gNode.executorMeta.component, + Instance: gNode.instance, + GraphAddNodeOpts: gNode.opts, + InputType: gNode.cr.inputType, + OutputType: gNode.cr.outputType, + Name: gNode.nodeInfo.name, + InputKey: gNode.cr.nodeInfo.inputKey, + OutputKey: gNode.cr.nodeInfo.outputKey, + Mappings: g.fieldMappingRecords[key], + } + + if gi, ok := key2SubGraphs[key]; ok { + gNodeInfo.GraphInfo = gi + } + + gInfo.Nodes[key] = *gNodeInfo + } + + return gInfo +} + +func (g *graph) onCompileFinish(ctx context.Context, opt *graphCompileOptions, key2SubGraphs map[string]*GraphInfo) { + if opt == nil { + return + } + + if len(opt.callbacks) == 0 { + return + } + + gInfo := g.toGraphInfo(opt, key2SubGraphs) + + for _, cb := range opt.callbacks { + cb.OnFinish(ctx, gInfo) + } +} + +func (g *graph) getGenericHelper() *genericHelper { + return g.genericHelper +} + +func (g *graph) GetType() string { + return "" +} + +func transferTask(script [][]string, invertedEdges map[string][]string) [][]string { + utilMap := map[string]bool{} + for i := len(script) - 1; i >= 0; i-- { + for j := 0; j < len(script[i]); j++ { + // deduplicate + if _, ok := utilMap[script[i][j]]; ok { + script[i] = append(script[i][:j], script[i][j+1:]...) + j-- + continue + } + utilMap[script[i][j]] = true + + target := i + for k := i + 1; k < len(script); k++ { + hasDependencies := false + for l := range script[k] { + for _, dependency := range invertedEdges[script[i][j]] { + if script[k][l] == dependency { + hasDependencies = true + break + } + } + if hasDependencies { + break + } + } + if hasDependencies { + break + } + target = k + } + if target != i { + script[target] = append(script[target], script[i][j]) + script[i] = append(script[i][:j], script[i][j+1:]...) + j-- + } + } + } + + return script +} + +func validateDAG(chanSubscribeTo map[string]*chanCall, controlPredecessors map[string][]string) error { + m := map[string]int{} + for node := range chanSubscribeTo { + if edges, ok := controlPredecessors[node]; ok { + m[node] = len(edges) + for _, pre := range edges { + if pre == START { + m[node] -= 1 + } + } + } else { + m[node] = 0 + } + } + hasChanged := true + for hasChanged { + hasChanged = false + for node := range m { + if m[node] == 0 { + hasChanged = true + for _, subNode := range chanSubscribeTo[node].controls { + if subNode == END { + continue + } + m[subNode]-- + } + for _, subBranch := range chanSubscribeTo[node].writeToBranches { + for subNode := range subBranch.endNodes { + if subNode == END { + continue + } + m[subNode]-- + } + } + m[node] = -1 + } + } + } + + var loopStarts []string + for k, v := range m { + if v > 0 { + loopStarts = append(loopStarts, k) + } + } + if len(loopStarts) > 0 { + return fmt.Errorf("%w: %s", DAGInvalidLoopErr, formatLoops(findLoops(loopStarts, chanSubscribeTo))) + } + return nil +} + +// DAGInvalidLoopErr indicates the graph contains a cycle and is invalid. +var DAGInvalidLoopErr = errors.New("DAG is invalid, has loop") + +func findLoops(startNodes []string, chanCalls map[string]*chanCall) [][]string { + controlSuccessors := map[string][]string{} + for node, ch := range chanCalls { + controlSuccessors[node] = append(controlSuccessors[node], ch.controls...) + for _, b := range ch.writeToBranches { + for end := range b.endNodes { + controlSuccessors[node] = append(controlSuccessors[node], end) + } + } + } + + visited := map[string]bool{} + var dfs func(path []string) [][]string + dfs = func(path []string) [][]string { + var ret [][]string + pathEnd := path[len(path)-1] + successors, ok := controlSuccessors[pathEnd] + if !ok { + return nil + } + for _, successor := range successors { + visited[successor] = true + + if successor == END { + continue + } + + var looped bool + for i, node := range path { + if node == successor { + ret = append(ret, append(path[i:], successor)) + looped = true + break + } + } + if looped { + continue + } + + ret = append(ret, dfs(append(path, successor))...) + } + return ret + } + + var ret [][]string + for _, node := range startNodes { + if !visited[node] { + ret = append(ret, dfs([]string{node})...) + } + } + return ret +} + +func formatLoops(loops [][]string) string { + sb := strings.Builder{} + for _, loop := range loops { + if len(loop) == 0 { + continue + } + sb.WriteString("[") + sb.WriteString(loop[0]) + for i := 1; i < len(loop); i++ { + sb.WriteString("->") + sb.WriteString(loop[i]) + } + sb.WriteString("]") + } + return sb.String() +} + +// NewNodePath specifies a path to a node in the graph, which is composed of node keys. +// Starting from the top graph, +// following this set of node keys can lead to a specific node in the top graph or a subgraph. +// +// e.g. +// NewNodePath("sub_graph_node_key", "node_key_within_sub_graph") +func NewNodePath(nodeKeyPath ...string) *NodePath { + return &NodePath{path: nodeKeyPath} +} + +// NodePath represents a path composed of node keys to locate a node. +type NodePath struct { + path []string +} + +// GetPath returns the sequence of node keys in the path. +func (p *NodePath) GetPath() []string { + return p.path +} diff --git a/compose/graph_add_node_options.go b/compose/graph_add_node_options.go new file mode 100644 index 0000000..b1db71f --- /dev/null +++ b/compose/graph_add_node_options.go @@ -0,0 +1,168 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "reflect" + + "github.com/cloudwego/eino/internal/generic" +) + +type graphAddNodeOpts struct { + nodeOptions *nodeOptions + processor *processorOpts + + needState bool +} + +// GraphAddNodeOpt is a functional option type for adding a node to a graph. +// e.g. +// +// graph.AddNode("node_name", node, compose.WithInputKey("input_key"), compose.WithOutputKey("output_key")) +type GraphAddNodeOpt func(o *graphAddNodeOpts) + +type nodeOptions struct { + nodeName string + + nodeKey string + + inputKey string + outputKey string + + graphCompileOption []GraphCompileOption // when this node is itself an AnyGraph, this option will be used to compile the node as a nested graph +} + +// WithNodeName sets the name of the node. +func WithNodeName(n string) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.nodeOptions.nodeName = n + } +} + +// WithNodeKey set the node key, which is used to identify the node in the chain. +// only for use in Chain/StateChain. +func WithNodeKey(key string) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.nodeOptions.nodeKey = key + } +} + +// WithInputKey sets the input key of the node. +// this will change the input value of the node, for example, if the pre node's output is map[string]any{"key01": "value01"}, +// and the current node's input key is "key01", then the current node's input value will be "value01". +func WithInputKey(k string) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.nodeOptions.inputKey = k + } +} + +// WithOutputKey sets the output key of the node. +// this will change the output value of the node, for example, if the current node's output key is "key01", +// then the node's output value will be map[string]any{"key01": value}. +func WithOutputKey(k string) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.nodeOptions.outputKey = k + } +} + +// WithGraphCompileOptions when the node is an AnyGraph, use this option to set compile option for the node. +// e.g. +// +// graph.AddNode("node_name", node, compose.WithGraphCompileOptions(compose.WithGraphName("my_sub_graph"))) +func WithGraphCompileOptions(opts ...GraphCompileOption) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.nodeOptions.graphCompileOption = opts + } +} + +// WithStatePreHandler modify node's input of I according to state S and input or store input information into state, and it's thread-safe. +// notice: this option requires Graph to be created with WithGenLocalState option. +// I: input type of the Node like ChatModel, Lambda, Retriever etc. +// S: state type defined in WithGenLocalState +func WithStatePreHandler[I, S any](pre StatePreHandler[I, S]) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.processor.statePreHandler = convertPreHandler(pre) + o.processor.preStateType = generic.TypeOf[S]() + o.needState = true + } +} + +// WithStatePostHandler modify node's output of O according to state S and output or store output information into state, and it's thread-safe. +// notice: this option requires Graph to be created with WithGenLocalState option. +// O: output type of the Node like ChatModel, Lambda, Retriever etc. +// S: state type defined in WithGenLocalState +func WithStatePostHandler[O, S any](post StatePostHandler[O, S]) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.processor.statePostHandler = convertPostHandler(post) + o.processor.postStateType = generic.TypeOf[S]() + o.needState = true + } +} + +// WithStreamStatePreHandler modify node's streaming input of I according to state S and input or store input information into state, and it's thread-safe. +// notice: this option requires Graph to be created with WithGenLocalState option. +// when to use: when upstream node's output is an actual stream, and you want the current node's input to remain an actual stream after state pre handler. +// caution: while StreamStatePreHandler is thread safe, modifying state within your own goroutine is NOT. +// I: input type of the Node like ChatModel, Lambda, Retriever etc. +// S: state type defined in WithGenLocalState +func WithStreamStatePreHandler[I, S any](pre StreamStatePreHandler[I, S]) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.processor.statePreHandler = streamConvertPreHandler(pre) + o.processor.preStateType = generic.TypeOf[S]() + o.needState = true + } +} + +// WithStreamStatePostHandler modify node's streaming output of O according to state S and output or store output information into state, and it's thread-safe. +// notice: this option requires Graph to be created with WithGenLocalState option. +// when to use: when current node's output is an actual stream, and you want the downstream node's input to remain an actual stream after state post handler. +// caution: while StreamStatePostHandler is thread safe, modifying state within your own goroutine is NOT. +// O: output type of the Node like ChatModel, Lambda, Retriever etc. +// S: state type defined in WithGenLocalState +func WithStreamStatePostHandler[O, S any](post StreamStatePostHandler[O, S]) GraphAddNodeOpt { + return func(o *graphAddNodeOpts) { + o.processor.statePostHandler = streamConvertPostHandler(post) + o.processor.postStateType = generic.TypeOf[S]() + o.needState = true + } +} + +type processorOpts struct { + statePreHandler *composableRunnable + preStateType reflect.Type // used for type validation + statePostHandler *composableRunnable + postStateType reflect.Type // used for type validation +} + +func getGraphAddNodeOpts(opts ...GraphAddNodeOpt) *graphAddNodeOpts { + opt := &graphAddNodeOpts{ + nodeOptions: &nodeOptions{ + nodeName: "", + nodeKey: "", + }, + processor: &processorOpts{ + statePreHandler: nil, + statePostHandler: nil, + }, + } + + for _, fn := range opts { + fn(opt) + } + + return opt +} diff --git a/compose/graph_call_options.go b/compose/graph_call_options.go new file mode 100644 index 0000000..eb2ab16 --- /dev/null +++ b/compose/graph_call_options.go @@ -0,0 +1,264 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" +) + +type graphCancelChanKey struct{} +type graphCancelChanVal struct { + ch chan *time.Duration +} + +type graphInterruptOptions struct { + timeout *time.Duration +} + +// GraphInterruptOption configures behavior when interrupting a running graph. +type GraphInterruptOption func(o *graphInterruptOptions) + +// WithGraphInterruptTimeout specifies the max waiting time before generating an interrupt. +// After the max waiting time, the graph will force an interrupt. Any unfinished tasks will be re-run when the graph is resumed. +func WithGraphInterruptTimeout(timeout time.Duration) GraphInterruptOption { + return func(o *graphInterruptOptions) { + o.timeout = &timeout + } +} + +// WithGraphInterrupt creates a context with graph cancellation support. +// When the returned context is used to invoke a graph or workflow, calling the interrupt function will trigger an interrupt. +// The graph will wait for current tasks to complete by default. +// +// Input Persistence: When WithGraphInterrupt is used, ALL nodes (in both root graph and subgraphs) will automatically +// persist their inputs (both streaming and non-streaming) before execution. If the graph is interrupted, these inputs +// are restored when the graph resumes from a checkpoint, ensuring interrupted nodes receive their original inputs. +// +// This behavior differs from internal interrupts triggered via compose.Interrupt() within a node's function body. +// Internal interrupts do NOT automatically persist inputs - the node author must manage input persistence manually, +// either by saving it in the global graph state or using compose.StatefulInterrupt() to store it in local interrupt state. +// WithGraphInterrupt enables automatic input persistence because external interrupts can occur at any point during +// node execution, making it impossible for the node to prepare for the interrupt. +// +// Why input persistence is not enabled by default for internal interrupts: Enabling it universally would break +// existing code that relies on checking "input == nil" to determine whether the node is running for the first time +// or resuming from an interrupt. The recommended approach is to use compose.GetInterruptState() to explicitly +// determine whether the current execution is a first run or a resume. +func WithGraphInterrupt(parent context.Context) (ctx context.Context, interrupt func(opts ...GraphInterruptOption)) { + ch := make(chan *time.Duration, 1) + ctx = context.WithValue(parent, graphCancelChanKey{}, &graphCancelChanVal{ + ch: ch, + }) + return ctx, func(opts ...GraphInterruptOption) { + o := &graphInterruptOptions{} + for _, opt := range opts { + opt(o) + } + ch <- o.timeout + close(ch) + } +} + +func getGraphCancel(ctx context.Context) *graphCancelChanVal { + val, ok := ctx.Value(graphCancelChanKey{}).(*graphCancelChanVal) + if !ok { + return nil + } + return val +} + +// Option is a functional option type for calling a graph. +type Option struct { + options []any + handler []callbacks.Handler + + paths []*NodePath + + maxRunSteps int + checkPointID *string + writeToCheckPointID *string + forceNewRun bool + stateModifier StateModifier +} + +func (o Option) deepCopy() Option { + nOptions := make([]any, len(o.options)) + copy(nOptions, o.options) + nHandler := make([]callbacks.Handler, len(o.handler)) + copy(nHandler, o.handler) + nPaths := make([]*NodePath, len(o.paths)) + for i, path := range o.paths { + nPath := *path + nPaths[i] = &nPath + } + return Option{ + options: nOptions, + handler: nHandler, + paths: nPaths, + maxRunSteps: o.maxRunSteps, + } +} + +// DesignateNode sets the key of the node to which the option will be applied. +// notice: only effective at the top graph. +// e.g. +// +// embeddingOption := compose.WithEmbeddingOption(embedding.WithModel("text-embedding-3-small")) +// runnable.Invoke(ctx, "input", embeddingOption.DesignateNode("embedding_node_key")) +func (o Option) DesignateNode(nodeKey ...string) Option { + nKeys := make([]*NodePath, len(nodeKey)) + for i, k := range nodeKey { + nKeys[i] = NewNodePath(k) + } + return o.DesignateNodeWithPath(nKeys...) +} + +// DesignateNodeWithPath sets the path of the node(s) to which the option will be applied. +// You can specify a node in the subgraph through `NodePath` to make the option only take effect at this node. +// +// e.g. +// nodePath := NewNodePath("sub_graph_node_key", "node_key_within_sub_graph") +// DesignateNodeWithPath(nodePath) +func (o Option) DesignateNodeWithPath(path ...*NodePath) Option { + o.paths = append(o.paths, path...) + return o +} + +// WithEmbeddingOption is a functional option type for embedding component. +// e.g. +// +// embeddingOption := compose.WithEmbeddingOption(embedding.WithModel("text-embedding-3-small")) +// runnable.Invoke(ctx, "input", embeddingOption) +func WithEmbeddingOption(opts ...embedding.Option) Option { + return withComponentOption(opts...) +} + +// WithRetrieverOption is a functional option type for retriever component. +// e.g. +// +// retrieverOption := compose.WithRetrieverOption(retriever.WithIndex("my_index")) +// runnable.Invoke(ctx, "input", retrieverOption) +func WithRetrieverOption(opts ...retriever.Option) Option { + return withComponentOption(opts...) +} + +// WithLoaderOption is a functional option type for loader component. +// e.g. +// +// loaderOption := compose.WithLoaderOption(document.WithCollection("my_collection")) +// runnable.Invoke(ctx, "input", loaderOption) +func WithLoaderOption(opts ...document.LoaderOption) Option { + return withComponentOption(opts...) +} + +// WithDocumentTransformerOption is a functional option type for document transformer component. +func WithDocumentTransformerOption(opts ...document.TransformerOption) Option { + return withComponentOption(opts...) +} + +// WithIndexerOption is a functional option type for indexer component. +// e.g. +// +// indexerOption := compose.WithIndexerOption(indexer.WithSubIndexes([]string{"my_sub_index"})) +// runnable.Invoke(ctx, "input", indexerOption) +func WithIndexerOption(opts ...indexer.Option) Option { + return withComponentOption(opts...) +} + +// WithChatModelOption is a functional option type for chat model component. +// e.g. +// +// chatModelOption := compose.WithChatModelOption(model.WithTemperature(0.7)) +// runnable.Invoke(ctx, "input", chatModelOption) +func WithChatModelOption(opts ...model.Option) Option { + return withComponentOption(opts...) +} + +// WithChatTemplateOption is a functional option type for chat template component. +func WithChatTemplateOption(opts ...prompt.Option) Option { + return withComponentOption(opts...) +} + +// WithToolsNodeOption is a functional option type for tools node component. +func WithToolsNodeOption(opts ...ToolsNodeOption) Option { + return withComponentOption(opts...) +} + +// WithLambdaOption is a functional option type for lambda component. +func WithLambdaOption(opts ...any) Option { + return Option{ + options: opts, + paths: make([]*NodePath, 0), + } +} + +// WithCallbacks set callback handlers for all components in a single call. +// e.g. +// +// runnable.Invoke(ctx, "input", compose.WithCallbacks(&myCallbacks{})) +func WithCallbacks(cbs ...callbacks.Handler) Option { + return Option{ + handler: cbs, + } +} + +// WithRuntimeMaxSteps sets the maximum number of steps for the graph runtime. +// e.g. +// +// runnable.Invoke(ctx, "input", compose.WithRuntimeMaxSteps(20)) +func WithRuntimeMaxSteps(maxSteps int) Option { + return Option{ + maxRunSteps: maxSteps, + } +} + +func withComponentOption[TOption any](opts ...TOption) Option { + o := make([]any, 0, len(opts)) + for i := range opts { + o = append(o, opts[i]) + } + return Option{ + options: o, + paths: make([]*NodePath, 0), + } +} + +func convertOption[TOption any](opts ...any) ([]TOption, error) { + if len(opts) == 0 { + return nil, nil + } + ret := make([]TOption, 0, len(opts)) + for i := range opts { + o, ok := opts[i].(TOption) + if !ok { + return nil, fmt.Errorf("unexpected component option type, expected:%s, actual:%s", reflect.TypeOf((*TOption)(nil)).Elem().String(), reflect.TypeOf(opts[i]).String()) + } + ret = append(ret, o) + } + return ret, nil +} diff --git a/compose/graph_call_options_test.go b/compose/graph_call_options_test.go new file mode 100644 index 0000000..708860b --- /dev/null +++ b/compose/graph_call_options_test.go @@ -0,0 +1,496 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/retriever" + mockDocument "github.com/cloudwego/eino/internal/mock/components/document" + mockEmbedding "github.com/cloudwego/eino/internal/mock/components/embedding" + mockRetriever "github.com/cloudwego/eino/internal/mock/components/retriever" + "github.com/cloudwego/eino/schema" +) + +var optionSuccess = true +var idx int + +func checkOption(opts ...model.Option) bool { + if len(opts) != 2 { + return false + } + o := model.GetCommonOptions(&model.Options{}, opts...) + if o.TopP == nil || *o.TopP != 1.0 { + return false + } + if o.Model == nil { + return false + } + if idx == 0 { + idx = 1 + if o.Model == nil || *o.Model != "123" { + return false + } + } else { + idx = 0 + if o.Model == nil || *o.Model != "456" { + return false + } + } + + return true +} + +type testModel struct{} + +func (t *testModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +func (t *testModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + if !checkOption(opts...) { + optionSuccess = false + } + return &schema.Message{}, nil +} + +func (t *testModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + if !checkOption(opts...) { + optionSuccess = false + } + sr, sw := schema.Pipe[*schema.Message](1) + sw.Send(nil, nil) + sw.Close() + return sr, nil +} + +func TestCallOption(t *testing.T) { + g := NewGraph[[]*schema.Message, *schema.Message]() + err := g.AddLambdaNode("1", InvokableLambdaWithOption(func(ctx context.Context, input []*schema.Message, opts ...string) (output []*schema.Message, err error) { + if len(opts) != 1 || opts[0] != "1" { + t.Fatalf("lambda option length isn't 1 or content isn't '1': %v", opts) + } + return input, nil + })) + assert.Nil(t, err) + + err = g.AddChatModelNode("2", &testModel{}) + assert.Nil(t, err) + + err = g.AddLambdaNode("-", InvokableLambda(func(ctx context.Context, input *schema.Message) (output []*schema.Message, err error) { + return []*schema.Message{input}, nil + })) + assert.Nil(t, err) + + err = g.AddChatModelNode("3", &testModel{}) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", "2") + assert.Nil(t, err) + + err = g.AddEdge("2", "-") + assert.Nil(t, err) + + err = g.AddEdge("-", "3") + assert.Nil(t, err) + + err = g.AddEdge("3", END) + assert.Nil(t, err) + + ctx := context.Background() + + r, err := g.Compile(ctx) + assert.Nil(t, err) + + sessionKey := struct{}{} + startCnt := 0 + endCnt := 0 + opts := []Option{ + WithChatModelOption( + model.WithModel("123"), + ).DesignateNode("2"), + WithChatModelOption( + model.WithModel("456"), + ).DesignateNode("3"), + WithChatModelOption( + model.WithTopP(1.0), + ), + WithCallbacks(callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + startCnt++ + return context.WithValue(ctx, sessionKey, "start") + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + if ctx.Value(sessionKey).(string) == "start" { + endCnt++ + return context.WithValue(ctx, sessionKey, "end") + } + return ctx + }).Build()).DesignateNode("3"), + WithLambdaOption("1").DesignateNode("1"), + } + + _, err = r.Invoke(ctx, []*schema.Message{}, + opts...) + if err != nil { + t.Fatal(err) + } + if !optionSuccess { + t.Fatal("invoke option fail") + } + if startCnt != 1 { + t.Fatal("node callback fail") + } + if endCnt != 1 { + t.Fatal("node callback fail") + } + _, err = r.Stream(ctx, []*schema.Message{}, + opts...) + if err != nil { + t.Fatal(err) + } + if !optionSuccess { + t.Fatal("stream option fail") + } + + srOfCollect, swOfCollect := schema.Pipe[[]*schema.Message](1) + swOfCollect.Send([]*schema.Message{}, nil) + swOfCollect.Close() + _, err = r.Collect(ctx, srOfCollect, opts...) + assert.Nil(t, err) + + if !optionSuccess { + t.Fatal("collect option fail") + } + + srOfTransform, swOfTransform := schema.Pipe[[]*schema.Message](1) + swOfTransform.Send([]*schema.Message{}, nil) + swOfTransform.Close() + _, err = r.Transform(ctx, srOfTransform, opts...) + assert.Nil(t, err) + + if !optionSuccess { + t.Fatal("transform option fail") + } +} + +func TestCallOptionsOneByOne(t *testing.T) { + ctx := context.Background() + t.Run("common_option", func(t *testing.T) { + type option struct { + uid int64 + } + + opt := withComponentOption(&option{uid: 100}) + assert.Len(t, opt.options, 1) + assert.IsType(t, &option{}, opt.options[0]) + assert.Equal(t, &option{uid: 100}, opt.options[0]) + }) + + t.Run("embedding_option", func(t *testing.T) { + ctrl := gomock.NewController(t) + inst := mockEmbedding.NewMockEmbedder(ctrl) + var opt *embedding.Options + inst.EXPECT().EmbedStrings(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, texts []string, opts ...embedding.Option) ([][]float64, error) { + opt = embedding.GetCommonOptions(&embedding.Options{}, opts...) + return nil, nil + }).Times(1) + ch := NewChain[map[string]any, map[string]any]() + ch.AppendEmbedding(inst, WithInputKey("input"), WithOutputKey("output")) + r, err := ch.Compile(ctx) + assert.NoError(t, err) + outs, err := r.Invoke(ctx, + map[string]any{"input": []string{}}, + WithEmbeddingOption(embedding.WithModel("123")), + ) + assert.NoError(t, err) + assert.Contains(t, outs, "output") + + assert.NotNil(t, opt.Model) + assert.Equal(t, "123", *opt.Model) + }) + + t.Run("retriever_option", func(t *testing.T) { + ctrl := gomock.NewController(t) + inst := mockRetriever.NewMockRetriever(ctrl) + var opt *retriever.Options + inst.EXPECT().Retrieve(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + opt = retriever.GetCommonOptions(&retriever.Options{}, opts...) + return nil, nil + }). + Times(1) + ch := NewChain[map[string]any, map[string]any]() + ch.AppendRetriever(inst, WithInputKey("input"), WithOutputKey("output")) + r, err := ch.Compile(ctx) + assert.NoError(t, err) + outs, err := r.Invoke(ctx, + map[string]any{"input": "hi"}, + WithRetrieverOption(retriever.WithIndex("123")), + ) + assert.NoError(t, err) + assert.Contains(t, outs, "output") + + assert.NotNil(t, opt.Index) + assert.Equal(t, "123", *opt.Index) + }) + + t.Run("loader_option", func(t *testing.T) { + ctrl := gomock.NewController(t) + inst := mockDocument.NewMockLoader(ctrl) + type implOption struct { + uid int64 + } + + type implOptFn func(o *implOption) + + withUID := func(uid int64) document.LoaderOption { + return document.WrapLoaderImplSpecificOptFn[implOption](func(i *implOption) { + i.uid = uid + }) + } + + var opt *implOption + + inst.EXPECT().Load(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, src document.Source, opts ...document.LoaderOption) ([]*schema.Document, error) { + opt = document.GetLoaderImplSpecificOptions[implOption](&implOption{uid: 1}, opts...) + return nil, nil + }). + Times(1) + ch := NewChain[map[string]any, map[string]any]() + ch.AppendLoader(inst, WithInputKey("input"), WithOutputKey("output")) + r, err := ch.Compile(ctx) + assert.NoError(t, err) + outs, err := r.Invoke(ctx, + map[string]any{"input": document.Source{}}, + WithLoaderOption(withUID(123)), + ) + assert.NoError(t, err) + assert.Contains(t, outs, "output") + + assert.Equal(t, int64(123), opt.uid) + }) +} + +func TestCallOptionInSubGraph(t *testing.T) { + ctx := context.Background() + + type child1Option string + type child2Option string + type parentOption string + type grandparentOption string + + child1 := NewGraph[string, string]() + err := child1.AddLambdaNode("1", InvokableLambdaWithOption(func(ctx context.Context, input string, opts ...child1Option) (output string, err error) { + if len(opts) != 1 || opts[0] != "child1-1" { + t.Fatal("child1-1 option error") + } + return input + " child1-1", nil + }), WithNodeName("child1-1")) + assert.NoError(t, err) + err = child1.AddEdge(START, "1") + assert.NoError(t, err) + err = child1.AddEdge("1", END) + assert.NoError(t, err) + + child2 := NewGraph[string, string]() + err = child2.AddLambdaNode("1", InvokableLambdaWithOption(func(ctx context.Context, input string, opts ...child2Option) (output string, err error) { + if len(opts) != 1 || opts[0] != "child2-1" { + t.Fatal("child2-1 option error") + } + return input + " child2-1", nil + }), WithNodeName("child2-1")) + assert.NoError(t, err) + err = child2.AddEdge(START, "1") + assert.NoError(t, err) + err = child2.AddEdge("1", END) + assert.NoError(t, err) + + parent := NewGraph[string, string]() + err = parent.AddLambdaNode("1", InvokableLambdaWithOption(func(ctx context.Context, input string, opts ...parentOption) (output string, err error) { + if len(opts) != 1 || opts[0] != "parent-1" { + t.Fatal("parent-1 option error") + } + return input + " parent-1", nil + }), WithNodeName("parent-1")) + assert.NoError(t, err) + err = parent.AddGraphNode("2", child1, WithNodeName("child1")) + assert.NoError(t, err) + err = parent.AddGraphNode("3", child2, WithNodeName("child2")) + assert.NoError(t, err) + err = parent.AddEdge(START, "1") + assert.NoError(t, err) + err = parent.AddEdge("1", "2") + assert.NoError(t, err) + err = parent.AddEdge("2", "3") + assert.NoError(t, err) + err = parent.AddEdge("3", END) + assert.NoError(t, err) + + grandParent := NewGraph[string, string]() + err = grandParent.AddLambdaNode("1", InvokableLambdaWithOption(func(ctx context.Context, input string, opts ...grandparentOption) (output string, err error) { + if len(opts) != 1 || opts[0] != "grandparent-1" { + t.Fatal("grandparent-1 option error") + } + return input + " grandparent-1", nil + }), WithNodeName("grandparent-1")) + assert.NoError(t, err) + err = grandParent.AddGraphNode("2", parent, WithNodeName("parent")) + assert.NoError(t, err) + err = grandParent.AddEdge(START, "1") + assert.NoError(t, err) + err = grandParent.AddEdge("1", "2") + assert.NoError(t, err) + err = grandParent.AddEdge("2", END) + assert.NoError(t, err) + + r, err := grandParent.Compile(ctx, WithGraphName("grandparent")) + assert.NoError(t, err) + + grandCommonTimes := 0 + grandCommonCB := callbacks.NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + switch grandCommonTimes { + case 0: + if info.Name != "grandparent" || info.Component != ComponentOfGraph { + t.Fatal("grandparent common callback 0 error") + } + case 1: + if info.Name != "grandparent-1" { + t.Fatal("grandparent common callback 1 error") + } + case 2: + if info.Name != "parent" { + t.Fatal("grandparent common callback 2 error") + } + case 3: + if info.Name != "parent-1" { + t.Fatal("grandparent common callback 3 error") + } + case 4: + if info.Name != "child1" { + t.Fatal("grandparent common callback 4 error") + } + case 5: + if info.Name != "child1-1" { + t.Fatal("grandparent common callback 5 error") + } + case 6: + if info.Name != "child2" { + t.Fatal("grandparent common callback 6 error") + } + case 7: + if info.Name != "child2-1" { + t.Fatal("grandparent common callback 7 error") + } + default: + t.Fatal("grandparent common callback too many") + } + grandCommonTimes++ + return ctx + }).Build() + grand1CB := callbacks.NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Name != "grandparent-1" { + t.Fatal("grandparent common callback 0 error") + } + return ctx + }).Build() + parentCommonCBTimes := 0 + parentCommonCB := callbacks.NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + switch parentCommonCBTimes { + case 0: + if info.Name != "parent" { + t.Fatal("parent common callback 0 error") + } + case 1: + if info.Name != "parent-1" { + t.Fatal("parent common callback 1 error") + } + case 2: + if info.Name != "child1" { + t.Fatal("parent common callback 2 error") + } + case 3: + if info.Name != "child1-1" { + t.Fatal("parent common callback 3 error") + } + case 4: + if info.Name != "child2" { + t.Fatal("parent common callback 4 error") + } + case 5: + if info.Name != "child2-1" { + t.Fatal("parent common callback 5 error") + } + default: + t.Fatal("parent common callback too many") + } + parentCommonCBTimes++ + return ctx + }).Build() + child1CommonCBTimes := 0 + child1CommonCB := callbacks.NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + switch child1CommonCBTimes { + case 0: + if info.Name != "child1" { + t.Fatal("child1 common callback 0 error") + } + case 1: + if info.Name != "child1-1" { + t.Fatal("child1 common callback 1 error") + } + default: + t.Fatal("child1 common callback too many") + } + child1CommonCBTimes++ + return ctx + }).Build() + child2CB := callbacks.NewHandlerBuilder().OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Name != "child2-1" { + t.Fatal("child2-1 common callback 0 error") + } + return ctx + }).Build() + + result, err := r.Invoke(ctx, "input", + WithCallbacks(grandCommonCB), + WithCallbacks(parentCommonCB).DesignateNodeWithPath(NewNodePath("2")), + WithCallbacks(grand1CB).DesignateNode("1"), + WithCallbacks(child1CommonCB).DesignateNodeWithPath(NewNodePath("2", "2")), + WithCallbacks(child2CB).DesignateNodeWithPath(NewNodePath("2", "3", "1")), + WithLambdaOption(grandparentOption("grandparent-1")).DesignateNodeWithPath(NewNodePath("1")), + WithLambdaOption(parentOption("parent-1")).DesignateNodeWithPath(NewNodePath("2", "1")), + WithLambdaOption(child1Option("child1-1")).DesignateNodeWithPath(NewNodePath("2", "2", "1")), + WithLambdaOption(child2Option("child2-1")).DesignateNodeWithPath(NewNodePath("2", "3", "1")), + ) + assert.NoError(t, err) + assert.Equal(t, result, "input grandparent-1 parent-1 child1-1 child2-1") +} diff --git a/compose/graph_compile_options.go b/compose/graph_compile_options.go new file mode 100644 index 0000000..2e0476f --- /dev/null +++ b/compose/graph_compile_options.go @@ -0,0 +1,133 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +type graphCompileOptions struct { + maxRunSteps int + graphName string + nodeTriggerMode NodeTriggerMode // default to AnyPredecessor (pregel) + + callbacks []GraphCompileCallback + + origOpts []GraphCompileOption + + checkPointStore CheckPointStore + serializer Serializer + interruptBeforeNodes []string + interruptAfterNodes []string + + eagerDisabled bool + + mergeConfigs map[string]FanInMergeConfig +} + +func newGraphCompileOptions(opts ...GraphCompileOption) *graphCompileOptions { + option := &graphCompileOptions{} + + for _, o := range opts { + o(option) + } + + option.origOpts = opts + + return option +} + +// GraphCompileOption options for compiling AnyGraph. +type GraphCompileOption func(*graphCompileOptions) + +// WithMaxRunSteps sets the maximum number of steps that a graph can run. +// This is useful to prevent infinite loops in graphs with cycles. +// If the number of steps exceeds maxSteps, the graph execution will be terminated with an error. +func WithMaxRunSteps(maxSteps int) GraphCompileOption { + return func(o *graphCompileOptions) { + o.maxRunSteps = maxSteps + } +} + +// WithGraphName sets a name for the graph. +// The name is used for debugging and logging purposes. +// If not set, a default name will be used. +func WithGraphName(graphName string) GraphCompileOption { + return func(o *graphCompileOptions) { + o.graphName = graphName + } +} + +// WithEagerExecution enables the eager execution mode for the graph. +// In eager mode, nodes will be executed immediately once they are ready to run, +// without waiting for the completion of a super step, ref: https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/orchestration_design_principles/#runtime-engine +// Note: Eager mode is not allowed when the graph's trigger mode is set to AnyPredecessor. +// Workflow uses eager mode by default. +// Deprecated: Eager execution is automatically enabled by default when a node's trigger mode is set to AllPredecessor. +// If you were using this option previously, it can be safely removed without changing behavior. +func WithEagerExecution() GraphCompileOption { + return func(o *graphCompileOptions) { + return + } +} + +// WithEagerExecutionDisabled disables the eager execution mode for the graph. +// By default, eager execution is enabled for Workflow and Graph with the AllPredecessor trigger mode. +// After using this option, nodes will wait for the completion of a super step instead of execute immediately once they are ready to run. +// ref: https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/orchestration_design_principles/#runtime-engine +func WithEagerExecutionDisabled() GraphCompileOption { + return func(o *graphCompileOptions) { + o.eagerDisabled = true + } +} + +// WithNodeTriggerMode sets the trigger mode for nodes in the graph. +// The trigger mode determines when a node is triggered during graph execution, ref: https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/orchestration_design_principles/#runtime-engine +// AnyPredecessor by default. +func WithNodeTriggerMode(triggerMode NodeTriggerMode) GraphCompileOption { + return func(o *graphCompileOptions) { + o.nodeTriggerMode = triggerMode + } +} + +// WithGraphCompileCallbacks sets callbacks for graph compilation. +func WithGraphCompileCallbacks(cbs ...GraphCompileCallback) GraphCompileOption { + return func(o *graphCompileOptions) { + o.callbacks = append(o.callbacks, cbs...) + } +} + +// FanInMergeConfig defines the configuration for fan-in merge operations. +// It allows specifying how multiple inputs are merged into a single input. +// StreamMergeWithSourceEOF indicates whether to emit a SourceEOF error for each stream +// when it ends, before the final merged output is produced. This is useful for +// tracking the completion of individual input streams in a named stream merge. +type FanInMergeConfig struct { + StreamMergeWithSourceEOF bool //indicates whether to emit a SourceEOF error for each stream +} + +// WithFanInMergeConfig sets the fan-in merge configurations +// for the graph nodes that receive inputs from multiple sources. +func WithFanInMergeConfig(confs map[string]FanInMergeConfig) GraphCompileOption { + return func(o *graphCompileOptions) { + o.mergeConfigs = confs + } +} + +// InitGraphCompileCallbacks set global graph compile callbacks, +// which ONLY will be added to top level graph compile options +func InitGraphCompileCallbacks(cbs []GraphCompileCallback) { + globalGraphCompileCallbacks = cbs +} + +var globalGraphCompileCallbacks []GraphCompileCallback diff --git a/compose/graph_manager.go b/compose/graph_manager.go new file mode 100644 index 0000000..46df348 --- /dev/null +++ b/compose/graph_manager.go @@ -0,0 +1,556 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "runtime/debug" + "time" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/internal/safe" +) + +type channel interface { + reportValues(map[string]any) error + reportDependencies([]string) + reportSkip([]string) bool + get(bool, string, *edgeHandlerManager) (any, bool, error) + convertValues(fn func(map[string]any) error) error + load(channel) error + + setMergeConfig(FanInMergeConfig) +} + +type edgeHandlerManager struct { + h map[string]map[string][]handlerPair +} + +func (e *edgeHandlerManager) handle(from, to string, value any, isStream bool) (any, error) { + if _, ok := e.h[from]; !ok { + return value, nil + } + if _, ok := e.h[from][to]; !ok { + return value, nil + } + if isStream { + for _, v := range e.h[from][to] { + value = v.transform(value.(streamReader)) + } + } else { + for _, v := range e.h[from][to] { + var err error + value, err = v.invoke(value) + if err != nil { + return nil, err + } + } + } + return value, nil +} + +type preNodeHandlerManager struct { + h map[string][]handlerPair +} + +func (p *preNodeHandlerManager) handle(nodeKey string, value any, isStream bool) (any, error) { + if _, ok := p.h[nodeKey]; !ok { + return value, nil + } + if isStream { + for _, v := range p.h[nodeKey] { + value = v.transform(value.(streamReader)) + } + } else { + for _, v := range p.h[nodeKey] { + var err error + value, err = v.invoke(value) + if err != nil { + return nil, err + } + } + } + return value, nil +} + +type preBranchHandlerManager struct { + h map[string][][]handlerPair +} + +func (p *preBranchHandlerManager) handle(nodeKey string, idx int, value any, isStream bool) (any, error) { + if _, ok := p.h[nodeKey]; !ok { + return value, nil + } + if isStream { + for _, v := range p.h[nodeKey][idx] { + value = v.transform(value.(streamReader)) + } + } else { + for _, v := range p.h[nodeKey][idx] { + var err error + value, err = v.invoke(value) + if err != nil { + return nil, err + } + } + } + return value, nil +} + +type channelManager struct { + isStream bool + channels map[string]channel + + successors map[string][]string + dataPredecessors map[string]map[string]struct{} + controlPredecessors map[string]map[string]struct{} + + edgeHandlerManager *edgeHandlerManager + preNodeHandlerManager *preNodeHandlerManager +} + +func (c *channelManager) loadChannels(channels map[string]channel) error { + for key, ch := range c.channels { + if nCh, ok := channels[key]; ok { + if err := ch.load(nCh); err != nil { + return fmt.Errorf("load channel[%s] fail: %w", key, err) + } + } + } + return nil +} + +func (c *channelManager) updateValues(_ context.Context, values map[string] /*to*/ map[string] /*from*/ any) error { + for target, fromMap := range values { + toChannel, ok := c.channels[target] + if !ok { + return fmt.Errorf("target channel doesn't existed: %s", target) + } + dps, ok := c.dataPredecessors[target] + if !ok { + dps = map[string]struct{}{} + } + nFromMap := make(map[string]any, len(fromMap)) + for from, value := range fromMap { + if _, ok = dps[from]; ok { + nFromMap[from] = fromMap[from] + } else { + if sr, okk := value.(streamReader); okk { + sr.close() + } + } + } + + err := toChannel.reportValues(nFromMap) + if err != nil { + return fmt.Errorf("update target channel[%s] fail: %w", target, err) + } + } + return nil +} + +func (c *channelManager) updateDependencies(_ context.Context, dependenciesMap map[string][]string) error { + for target, dependencies := range dependenciesMap { + toChannel, ok := c.channels[target] + if !ok { + return fmt.Errorf("target channel doesn't existed: %s", target) + } + cps, ok := c.controlPredecessors[target] + if !ok { + cps = map[string]struct{}{} + } + var deps []string + for _, from := range dependencies { + if _, ok = cps[from]; ok { + deps = append(deps, from) + } + } + + toChannel.reportDependencies(deps) + } + return nil +} + +func (c *channelManager) getFromReadyChannels(_ context.Context) (map[string]any, error) { + result := make(map[string]any) + for target, ch := range c.channels { + v, ready, err := ch.get(c.isStream, target, c.edgeHandlerManager) + if err != nil { + return nil, fmt.Errorf("get value from ready channel[%s] fail: %w", target, err) + } + if ready { + v, err = c.preNodeHandlerManager.handle(target, v, c.isStream) + if err != nil { + return nil, err + } + result[target] = v + } + } + return result, nil +} + +func (c *channelManager) updateAndGet(ctx context.Context, values map[string]map[string]any, dependencies map[string][]string) (map[string]any, error) { + err := c.updateValues(ctx, values) + if err != nil { + return nil, fmt.Errorf("update channel fail: %w", err) + } + err = c.updateDependencies(ctx, dependencies) + if err != nil { + return nil, fmt.Errorf("update channel fail: %w", err) + } + return c.getFromReadyChannels(ctx) +} + +func (c *channelManager) reportBranch(from string, skippedNodes []string) error { + var nKeys []string + for _, node := range skippedNodes { + skipped := c.channels[node].reportSkip([]string{from}) + if skipped { + nKeys = append(nKeys, node) + } + } + + for i := 0; i < len(nKeys); i++ { + key := nKeys[i] + + if key == END { + continue + } + if _, ok := c.successors[key]; !ok { + return fmt.Errorf("unknown node: %s", key) + } + for _, successor := range c.successors[key] { + skipped := c.channels[successor].reportSkip([]string{key}) + if skipped { + nKeys = appendIfNotExist(nKeys, successor) + } + // todo: detect if end node has been skipped? + } + } + return nil +} + +func appendIfNotExist(s []string, elem string) []string { + for _, i := range s { + if i == elem { + return s + } + } + return append(s, elem) +} + +type task struct { + ctx context.Context + nodeKey string + call *chanCall + input any + originalInput any + output any + option []any + err error + skipPreHandler bool +} + +type taskManager struct { + runWrapper runnableCallWrapper + opts []Option + needAll bool + + num uint32 + done *internal.UnboundedChan[*task] + runningTasks map[string]*task + + cancelCh chan *time.Duration + canceled bool + deadline *time.Time + + persistRerunInput bool +} + +func (t *taskManager) execute(currentTask *task) { + defer func() { + panicInfo := recover() + if panicInfo != nil { + currentTask.output = nil + currentTask.err = safe.NewPanicErr(panicInfo, debug.Stack()) + } + + t.done.Send(currentTask) + }() + + ctx := initNodeCallbacks(currentTask.ctx, currentTask.nodeKey, currentTask.call.action.nodeInfo, currentTask.call.action.meta, t.opts...) + currentTask.output, currentTask.err = t.runWrapper(ctx, currentTask.call.action, currentTask.input, currentTask.option...) +} + +func (t *taskManager) submit(tasks []*task) error { + if len(tasks) == 0 { + return nil + } + + // synchronously execute one task, if there are no other tasks in the task pool and meet one of the following conditions: + // 1. the new task is the only one + // 2. the task manager mode is set to needAll + for i := 0; i < len(tasks); i++ { + currentTask := tasks[i] + + if t.persistRerunInput { + if sr, ok := currentTask.input.(streamReader); ok { + copies := sr.copy(2) + currentTask.originalInput, currentTask.input = copies[0], copies[1] + } else { + currentTask.originalInput = currentTask.input + } + } + + err := runPreHandler(currentTask, t.runWrapper) + if err != nil { + // pre-handler error, regarded as a failure of the task itself + currentTask.err = err + tasks = append(tasks[:i], tasks[i+1:]...) + i-- + t.num++ + t.done.Send(currentTask) + } + + t.runningTasks[currentTask.nodeKey] = currentTask + } + if len(tasks) == 0 { + // all tasks' pre-handler failed + return nil + } + + var syncTask *task + if t.num == 0 && (len(tasks) == 1 || t.needAll) && t.cancelCh == nil /*if graph can be interrupted by user, shouldn't sync run task*/ { + syncTask = tasks[0] + tasks = tasks[1:] + } + for _, currentTask := range tasks { + t.num += 1 + go t.execute(currentTask) + } + if syncTask != nil { + t.num += 1 + t.execute(syncTask) + } + return nil +} + +func (t *taskManager) wait() (tasks []*task, canceled bool, canceledTasks []*task) { + if t.needAll { + tasks, canceledTasks = t.waitAll() + return tasks, t.canceled, canceledTasks + } + + ta, success, canceled := t.waitOne() + if canceled { + // has canceled and timeout, return canceled tasks + for _, rta := range t.runningTasks { + canceledTasks = append(canceledTasks, rta) + } + t.runningTasks = make(map[string]*task) + t.num = 0 + return nil, true, canceledTasks + } + if t.canceled { + // has canceled, but not timeout, wait all + tasks, canceledTasks = t.waitAll() + return append(tasks, ta), true, canceledTasks + } + if !success { + return []*task{}, t.canceled, nil + } + + return []*task{ta}, t.canceled, nil +} + +func (t *taskManager) waitOne() (ta *task, success bool, canceled bool) { + if t.num == 0 { + return nil, false, false + } + + if t.cancelCh == nil { + ta, _ = t.done.Receive() + } else { + ta, _, canceled = t.receive(t.done.Receive) + } + + t.num-- + + if canceled { + return nil, false, true + } + + delete(t.runningTasks, ta.nodeKey) + + if ta.originalInput != nil && (ta.err == nil || !isInterruptError(ta.err)) { + if sr, ok := ta.originalInput.(streamReader); ok { + sr.close() + } + ta.originalInput = nil + } + + if ta.err != nil { + // biz error, jump post processor + return ta, true, false + } + runPostHandler(ta, t.runWrapper) + return ta, true, false +} + +func (t *taskManager) waitAll() (successTasks []*task, canceledTasks []*task) { + result := make([]*task, 0, t.num) + for { + ta, success, canceled := t.waitOne() + if canceled { + for _, rt := range t.runningTasks { + canceledTasks = append(canceledTasks, rt) + } + t.runningTasks = make(map[string]*task) + t.num = 0 + return result, canceledTasks + } + if !success { + return result, nil + } + result = append(result, ta) + } +} + +func (t *taskManager) receive(recv func() (*task, bool)) (ta *task, closed bool, canceled bool) { + if t.deadline != nil { + // have canceled, receive in a certain time + return receiveWithDeadline(recv, *t.deadline) + } + if t.canceled { + // canceled without timeout + ta, closed = recv() + return ta, closed, false + } + if t.cancelCh != nil { + // have not canceled, receive while listening + ta, closed, canceled, t.canceled, t.deadline = receiveWithListening(recv, t.cancelCh) + return ta, closed, canceled + } + // won't cancel + ta, closed = recv() + return ta, closed, false +} + +func receiveWithDeadline(recv func() (*task, bool), deadline time.Time) (ta *task, closed bool, canceled bool) { + now := time.Now() + if deadline.Before(now) { + return nil, false, true + } + + timeout := deadline.Sub(now) + + resultCh := make(chan struct{}, 1) + + go func() { + ta, closed = recv() + resultCh <- struct{}{} + }() + + timeoutCh := time.After(timeout) + + select { + case <-resultCh: + return ta, closed, false + case <-timeoutCh: + return nil, false, true + } +} + +func receiveWithListening(recv func() (*task, bool), cancel chan *time.Duration) (*task, bool, bool, bool, *time.Time) { + type pair struct { + ta *task + closed bool + } + resultCh := make(chan pair, 1) + var timeoutCh <-chan time.Time + + var deadline *time.Time + canceled := false + go func() { + ta, closed := recv() + resultCh <- pair{ta, closed} + }() + + select { + case p := <-resultCh: + return p.ta, p.closed, false, false, nil + case timeout, ok := <-cancel: + if !ok { + // The cancel channel has been closed — this means a previous call to + // receiveWithListening already consumed the cancel signal (task completed + // at the same time as cancel, and select picked the task result). Since + // cancel was already issued, treat this as an immediate cancel rather than + // blocking forever on resultCh. + return nil, false, true, true, nil + } + canceled = true + if timeout == nil { + break + } + timeoutCh = time.After(*timeout) + dt := time.Now().Add(*timeout) + deadline = &dt + } + + if timeoutCh != nil { + select { + case p := <-resultCh: + return p.ta, p.closed, false, canceled, deadline + case <-timeoutCh: + return nil, false, true, canceled, deadline + } + } + p := <-resultCh + return p.ta, p.closed, false, canceled, nil +} + +func runPreHandler(ta *task, runWrapper runnableCallWrapper) (err error) { + defer func() { + if e := recover(); e != nil { + err = safe.NewPanicErr(fmt.Errorf("panic in pre handler: %v", e), debug.Stack()) + } + }() + if ta.call.preProcessor != nil && !ta.skipPreHandler { + nInput, err := runWrapper(ta.ctx, ta.call.preProcessor, ta.input, ta.option...) + if err != nil { + return fmt.Errorf("run node[%s] pre processor fail: %w", ta.nodeKey, err) + } + ta.input = nInput + } + return nil +} + +func runPostHandler(ta *task, runWrapper runnableCallWrapper) { + defer func() { + if e := recover(); e != nil { + ta.err = safe.NewPanicErr(fmt.Errorf("panic in post handler: %v", e), debug.Stack()) + } + }() + if ta.call.postProcessor != nil { + nOutput, err := runWrapper(ta.ctx, ta.call.postProcessor, ta.output, ta.option...) + if err != nil { + ta.err = fmt.Errorf("run node[%s] post processor fail: %w", ta.nodeKey, err) + } + ta.output = nOutput + } +} diff --git a/compose/graph_node.go b/compose/graph_node.go new file mode 100644 index 0000000..47091eb --- /dev/null +++ b/compose/graph_node.go @@ -0,0 +1,177 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "reflect" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/internal/generic" +) + +// the info of most original executable object directly provided by the user +type executorMeta struct { + + // automatically identified based on the way of addNode + component component + + // indicates whether the executable object user provided could execute the callback aspect itself. + // if it could, the callback in the corresponding graph node won't be executed + // for components, the value comes from callbacks.Checker + isComponentCallbackEnabled bool + + // for components, the value comes from components.Typer + // for lambda, the value comes from the user's explicit config + // if componentImplType is empty, then the class name or func name in the instance will be inferred, but no guarantee. + componentImplType string +} + +type nodeInfo struct { + + // the name of graph node for display purposes, not unique. + // passed from WithNodeName() + name string + + inputKey string + outputKey string + + preProcessor, postProcessor *composableRunnable + + compileOption *graphCompileOptions // if the node is an AnyGraph, it will need compile options of its own +} + +// graphNode the complete information of the node in graph +type graphNode struct { + cr *composableRunnable + + g AnyGraph + + nodeInfo *nodeInfo + executorMeta *executorMeta + + instance any + opts []GraphAddNodeOpt +} + +func (gn *graphNode) getGenericHelper() *genericHelper { + var ret *genericHelper + if gn.g != nil { + ret = gn.g.getGenericHelper() + } else if gn.cr != nil { + ret = gn.cr.genericHelper + } else { + return nil + } + + if gn.nodeInfo != nil { + if len(gn.nodeInfo.inputKey) > 0 { + ret = ret.forMapInput() + } + if len(gn.nodeInfo.outputKey) > 0 { + ret = ret.forMapOutput() + } + } + return ret +} + +func (gn *graphNode) inputType() reflect.Type { + if gn.nodeInfo != nil && len(gn.nodeInfo.inputKey) != 0 { + return generic.TypeOf[map[string]any]() + } + // priority follow compile + if gn.g != nil { + return gn.g.inputType() + } else if gn.cr != nil { + return gn.cr.inputType + } + + return nil +} + +func (gn *graphNode) outputType() reflect.Type { + if gn.nodeInfo != nil && len(gn.nodeInfo.outputKey) != 0 { + return generic.TypeOf[map[string]any]() + } + // priority follow compile + if gn.g != nil { + return gn.g.outputType() + } else if gn.cr != nil { + return gn.cr.outputType + } + + return nil +} + +func (gn *graphNode) compileIfNeeded(ctx context.Context) (*composableRunnable, error) { + var r *composableRunnable + if gn.g != nil { + cr, err := gn.g.compile(ctx, gn.nodeInfo.compileOption) + if err != nil { + return nil, err + } + + r = cr + gn.cr = cr + } else if gn.cr != nil { + r = gn.cr + } else { + return nil, errors.New("no graph or component provided") + } + + r.meta = gn.executorMeta + r.nodeInfo = gn.nodeInfo + + if gn.nodeInfo.outputKey != "" { + r = outputKeyedComposableRunnable(gn.nodeInfo.outputKey, r) + } + + if gn.nodeInfo.inputKey != "" { + r = inputKeyedComposableRunnable(gn.nodeInfo.inputKey, r) + } + + return r, nil +} + +func parseExecutorInfoFromComponent(c component, executor any) *executorMeta { + + componentImplType, ok := components.GetType(executor) + if !ok { + componentImplType = generic.ParseTypeName(reflect.ValueOf(executor)) + } + + return &executorMeta{ + component: c, + isComponentCallbackEnabled: components.IsCallbacksEnabled(executor), + componentImplType: componentImplType, + } +} + +func getNodeInfo(opts ...GraphAddNodeOpt) (*nodeInfo, *graphAddNodeOpts) { + + opt := getGraphAddNodeOpts(opts...) + + return &nodeInfo{ + name: opt.nodeOptions.nodeName, + inputKey: opt.nodeOptions.inputKey, + outputKey: opt.nodeOptions.outputKey, + preProcessor: opt.processor.statePreHandler, + postProcessor: opt.processor.statePostHandler, + compileOption: newGraphCompileOptions(opt.nodeOptions.graphCompileOption...), + }, opt +} diff --git a/compose/graph_run.go b/compose/graph_run.go new file mode 100644 index 0000000..02b4fca --- /dev/null +++ b/compose/graph_run.go @@ -0,0 +1,1055 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/internal/serialization" +) + +type chanCall struct { + action *composableRunnable + writeTo []string + writeToBranches []*GraphBranch + + controls []string // branch must control + + preProcessor, postProcessor *composableRunnable +} + +type chanBuilder func(dependencies []string, indirectDependencies []string, zeroValue func() any, emptyStream func() streamReader) channel + +type runner struct { + chanSubscribeTo map[string]*chanCall + + successors map[string][]string + dataPredecessors map[string][]string + controlPredecessors map[string][]string + + inputChannels *chanCall + + chanBuilder chanBuilder // could be nil + eager bool + dag bool + + runCtx func(ctx context.Context) context.Context + + options graphCompileOptions + + inputType reflect.Type + outputType reflect.Type + + // take effect as a subgraph through toComposableRunnable + inputStreamFilter streamMapFilter + inputConverter handlerPair + inputFieldMappingConverter handlerPair + inputConvertStreamPair, outputConvertStreamPair streamConvertPair + + *genericHelper + + // checks need to do because cannot check at compile + runtimeCheckEdges map[string]map[string]bool + runtimeCheckBranches map[string][]bool + + edgeHandlerManager *edgeHandlerManager + preNodeHandlerManager *preNodeHandlerManager + preBranchHandlerManager *preBranchHandlerManager + + checkPointer *checkPointer + interruptBeforeNodes []string + interruptAfterNodes []string + + mergeConfigs map[string]FanInMergeConfig +} + +func (r *runner) invoke(ctx context.Context, input any, opts ...Option) (any, error) { + return r.run(ctx, false, input, opts...) +} + +func (r *runner) transform(ctx context.Context, input streamReader, opts ...Option) (streamReader, error) { + s, err := r.run(ctx, true, input, opts...) + if err != nil { + return nil, err + } + + return s.(streamReader), nil +} + +type runnableCallWrapper func(context.Context, *composableRunnable, any, ...any) (any, error) + +func runnableInvoke(ctx context.Context, r *composableRunnable, input any, opts ...any) (any, error) { + return r.i(ctx, input, opts...) +} + +func runnableTransform(ctx context.Context, r *composableRunnable, input any, opts ...any) (any, error) { + return r.t(ctx, input.(streamReader), opts...) +} + +func (r *runner) run(ctx context.Context, isStream bool, input any, opts ...Option) (result any, err error) { + haveOnStart := false // delay triggering onGraphStart until state initialization is complete, so that the state can be accessed within onGraphStart. + defer func() { + if !haveOnStart { + ctx, input = onGraphStart(ctx, input, isStream) + } + if err != nil { + ctx, err = onGraphError(ctx, err) + } else { + ctx, result = onGraphEnd(ctx, result, isStream) + } + }() + + var runWrapper runnableCallWrapper + runWrapper = runnableInvoke + if isStream { + runWrapper = runnableTransform + } + + // Initialize channel and task managers. + cm := r.initChannelManager(isStream) + tm := r.initTaskManager(runWrapper, getGraphCancel(ctx), opts...) + maxSteps := r.options.maxRunSteps + + maxSteps, err = r.resolveMaxSteps(maxSteps, opts) + if err != nil { + return nil, err + } + + // Extract and validate options for each node. + optMap, extractErr := extractOption(r.chanSubscribeTo, opts...) + if extractErr != nil { + return nil, newGraphRunError(fmt.Errorf("graph extract option fail: %w", extractErr)) + } + + // Extract CheckPointID + checkPointID, writeToCheckPointID, stateModifier, forceNewRun := getCheckPointInfo(opts...) + if checkPointID != nil && r.checkPointer.store == nil { + return nil, newGraphRunError(fmt.Errorf("receive checkpoint id but have not set checkpoint store")) + } + + // Extract subgraph + path, isSubGraph := getNodePath(ctx) + + // load checkpoint from ctx/store or init graph + initialized := false + var nextTasks []*task + if cp := getCheckPointFromCtx(ctx); cp != nil { + // in subgraph, try to load checkpoint from ctx + initialized = true + + ctx, err = r.restoreCheckPointState(ctx, *path, getStateModifier(ctx), cp, isStream, cm) + if err != nil { + return nil, err + } + + ctx, input = onGraphStart(ctx, input, isStream) + haveOnStart = true + + nextTasks, err = r.restoreTasks(ctx, cp.Inputs, cp.SkipPreHandler, cp.RerunNodes, isStream, optMap) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("restore tasks fail: %w", err)) + } + } else if checkPointID != nil && !forceNewRun { + cp, err = getCheckPointFromStore(ctx, *checkPointID, r.checkPointer) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("load checkpoint from store fail: %w", err)) + } + if cp != nil { + // load checkpoint from store + initialized = true + + ctx = setStateModifier(ctx, stateModifier) + ctx = setCheckPointToCtx(ctx, cp) + + ctx, err = r.restoreCheckPointState(ctx, *NewNodePath(), stateModifier, cp, isStream, cm) + if err != nil { + return nil, err + } + + ctx, input = onGraphStart(ctx, input, isStream) + haveOnStart = true + + nextTasks, err = r.restoreTasks(ctx, cp.Inputs, cp.SkipPreHandler, cp.RerunNodes, isStream, optMap) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("restore tasks fail: %w", err)) + } + } + } + if !initialized { + // have not inited from checkpoint + if r.runCtx != nil { + ctx = r.runCtx(ctx) + } + + ctx, input = onGraphStart(ctx, input, isStream) + haveOnStart = true + + var isEnd bool + nextTasks, result, isEnd, err = r.calculateNextTasks(ctx, []*task{{ + nodeKey: START, + call: r.inputChannels, + output: input, + }}, isStream, cm, optMap) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("calculate next tasks fail: %w", err)) + } + if isEnd { + return result, nil + } + if len(nextTasks) == 0 { + return nil, newGraphRunError(fmt.Errorf("no tasks to execute after graph start")) + } + + if keys := getHitKey(nextTasks, r.interruptBeforeNodes); len(keys) > 0 { + tempInfo := newInterruptTempInfo() + tempInfo.interruptBeforeNodes = append(tempInfo.interruptBeforeNodes, keys...) + return nil, r.handleInterrupt(ctx, + tempInfo, + nextTasks, + cm.channels, + isStream, + isSubGraph, + writeToCheckPointID, + ) + } + } + + // used to reporting NoTask error + var lastCompletedTask []*task + + // Main execution loop. + for step := 0; ; step++ { + // Check for context cancellation. + select { + case <-ctx.Done(): + _, _ = tm.waitAll() + return nil, newGraphRunError(fmt.Errorf("context has been canceled: %w", ctx.Err())) + default: + } + if !r.dag && step >= maxSteps { + return nil, newGraphRunError(ErrExceedMaxSteps) + } + + // 1. submit next tasks + // 2. get completed tasks + // 3. calculate next tasks + + err = tm.submit(nextTasks) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("failed to submit tasks: %w", err)) + } + + var totalCanceledTasks []*task + + completedTasks, canceled, canceledTasks := tm.wait() + totalCanceledTasks = append(totalCanceledTasks, canceledTasks...) + tempInfo := newInterruptTempInfo() + tempInfo.collectCanceledInfo(canceled, canceledTasks, completedTasks) + + err = r.resolveInterruptCompletedTasks(tempInfo, completedTasks) + if err != nil { + return nil, err // err has been wrapped + } + + if len(tempInfo.subGraphInterrupts)+len(tempInfo.interruptRerunNodes) > 0 { + var newCompletedTasks []*task + newCompletedTasks, canceledTasks = tm.waitAll() + totalCanceledTasks = append(totalCanceledTasks, canceledTasks...) + for _, ct := range canceledTasks { + // handle timeout tasks as rerun + tempInfo.interruptRerunNodes = append(tempInfo.interruptRerunNodes, ct.nodeKey) + } + + err = r.resolveInterruptCompletedTasks(tempInfo, newCompletedTasks) + if err != nil { + return nil, err // err has been wrapped + } + + // subgraph has interrupted + // save other completed tasks to channel + // save interrupted subgraph as next task with SkipPreHandler + // report current graph interrupt info + return nil, r.handleInterruptWithSubGraphAndRerunNodes( + ctx, + tempInfo, + append(append(completedTasks, newCompletedTasks...), totalCanceledTasks...), // canceled tasks are handled as rerun + writeToCheckPointID, + isSubGraph, + cm, + isStream, + ) + } + + if len(completedTasks) == 0 { + return nil, newGraphRunError(fmt.Errorf("no tasks to execute, last completed nodes: %v", printTask(lastCompletedTask))) + } + lastCompletedTask = completedTasks + + var isEnd bool + nextTasks, result, isEnd, err = r.calculateNextTasks(ctx, completedTasks, isStream, cm, optMap) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("failed to calculate next tasks: %w", err)) + } + if isEnd { + return result, nil + } + + tempInfo.interruptBeforeNodes = getHitKey(nextTasks, r.interruptBeforeNodes) + + if len(tempInfo.interruptBeforeNodes) > 0 || len(tempInfo.interruptAfterNodes) > 0 { + var newCompletedTasks []*task + newCompletedTasks, canceledTasks = tm.waitAll() + totalCanceledTasks = append(totalCanceledTasks, canceledTasks...) + for _, ct := range canceledTasks { + tempInfo.interruptRerunNodes = append(tempInfo.interruptRerunNodes, ct.nodeKey) + } + + err = r.resolveInterruptCompletedTasks(tempInfo, newCompletedTasks) + if err != nil { + return nil, err // err has been wrapped + } + + if len(tempInfo.subGraphInterrupts)+len(tempInfo.interruptRerunNodes) > 0 { + return nil, r.handleInterruptWithSubGraphAndRerunNodes( + ctx, + tempInfo, + append(append(completedTasks, newCompletedTasks...), totalCanceledTasks...), + writeToCheckPointID, + isSubGraph, + cm, + isStream, + ) + } + + var newNextTasks []*task + newNextTasks, result, isEnd, err = r.calculateNextTasks(ctx, newCompletedTasks, isStream, cm, optMap) + if err != nil { + return nil, newGraphRunError(fmt.Errorf("failed to calculate next tasks: %w", err)) + } + + if isEnd { + return result, nil + } + + tempInfo.interruptBeforeNodes = append(tempInfo.interruptBeforeNodes, getHitKey(newNextTasks, r.interruptBeforeNodes)...) + + // simple interrupt + return nil, r.handleInterrupt(ctx, tempInfo, append(nextTasks, newNextTasks...), cm.channels, isStream, isSubGraph, writeToCheckPointID) + } + } +} + +func (r *runner) resolveMaxSteps(maxSteps int, opts []Option) (int, error) { + if r.dag { + for i := range opts { + if opts[i].maxRunSteps > 0 { + return 0, newGraphRunError(fmt.Errorf("cannot set max run steps in dag")) + } + } + return maxSteps, nil + } + for i := range opts { + if opts[i].maxRunSteps > 0 { + maxSteps = opts[i].maxRunSteps + } + } + if maxSteps < 1 { + return 0, newGraphRunError(errors.New("max run steps limit must be at least 1")) + } + return maxSteps, nil +} + +func (r *runner) restoreCheckPointState( + ctx context.Context, + path NodePath, + sm StateModifier, + cp *checkpoint, + isStream bool, + cm *channelManager, +) (context.Context, error) { + err := r.checkPointer.restoreCheckPoint(cp, isStream) + if err != nil { + return ctx, newGraphRunError(fmt.Errorf("restore checkpoint fail: %w", err)) + } + + err = cm.loadChannels(cp.Channels) + if err != nil { + return ctx, newGraphRunError(err) + } + if sm != nil && cp.State != nil { + err = sm(ctx, path, cp.State) + if err != nil { + return ctx, newGraphRunError(fmt.Errorf("state modifier fail: %w", err)) + } + } + if cp.State != nil { + isResumeTarget, hasData, data := GetResumeContext[any](ctx) + if isResumeTarget && hasData { + cp.State = data + } + + var parent *internalState + if prev := ctx.Value(stateKey{}); prev != nil { + if p, ok := prev.(*internalState); ok { + parent = p + } + } + + ctx = context.WithValue(ctx, stateKey{}, &internalState{state: cp.State, parent: parent}) + } + + return ctx, nil +} + +func newInterruptTempInfo() *interruptTempInfo { + return &interruptTempInfo{ + subGraphInterrupts: map[string]*subGraphInterruptError{}, + interruptRerunExtra: map[string]any{}, + } +} + +type interruptTempInfo struct { + subGraphInterrupts map[string]*subGraphInterruptError + interruptRerunNodes []string + interruptBeforeNodes []string + interruptAfterNodes []string + interruptRerunExtra map[string]any + + signals []*core.InterruptSignal +} + +func (ti *interruptTempInfo) collectCanceledInfo(canceled bool, canceledTasks, completedTasks []*task) { + if !canceled { + return + } + + if len(canceledTasks) > 0 { + for _, t := range canceledTasks { + ti.interruptRerunNodes = append(ti.interruptRerunNodes, t.nodeKey) + } + } else { + for _, t := range completedTasks { + ti.interruptAfterNodes = append(ti.interruptAfterNodes, t.nodeKey) + } + } +} + +func (r *runner) resolveInterruptCompletedTasks(tempInfo *interruptTempInfo, completedTasks []*task) (err error) { + for _, completedTask := range completedTasks { + if completedTask.err != nil { + if info := isSubGraphInterrupt(completedTask.err); info != nil { + tempInfo.subGraphInterrupts[completedTask.nodeKey] = info + tempInfo.signals = append(tempInfo.signals, info.signal) + continue + } + + ire := &core.InterruptSignal{} + if errors.As(completedTask.err, &ire) { + tempInfo.interruptRerunNodes = append(tempInfo.interruptRerunNodes, completedTask.nodeKey) + if ire.Info != nil { + tempInfo.interruptRerunExtra[completedTask.nodeKey] = ire.InterruptInfo.Info + } + + tempInfo.signals = append(tempInfo.signals, ire) + continue + } + + return wrapGraphNodeError(completedTask.nodeKey, completedTask.err) + } + + for _, key := range r.interruptAfterNodes { + if key == completedTask.nodeKey { + tempInfo.interruptAfterNodes = append(tempInfo.interruptAfterNodes, key) + break + } + } + } + return nil +} + +func getHitKey(tasks []*task, keys []string) []string { + var ret []string + for _, t := range tasks { + for _, key := range keys { + if key == t.nodeKey { + ret = append(ret, t.nodeKey) + } + } + } + return ret +} + +func (r *runner) handleInterrupt( + ctx context.Context, + tempInfo *interruptTempInfo, + nextTasks []*task, + channels map[string]channel, + isStream bool, + isSubGraph bool, + checkPointID *string, +) error { + cp := &checkpoint{ + Channels: channels, + Inputs: make(map[string]any), + SkipPreHandler: map[string]bool{}, + } + if r.runCtx != nil { + // current graph has enable state + if state, ok := ctx.Value(stateKey{}).(*internalState); ok { + state.mu.Lock() + copiedState, err := deepCopyState(state.state) + state.mu.Unlock() + if err != nil { + return fmt.Errorf("failed to copy state: %w", err) + } + cp.State = copiedState + } + } + + intInfo := &InterruptInfo{ + State: cp.State, + AfterNodes: tempInfo.interruptAfterNodes, + BeforeNodes: tempInfo.interruptBeforeNodes, + RerunNodes: tempInfo.interruptRerunNodes, + RerunNodesExtra: tempInfo.interruptRerunExtra, + SubGraphs: make(map[string]*InterruptInfo), + } + + info := cp.State + + is, err := core.Interrupt(ctx, info, nil, tempInfo.signals) + if err != nil { + return fmt.Errorf("failed to interrupt: %w", err) + } + + cp.InterruptID2Addr, cp.InterruptID2State = core.SignalToPersistenceMaps(is) + + for _, t := range nextTasks { + cp.Inputs[t.nodeKey] = t.input + } + err = r.checkPointer.convertCheckPoint(cp, isStream) + if err != nil { + return fmt.Errorf("failed to convert checkpoint: %w", err) + } + if isSubGraph { + return &subGraphInterruptError{ + Info: intInfo, + CheckPoint: cp, + signal: is, + } + } else if checkPointID != nil { + err := r.checkPointer.set(ctx, *checkPointID, cp) + if err != nil { + return fmt.Errorf("failed to set checkpoint: %w, checkPointID: %s", err, *checkPointID) + } + } + + intInfo.InterruptContexts = core.ToInterruptContexts(is, nil) + return &interruptError{Info: intInfo} +} + +// deepCopyState creates a deep copy of the state using serialization +func deepCopyState(state any) (any, error) { + if state == nil { + return nil, nil + } + serializer := &serialization.InternalSerializer{} + data, err := serializer.Marshal(state) + if err != nil { + return nil, fmt.Errorf("failed to marshal state: %w", err) + } + + // Create new instance of the same type + stateType := reflect.TypeOf(state) + isPtr := stateType.Kind() == reflect.Ptr + if isPtr { + stateType = stateType.Elem() + } + newStatePtr := reflect.New(stateType).Interface() + if err := serializer.Unmarshal(data, newStatePtr); err != nil { + return nil, fmt.Errorf("failed to unmarshal state: %w", err) + } + if isPtr { + return newStatePtr, nil + } + return reflect.ValueOf(newStatePtr).Elem().Interface(), nil +} + +func (r *runner) handleInterruptWithSubGraphAndRerunNodes( + ctx context.Context, + tempInfo *interruptTempInfo, + completeTasks []*task, + checkPointID *string, + isSubGraph bool, + cm *channelManager, + isStream bool, +) error { + var rerunTasks, subgraphTasks, otherTasks []*task + skipPreHandler := map[string]bool{} + for _, t := range completeTasks { + if _, ok := tempInfo.subGraphInterrupts[t.nodeKey]; ok { + subgraphTasks = append(subgraphTasks, t) + skipPreHandler[t.nodeKey] = true // subgraph won't run pre-handler again, but rerun nodes will + continue + } + rerun := false + for _, key := range tempInfo.interruptRerunNodes { + if key == t.nodeKey { + rerunTasks = append(rerunTasks, t) + rerun = true + break + } + } + if !rerun { + otherTasks = append(otherTasks, t) + } + } + + // forward completed tasks + toValue, controls, err := r.resolveCompletedTasks(ctx, otherTasks, isStream, cm) + if err != nil { + return fmt.Errorf("failed to resolve completed tasks in interrupt: %w", err) + } + err = cm.updateValues(ctx, toValue) + if err != nil { + return fmt.Errorf("failed to update values in interrupt: %w", err) + } + err = cm.updateDependencies(ctx, controls) + if err != nil { + return fmt.Errorf("failed to update dependencies in interrupt: %w", err) + } + + cp := &checkpoint{ + Channels: cm.channels, + Inputs: make(map[string]any), + SkipPreHandler: skipPreHandler, + SubGraphs: make(map[string]*checkpoint), + } + if r.runCtx != nil { + // current graph has enable state + if state, ok := ctx.Value(stateKey{}).(*internalState); ok { + state.mu.Lock() + copiedState, err_ := deepCopyState(state.state) + state.mu.Unlock() + if err_ != nil { + return fmt.Errorf("failed to copy state: %w", err_) + } + cp.State = copiedState + } + } + + intInfo := &InterruptInfo{ + State: cp.State, + BeforeNodes: tempInfo.interruptBeforeNodes, + AfterNodes: tempInfo.interruptAfterNodes, + RerunNodes: tempInfo.interruptRerunNodes, + RerunNodesExtra: tempInfo.interruptRerunExtra, + SubGraphs: make(map[string]*InterruptInfo), + } + + info := cp.State + + is, err := core.Interrupt(ctx, info, nil, tempInfo.signals) + if err != nil { + return fmt.Errorf("failed to interrupt: %w", err) + } + + cp.InterruptID2Addr, cp.InterruptID2State = core.SignalToPersistenceMaps(is) + + for _, t := range subgraphTasks { + cp.RerunNodes = append(cp.RerunNodes, t.nodeKey) + cp.SubGraphs[t.nodeKey] = tempInfo.subGraphInterrupts[t.nodeKey].CheckPoint + intInfo.SubGraphs[t.nodeKey] = tempInfo.subGraphInterrupts[t.nodeKey].Info + } + for _, t := range rerunTasks { + cp.RerunNodes = append(cp.RerunNodes, t.nodeKey) + if t.originalInput != nil { + cp.Inputs[t.nodeKey] = t.originalInput + } + } + err = r.checkPointer.convertCheckPoint(cp, isStream) + if err != nil { + return fmt.Errorf("failed to convert checkpoint: %w", err) + } + if isSubGraph { + return &subGraphInterruptError{ + Info: intInfo, + CheckPoint: cp, + signal: is, + } + } else if checkPointID != nil { + err = r.checkPointer.set(ctx, *checkPointID, cp) + if err != nil { + return fmt.Errorf("failed to set checkpoint: %w, checkPointID: %s", err, *checkPointID) + } + } + intInfo.InterruptContexts = core.ToInterruptContexts(is, nil) + return &interruptError{Info: intInfo} +} + +func (r *runner) calculateNextTasks(ctx context.Context, completedTasks []*task, isStream bool, cm *channelManager, optMap map[string][]any) ([]*task, any, bool, error) { + writeChannelValues, controls, err := r.resolveCompletedTasks(ctx, completedTasks, isStream, cm) + if err != nil { + return nil, nil, false, err + } + nodeMap, err := cm.updateAndGet(ctx, writeChannelValues, controls) + if err != nil { + return nil, nil, false, fmt.Errorf("failed to update and get channels: %w", err) + } + var nextTasks []*task + if len(nodeMap) > 0 { + // Check if we've reached the END node. + if v, ok := nodeMap[END]; ok { + return nil, v, true, nil + } + + // Create and submit the next batch of tasks. + nextTasks, err = r.createTasks(ctx, nodeMap, optMap) + if err != nil { + return nil, nil, false, fmt.Errorf("failed to create tasks: %w", err) + } + } + return nextTasks, nil, false, nil +} + +func (r *runner) createTasks(ctx context.Context, nodeMap map[string]any, optMap map[string][]any) ([]*task, error) { + var nextTasks []*task + for nodeKey, nodeInput := range nodeMap { + call, ok := r.chanSubscribeTo[nodeKey] + if !ok { + return nil, fmt.Errorf("node[%s] has not been registered", nodeKey) + } + + if call.action.nodeInfo != nil && call.action.nodeInfo.compileOption != nil { + ctx = forwardCheckPoint(ctx, nodeKey) + } + + nextTasks = append(nextTasks, &task{ + ctx: AppendAddressSegment(ctx, AddressSegmentNode, nodeKey), + nodeKey: nodeKey, + call: call, + input: nodeInput, + option: optMap[nodeKey], + }) + } + return nextTasks, nil +} + +func getCheckPointInfo(opts ...Option) (checkPointID *string, writeToCheckPointID *string, stateModifier StateModifier, forceNewRun bool) { + for _, opt := range opts { + if opt.checkPointID != nil { + checkPointID = opt.checkPointID + } + if opt.writeToCheckPointID != nil { + writeToCheckPointID = opt.writeToCheckPointID + } + if opt.stateModifier != nil { + stateModifier = opt.stateModifier + } + forceNewRun = opt.forceNewRun + } + if writeToCheckPointID == nil { + writeToCheckPointID = checkPointID + } + return +} + +func (r *runner) restoreTasks( + ctx context.Context, + inputs map[string]any, + skipPreHandler map[string]bool, + rerunNodes []string, + isStream bool, + optMap map[string][]any) ([]*task, error) { + ret := make([]*task, 0, len(inputs)) + for _, key := range rerunNodes { + if _, hasInput := inputs[key]; hasInput { + continue + } + + call, ok := r.chanSubscribeTo[key] + if !ok { + return nil, fmt.Errorf("channel[%s] from checkpoint is not registered", key) + } + if isStream { + inputs[key] = call.action.inputEmptyStream() + } else { + inputs[key] = call.action.inputZeroValue() + } + } + for key, input := range inputs { + call, ok := r.chanSubscribeTo[key] + if !ok { + return nil, fmt.Errorf("channel[%s] from checkpoint is not registered", key) + } + + if call.action.nodeInfo != nil && call.action.nodeInfo.compileOption != nil { + // sub graph + ctx = forwardCheckPoint(ctx, key) + } + + newTask := &task{ + ctx: AppendAddressSegment(ctx, AddressSegmentNode, key), + nodeKey: key, + call: call, + input: input, + option: nil, + skipPreHandler: skipPreHandler[key], + } + if opt, ok := optMap[key]; ok { + newTask.option = opt + } + + ret = append(ret, newTask) + } + return ret, nil +} + +func (r *runner) resolveCompletedTasks(ctx context.Context, completedTasks []*task, isStream bool, cm *channelManager) (map[string]map[string]any, map[string][]string, error) { + writeChannelValues := make(map[string]map[string]any) + newDependencies := make(map[string][]string) + for _, t := range completedTasks { + for _, key := range t.call.controls { + newDependencies[key] = append(newDependencies[key], t.nodeKey) + } + + // update channel & new_next_tasks + vs := copyItem(t.output, len(t.call.writeTo)+len(t.call.writeToBranches)*2) + nextNodeKeys, err := r.calculateBranch(ctx, t.nodeKey, t.call, + vs[len(t.call.writeTo)+len(t.call.writeToBranches):], isStream, cm) + if err != nil { + return nil, nil, fmt.Errorf("calculate next step fail, node: %s, error: %w", t.nodeKey, err) + } + + for _, key := range nextNodeKeys { + newDependencies[key] = append(newDependencies[key], t.nodeKey) + } + nextNodeKeys = append(nextNodeKeys, t.call.writeTo...) + + // If branches generates more than one successor, the inputs need to be copied accordingly. + if len(nextNodeKeys) > 0 { + toCopyNum := len(nextNodeKeys) - len(t.call.writeTo) - len(t.call.writeToBranches) + nVs := copyItem(vs[len(t.call.writeTo)+len(t.call.writeToBranches)-1], toCopyNum+1) + vs = append(vs[:len(t.call.writeTo)+len(t.call.writeToBranches)-1], nVs...) + + for i, next := range nextNodeKeys { + if _, ok := writeChannelValues[next]; !ok { + writeChannelValues[next] = make(map[string]any) + } + writeChannelValues[next][t.nodeKey] = vs[i] + } + } + } + return writeChannelValues, newDependencies, nil +} + +func (r *runner) calculateBranch(ctx context.Context, curNodeKey string, startChan *chanCall, input []any, isStream bool, cm *channelManager) ([]string, error) { + if len(input) < len(startChan.writeToBranches) { + // unreachable + return nil, errors.New("calculate next input length is shorter than branches") + } + + ret := make([]string, 0, len(startChan.writeToBranches)) + + skippedNodes := make(map[string]struct{}) + for i, branch := range startChan.writeToBranches { + // check branch input type if needed + var err error + input[i], err = r.preBranchHandlerManager.handle(curNodeKey, i, input[i], isStream) + if err != nil { + return nil, fmt.Errorf("branch[%s]-[%d] pre handler fail: %w", curNodeKey, branch.idx, err) + } + + // process branch output + var ws []string + if isStream { + ws, err = branch.collect(ctx, input[i].(streamReader)) + if err != nil { + return nil, fmt.Errorf("branch collect run error: %w", err) + } + } else { + ws, err = branch.invoke(ctx, input[i]) + if err != nil { + return nil, fmt.Errorf("branch invoke run error: %w", err) + } + } + + for node := range branch.endNodes { + skipped := true + for _, w := range ws { + if node == w { + skipped = false + break + } + } + if skipped { + skippedNodes[node] = struct{}{} + } + } + + ret = append(ret, ws...) + } + + // When a node has multiple branches, + // there may be a situation where a succeeding node is selected by some branches and discarded by the other branches, + // in which case the succeeding node should not be skipped. + var skippedNodeList []string + for _, selected := range ret { + if _, ok := skippedNodes[selected]; ok { + delete(skippedNodes, selected) + } + } + for skipped := range skippedNodes { + skippedNodeList = append(skippedNodeList, skipped) + } + + err := cm.reportBranch(curNodeKey, skippedNodeList) + if err != nil { + return nil, err + } + return ret, nil +} + +func (r *runner) initTaskManager(runWrapper runnableCallWrapper, cancelVal *graphCancelChanVal, opts ...Option) *taskManager { + tm := &taskManager{ + runWrapper: runWrapper, + opts: opts, + needAll: !r.eager, + done: internal.NewUnboundedChan[*task](), + runningTasks: make(map[string]*task), + persistRerunInput: cancelVal != nil, + } + if cancelVal != nil { + tm.cancelCh = cancelVal.ch + } + return tm +} + +func (r *runner) initChannelManager(isStream bool) *channelManager { + builder := r.chanBuilder + if builder == nil { + builder = pregelChannelBuilder + } + + chs := make(map[string]channel) + for ch := range r.chanSubscribeTo { + chs[ch] = builder(r.controlPredecessors[ch], r.dataPredecessors[ch], r.chanSubscribeTo[ch].action.inputZeroValue, r.chanSubscribeTo[ch].action.inputEmptyStream) + } + + chs[END] = builder(r.controlPredecessors[END], r.dataPredecessors[END], r.outputZeroValue, r.outputEmptyStream) + + dataPredecessors := make(map[string]map[string]struct{}) + for k, vs := range r.dataPredecessors { + dataPredecessors[k] = make(map[string]struct{}) + for _, v := range vs { + dataPredecessors[k][v] = struct{}{} + } + } + controlPredecessors := make(map[string]map[string]struct{}) + for k, vs := range r.controlPredecessors { + controlPredecessors[k] = make(map[string]struct{}) + for _, v := range vs { + controlPredecessors[k][v] = struct{}{} + } + } + + for k, v := range chs { + if cfg, ok := r.mergeConfigs[k]; ok { + v.setMergeConfig(cfg) + } + } + + return &channelManager{ + isStream: isStream, + channels: chs, + successors: r.successors, + dataPredecessors: dataPredecessors, + controlPredecessors: controlPredecessors, + + edgeHandlerManager: r.edgeHandlerManager, + preNodeHandlerManager: r.preNodeHandlerManager, + } +} + +func (r *runner) toComposableRunnable() *composableRunnable { + cr := &composableRunnable{ + i: func(ctx context.Context, input any, opts ...any) (output any, err error) { + tos, err := convertOption[Option](opts...) + if err != nil { + return nil, err + } + return r.invoke(ctx, input, tos...) + }, + t: func(ctx context.Context, input streamReader, opts ...any) (output streamReader, err error) { + tos, err := convertOption[Option](opts...) + if err != nil { + return nil, err + } + return r.transform(ctx, input, tos...) + }, + + inputType: r.inputType, + outputType: r.outputType, + genericHelper: r.genericHelper, + optionType: nil, // if option type is nil, graph will transmit all options. + } + + return cr +} + +func copyItem(item any, n int) []any { + if n < 2 { + return []any{item} + } + + ret := make([]any, n) + if s, ok := item.(streamReader); ok { + ss := s.copy(n) + for i := range ret { + ret[i] = ss[i] + } + + return ret + } + + for i := range ret { + ret[i] = item + } + + return ret +} + +func printTask(ts []*task) string { + if len(ts) == 0 { + return "[]" + } + sb := strings.Builder{} + sb.WriteString("[") + for i := 0; i < len(ts)-1; i++ { + sb.WriteString(ts[i].nodeKey) + sb.WriteString(", ") + } + sb.WriteString(ts[len(ts)-1].nodeKey) + sb.WriteString("]") + return sb.String() +} diff --git a/compose/graph_test.go b/compose/graph_test.go new file mode 100644 index 0000000..c2020d9 --- /dev/null +++ b/compose/graph_test.go @@ -0,0 +1,2071 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "io" + "reflect" + "sort" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/schema" +) + +func TestSingleGraph(t *testing.T) { + + const ( + nodeOfModel = "model" + nodeOfPrompt = "prompt" + ) + + ctx := context.Background() + g := NewGraph[map[string]any, *schema.Message]() + + pt := prompt.FromMessages(schema.FString, + schema.UserMessage("what's the weather in {location}?"), + ) + + err := g.AddChatTemplateNode("prompt", pt) + assert.NoError(t, err) + + cm := &chatModel{ + msgs: []*schema.Message{ + { + Role: schema.Assistant, + Content: "the weather is good", + }, + }, + } + + err = g.AddChatModelNode(nodeOfModel, cm, WithNodeName("MockChatModel")) + assert.NoError(t, err) + + err = g.AddEdge(START, nodeOfPrompt) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfPrompt, nodeOfModel) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfModel, END) + assert.NoError(t, err) + + r, err := g.Compile(context.Background(), WithMaxRunSteps(10)) + assert.NoError(t, err) + + in := map[string]any{"location": "beijing"} + _, err = r.Invoke(ctx, in) + assert.NoError(t, err) + + // stream + s, err := r.Stream(ctx, in) + assert.NoError(t, err) + + _, err = concatStreamReader(s) + assert.NoError(t, err) + + sr, sw := schema.Pipe[map[string]any](1) + _ = sw.Send(in, nil) + sw.Close() + + // transform + s, err = r.Transform(ctx, sr) + assert.NoError(t, err) + + _, err = concatStreamReader(s) + assert.NoError(t, err) + + // error test + in = map[string]any{"wrong key": 1} + _, err = r.Invoke(ctx, in) + assert.Errorf(t, err, "could not find key: location") + + _, err = r.Stream(ctx, in) + assert.Errorf(t, err, "could not find key: location") + + sr, sw = schema.Pipe[map[string]any](1) + _ = sw.Send(in, nil) + sw.Close() + + _, err = r.Transform(ctx, sr) + assert.Errorf(t, err, "could not find key: location") +} + +type person interface { + Say() string +} + +type doctor struct { + say string +} + +func (d *doctor) Say() string { + return d.say +} + +func TestGraphWithImplementableType(t *testing.T) { + + const ( + node1 = "1st" + node2 = "2nd" + ) + + ctx := context.Background() + + g := NewGraph[string, string]() + + err := g.AddLambdaNode(node1, InvokableLambda(func(ctx context.Context, input string) (output *doctor, err error) { + return &doctor{say: input}, nil + })) + assert.NoError(t, err) + + err = g.AddLambdaNode(node2, InvokableLambda(func(ctx context.Context, input person) (output string, err error) { + return input.Say(), nil + })) + assert.NoError(t, err) + + err = g.AddEdge(START, node1) + assert.NoError(t, err) + + err = g.AddEdge(node1, node2) + assert.NoError(t, err) + + err = g.AddEdge(node2, END) + assert.NoError(t, err) + + r, err := g.Compile(context.Background(), WithMaxRunSteps(10)) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "how are you", WithRuntimeMaxSteps(1)) + assert.Error(t, err) + assert.ErrorContains(t, err, "exceeds max steps") + + _, err = r.Invoke(ctx, "how are you", WithRuntimeMaxSteps(1)) + assert.Error(t, err) + assert.ErrorContains(t, err, "exceeds max steps") + + out, err := r.Invoke(ctx, "how are you") + assert.NoError(t, err) + assert.Equal(t, "how are you", out) + + outStream, err := r.Stream(ctx, "i'm fine") + assert.NoError(t, err) + defer outStream.Close() + + say, err := outStream.Recv() + assert.NoError(t, err) + assert.Equal(t, "i'm fine", say) +} + +func TestNestedGraph(t *testing.T) { + const ( + nodeOfLambda1 = "lambda1" + nodeOfLambda2 = "lambda2" + nodeOfSubGraph = "sub_graph" + nodeOfModel = "model" + nodeOfPrompt = "prompt" + ) + + ctx := context.Background() + g := NewGraph[string, *schema.Message]() + sg := NewGraph[map[string]any, *schema.Message]() + + l1 := InvokableLambda[string, map[string]any]( + func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"location": input}, nil + }) + + l2 := InvokableLambda[*schema.Message, *schema.Message]( + func(ctx context.Context, input *schema.Message) (output *schema.Message, err error) { + input.Content = fmt.Sprintf("after lambda 2: %s", input.Content) + return input, nil + }) + + pt := prompt.FromMessages(schema.FString, + schema.UserMessage("what's the weather in {location}?"), + ) + + err := sg.AddChatTemplateNode("prompt", pt) + assert.NoError(t, err) + + cm := &chatModel{ + msgs: []*schema.Message{ + { + Role: schema.Assistant, + Content: "the weather is good", + }, + }, + } + + err = sg.AddChatModelNode(nodeOfModel, cm, WithNodeName("MockChatModel")) + assert.NoError(t, err) + + err = sg.AddEdge(START, nodeOfPrompt) + assert.NoError(t, err) + + err = sg.AddEdge(nodeOfPrompt, nodeOfModel) + assert.NoError(t, err) + + err = sg.AddEdge(nodeOfModel, END) + assert.NoError(t, err) + + err = g.AddLambdaNode(nodeOfLambda1, l1, WithNodeName("Lambda1")) + assert.NoError(t, err) + + err = g.AddGraphNode(nodeOfSubGraph, sg, WithNodeName("SubGraphName")) + assert.NoError(t, err) + + err = g.AddLambdaNode(nodeOfLambda2, l2, WithNodeName("Lambda2")) + assert.NoError(t, err) + + err = g.AddEdge(START, nodeOfLambda1) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfLambda1, nodeOfSubGraph) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfSubGraph, nodeOfLambda2) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfLambda2, END) + assert.NoError(t, err) + + r, err := g.Compile(context.Background(), + WithMaxRunSteps(10), + WithGraphName("GraphName"), + ) + assert.NoError(t, err) + + ck := "depth" + cb := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + v, ok := ctx.Value(ck).(int) + if ok { + v++ + } + + return context.WithValue(ctx, ck, v) + }). + OnStartWithStreamInputFn(func(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + input.Close() + + v, ok := ctx.Value(ck).(int) + if ok { + v++ + } + + return context.WithValue(ctx, ck, v) + }).Build() + + // invoke + _, err = r.Invoke(ctx, "london", WithCallbacks(cb)) + assert.NoError(t, err) + + // stream + rs, err := r.Stream(ctx, "london", WithCallbacks(cb)) + assert.NoError(t, err) + for { + _, err = rs.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + } + + // collect + sr, sw := schema.Pipe[string](5) + _ = sw.Send("london", nil) + sw.Close() + + _, err = r.Collect(ctx, sr, WithCallbacks(cb)) + assert.NoError(t, err) + + // transform + sr, sw = schema.Pipe[string](5) + _ = sw.Send("london", nil) + sw.Close() + + rt, err := r.Transform(ctx, sr, WithCallbacks(cb)) + assert.NoError(t, err) + for { + _, err = rt.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + } +} + +type chatModel struct { + msgs []*schema.Message +} + +func (c *chatModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +func (c *chatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return c.msgs[0], nil +} + +func (c *chatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](len(c.msgs)) + go func() { + for _, msg := range c.msgs { + sw.Send(msg, nil) + } + sw.Close() + }() + return sr, nil +} + +func TestValidate(t *testing.T) { + // test unmatched nodes + g := NewGraph[string, string]() + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return "", nil })) + assert.NoError(t, err) + + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input int) (output string, err error) { return "", nil })) + assert.NoError(t, err) + + err = g.AddEdge("1", "2") + assert.ErrorContains(t, err, "graph edge[1]-[2]: start node's output type[string] and end node's input type[int] mismatch") + + // test unmatched passthrough node + g = NewGraph[string, string]() + err = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return "", nil })) + assert.NoError(t, err) + + err = g.AddPassthroughNode("2") + assert.NoError(t, err) + + err = g.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input int) (output string, err error) { return "", nil })) + assert.NoError(t, err) + + err = g.AddEdge("1", "2") + assert.NoError(t, err) + + err = g.AddEdge("2", "3") + assert.ErrorContains(t, err, "graph edge[2]-[3]: start node's output type[string] and end node's input type[int] mismatch") + + // test may matched passthrough + g2 := NewGraph[any, string]() + err = g2.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input any) (output any, err error) { return input, nil })) + assert.NoError(t, err) + err = g2.AddPassthroughNode("2") + assert.NoError(t, err) + err = g2.AddLambdaNode("3", InvokableLambda(func(ctx context.Context, input int) (output string, err error) { return strconv.Itoa(input), nil })) + assert.NoError(t, err) + err = g2.AddEdge(START, "1") + assert.NoError(t, err) + err = g2.AddEdge("2", "3") + assert.NoError(t, err) + err = g2.AddEdge("1", "2") + assert.NoError(t, err) + err = g2.AddEdge("3", END) + assert.NoError(t, err) + ru, err := g2.Compile(context.Background()) + assert.NoError(t, err) + // success + result, err := ru.Invoke(context.Background(), 1) + assert.NoError(t, err) + assert.Equal(t, result, "1") + // fail + _, err = ru.Invoke(context.Background(), "1") + assert.ErrorContains(t, err, "runtime type check") + + // test unmatched graph type + g = NewGraph[string, string]() + err = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input int) (output string, err error) { return "", nil })) + assert.NoError(t, err) + + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output int, err error) { return 0, nil })) + assert.NoError(t, err) + + err = g.AddEdge("1", "2") + assert.NoError(t, err) + + err = g.AddEdge(START, "1") + assert.ErrorContains(t, err, "graph edge[start]-[1]: start node's output type[string] and end node's input type[int] mismatch") + + // sub graph implement + type A interface { + A() + } + type B interface { + B() + } + + type AB interface { + A + B + } + lA := InvokableLambda(func(ctx context.Context, input A) (output string, err error) { return "", nil }) + lB := InvokableLambda(func(ctx context.Context, input B) (output string, err error) { return "", nil }) + lAB := InvokableLambda(func(ctx context.Context, input string) (output AB, err error) { return nil, nil }) + + p := NewParallel().AddLambda("1", lA).AddLambda("2", lB) + c := NewChain[string, map[string]any]().AppendLambda(lAB).AppendParallel(p) + _, err = c.Compile(context.Background()) + assert.NoError(t, err) + + // error usage + p = NewParallel().AddLambda("1", lA).AddLambda("2", lAB) + c = NewChain[string, map[string]any]().AppendParallel(p) + _, err = c.Compile(context.Background()) + assert.ErrorContains(t, err, "add parallel edge failed, from=start, to=node_0_parallel_0, err: graph edge[start]-[node_0_parallel_0]: start node's output type[string] and end node's input type[compose.A] mismatch") + + // test graph output type check + gg := NewGraph[string, A]() + err = gg.AddLambdaNode("nodeA", InvokableLambda(func(ctx context.Context, input string) (output A, err error) { return nil, nil })) + assert.NoError(t, err) + + err = gg.AddLambdaNode("nodeA2", InvokableLambda(func(ctx context.Context, input string) (output A, err error) { return nil, nil })) + assert.NoError(t, err) + + err = gg.AddLambdaNode("nodeB", InvokableLambda(func(ctx context.Context, input string) (output B, err error) { return nil, nil })) + assert.NoError(t, err) + + err = gg.AddEdge("nodeA", END) + assert.NoError(t, err) + + err = gg.AddEdge("nodeB", END) + assert.ErrorContains(t, err, "graph edge[nodeB]-[end]: start node's output type[compose.B] and end node's input type[compose.A] mismatch") + + err = gg.AddEdge("nodeA2", END) + assert.ErrorContains(t, err, "graph edge[nodeB]-[end]: start node's output type[compose.B] and end node's input type[compose.A] mismatch") + + // test any type + anyG := NewGraph[any, string]() + err = anyG.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node1", nil })) + assert.NoError(t, err) + + err = anyG.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node2", nil })) + assert.NoError(t, err) + + err = anyG.AddEdge(START, "node1") + assert.NoError(t, err) + + err = anyG.AddEdge("node1", "node2") + assert.NoError(t, err) + + err = anyG.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + r, err := anyG.Compile(context.Background()) + assert.NoError(t, err) + + result, err = r.Invoke(context.Background(), "start") + assert.NoError(t, err) + assert.Equal(t, "startnode1node2", result) + + streamResult, err := r.Stream(context.Background(), "start") + assert.NoError(t, err) + + result = "" + for { + chunk, err := streamResult.Recv() + if err != nil { + if err == io.EOF { + break + } + assert.NoError(t, err) + } + result += chunk + } + + assert.Equal(t, "startnode1node2", result) + + // test any type runtime error + anyG = NewGraph[any, string]() + err = anyG.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return 123, nil })) + if err != nil { + t.Fatal(err) + } + err = anyG.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node2", nil })) + if err != nil { + t.Fatal(err) + } + err = anyG.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = anyG.AddEdge("node1", "node2") + if err != nil { + t.Fatal(err) + } + err = anyG.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + r, err = anyG.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + _, err = r.Invoke(context.Background(), "start") + if err == nil || !strings.Contains(err.Error(), "runtime") { + t.Fatal("test any type runtime error fail, error is nil or error doesn't contain key word runtime") + } + _, err = r.Stream(context.Background(), "start") + if err == nil || !strings.Contains(err.Error(), "runtime") { + t.Fatal("test any type runtime error fail, error is nil or error doesn't contain key word runtime") + } + + // test branch any type + // success + g = NewGraph[string, string]() + err = g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node1", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node2", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node3", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node3", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch("node1", NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + return "node2", nil + }, map[string]bool{"node2": true, "node3": true})) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node3", END) + if err != nil { + t.Fatal(err) + } + rr, err := g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + ret, err := rr.Invoke(context.Background(), "start") + if err != nil { + t.Fatal(err) + } + if ret != "startnode1node2" { + t.Fatal("test branch any type fail, result is unexpected") + } + streamResult, err = rr.Stream(context.Background(), "start") + if err != nil { + t.Fatal(err) + } + ret, err = concatStreamReader(streamResult) + if err != nil { + t.Fatal(err) + } + if ret != "startnode1node2" { + t.Fatal("test branch any type fail, result is unexpected") + } + // fail + g = NewGraph[string, string]() + err = g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return 1 /*error type*/, nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node2", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node3", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node3", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch("node1", NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + return "node2", nil + }, map[string]bool{"node2": true, "node3": true})) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node3", END) + if err != nil { + t.Fatal(err) + } + rr, err = g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + _, err = rr.Invoke(context.Background(), "start") + if err == nil || !strings.Contains(err.Error(), "runtime") { + t.Fatal("test branch any type fail, haven't report runtime error") + } + _, err = rr.Stream(context.Background(), "start") + if err == nil || !strings.Contains(err.Error(), "runtime") { + t.Fatal("test branch any type fail, haven't report runtime error") + } +} + +func TestValidateMultiAnyValueBranch(t *testing.T) { + // success + g := NewGraph[string, map[string]any]() + err := g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node1", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node2": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node3", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node3": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node4", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node4": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node5", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node5": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch("node1", NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + return "node2", nil + }, map[string]bool{"node2": true, "node3": true})) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch("node1", NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + return "node4", nil + }, map[string]bool{"node4": true, "node5": true})) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node3", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node4", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node5", END) + if err != nil { + t.Fatal(err) + } + rr, err := g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + ret, err := rr.Invoke(context.Background(), "start") + if err != nil { + t.Fatal(err) + } + if !ret["node2"].(bool) || !ret["node4"].(bool) { + t.Fatal("test branch any type fail, result is unexpected") + } + streamResult, err := rr.Stream(context.Background(), "start") + if err != nil { + t.Fatal(err) + } + ret, err = concatStreamReader(streamResult) + if err != nil { + t.Fatal(err) + } + if !ret["node2"].(bool) || !ret["node4"].(bool) { + t.Fatal("test branch any type fail, result is unexpected") + } + + // fail + g = NewGraph[string, map[string]any]() + err = g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node1", nil })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node2": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node3", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node3": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node4", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node4": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node5", InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{"node5": true}, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch("node1", NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + return "node2", nil + }, map[string]bool{"node2": true, "node3": true})) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch("node1", NewGraphBranch(func(ctx context.Context, in int /*error type*/) (endNode string, err error) { + return "node4", nil + }, map[string]bool{"node4": true, "node5": true})) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node3", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node4", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node5", END) + if err != nil { + t.Fatal(err) + } + rr, err = g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + _, err = rr.Invoke(context.Background(), "start") + if err == nil || !strings.Contains(err.Error(), "runtime") { + t.Fatal("test multi branch any type fail, haven't report runtime error") + } + _, err = rr.Stream(context.Background(), "start") + if err == nil || !strings.Contains(err.Error(), "runtime") { + t.Fatal("test multi branch any type fail, haven't report runtime error") + } +} + +func TestAnyTypeWithKey(t *testing.T) { + g := NewGraph[any, map[string]any]() + err := g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node1", nil }), WithInputKey("node1")) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) { return input + "node2", nil }), WithOutputKey("node2")) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node1", "node2") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + r, err := g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + result, err := r.Invoke(context.Background(), map[string]any{"node1": "start"}) + if err != nil { + t.Fatal(err) + } + if result["node2"] != "startnode1node2" { + t.Fatal("test any type with key fail, result is unexpected") + } + + streamResult, err := r.Stream(context.Background(), map[string]any{"node1": "start"}) + if err != nil { + t.Fatal(err) + } + ret, err := concatStreamReader(streamResult) + if err != nil { + t.Fatal(err) + } + if ret["node2"] != "startnode1node2" { + t.Fatal("test any type with key fail, result is unexpected") + } +} + +func TestInputKey(t *testing.T) { + g := NewGraph[map[string]any, map[string]any]() + err := g.AddChatTemplateNode("1", prompt.FromMessages(schema.FString, schema.UserMessage("{var1}")), WithOutputKey("1"), WithInputKey("1")) + if err != nil { + t.Fatal(err) + } + err = g.AddChatTemplateNode("2", prompt.FromMessages(schema.FString, schema.UserMessage("{var2}")), WithOutputKey("2"), WithInputKey("2")) + if err != nil { + t.Fatal(err) + } + err = g.AddChatTemplateNode("3", prompt.FromMessages(schema.FString, schema.UserMessage("{var3}")), WithOutputKey("3"), WithInputKey("3")) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "2") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "3") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("2", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("3", END) + if err != nil { + t.Fatal(err) + } + r, err := g.Compile(context.Background(), WithMaxRunSteps(100)) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + result, err := r.Invoke(ctx, map[string]any{ + "1": map[string]any{"var1": "a"}, + "2": map[string]any{"var2": "b"}, + "3": map[string]any{"var3": "c"}, + }) + if err != nil { + t.Fatal(err) + } + if result["1"].([]*schema.Message)[0].Content != "a" || + result["2"].([]*schema.Message)[0].Content != "b" || + result["3"].([]*schema.Message)[0].Content != "c" { + t.Fatal("invoke different") + } + + sr, sw := schema.Pipe[map[string]any](10) + sw.Send(map[string]any{"1": map[string]any{"var1": "a"}}, nil) + sw.Send(map[string]any{"2": map[string]any{"var2": "b"}}, nil) + sw.Send(map[string]any{"3": map[string]any{"var3": "c"}}, nil) + sw.Close() + + streamResult, err := r.Transform(ctx, sr) + if err != nil { + t.Fatal(err) + } + defer streamResult.Close() + + result = make(map[string]any) + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + for k, v := range chunk { + result[k] = v + } + } + if result["1"].([]*schema.Message)[0].Content != "a" || + result["2"].([]*schema.Message)[0].Content != "b" || + result["3"].([]*schema.Message)[0].Content != "c" { + t.Fatal("transform different") + } +} + +func TestTransferTask(t *testing.T) { + in := [][]string{ + { + "1", + "2", + }, + { + "3", + "4", + "5", + "6", + }, + { + "5", + "6", + "7", + }, + { + "7", + "8", + }, + { + "8", + }, + } + invertedEdges := map[string][]string{ + "1": {"3", "4"}, + "2": {"5", "6"}, + "3": {"5"}, + "4": {"6"}, + "5": {"7"}, + "7": {"8"}, + } + in = transferTask(in, invertedEdges) + + if !reflect.DeepEqual( + [][]string{ + { + "1", + }, + { + "3", + "2", + }, + { + "5", + }, + { + "7", + "4", + }, + { + "8", + "6", + }, + }, in) { + t.Fatal("not equal") + } +} + +func TestPregelEnd(t *testing.T) { + g := NewGraph[string, string]() + err := g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "node1", nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "node2", nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node1", END) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node1", "node2") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node2", END) + if err != nil { + t.Fatal(err) + } + runner, err := g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + + out, err := runner.Invoke(context.Background(), "") + if err != nil { + t.Fatal(err) + } + if out != "node1" { + t.Fatal("graph output is unexpected") + } +} + +type cb struct { + gInfo *GraphInfo +} + +func (c *cb) OnFinish(ctx context.Context, info *GraphInfo) { + c.gInfo = info +} + +func TestGraphCompileCallback(t *testing.T) { + t.Run("graph compile callback", func(t *testing.T) { + type s struct{} + + g := NewGraph[map[string]any, map[string]any](WithGenLocalState(func(ctx context.Context) *s { return &s{} })) + + lambda := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "node1", nil + }) + lambdaOpts := []GraphAddNodeOpt{WithNodeName("lambda_1"), WithInputKey("input_key")} + err := g.AddLambdaNode("node1", lambda, lambdaOpts...) + assert.NoError(t, err) + + err = g.AddPassthroughNode("pass1") + assert.NoError(t, err) + err = g.AddPassthroughNode("pass2") + assert.NoError(t, err) + + condition := func(ctx context.Context, input string) (string, error) { + return input, nil + } + + branch := NewGraphBranch(condition, map[string]bool{"pass1": true, "pass2": true}) + err = g.AddBranch("node1", branch) + assert.NoError(t, err) + + err = g.AddEdge(START, "node1") + assert.NoError(t, err) + + lambda2 := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "node2", nil + }) + lambdaOpts2 := []GraphAddNodeOpt{WithNodeName("lambda_2")} + subSubGraph := NewGraph[string, string]() + err = subSubGraph.AddLambdaNode("sub1", lambda2, lambdaOpts2...) + assert.NoError(t, err) + err = subSubGraph.AddEdge(START, "sub1") + assert.NoError(t, err) + err = subSubGraph.AddEdge("sub1", END) + assert.NoError(t, err) + + subGraph := NewGraph[string, string]() + var ssGraphCompileOpts []GraphCompileOption + ssGraphOpts := []GraphAddNodeOpt{WithGraphCompileOptions(ssGraphCompileOpts...)} + err = subGraph.AddGraphNode("sub_sub_1", subSubGraph, ssGraphOpts...) + assert.NoError(t, err) + err = subGraph.AddEdge(START, "sub_sub_1") + assert.NoError(t, err) + err = subGraph.AddEdge("sub_sub_1", END) + assert.NoError(t, err) + + subGraphCompileOpts := []GraphCompileOption{WithMaxRunSteps(2), WithGraphName("sub_graph")} + subGraphOpts := []GraphAddNodeOpt{WithGraphCompileOptions(subGraphCompileOpts...)} + err = g.AddGraphNode("sub_graph", subGraph, subGraphOpts...) + assert.NoError(t, err) + + err = g.AddEdge("pass1", "sub_graph") + assert.NoError(t, err) + err = g.AddEdge("pass2", "sub_graph") + assert.NoError(t, err) + + lambda3 := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "node3", nil + }) + lambdaOpts3 := []GraphAddNodeOpt{WithNodeName("lambda_3"), WithOutputKey("lambda_3")} + err = g.AddLambdaNode("node3", lambda3, lambdaOpts3...) + assert.NoError(t, err) + + lambda4 := InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "node4", nil + }) + lambdaOpts4 := []GraphAddNodeOpt{WithNodeName("lambda_4"), WithOutputKey("lambda_4")} + err = g.AddLambdaNode("node4", lambda4, lambdaOpts4...) + assert.NoError(t, err) + + err = g.AddEdge("sub_graph", "node3") + assert.NoError(t, err) + err = g.AddEdge("sub_graph", "node4") + assert.NoError(t, err) + err = g.AddEdge("node3", END) + assert.NoError(t, err) + err = g.AddEdge("node4", END) + assert.NoError(t, err) + + c := &cb{} + opt := []GraphCompileOption{WithGraphCompileCallbacks(c), WithGraphName("top_level")} + _, err = g.Compile(context.Background(), opt...) + assert.NoError(t, err) + expected := &GraphInfo{ + CompileOptions: opt, + Nodes: map[string]GraphNodeInfo{ + "node1": { + Component: ComponentOfLambda, + Instance: lambda, + GraphAddNodeOpts: lambdaOpts, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "lambda_1", + InputKey: "input_key", + }, + "pass1": { + Component: ComponentOfPassthrough, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "", + }, + "pass2": { + Component: ComponentOfPassthrough, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "", + }, + "sub_graph": { + Component: ComponentOfGraph, + Instance: subGraph, + GraphAddNodeOpts: subGraphOpts, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "", + GraphInfo: &GraphInfo{ + CompileOptions: subGraphCompileOpts, + Nodes: map[string]GraphNodeInfo{ + "sub_sub_1": { + Component: ComponentOfGraph, + Instance: subSubGraph, + GraphAddNodeOpts: ssGraphOpts, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "", + GraphInfo: &GraphInfo{ + CompileOptions: ssGraphCompileOpts, + Nodes: map[string]GraphNodeInfo{ + "sub1": { + Component: ComponentOfLambda, + Instance: lambda2, + GraphAddNodeOpts: lambdaOpts2, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "lambda_2", + }, + }, + Edges: map[string][]string{ + START: {"sub1"}, + "sub1": {END}, + }, + DataEdges: map[string][]string{ + START: {"sub1"}, + "sub1": {END}, + }, + Branches: map[string][]GraphBranch{}, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + }, + }, + }, + Edges: map[string][]string{ + START: {"sub_sub_1"}, + "sub_sub_1": {END}, + }, + DataEdges: map[string][]string{ + START: {"sub_sub_1"}, + "sub_sub_1": {END}, + }, + Branches: map[string][]GraphBranch{}, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "sub_graph", + }, + }, + "node3": { + Component: ComponentOfLambda, + Instance: lambda3, + GraphAddNodeOpts: lambdaOpts3, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "lambda_3", + OutputKey: "lambda_3", + }, + "node4": { + Component: ComponentOfLambda, + Instance: lambda4, + GraphAddNodeOpts: lambdaOpts4, + InputType: reflect.TypeOf(""), + OutputType: reflect.TypeOf(""), + Name: "lambda_4", + OutputKey: "lambda_4", + }, + }, + Edges: map[string][]string{ + START: {"node1"}, + "pass1": {"sub_graph"}, + "pass2": {"sub_graph"}, + "sub_graph": {"node3", "node4"}, + "node3": {END}, + "node4": {END}, + }, + DataEdges: map[string][]string{ + START: {"node1"}, + "pass1": {"sub_graph"}, + "pass2": {"sub_graph"}, + "sub_graph": {"node3", "node4"}, + "node3": {END}, + "node4": {END}, + }, + Branches: map[string][]GraphBranch{ + "node1": {*branch}, + }, + InputType: reflect.TypeOf(map[string]any{}), + OutputType: reflect.TypeOf(map[string]any{}), + Name: "top_level", + } + + stateFn := c.gInfo.GenStateFn + assert.NotNil(t, stateFn) + assert.Equal(t, &s{}, stateFn(context.Background())) + + assert.Equal(t, 1, len(c.gInfo.NewGraphOptions)) + c.gInfo.NewGraphOptions = nil + + c.gInfo.GenStateFn = nil + + actualCompileOptions := newGraphCompileOptions(c.gInfo.CompileOptions...) + expectedCompileOptions := newGraphCompileOptions(expected.CompileOptions...) + assert.Equal(t, len(expectedCompileOptions.callbacks), len(actualCompileOptions.callbacks)) + assert.Same(t, expectedCompileOptions.callbacks[0], actualCompileOptions.callbacks[0]) + actualCompileOptions.callbacks = nil + actualCompileOptions.origOpts = nil + expectedCompileOptions.callbacks = nil + expectedCompileOptions.origOpts = nil + assert.Equal(t, expectedCompileOptions, actualCompileOptions) + + c.gInfo.CompileOptions = nil + expected.CompileOptions = nil + assert.Equal(t, expected.Branches["node1"][0].endNodes, c.gInfo.Branches["node1"][0].endNodes) + assert.Equal(t, expected.Branches["node1"][0].inputType, c.gInfo.Branches["node1"][0].inputType) + + expected.Branches["node1"] = []GraphBranch{} + c.gInfo.Branches["node1"] = []GraphBranch{} + assert.Equal(t, expected, c.gInfo) + }) +} + +func TestCheckAddEdge(t *testing.T) { + g := NewGraph[string, string]() + err := g.AddPassthroughNode("1") + if err != nil { + t.Fatal(err) + } + err = g.AddPassthroughNode("2") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", "2") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", "2") + if err == nil { + t.Fatal("add edge repeatedly haven't report error") + } +} + +func TestStartWithEnd(t *testing.T) { + g := NewGraph[string, string]() + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + return END, nil + }, map[string]bool{"1": true, END: true})) + if err != nil { + t.Fatal(err) + } + r, err := g.Compile(context.Background()) + if err != nil { + t.Fatal(err) + } + sr, sw := schema.Pipe[string](1) + sw.Send("test", nil) + sw.Close() + result, err := r.Transform(context.Background(), sr) + if err != nil { + t.Fatal(err) + } + for { + chunk, err := result.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + if chunk != "test" { + t.Fatal("result is out of expect") + } + } +} + +func TestToString(t *testing.T) { + ps := runTypePregel.String() + assert.Equal(t, "Pregel", ps) + + ds := runTypeDAG + assert.Equal(t, "DAG", ds.String()) +} + +func TestInputKeyError(t *testing.T) { + g := NewGraph[map[string]any, string]() + err := g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithInputKey("node1")) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "node1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("node1", END) + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + r, err := g.Compile(ctx) + if err != nil { + t.Fatal(err) + } + // invoke + _, err = r.Invoke(ctx, map[string]any{"unknown": "123"}) + if err == nil || !strings.Contains(err.Error(), "cannot find input key: node1") { + t.Fatal("cannot report input key error correctly") + } + + // transform + sr, sw := schema.Pipe[map[string]any](1) + sw.Send(map[string]any{"unknown": "123"}, nil) + sw.Close() + _, err = r.Transform(ctx, sr) + if err == nil || !strings.Contains(err.Error(), "stream reader is empty, concat fail") { + t.Fatal("cannot report input key error correctly") + } +} + +func TestContextCancel(t *testing.T) { + ctx := context.Background() + g := NewGraph[string, string]() + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", END) + if err != nil { + t.Fatal(err) + } + r, err := g.Compile(ctx) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(ctx) + cancel() + _, err = r.Invoke(ctx, "test") + if !strings.Contains(err.Error(), "context has been canceled") { + t.Fatal("graph have not returned canceled error") + } +} + +func TestDAGStart(t *testing.T) { + g := NewGraph[map[string]any, map[string]any]() + err := g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input map[string]any) (output map[string]any, err error) { + return map[string]any{"1": "1"}, nil + })) + assert.NoError(t, err) + err = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input map[string]any) (output map[string]any, err error) { + return input, nil + })) + assert.NoError(t, err) + err = g.AddEdge(START, "1") + assert.NoError(t, err) + err = g.AddEdge("1", "2") + assert.NoError(t, err) + err = g.AddEdge(START, "2") + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + r, err := g.Compile(context.Background(), WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + result, err := r.Invoke(context.Background(), map[string]any{"start": "start"}) + assert.NoError(t, err) + assert.Equal(t, map[string]any{"start": "start", "1": "1"}, result) +} + +func concatLambda(s string) *Lambda { + return InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input + s, nil }) +} +func mapLambda(k, v string) *Lambda { + return InvokableLambda(func(ctx context.Context, input map[string]string) (output map[string]string, err error) { + return map[string]string{ + k: v, + }, nil + }) +} + +func TestBaseDAGBranch(t *testing.T) { + g := NewGraph[string, string]() + + err := g.AddLambdaNode("1", concatLambda("1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", concatLambda("2")) + assert.NoError(t, err) + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + if len(in) > 3 { + return "2", nil + } + return "1", nil + }, map[string]bool{"1": true, "2": true})) + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + result, err := r.Invoke(ctx, "hi") + assert.NoError(t, err) + assert.Equal(t, "hi1", result) +} + +func TestMultiDAGBranch(t *testing.T) { + g := NewGraph[map[string]string, map[string]string]() + + err := g.AddLambdaNode("1", mapLambda("1", "1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", mapLambda("2", "2")) + assert.NoError(t, err) + err = g.AddLambdaNode("3", mapLambda("3", "3")) + assert.NoError(t, err) + err = g.AddLambdaNode("4", mapLambda("4", "4")) + assert.NoError(t, err) + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in map[string]string) (endNode string, err error) { + if len(in["input"]) > 3 { + return "2", nil + } + return "1", nil + }, map[string]bool{"1": true, "2": true})) + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in map[string]string) (endNode string, err error) { + if len(in["input"]) > 3 { + return "4", nil + } + return "3", nil + }, map[string]bool{"3": true, "4": true})) + assert.NoError(t, err) + + err = g.AddEdge("1", END) + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + err = g.AddEdge("4", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + result, err := r.Invoke(ctx, map[string]string{"input": "hi"}) + assert.NoError(t, err) + assert.Equal(t, map[string]string{ + "1": "1", + "3": "3", + }, result) +} + +func TestCrossDAGBranch(t *testing.T) { + g := NewGraph[map[string]string, map[string]string]() + + err := g.AddLambdaNode("1", mapLambda("1", "1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", mapLambda("2", "2")) + assert.NoError(t, err) + err = g.AddLambdaNode("3", mapLambda("3", "3")) + assert.NoError(t, err) + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in map[string]string) (endNode string, err error) { + if len(in["input"]) > 3 { + return "2", nil + } + return "1", nil + }, map[string]bool{"1": true, "2": true})) + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in map[string]string) (endNode string, err error) { + if len(in["input"]) > 3 { + return "3", nil + } + return "2", nil + }, map[string]bool{"2": true, "3": true})) + assert.NoError(t, err) + + err = g.AddEdge("1", END) + assert.NoError(t, err) + err = g.AddEdge("2", END) + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + result, err := r.Invoke(ctx, map[string]string{"input": "hi"}) + assert.NoError(t, err) + assert.Equal(t, map[string]string{ + "1": "1", + "2": "2", + }, result) +} + +func TestNestedDAGBranch(t *testing.T) { + g := NewGraph[string, string]() + + err := g.AddLambdaNode("1", concatLambda("1")) + assert.NoError(t, err) + err = g.AddLambdaNode("2", concatLambda("2")) + assert.NoError(t, err) + err = g.AddLambdaNode("3", concatLambda("3")) + assert.NoError(t, err) + err = g.AddLambdaNode("4", concatLambda("4")) + assert.NoError(t, err) + err = g.AddBranch(START, NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + if len(in) > 3 { + return "2", nil + } + return "1", nil + }, map[string]bool{"1": true, "2": true})) + err = g.AddBranch("2", NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) { + if len(in) > 10 { + return "4", nil + } + return "3", nil + }, map[string]bool{"3": true, "4": true})) + assert.NoError(t, err) + err = g.AddEdge("1", END) + assert.NoError(t, err) + err = g.AddEdge("3", END) + assert.NoError(t, err) + err = g.AddEdge("4", END) + assert.NoError(t, err) + + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + result, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, "hello23", result) + result, err = r.Invoke(ctx, "hi") + assert.NoError(t, err) + assert.Equal(t, "hi1", result) + result, err = r.Invoke(ctx, "hellohello") + assert.NoError(t, err) + assert.Equal(t, "hellohello24", result) +} + +func TestHandlerTypeValidate(t *testing.T) { + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state string) { + return "" + })) + // passthrough pre fail + err := g.AddPassthroughNode("1", WithStatePreHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + })) + assert.ErrorContains(t, err, "passthrough node[1]'s pre handler type isn't any") + g.buildError = nil + // passthrough pre fail with input key + err = g.AddPassthroughNode("1", WithStatePreHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + }), WithInputKey("input")) + assert.ErrorContains(t, err, "node[1]'s pre handler type[string] is different from its input type[map[string]interface {}]") + g.buildError = nil + // passthrough post fail + err = g.AddPassthroughNode("1", WithStatePostHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + })) + assert.ErrorContains(t, err, "passthrough node[1]'s post handler type isn't any") + g.buildError = nil + // passthrough post fail with input key + err = g.AddPassthroughNode("1", WithStatePostHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + }), WithInputKey("input")) + assert.ErrorContains(t, err, "passthrough node[1]'s post handler type isn't any") + g.buildError = nil + // passthrough pre success + err = g.AddPassthroughNode("1", WithStatePreHandler(func(ctx context.Context, in any, state string) (any, error) { + return "", nil + })) + assert.NoError(t, err) + // passthrough pre success with input key + err = g.AddPassthroughNode("2", WithStatePreHandler(func(ctx context.Context, in map[string]any, state string) (map[string]any, error) { + return nil, nil + }), WithInputKey("input")) + assert.NoError(t, err) + // passthrough post success + err = g.AddPassthroughNode("3", WithStatePostHandler(func(ctx context.Context, in any, state string) (any, error) { + return "", nil + })) + assert.NoError(t, err) + // passthrough post success with output key + err = g.AddPassthroughNode("4", WithStatePostHandler(func(ctx context.Context, in map[string]any, state string) (map[string]any, error) { + return nil, nil + }), WithOutputKey("output")) + assert.NoError(t, err) + // common node pre fail + err = g.AddLambdaNode("5", InvokableLambda(func(ctx context.Context, input int) (output int, err error) { + return 0, nil + }), WithStatePreHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + })) + assert.ErrorContains(t, err, "node[5]'s pre handler type[string] is different from its input type[int]") + g.buildError = nil + // common node post fail + err = g.AddLambdaNode("5", InvokableLambda(func(ctx context.Context, input int) (output int, err error) { + return 0, nil + }), WithStatePostHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + })) + assert.ErrorContains(t, err, "node[5]'s post handler type[string] is different from its output type[int]") + g.buildError = nil + // common node pre success + err = g.AddLambdaNode("5", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", nil + }), WithStatePreHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + })) + assert.NoError(t, err) + // common node post success + err = g.AddLambdaNode("6", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", nil + }), WithStatePostHandler(func(ctx context.Context, in string, state string) (string, error) { + return "", nil + })) + assert.NoError(t, err) + // pre state fail + err = g.AddLambdaNode("7", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", nil + }), WithStatePreHandler(func(ctx context.Context, in string, state int) (string, error) { + return "", nil + })) + assert.ErrorContains(t, err, "node[7]'s pre handler state type[int] is different from graph[string]") + g.buildError = nil + // post state fail + err = g.AddLambdaNode("7", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", nil + }), WithStatePostHandler(func(ctx context.Context, in string, state int) (string, error) { + return "", nil + })) + assert.ErrorContains(t, err, "node[7]'s post handler state type[int] is different from graph[string]") + g.buildError = nil + // common pre success with input key + err = g.AddLambdaNode("7", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", nil + }), WithStatePreHandler(func(ctx context.Context, in map[string]any, state string) (map[string]any, error) { + return nil, nil + }), WithInputKey("input")) + assert.NoError(t, err) + // common post success with output key + err = g.AddLambdaNode("8", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return "", nil + }), WithStatePostHandler(func(ctx context.Context, in map[string]any, state string) (map[string]any, error) { + return nil, nil + }), WithOutputKey("output")) + assert.NoError(t, err) +} + +func TestSetFanInMergeConfig_RealStreamNode(t *testing.T) { + for _, triggerMode := range []NodeTriggerMode{AnyPredecessor, AllPredecessor} { + t.Run(string(triggerMode), func(t *testing.T) { + g := NewGraph[int, map[string]any]() + + // Add two stream nodes that output streams of int slices + err := g.AddLambdaNode("s1", StreamableLambda(func(ctx context.Context, input int) (*schema.StreamReader[int], error) { + sr, sw := schema.Pipe[int](2) + sw.Send(input+1, nil) + sw.Send(input+2, nil) + sw.Close() + return sr, nil + }), WithOutputKey("s1")) + assert.NoError(t, err) + err = g.AddLambdaNode("s2", StreamableLambda(func(ctx context.Context, input int) (*schema.StreamReader[int], error) { + sr, sw := schema.Pipe[int](2) + sw.Send(input+10, nil) + sw.Send(input+20, nil) + sw.Close() + return sr, nil + }), WithOutputKey("s2")) + assert.NoError(t, err) + + // Connect edges: START -> s1, START -> s2, s1 -> END, s2 -> END + err = g.AddEdge(START, "s1") + assert.NoError(t, err) + err = g.AddEdge(START, "s2") + assert.NoError(t, err) + err = g.AddEdge("s1", END) + assert.NoError(t, err) + err = g.AddEdge("s2", END) + assert.NoError(t, err) + + r, err := g.Compile(context.Background(), WithNodeTriggerMode(triggerMode), + WithFanInMergeConfig(map[string]FanInMergeConfig{END: {StreamMergeWithSourceEOF: true}})) + assert.NoError(t, err) + + // Run the graph in stream mode and check for SourceEOF events + sr, err := r.Stream(context.Background(), 1) + assert.NoError(t, err) + + merged := make(map[string]map[int]bool) + var sourceEOFCount int + sourceNames := make(map[string]bool) + for { + m, e := sr.Recv() + if e != nil { + if name, ok := schema.GetSourceName(e); ok { + sourceEOFCount++ + sourceNames[name] = true + continue + } + if e == io.EOF { + break + } + assert.NoError(t, e) + } + + for k, v := range m { + if merged[k] == nil { + merged[k] = make(map[int]bool) + } + + merged[k][v.(int)] = true + } + } + + // The merged map should contain both results + assert.Equal(t, map[string]map[int]bool{"s1": {2: true, 3: true}, "s2": {11: true, 21: true}}, merged) + assert.Equal(t, 2, sourceEOFCount, "should receive SourceEOF for each input stream when StreamMergeWithSourceEOF is true") + assert.True(t, sourceNames["s1"], "should receive SourceEOF from s1") + assert.True(t, sourceNames["s2"], "should receive SourceEOF from s2") + }) + } +} + +func TestFindLoops(t *testing.T) { + tests := []struct { + name string + startNodes []string + chanCalls map[string]*chanCall + expected [][]string + }{ + { + name: "Graph without cycles", + startNodes: []string{"A"}, + chanCalls: map[string]*chanCall{ + "A": { + controls: []string{"B", "C"}, + }, + "B": { + controls: []string{"D"}, + }, + "C": { + controls: []string{"E"}, + }, + "D": { + controls: []string{}, + }, + "E": { + controls: []string{}, + }, + }, + expected: [][]string{}, + }, + { + name: "Graph with self-loop", + startNodes: []string{"A"}, + chanCalls: map[string]*chanCall{ + "A": { + controls: []string{"A", "B"}, + }, + "B": { + controls: []string{}, + }, + }, + expected: [][]string{{"A", "A"}}, + }, + { + name: "Graph with simple cycle", + startNodes: []string{"A", "B", "C"}, + chanCalls: map[string]*chanCall{ + "A": { + controls: []string{"B"}, + }, + "B": { + controls: []string{"C"}, + }, + "C": { + controls: []string{"A"}, + }, + }, + expected: [][]string{{"A", "B", "C", "A"}}, + }, + { + name: "Graph with multiple cycles", + startNodes: []string{"A", "B", "C", "D", "E", "F"}, + chanCalls: map[string]*chanCall{ + "A": { + controls: []string{"B", "D"}, + }, + "B": { + controls: []string{"C"}, + }, + "C": { + controls: []string{"B"}, + }, + "D": { + controls: []string{"E"}, + }, + "E": { + controls: []string{"F"}, + }, + "F": { + controls: []string{"D"}, + }, + }, + expected: [][]string{{"B", "C", "B"}, {"D", "E", "F", "D"}}, + }, + { + name: "Graph with branch cycle", + startNodes: []string{"A", "C"}, + chanCalls: map[string]*chanCall{ + "A": { + controls: []string{"B"}, + writeToBranches: []*GraphBranch{ + { + endNodes: map[string]bool{ + "C": true, + }, + }, + }, + }, + "B": { + controls: []string{}, + }, + "C": { + controls: []string{"A"}, + }, + }, + expected: [][]string{{"A", "C", "A"}}, + }, + { + name: "Empty graph", + startNodes: []string{}, + chanCalls: map[string]*chanCall{}, + expected: [][]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + loops := findLoops(tt.startNodes, tt.chanCalls) + + assert.Equal(t, len(tt.expected), len(loops)) + + if len(tt.expected) > 0 { + normalizedExpected := normalizeLoops(tt.expected) + normalizedActual := normalizeLoops(loops) + assert.Equal(t, normalizedExpected, normalizedActual) + } + }) + } +} +func normalizeLoops(loops [][]string) []string { + result := make([]string, 0, len(loops)) + + for _, loop := range loops { + if len(loop) == 0 { + continue + } + + normalizedLoop := make([]string, len(loop)) + copy(normalizedLoop, loop) + if normalizedLoop[0] != normalizedLoop[len(normalizedLoop)-1] { + normalizedLoop = append(normalizedLoop, normalizedLoop[0]) + } + + minIdx := 0 + for i := 1; i < len(normalizedLoop)-1; i++ { + if normalizedLoop[i] < normalizedLoop[minIdx] { + minIdx = i + } + } + + canonicalLoop := "" + for i := 0; i < len(normalizedLoop)-1; i++ { + idx := (minIdx + i) % (len(normalizedLoop) - 1) + canonicalLoop += normalizedLoop[idx] + "," + } + canonicalLoop += normalizedLoop[minIdx] + + result = append(result, canonicalLoop) + } + + sort.Strings(result) + return result +} + +func TestPrintTasks(t *testing.T) { + var ts []*task + assert.Equal(t, "[]", printTask(ts)) + ts = []*task{{nodeKey: "1"}} + assert.Equal(t, "[1]", printTask(ts)) + ts = []*task{{nodeKey: "1"}, {nodeKey: "2"}, {nodeKey: "3"}} + assert.Equal(t, "[1, 2, 3]", printTask(ts)) +} + +func TestSkipBranch(t *testing.T) { + g := NewGraph[string, string]() + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + _ = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + _ = g.AddEdge(START, "1") + _ = g.AddBranch("1", NewGraphMultiBranch(func(ctx context.Context, in string) (endNode map[string]bool, err error) { + return map[string]bool{}, nil + }, map[string]bool{"2": true})) + _ = g.AddEdge("2", END) + + ctx := context.Background() + r, err := g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + _, err = r.Invoke(ctx, "input") + assert.ErrorContains(t, err, "[GraphRunError] no tasks to execute, last completed nodes: [1]") + + g = NewGraph[string, string]() + _ = g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + _ = g.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })) + _ = g.AddEdge(START, "1") + _ = g.AddBranch("1", NewGraphMultiBranch(func(ctx context.Context, in string) (endNode map[string]bool, err error) { + return map[string]bool{}, nil + }, map[string]bool{"2": true})) + _ = g.AddEdge("2", END) + _ = g.AddEdge(START, "2") + r, err = g.Compile(ctx, WithNodeTriggerMode(AllPredecessor)) + assert.NoError(t, err) + result, err := r.Invoke(ctx, "input") + assert.NoError(t, err) + assert.Equal(t, "input", result) +} + +func TestGetStateInGraphCallback(t *testing.T) { + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (s *state) { + return &state{} + })) + assert.NoError(t, g.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }))) + assert.NoError(t, g.AddEdge(START, "1")) + assert.NoError(t, g.AddEdge("1", END)) + + ctx := context.Background() + r, err := g.Compile(ctx) + assert.NoError(t, err) + + _, err = r.Invoke(ctx, "input", WithCallbacks(&testGraphStateCallbackHandler{t: t})) + assert.NoError(t, err) +} + +type state struct { + A string +} + +type testGraphStateCallbackHandler struct { + t *testing.T +} + +func (t *testGraphStateCallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + assert.NoError(t.t, ProcessState[*state](ctx, func(ctx context.Context, s *state) error { + s.A = "test" + return nil + })) + return ctx +} + +func (t *testGraphStateCallbackHandler) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + return ctx +} + +func (t *testGraphStateCallbackHandler) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + return ctx +} + +func (t *testGraphStateCallbackHandler) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + return ctx +} + +func (t *testGraphStateCallbackHandler) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context { + return ctx +} + +func TestUniqueSlice(t *testing.T) { + assert.Equal(t, []string{"a", "b", "c"}, uniqueSlice([]string{"a", "b", "a", "c", "b"})) + assert.Equal(t, []string{}, uniqueSlice([]string{})) +} diff --git a/compose/interrupt.go b/compose/interrupt.go new file mode 100644 index 0000000..98a5eee --- /dev/null +++ b/compose/interrupt.go @@ -0,0 +1,363 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + + "github.com/google/uuid" + + "github.com/cloudwego/eino/internal/core" + "github.com/cloudwego/eino/schema" +) + +// WithInterruptBeforeNodes instructs to interrupt before the given nodes. +func WithInterruptBeforeNodes(nodes []string) GraphCompileOption { + return func(options *graphCompileOptions) { + options.interruptBeforeNodes = nodes + } +} + +// WithInterruptAfterNodes instructs to interrupt after the given nodes. +func WithInterruptAfterNodes(nodes []string) GraphCompileOption { + return func(options *graphCompileOptions) { + options.interruptAfterNodes = nodes + } +} + +// Deprecated: prefer Interrupt/StatefulInterrupt and CompositeInterrupt. +// If you need to pass the legacy error into CompositeInterrupt, wrap it using WrapInterruptAndRerunIfNeeded first. +var InterruptAndRerun = deprecatedInterruptAndRerun +var deprecatedInterruptAndRerun = errors.New("interrupt and rerun") + +// NewInterruptAndRerunErr creates a legacy interrupt-and-rerun error. +// Deprecated: prefer Interrupt(ctx, info) or StatefulInterrupt(ctx, info, state). +// If passing into CompositeInterrupt, wrap using WrapInterruptAndRerunIfNeeded first. +func NewInterruptAndRerunErr(extra any) error { + return deprecatedInterruptAndRerunErr(extra) +} +func deprecatedInterruptAndRerunErr(extra any) error { + return &core.InterruptSignal{InterruptInfo: core.InterruptInfo{ + Info: extra, + IsRootCause: true, + }} +} + +type wrappedInterruptAndRerun struct { + ps Address + inner error +} + +func (w *wrappedInterruptAndRerun) Error() string { + return fmt.Sprintf("interrupt and rerun at address %s: %s", w.ps.String(), w.inner.Error()) +} + +func (w *wrappedInterruptAndRerun) Unwrap() error { + return w.inner +} + +// WrapInterruptAndRerunIfNeeded wraps the deprecated old interrupt errors, with the current execution address. +// If the error is returned by either Interrupt, StatefulInterrupt or CompositeInterrupt, +// it will be returned as-is without wrapping +func WrapInterruptAndRerunIfNeeded(ctx context.Context, step AddressSegment, err error) error { + addr := GetCurrentAddress(ctx) + newAddr := append(append([]AddressSegment{}, addr...), step) + if errors.Is(err, deprecatedInterruptAndRerun) { + return &wrappedInterruptAndRerun{ + ps: newAddr, + inner: err, + } + } + + ire := &core.InterruptSignal{} + if errors.As(err, &ire) { + if ire.Address == nil { + return &wrappedInterruptAndRerun{ + ps: newAddr, + inner: err, + } + } + return ire + } + + return fmt.Errorf("failed to wrap error as addressed InterruptAndRerun: %w", err) +} + +// Interrupt creates a special error that signals the execution engine to interrupt +// the current run at the component's specific address and save a checkpoint. +// +// This is the standard way for a single, non-composite component to signal a resumable interruption. +// +// - ctx: The context of the running component, used to retrieve the current execution address. +// - info: User-facing information about the interrupt. This is not persisted but is exposed to the +// calling application via the InterruptCtx to provide context (e.g., a reason for the pause). +func Interrupt(ctx context.Context, info any) error { + is, err := core.Interrupt(ctx, info, nil, nil) + if err != nil { + return err + } + + return is +} + +// StatefulInterrupt creates a special error that signals the execution engine to interrupt +// the current run at the component's specific address and save a checkpoint. +// +// This is the standard way for a single, non-composite component to signal a resumable interruption. +// +// - ctx: The context of the running component, used to retrieve the current execution address. +// - info: User-facing information about the interrupt. This is not persisted but is exposed to the +// calling application via the InterruptCtx to provide context (e.g., a reason for the pause). +// - state: The internal state that the interrupting component needs to persist to be able to resume +// its work later. This state is saved in the checkpoint and will be provided back to the component +// upon resumption via GetInterruptState. +func StatefulInterrupt(ctx context.Context, info any, state any) error { + is, err := core.Interrupt(ctx, info, state, nil) + if err != nil { + return err + } + + return is +} + +// CompositeInterrupt creates a special error that signals a composite interruption. +// It is designed for "composite" nodes (like ToolsNode) that manage multiple, independent, +// interruptible sub-processes. It bundles multiple sub-interrupt errors into a single error +// that the engine can deconstruct into a flat list of resumable points. +// +// This function is robust and can handle several types of errors from sub-processes: +// +// - A `Interrupt` or `StatefulInterrupt` error from a simple component. +// +// - A nested `CompositeInterrupt` error from another composite component. +// +// - An error containing `InterruptInfo` returned by a `Runnable` (e.g., a Graph within a lambda node). +// +// - An error returned by \'WrapInterruptAndRerunIfNeeded\' for the legacy old interrupt and rerun error, +// and for the error returned by the deprecated old interrupt errors. +// +// Parameters: +// +// - ctx: The context of the running composite node. +// +// - info: User-facing information for the composite node itself. Can be nil. +// This info will be attached to InterruptInfo.RerunNodeExtra. +// Provided mainly for compatibility purpose as the composite node itself +// is not an interrupt point with interrupt ID, +// which means it lacks enough reason to give a user-facing info. +// +// - state: The state for the composite node itself. Can be nil. +// This could be useful when the composite node needs to restore state, +// such as its input (e.g. ToolsNode). +// +// - errs: a list of errors emitted by sub-processes. +// +// NOTE: if the error you passed in is the deprecated old interrupt and rerun err, or an error returned by +// the deprecated old interrupt function, you must wrap it using WrapInterruptAndRerunIfNeeded first +// before passing them into this function. +func CompositeInterrupt(ctx context.Context, info any, state any, errs ...error) error { + if len(errs) == 0 { + return StatefulInterrupt(ctx, info, state) + } + + var cErrs []*core.InterruptSignal + for _, err := range errs { + wrapped := &wrappedInterruptAndRerun{} + if errors.As(err, &wrapped) { + inner := wrapped.Unwrap() + if errors.Is(inner, deprecatedInterruptAndRerun) { + id := uuid.NewString() + cErrs = append(cErrs, &core.InterruptSignal{ + ID: id, + Address: wrapped.ps, + InterruptInfo: core.InterruptInfo{ + Info: nil, + IsRootCause: true, + }, + }) + continue + } + + ire := &core.InterruptSignal{} + if errors.As(err, &ire) { + id := uuid.NewString() + cErrs = append(cErrs, &core.InterruptSignal{ + ID: id, + Address: wrapped.ps, + InterruptInfo: core.InterruptInfo{ + Info: ire.InterruptInfo.Info, + IsRootCause: ire.InterruptInfo.IsRootCause, + }, + InterruptState: core.InterruptState{ + State: ire.InterruptState.State, + }, + }) + } + + continue + } + + ire := &core.InterruptSignal{} + if errors.As(err, &ire) { + cErrs = append(cErrs, ire) + continue + } + + ie := &interruptError{} + if errors.As(err, &ie) { + is := core.FromInterruptContexts(ie.Info.InterruptContexts) + cErrs = append(cErrs, is) + continue + } + + return fmt.Errorf("composite interrupt but one of the sub error is not interrupt and rerun error: %w", err) + } + + is, err := core.Interrupt(ctx, info, state, cErrs) + if err != nil { + return err + } + return is +} + +// IsInterruptRerunError reports whether the error represents an interrupt-and-rerun +// and returns any attached info. +func IsInterruptRerunError(err error) (any, bool) { + info, _, ok := isInterruptRerunError(err) + return info, ok +} + +func isInterruptRerunError(err error) (info any, state any, ok bool) { + if errors.Is(err, deprecatedInterruptAndRerun) { + return nil, nil, true + } + ire := &core.InterruptSignal{} + if errors.As(err, &ire) { + return ire.Info, ire.State, true + } + return nil, nil, false +} + +// InterruptInfo aggregates interrupt metadata for composite or nested runs. +type InterruptInfo struct { + State any + BeforeNodes []string + AfterNodes []string + RerunNodes []string + RerunNodesExtra map[string]any + SubGraphs map[string]*InterruptInfo + InterruptContexts []*InterruptCtx +} + +func init() { + schema.RegisterName[*InterruptInfo]("_eino_compose_interrupt_info") +} + +// AddressSegmentType defines the type of a segment in an execution address. +type AddressSegmentType = core.AddressSegmentType + +const ( + // AddressSegmentNode represents a segment of an address that corresponds to a graph node. + AddressSegmentNode AddressSegmentType = "node" + // AddressSegmentTool represents a segment of an address that corresponds to a specific tool call within a ToolsNode. + AddressSegmentTool AddressSegmentType = "tool" + // AddressSegmentRunnable represents a segment of an address that corresponds to an instance of the Runnable interface. + // Currently the possible Runnable types are: Graph, Workflow and Chain. + // Note that for sub-graphs added through AddGraphNode to another graph is not a Runnable. + // So a AddressSegmentRunnable indicates a standalone Root level Graph, + // or a Root level Graph inside a node such as Lambda node. + AddressSegmentRunnable AddressSegmentType = "runnable" +) + +// Address represents a full, hierarchical address to a point in the execution structure. +type Address = core.Address + +// AddressSegment represents a single segment in the hierarchical address of an execution point. +// A sequence of AddressSegments uniquely identifies a location within a potentially nested structure. +type AddressSegment = core.AddressSegment + +// InterruptCtx provides a complete, user-facing context for a single, resumable interrupt point. +type InterruptCtx = core.InterruptCtx + +// ExtractInterruptInfo extracts InterruptInfo from an error if present. +func ExtractInterruptInfo(err error) (info *InterruptInfo, existed bool) { + if err == nil { + return nil, false + } + var iE *interruptError + if errors.As(err, &iE) { + return iE.Info, true + } + var sIE *subGraphInterruptError + if errors.As(err, &sIE) { + return sIE.Info, true + } + return nil, false +} + +type interruptError struct { + Info *InterruptInfo +} + +func (e *interruptError) Error() string { + return fmt.Sprintf("interrupt happened, info: %+v", e.Info) +} + +func (e *interruptError) GetInterruptContexts() []*InterruptCtx { + if e.Info == nil { + return nil + } + return e.Info.InterruptContexts +} + +func isSubGraphInterrupt(err error) *subGraphInterruptError { + if err == nil { + return nil + } + var iE *subGraphInterruptError + if errors.As(err, &iE) { + return iE + } + return nil +} + +type subGraphInterruptError struct { + Info *InterruptInfo + CheckPoint *checkpoint + + signal *core.InterruptSignal +} + +func (e *subGraphInterruptError) Error() string { + return fmt.Sprintf("interrupt happened, info: %+v", e.Info) +} + +func isInterruptError(err error) bool { + if _, ok := ExtractInterruptInfo(err); ok { + return true + } + if info := isSubGraphInterrupt(err); info != nil { + return true + } + if _, ok := IsInterruptRerunError(err); ok { + return true + } + + return false +} diff --git a/compose/introspect.go b/compose/introspect.go new file mode 100644 index 0000000..f15d2f7 --- /dev/null +++ b/compose/introspect.go @@ -0,0 +1,57 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "reflect" + + "github.com/cloudwego/eino/components" +) + +// GraphNodeInfo the info which end users pass in when they are adding nodes to graph. +type GraphNodeInfo struct { + Component components.Component + Instance any + GraphAddNodeOpts []GraphAddNodeOpt + InputType, OutputType reflect.Type // mainly for lambda, whose input and output types cannot be inferred by component type + Name string + InputKey, OutputKey string + GraphInfo *GraphInfo + Mappings []*FieldMapping +} + +// GraphInfo the info which end users pass in when they are compiling a graph. +// it is used in compile callback for user to get the node info and instance. +// you may need all details info of the graph for observation. +type GraphInfo struct { + CompileOptions []GraphCompileOption + Nodes map[string]GraphNodeInfo // node key -> node info + Edges map[string][]string // edge start node key -> edge end node key, control edges + DataEdges map[string][]string + Branches map[string][]GraphBranch // branch start node key -> branch + InputType, OutputType reflect.Type + Name string + + NewGraphOptions []NewGraphOption + GenStateFn func(context.Context) any +} + +// GraphCompileCallback is the callback which will be called when graph compilation finishes. +type GraphCompileCallback interface { + OnFinish(ctx context.Context, info *GraphInfo) +} diff --git a/compose/pregel.go b/compose/pregel.go new file mode 100644 index 0000000..583dec9 --- /dev/null +++ b/compose/pregel.go @@ -0,0 +1,95 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import "fmt" + +func pregelChannelBuilder(_ []string, _ []string, _ func() any, _ func() streamReader) channel { + return &pregelChannel{Values: make(map[string]any)} +} + +type pregelChannel struct { + Values map[string]any + + mergeConfig FanInMergeConfig +} + +func (ch *pregelChannel) setMergeConfig(cfg FanInMergeConfig) { + ch.mergeConfig.StreamMergeWithSourceEOF = cfg.StreamMergeWithSourceEOF +} + +func (ch *pregelChannel) load(c channel) error { + dc, ok := c.(*pregelChannel) + if !ok { + return fmt.Errorf("load pregel channel fail, got %T, want *pregelChannel", c) + } + ch.Values = dc.Values + return nil +} + +func (ch *pregelChannel) convertValues(fn func(map[string]any) error) error { + return fn(ch.Values) +} + +func (ch *pregelChannel) reportValues(ins map[string]any) error { + for k, v := range ins { + ch.Values[k] = v + } + return nil +} + +func (ch *pregelChannel) get(isStream bool, name string, edgeHandler *edgeHandlerManager) ( + any, bool, error) { + if len(ch.Values) == 0 { + return nil, false, nil + } + defer func() { ch.Values = map[string]any{} }() + values := make([]any, len(ch.Values)) + names := make([]string, len(ch.Values)) + i := 0 + for k, v := range ch.Values { + resolvedV, err := edgeHandler.handle(k, name, v, isStream) + if err != nil { + return nil, false, err + } + values[i] = resolvedV + names[i] = k + i++ + } + + if len(values) == 1 { + return values[0], true, nil + } + + // merge + mergeOpts := &mergeOptions{ + streamMergeWithSourceEOF: ch.mergeConfig.StreamMergeWithSourceEOF, + names: names, + } + v, err := mergeValues(values, mergeOpts) + if err != nil { + return nil, false, err + } + return v, true, nil +} + +func (ch *pregelChannel) reportSkip(_ []string) bool { + return false +} +func (ch *pregelChannel) reportDependencies(_ []string) { + return +} diff --git a/compose/resume.go b/compose/resume.go new file mode 100644 index 0000000..a4513fc --- /dev/null +++ b/compose/resume.go @@ -0,0 +1,156 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + + "github.com/cloudwego/eino/internal/core" +) + +// GetInterruptState provides a type-safe way to check for and retrieve the persisted state from a previous interruption. +// It is the primary function a component should use to understand its past state. +// +// It returns three values: +// - wasInterrupted (bool): True if the node was part of a previous interruption, regardless of whether state was provided. +// - state (T): The typed state object, if it was provided and matches type `T`. +// - hasState (bool): True if state was provided during the original interrupt and successfully cast to type `T`. +func GetInterruptState[T any](ctx context.Context) (wasInterrupted bool, hasState bool, state T) { + return core.GetInterruptState[T](ctx) +} + +// GetResumeContext checks if the current component is the target of a resume operation +// and retrieves any data provided by the user for that resumption. +// +// This function is typically called *after* a component has already determined it is in a +// resumed state by calling GetInterruptState. +// +// It returns three values: +// - isResumeFlow: A boolean that is true if the current component's address was explicitly targeted +// by a call to Resume() or ResumeWithData(). +// - hasData: A boolean that is true if data was provided for this component (i.e., not nil). +// - data: The typed data provided by the user. +// +// ### How to Use This Function: A Decision Framework +// +// The correct usage pattern depends on the application's desired resume strategy. +// +// #### Strategy 1: Implicit "Resume All" +// In some use cases, any resume operation implies that *all* interrupted points should proceed. +// For example, if an application's UI only provides a single "Continue" button for a set of +// interruptions. In this model, a component can often just use `GetInterruptState` to see if +// `wasInterrupted` is true and then proceed with its logic, as it can assume it is an intended target. +// It may still call `GetResumeContext` to check for optional data, but the `isResumeFlow` flag is less critical. +// +// #### Strategy 2: Explicit "Targeted Resume" (Most Common) +// For applications with multiple, distinct interrupt points that must be resumed independently, it is +// crucial to differentiate which point is being resumed. This is the primary use case for the `isResumeFlow` flag. +// - If `isResumeFlow` is `true`: Your component is the explicit target. You should consume +// the `data` (if any) and complete your work. +// - If `isResumeFlow` is `false`: Another component is the target. You MUST re-interrupt +// (e.g., by returning `StatefulInterrupt(...)`) to preserve your state and allow the +// resume signal to propagate. +// +// ### Guidance for Composite Components +// +// Composite components (like `Graph` or other `Runnable`s that contain sub-processes) have a dual role: +// 1. Check for Self-Targeting: A composite component can itself be the target of a resume +// operation, for instance, to modify its internal state. It may call `GetResumeContext` +// to check for data targeted at its own address. +// 2. Act as a Conduit: After checking for itself, its primary role is to re-execute its children, +// allowing the resume context to flow down to them. It must not consume a resume signal +// intended for one of its descendants. +func GetResumeContext[T any](ctx context.Context) (isResumeFlow bool, hasData bool, data T) { + return core.GetResumeContext[T](ctx) +} + +// GetCurrentAddress returns the hierarchical address of the currently executing component. +// The address is a sequence of segments, each identifying a structural part of the execution +// like an agent, a graph node, or a tool call. This can be useful for logging or debugging. +func GetCurrentAddress(ctx context.Context) Address { + return core.GetCurrentAddress(ctx) +} + +// Resume prepares a context for an "Explicit Targeted Resume" operation by targeting one or more +// components without providing data. It is a convenience wrapper around BatchResumeWithData. +// +// This is useful when the act of resuming is itself the signal, and no extra data is needed. +// The components at the provided addresses (interrupt IDs) will receive `isResumeFlow = true` +// when they call `GetResumeContext`. +func Resume(ctx context.Context, interruptIDs ...string) context.Context { + resumeData := make(map[string]any, len(interruptIDs)) + for _, addr := range interruptIDs { + resumeData[addr] = nil + } + return BatchResumeWithData(ctx, resumeData) +} + +// ResumeWithData prepares a context to resume a single, specific component with data. +// It is the primary function for the "Explicit Targeted Resume" strategy when data is required. +// It is a convenience wrapper around BatchResumeWithData. +// The `interruptID` parameter is the unique interrupt ID of the target component. +func ResumeWithData(ctx context.Context, interruptID string, data any) context.Context { + return BatchResumeWithData(ctx, map[string]any{interruptID: data}) +} + +// BatchResumeWithData is the core function for preparing a resume context. It injects a map +// of resume targets and their corresponding data into the context. +// +// The `resumeData` map should contain the interrupt IDs (which are the string form of addresses) of the +// components to be resumed as keys. The value can be the resume data for that component, or `nil` +// if no data is needed (equivalent to using `Resume`). +// +// This function is the foundation for the "Explicit Targeted Resume" strategy. Components whose interrupt IDs +// are present as keys in the map will receive `isResumeFlow = true` when they call `GetResumeContext`. +func BatchResumeWithData(ctx context.Context, resumeData map[string]any) context.Context { + return core.BatchResumeWithData(ctx, resumeData) +} + +func getNodePath(ctx context.Context) (*NodePath, bool) { + currentAddress := GetCurrentAddress(ctx) + if len(currentAddress) == 0 { + return nil, false + } + + nodePath := make([]string, 0, len(currentAddress)) + for _, p := range currentAddress { + if p.Type == AddressSegmentRunnable { + nodePath = []string{} + continue + } + + nodePath = append(nodePath, p.ID) + } + + return NewNodePath(nodePath...), len(nodePath) > 0 +} + +// AppendAddressSegment creates a new execution context for a sub-component (e.g., a graph node or a tool call). +// +// It extends the current context's address with a new segment and populates the new context with the +// appropriate interrupt state and resume data for that specific sub-address. +// +// - ctx: The parent context, typically the one passed into the component's Invoke/Stream method. +// - segType: The type of the new address segment (e.g., "node", "tool"). +// - segID: The unique ID for the new address segment. +func AppendAddressSegment(ctx context.Context, segType AddressSegmentType, segID string) context.Context { + return core.AppendAddressSegment(ctx, segType, segID, "") +} + +func appendToolAddressSegment(ctx context.Context, segID string, subID string) context.Context { + return core.AppendAddressSegment(ctx, AddressSegmentTool, segID, subID) +} diff --git a/compose/resume_test.go b/compose/resume_test.go new file mode 100644 index 0000000..da6056d --- /dev/null +++ b/compose/resume_test.go @@ -0,0 +1,1208 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "encoding/json" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +type myInterruptState struct { + OriginalInput string +} + +type myResumeData struct { + Message string +} + +type resumeTestState struct { + OnStartCalledOnResume bool `json:"on_start_called_on_resume"` + Counter int `json:"counter"` +} + +func init() { + schema.Register[resumeTestState]() +} + +func TestInterruptStateAndResumeForRootGraph(t *testing.T) { + // create a graph with a lambda node + // this lambda node will interrupt with a typed state and an info for end-user + // verify the info thrown by the lambda node + // resume with a structured resume data + // within the lambda node, getRunCtx and verify the state and resume data + g := NewGraph[string, string]() + + lambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + wasInterrupted, hasState, state := GetInterruptState[*myInterruptState](ctx) + if !wasInterrupted { + // First run: interrupt with state + return "", StatefulInterrupt(ctx, + map[string]any{"reason": "scheduled maintenance"}, + &myInterruptState{OriginalInput: input}, + ) + } + + // This is a resumed run. + assert.True(t, hasState) + assert.Equal(t, "initial input", state.OriginalInput) + + isResume, hasData, data := GetResumeContext[*myResumeData](ctx) + assert.True(t, isResume) + assert.True(t, hasData) + assert.Equal(t, "let's continue", data.Message) + + return "Resumed successfully with input: " + state.OriginalInput, nil + }) + + _ = g.AddLambdaNode("lambda", lambda) + _ = g.AddEdge(START, "lambda") + _ = g.AddEdge("lambda", END) + + graph, err := g.Compile(context.Background(), WithCheckPointStore(newInMemoryStore()), WithGraphName("root")) + assert.NoError(t, err) + + // First invocation, which should be interrupted + checkPointID := "test-checkpoint-1" + _, err = graph.Invoke(context.Background(), "initial input", WithCheckPointID(checkPointID)) + + // Verify the interrupt error and extracted info + assert.Error(t, err) + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + assert.NotNil(t, interruptInfo) + + interruptContexts := interruptInfo.InterruptContexts + assert.Equal(t, 1, len(interruptContexts)) + assert.Equal(t, "runnable:root;node:lambda", interruptContexts[0].Address.String()) + assert.Equal(t, map[string]any{"reason": "scheduled maintenance"}, interruptContexts[0].Info) + + // Prepare resume data + ctx := ResumeWithData(context.Background(), interruptContexts[0].ID, + &myResumeData{Message: "let's continue"}) + + // Resume execution + output, err := graph.Invoke(ctx, "", WithCheckPointID(checkPointID)) + + // Verify the final result + assert.NoError(t, err) + assert.Equal(t, "Resumed successfully with input: initial input", output) +} + +func TestProcessStateInOnStartDuringResume(t *testing.T) { + graphOnStartCallCount := 0 + processStateErrorOnResume := error(nil) + + cb := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + if info.Name == "test-process-state-onstart" { + graphOnStartCallCount++ + err := ProcessState[*resumeTestState](ctx, func(ctx context.Context, s *resumeTestState) error { + s.Counter++ + return nil + }) + if graphOnStartCallCount > 1 { + processStateErrorOnResume = err + } + } + return ctx + }). + Build() + + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) *resumeTestState { + return &resumeTestState{} + })) + + lambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + wasInterrupted, _, _ := GetInterruptState[*myInterruptState](ctx) + if !wasInterrupted { + return "", StatefulInterrupt(ctx, + map[string]any{"reason": "test interrupt"}, + &myInterruptState{OriginalInput: input}, + ) + } + + var stateCounter int + err := ProcessState[*resumeTestState](ctx, func(ctx context.Context, s *resumeTestState) error { + stateCounter = s.Counter + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 2, stateCounter, "Counter should be 2 (first run OnStart + resume OnStart)") + + return "success", nil + }) + + _ = g.AddLambdaNode("lambda", lambda) + _ = g.AddEdge(START, "lambda") + _ = g.AddEdge("lambda", END) + + graph, err := g.Compile(context.Background(), + WithCheckPointStore(newInMemoryStore()), + WithGraphName("test-process-state-onstart"), + ) + assert.NoError(t, err) + + checkPointID := "test-checkpoint-process-state" + _, err = graph.Invoke(context.Background(), "test input", WithCheckPointID(checkPointID), WithCallbacks(cb)) + + assert.Error(t, err, "First invocation should return an error") + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt, "Should be an interrupt error") + assert.NotNil(t, interruptInfo) + assert.Equal(t, 1, graphOnStartCallCount, "Graph OnStart should be called once on first run") + + ctx := ResumeWithData(context.Background(), interruptInfo.InterruptContexts[0].ID, &myResumeData{}) + + output, err := graph.Invoke(ctx, "", WithCheckPointID(checkPointID), WithCallbacks(cb)) + assert.NoError(t, err) + assert.Equal(t, "success", output) + assert.Equal(t, 2, graphOnStartCallCount, "Graph OnStart should be called twice (first run + resume)") + assert.NoError(t, processStateErrorOnResume, "ProcessState should work in OnStart during resume") +} + +func TestInterruptStateAndResumeForSubGraph(t *testing.T) { + // create a graph + // create a another graph with a lambda node, as this graph as a sub-graph of the previous graph + // this lambda node will interrupt with a typed state and an info for end-user + // verify the info thrown by the lambda node + // resume with a structured resume data + // within the lambda node, getRunCtx and verify the state and resume data + subGraph := NewGraph[string, string]() + + lambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + wasInterrupted, hasState, state := GetInterruptState[*myInterruptState](ctx) + if !wasInterrupted { + // First run: interrupt with state + return "", StatefulInterrupt(ctx, + map[string]any{"reason": "sub-graph maintenance"}, + &myInterruptState{OriginalInput: input}, + ) + } + + // Second (resumed) run + assert.True(t, hasState) + assert.Equal(t, "main input", state.OriginalInput) + + isResume, hasData, data := GetResumeContext[*myResumeData](ctx) + assert.True(t, isResume) + assert.True(t, hasData) + assert.Equal(t, "let's continue sub-graph", data.Message) + + return "Sub-graph resumed successfully", nil + }) + + _ = subGraph.AddLambdaNode("inner_lambda", lambda) + _ = subGraph.AddEdge(START, "inner_lambda") + _ = subGraph.AddEdge("inner_lambda", END) + + // Create the main graph + mainGraph := NewGraph[string, string]() + _ = mainGraph.AddGraphNode("sub_graph_node", subGraph) + _ = mainGraph.AddEdge(START, "sub_graph_node") + _ = mainGraph.AddEdge("sub_graph_node", END) + + compiledMainGraph, err := mainGraph.Compile(context.Background(), WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + // First invocation, which should be interrupted + checkPointID := "test-subgraph-checkpoint-1" + _, err = compiledMainGraph.Invoke(context.Background(), "main input", WithCheckPointID(checkPointID)) + + // Verify the interrupt error and extracted info + assert.Error(t, err) + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + assert.NotNil(t, interruptInfo) + + interruptContexts := interruptInfo.InterruptContexts + assert.Equal(t, 1, len(interruptContexts)) + assert.Equal(t, "runnable:;node:sub_graph_node;node:inner_lambda", interruptContexts[0].Address.String()) + assert.Equal(t, map[string]any{"reason": "sub-graph maintenance"}, interruptContexts[0].Info) + + // Prepare resume data + ctx := ResumeWithData(context.Background(), interruptContexts[0].ID, + &myResumeData{Message: "let's continue sub-graph"}) + + // Resume execution + output, err := compiledMainGraph.Invoke(ctx, "", WithCheckPointID(checkPointID)) + + // Verify the final result + assert.NoError(t, err) + assert.Equal(t, "Sub-graph resumed successfully", output) +} + +func TestInterruptStateAndResumeForToolInNestedSubGraph(t *testing.T) { + // create a ROOT graph. + // create a sub graph A, add A to ROOT graph using AddGraphNode. + // create a sub-sub graph B, add B to A using AddGraphNode. + // within sub-sub graph B, add a ChatModelNode, which is a Mock chat model that implements the ToolCallingChatModel + // interface. + // add a Mock InvokableTool to this mock chat model. + // within sub-sub graph B, also add a ToolsNode that will execute this Mock InvokableTool. + // this tool will interrupt with a typed state and an info for end-user + // verify the info thrown by the tool. + // resume with a structured resume data. + // within the Tool, getRunCtx and verify the state and resume data + ctrl := gomock.NewController(t) + + // 1. Define the interrupting tool + mockTool := &mockInterruptingTool{tt: t} + + // 2. Define the sub-sub-graph (B) + subSubGraphB := NewGraph[[]*schema.Message, []*schema.Message]() + + // Mock Chat Model that calls the tool + mockChatModel := mockModel.NewMockToolCallingChatModel(ctrl) + mockChatModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).Return(&schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "tool_call_123", Function: schema.FunctionCall{Name: "interrupt_tool", Arguments: `{"input": "test"}`}}, + }, + }, nil).AnyTimes() + mockChatModel.EXPECT().WithTools(gomock.Any()).Return(mockChatModel, nil).AnyTimes() + + toolsNode, err := NewToolNode(context.Background(), &ToolsNodeConfig{Tools: []tool.BaseTool{mockTool}}) + assert.NoError(t, err) + + _ = subSubGraphB.AddChatModelNode("model", mockChatModel) + _ = subSubGraphB.AddToolsNode("tools", toolsNode) + _ = subSubGraphB.AddEdge(START, "model") + _ = subSubGraphB.AddEdge("model", "tools") + _ = subSubGraphB.AddEdge("tools", END) + + // 3. Define sub-graph (A) + subGraphA := NewGraph[[]*schema.Message, []*schema.Message]() + _ = subGraphA.AddGraphNode("sub_graph_b", subSubGraphB) + _ = subGraphA.AddEdge(START, "sub_graph_b") + _ = subGraphA.AddEdge("sub_graph_b", END) + + // 4. Define root graph + rootGraph := NewGraph[[]*schema.Message, []*schema.Message]() + _ = rootGraph.AddGraphNode("sub_graph_a", subGraphA) + _ = rootGraph.AddEdge(START, "sub_graph_a") + _ = rootGraph.AddEdge("sub_graph_a", END) + + // 5. Compile and run + compiledRootGraph, err := rootGraph.Compile(context.Background(), WithCheckPointStore(newInMemoryStore()), + WithGraphName("root")) + assert.NoError(t, err) + + // First invocation - should interrupt + checkPointID := "test-nested-tool-interrupt" + initialInput := []*schema.Message{schema.UserMessage("hello")} + _, err = compiledRootGraph.Invoke(context.Background(), initialInput, WithCheckPointID(checkPointID)) + + // 6. Verify the interrupt + assert.Error(t, err) + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + assert.NotNil(t, interruptInfo) + + interruptContexts := interruptInfo.InterruptContexts + assert.Len(t, interruptContexts, 1) // Only the root cause is returned + + // Verify the root cause context + rootCause := interruptContexts[0] + expectedPath := "runnable:root;node:sub_graph_a;node:sub_graph_b;node:tools;tool:interrupt_tool:tool_call_123" + assert.Equal(t, expectedPath, rootCause.Address.String()) + assert.True(t, rootCause.IsRootCause) + assert.Equal(t, map[string]any{"reason": "tool maintenance"}, rootCause.Info) + + // Verify the parent via the Parent field + assert.NotNil(t, rootCause.Parent) + assert.Equal(t, "runnable:root;node:sub_graph_a;node:sub_graph_b;node:tools", rootCause.Parent.Address.String()) + assert.False(t, rootCause.Parent.IsRootCause) + + // 7. Resume execution + ctx := ResumeWithData(context.Background(), rootCause.ID, &myResumeData{Message: "let's continue tool"}) + output, err := compiledRootGraph.Invoke(ctx, initialInput, WithCheckPointID(checkPointID)) + + // 8. Verify final result + assert.NoError(t, err) + assert.NotNil(t, output) + assert.Len(t, output, 1) + assert.Equal(t, "Tool resumed successfully", output[0].Content) +} + +const PathSegmentTypeProcess AddressSegmentType = "process" + +// processState is the state for a single sub-process in the batch test. +type processState struct { + Step int +} + +// batchState is the composite state for the whole batch lambda. +type batchState struct { + ProcessStates map[string]*processState + Results map[string]string +} + +type processResumeData struct { + Instruction string +} + +func init() { + schema.RegisterName[*myInterruptState]("my_interrupt_state") + schema.RegisterName[*batchState]("batch_state") + schema.RegisterName[*processState]("process_state") +} + +func TestMultipleInterruptsAndResumes(t *testing.T) { + // define a new lambda node that act as a 'batch' node + // it kick starts 3 parallel processes, each will interrupt on first run, while preserving their own state. + // each of the process should have their own user-facing interrupt info. + // define a new AddressSegmentType for these sub processes. + // the lambda should use StatefulInterrupt to interrupt and preserve the state, + // which is a specific struct type that implements the CompositeInterruptState interface. + // there should also be a specific struct that that implements the CompositeInterruptInfo interface, + // which helps the end-user to fetch the nested interrupt info. + // put this lambda node within a graph and invoke the graph. + // simulate the user getting the flat list of 3 interrupt points using GetInterruptContexts + // the user then decides to resume two of the three interrupt points + // the first resume has resume data, while the second resume does not.(ResumeWithData vs. Resume) + // verify the resume data and state for the resumed interrupt points. + processIDs := []string{"p0", "p1", "p2"} + + // This is the logic for a single "process" + runProcess := func(ctx context.Context, id string) (string, error) { + // Check if this specific process was interrupted before + wasInterrupted, hasState, pState := GetInterruptState[*processState](ctx) + if !wasInterrupted { + // First run for this process, interrupt it. + return "", StatefulInterrupt(ctx, + map[string]any{"reason": "process " + id + " needs input"}, + &processState{Step: 1}, + ) + } + + assert.True(t, hasState) + assert.Equal(t, 1, pState.Step) + + // Check if we are being resumed + isResume, hasData, pData := GetResumeContext[*processResumeData](ctx) + if !isResume { + // Not being resumed, so interrupt again. + return "", StatefulInterrupt(ctx, + map[string]any{"reason": "process " + id + " still needs input"}, + pState, + ) + } + + // We are being resumed. + if hasData { + // Resumed with data + return "process " + id + " done with instruction: " + pData.Instruction, nil + } + // Resumed without data + return "process " + id + " done", nil + } + + // This is the main "batch" lambda that orchestrates the processes + batchLambda := InvokableLambda(func(ctx context.Context, _ string) (map[string]string, error) { + // Restore the state of the batch node itself + _, _, persistedBatchState := GetInterruptState[*batchState](ctx) + if persistedBatchState == nil { + persistedBatchState = &batchState{ + Results: make(map[string]string), + } + } + + var errs []error + + for _, id := range processIDs { + // If this process already completed in a previous run, skip it. + if _, done := persistedBatchState.Results[id]; done { + continue + } + + // Create a sub-context for each process + subCtx := AppendAddressSegment(ctx, PathSegmentTypeProcess, id) + res, err := runProcess(subCtx, id) + + if err != nil { + _, ok := IsInterruptRerunError(err) + assert.True(t, ok) + errs = append(errs, err) + } else { + // Process completed, save its result to the state for the next run. + persistedBatchState.Results[id] = res + } + } + + if len(errs) > 0 { + return nil, CompositeInterrupt(ctx, nil, persistedBatchState, errs...) + } + + return persistedBatchState.Results, nil + }) + + g := NewGraph[string, map[string]string]() + _ = g.AddLambdaNode("batch", batchLambda) + _ = g.AddEdge(START, "batch") + _ = g.AddEdge("batch", END) + + graph, err := g.Compile(context.Background(), WithCheckPointStore(newInMemoryStore()), + WithGraphName("root")) + assert.NoError(t, err) + + // --- 1. First invocation, all 3 processes should interrupt --- + checkPointID := "multi-interrupt-test" + _, err = graph.Invoke(context.Background(), "", WithCheckPointID(checkPointID)) + + assert.Error(t, err) + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + interruptContexts := interruptInfo.InterruptContexts + assert.Len(t, interruptContexts, 3) // Only the 3 root causes + + found := make(map[string]bool) + addrToID := make(map[string]string) + var parentCtx *InterruptCtx + for _, iCtx := range interruptContexts { + addrStr := iCtx.Address.String() + found[addrStr] = true + addrToID[addrStr] = iCtx.ID + assert.True(t, iCtx.IsRootCause) + assert.Equal(t, map[string]any{"reason": "process " + iCtx.Address[2].ID + " needs input"}, iCtx.Info) + // Check that all share the same parent + assert.NotNil(t, iCtx.Parent) + if parentCtx == nil { + parentCtx = iCtx.Parent + assert.Equal(t, "runnable:root;node:batch", parentCtx.Address.String()) + assert.False(t, parentCtx.IsRootCause) + } else { + assert.Same(t, parentCtx, iCtx.Parent) + } + } + assert.True(t, found["runnable:root;node:batch;process:p0"]) + assert.True(t, found["runnable:root;node:batch;process:p1"]) + assert.True(t, found["runnable:root;node:batch;process:p2"]) + + // --- 2. Second invocation, resume 2 of 3 processes --- + // Resume p0 with data, and p2 without data. p1 remains interrupted. + resumeCtx := ResumeWithData(context.Background(), addrToID["runnable:root;node:batch;process:p0"], &processResumeData{Instruction: "do it"}) + resumeCtx = Resume(resumeCtx, addrToID["runnable:root;node:batch;process:p2"]) + + _, err = graph.Invoke(resumeCtx, "", WithCheckPointID(checkPointID)) + + // Expect an interrupt again, but only for p1 + assert.Error(t, err) + interruptInfo2, isInterrupt2 := ExtractInterruptInfo(err) + assert.True(t, isInterrupt2) + interruptContexts2 := interruptInfo2.InterruptContexts + assert.Len(t, interruptContexts2, 1) // Only p1 is left + rootCause2 := interruptContexts2[0] + assert.Equal(t, "runnable:root;node:batch;process:p1", rootCause2.Address.String()) + assert.NotNil(t, rootCause2.Parent) + assert.Equal(t, "runnable:root;node:batch", rootCause2.Parent.Address.String()) + + // --- 3. Third invocation, resume the last process --- + finalResumeCtx := Resume(context.Background(), rootCause2.ID) + finalOutput, err := graph.Invoke(finalResumeCtx, "", WithCheckPointID(checkPointID)) + + assert.NoError(t, err) + assert.Equal(t, "process p0 done with instruction: do it", finalOutput["p0"]) + assert.Equal(t, "process p1 done", finalOutput["p1"]) + assert.Equal(t, "process p2 done", finalOutput["p2"]) +} + +// toolsNodeResumeTargetCallback captures isResumeTarget for ToolsNode during OnStart +type toolsNodeResumeTargetCallback struct { + mu sync.Mutex + isResumeTargetLog []bool +} + +func (c *toolsNodeResumeTargetCallback) OnStart(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackInput) context.Context { + if info.Component == ComponentOfToolsNode { + isResumeTarget, _, _ := GetResumeContext[any](ctx) + c.mu.Lock() + c.isResumeTargetLog = append(c.isResumeTargetLog, isResumeTarget) + c.mu.Unlock() + } + return ctx +} + +func (c *toolsNodeResumeTargetCallback) OnEnd(ctx context.Context, _ *callbacks.RunInfo, _ callbacks.CallbackOutput) context.Context { + return ctx +} + +func (c *toolsNodeResumeTargetCallback) OnError(ctx context.Context, _ *callbacks.RunInfo, _ error) context.Context { + return ctx +} + +func (c *toolsNodeResumeTargetCallback) OnStartWithStreamInput(ctx context.Context, _ *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + input.Close() + return ctx +} + +func (c *toolsNodeResumeTargetCallback) OnEndWithStreamOutput(ctx context.Context, _ *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context { + output.Close() + return ctx +} + +// mockReentryTool is a helper for the reentry test +type mockReentryTool struct { + t *testing.T + mu sync.Mutex + isResumeTargetByRunID map[string]bool +} + +func (t *mockReentryTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "reentry_tool", + Desc: "A tool that can be re-entered in a resumed graph.", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{"input": {Type: schema.String}}), + }, nil +} + +func (t *mockReentryTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + wasInterrupted, hasState, _ := tool.GetInterruptState[any](ctx) + isResume, hasData, data := tool.GetResumeContext[*myResumeData](ctx) + + callID := GetToolCallID(ctx) + + t.mu.Lock() + if t.isResumeTargetByRunID != nil { + t.isResumeTargetByRunID[callID] = isResume + } + t.mu.Unlock() + + // Special handling for the re-entrant call to make assertions explicit. + if callID == "call_3" { + if !isResume { + // This is the first run of the re-entrant call. Its context must be clean. + // This is the core assertion for this test. + assert.False(t.t, wasInterrupted, "re-entrant call 'call_3' should not have been interrupted on its first run") + assert.False(t.t, hasState, "re-entrant call 'call_3' should not have state on its first run") + // Now, interrupt it as part of the test flow. + return "", tool.StatefulInterrupt(ctx, nil, "some state for "+callID) + } + // This is the resumed run of the re-entrant call. + assert.True(t.t, wasInterrupted, "resumed call 'call_3' must have been interrupted") + assert.True(t.t, hasData, "resumed call 'call_3' should have data") + return "Resumed " + data.Message, nil + } + + // Standard logic for the initial calls (call_1, call_2) + if !wasInterrupted { + // First run for call_1 and call_2, should interrupt. + return "", tool.StatefulInterrupt(ctx, nil, "some state for "+callID) + } + + // From here, wasInterrupted is true for call_1 and call_2. + if isResume { + // The user is explicitly resuming this call. + assert.True(t.t, hasData, "call %s should have resume data", callID) + return "Resumed " + data.Message, nil + } + + // The tool was interrupted before, but is not being resumed now. Re-interrupt. + return "", tool.StatefulInterrupt(ctx, nil, "some state for "+callID) +} + +func TestReentryForResumedTools(t *testing.T) { + // create a 'ReAct' style graph with a ChatModel node and a ToolsNode. + // within the ToolsNode there is an interruptible tool that will emit interrupt on first run. + // During the first invocation of the graph, there should be two tool calls (of the same tool) that interrupt. + // The user chooses to resume one of the interrupted tool call in second invocation, + // and this time, the resumed tool call should be successful, while the other should interrupt immediately again. + // The user then chooses to resume the other interrupted tool call in third invocation, + // and this time, the ChatModel decides to call the tool again, + // and this time the tool's runCtx should think it was not interrupted nor resumed. + ctrl := gomock.NewController(t) + + // 1. Define the interrupting tool and callback + reentryTool := &mockReentryTool{t: t, isResumeTargetByRunID: make(map[string]bool)} + toolsNodeCB := &toolsNodeResumeTargetCallback{} + + // 2. Define the graph + g := NewGraph[[]*schema.Message, *schema.Message]() + + // Mock Chat Model that drives the ReAct loop + mockChatModel := mockModel.NewMockToolCallingChatModel(ctrl) + toolsNode, err := NewToolNode(context.Background(), &ToolsNodeConfig{Tools: []tool.BaseTool{reentryTool}}) + assert.NoError(t, err) + + // Expectation for the 1st invocation: model returns two tool calls + mockChatModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).Return(&schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "reentry_tool", Arguments: `{"input": "a"}`}}, + {ID: "call_2", Function: schema.FunctionCall{Name: "reentry_tool", Arguments: `{"input": "b"}`}}, + }, + }, nil).Times(1) + + // Expectation for the 2nd invocation (after resuming call_1): model does nothing, graph continues + // Expectation for the 3rd invocation (after resuming call_2): model calls the tool again + mockChatModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, msgs []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_3", Function: schema.FunctionCall{Name: "reentry_tool", Arguments: `{"input": "c"}`}}, + }, + }, nil + }).Times(1) + + // Expectation for the final invocation: model returns final answer + mockChatModel.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).Return(&schema.Message{ + Role: schema.Assistant, + Content: "all done", + }, nil).Times(1) + + _ = g.AddChatModelNode("model", mockChatModel) + _ = g.AddToolsNode("tools", toolsNode) + _ = g.AddEdge(START, "model") + + // Add the crucial branch to decide whether to call tools or end. + modelBranch := func(ctx context.Context, msg *schema.Message) (string, error) { + if len(msg.ToolCalls) > 0 { + return "tools", nil + } + return END, nil + } + err = g.AddBranch("model", NewGraphBranch(modelBranch, map[string]bool{"tools": true, END: true})) + assert.NoError(t, err) + + _ = g.AddEdge("tools", "model") // Loop back for ReAct style + + // 3. Compile and run + graph, err := g.Compile(context.Background(), WithCheckPointStore(newInMemoryStore()), + WithGraphName("root")) + assert.NoError(t, err) + checkPointID := "reentry-test" + + // --- 1. First invocation: call_1 and call_2 should interrupt --- + _, err = graph.Invoke(context.Background(), []*schema.Message{schema.UserMessage("start")}, WithCheckPointID(checkPointID), WithCallbacks(toolsNodeCB)) + assert.Error(t, err) + interruptInfo1, _ := ExtractInterruptInfo(err) + interrupts1 := interruptInfo1.InterruptContexts + assert.Len(t, interrupts1, 2) // Only the two tool calls + found1 := make(map[string]bool) + addrToID1 := make(map[string]string) + for _, iCtx := range interrupts1 { + addrStr := iCtx.Address.String() + found1[addrStr] = true + addrToID1[addrStr] = iCtx.ID + assert.True(t, iCtx.IsRootCause) + assert.NotNil(t, iCtx.Parent) + assert.Equal(t, "runnable:root;node:tools", iCtx.Parent.Address.String()) + } + assert.True(t, found1["runnable:root;node:tools;tool:reentry_tool:call_1"]) + assert.True(t, found1["runnable:root;node:tools;tool:reentry_tool:call_2"]) + + // First invocation: neither call_1 nor call_2 should be resume targets + assert.False(t, reentryTool.isResumeTargetByRunID["call_1"], "first run: call_1 should not be resume target") + assert.False(t, reentryTool.isResumeTargetByRunID["call_2"], "first run: call_2 should not be resume target") + + // First invocation: ToolsNode should NOT be a resume target + assert.Len(t, toolsNodeCB.isResumeTargetLog, 1, "ToolsNode OnStart should be called once in first invocation") + assert.False(t, toolsNodeCB.isResumeTargetLog[0], "first run: ToolsNode should NOT be resume target") + + // Clear for next invocation + reentryTool.isResumeTargetByRunID = make(map[string]bool) + toolsNodeCB.isResumeTargetLog = nil + + // --- 2. Second invocation: resume call_1, expect call_2 to interrupt again --- + resumeCtx2 := ResumeWithData(context.Background(), addrToID1["runnable:root;node:tools;tool:reentry_tool:call_1"], + &myResumeData{Message: "resume call 1"}) + _, err = graph.Invoke(resumeCtx2, []*schema.Message{schema.UserMessage("start")}, WithCheckPointID(checkPointID), WithCallbacks(toolsNodeCB)) + assert.Error(t, err) + interruptInfo2, _ := ExtractInterruptInfo(err) + interrupts2 := interruptInfo2.InterruptContexts + assert.Len(t, interrupts2, 1) // Only call_2 + rootCause2 := interrupts2[0] + assert.Equal(t, "runnable:root;node:tools;tool:reentry_tool:call_2", rootCause2.Address.String()) + assert.NotNil(t, rootCause2.Parent) + assert.Equal(t, "runnable:root;node:tools", rootCause2.Parent.Address.String()) + + // Second invocation: call_1 is resumed, call_2 is NOT resumed (re-interrupts) + assert.True(t, reentryTool.isResumeTargetByRunID["call_1"], "second run: call_1 should be resume target") + assert.False(t, reentryTool.isResumeTargetByRunID["call_2"], "second run: call_2 should NOT be resume target (it re-interrupts)") + + // Second invocation: ToolsNode SHOULD be a resume target (because call_1 child is being resumed) + assert.Len(t, toolsNodeCB.isResumeTargetLog, 1, "ToolsNode OnStart should be called once in second invocation") + assert.True(t, toolsNodeCB.isResumeTargetLog[0], "second run: ToolsNode SHOULD be resume target (child call_1 is being resumed)") + + // Clear for next invocation + reentryTool.isResumeTargetByRunID = make(map[string]bool) + toolsNodeCB.isResumeTargetLog = nil + + // --- 3. Third invocation: resume call_2, model makes a new call (call_3) which should interrupt --- + resumeCtx3 := ResumeWithData(context.Background(), rootCause2.ID, &myResumeData{Message: "resume call 2"}) + _, err = graph.Invoke(resumeCtx3, []*schema.Message{schema.UserMessage("start")}, WithCheckPointID(checkPointID), WithCallbacks(toolsNodeCB)) + assert.Error(t, err) + interruptInfo3, _ := ExtractInterruptInfo(err) + interrupts3 := interruptInfo3.InterruptContexts + assert.Len(t, interrupts3, 1) // Only call_3 + rootCause3 := interrupts3[0] + assert.Equal(t, "runnable:root;node:tools;tool:reentry_tool:call_3", rootCause3.Address.String()) // Note: this is the new call_3 + assert.NotNil(t, rootCause3.Parent) + assert.Equal(t, "runnable:root;node:tools", rootCause3.Parent.Address.String()) + + // Third invocation: call_2 is resumed, call_3 is new (not resumed) + assert.True(t, reentryTool.isResumeTargetByRunID["call_2"], "third run: call_2 should be resume target") + assert.False(t, reentryTool.isResumeTargetByRunID["call_3"], "third run: call_3 should NOT be resume target (it's new)") + + // Third invocation: ToolsNode is called twice (once for call_2 resume, once for call_3 new) + // First call: ToolsNode SHOULD be resume target (call_2 is being resumed) + // Second call: ToolsNode should NOT be resume target (call_3 is new, no children to resume) + assert.Len(t, toolsNodeCB.isResumeTargetLog, 2, "ToolsNode OnStart should be called twice in third invocation") + assert.True(t, toolsNodeCB.isResumeTargetLog[0], "third run first ToolsNode call: SHOULD be resume target (child call_2 is being resumed)") + assert.False(t, toolsNodeCB.isResumeTargetLog[1], "third run second ToolsNode call: should NOT be resume target (call_3 is new)") + + // Clear for next invocation + reentryTool.isResumeTargetByRunID = make(map[string]bool) + toolsNodeCB.isResumeTargetLog = nil + + // --- 4. Final invocation: resume call_3, expect final answer --- + resumeCtx4 := ResumeWithData(context.Background(), rootCause3.ID, + &myResumeData{Message: "resume call 3"}) + output, err := graph.Invoke(resumeCtx4, []*schema.Message{schema.UserMessage("start")}, WithCheckPointID(checkPointID), WithCallbacks(toolsNodeCB)) + assert.NoError(t, err) + assert.Equal(t, "all done", output.Content) + + // Fourth invocation: call_3 is resumed + assert.True(t, reentryTool.isResumeTargetByRunID["call_3"], "fourth run: call_3 should be resume target") + + // Fourth invocation: ToolsNode SHOULD be resume target (call_3 is being resumed) + assert.Len(t, toolsNodeCB.isResumeTargetLog, 1, "ToolsNode OnStart should be called once in fourth invocation") + assert.True(t, toolsNodeCB.isResumeTargetLog[0], "fourth run: ToolsNode SHOULD be resume target (child call_3 is being resumed)") +} + +// mockInterruptingTool is a helper for the nested tool interrupt test +type mockInterruptingTool struct { + tt *testing.T +} + +func (t *mockInterruptingTool) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "interrupt_tool", + Desc: "A tool that interrupts execution.", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "input": {Type: schema.String, Desc: "Some input", Required: true}, + }), + }, nil +} + +func (t *mockInterruptingTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + var args map[string]string + _ = json.Unmarshal([]byte(argumentsInJSON), &args) + + wasInterrupted, hasState, state := tool.GetInterruptState[*myInterruptState](ctx) + if !wasInterrupted { + // First run: interrupt + return "", tool.StatefulInterrupt(ctx, + map[string]any{"reason": "tool maintenance"}, + &myInterruptState{OriginalInput: args["input"]}, + ) + } + + // Second (resumed) run + assert.True(t.tt, hasState) + assert.Equal(t.tt, "test", state.OriginalInput) + + isResume, hasData, data := tool.GetResumeContext[*myResumeData](ctx) + assert.True(t.tt, isResume) + assert.True(t.tt, hasData) + assert.Equal(t.tt, "let's continue tool", data.Message) + + return "Tool resumed successfully", nil +} + +func TestGraphInterruptWithinLambda(t *testing.T) { + // this test case aims to verify behaviors when a standalone graph is within a lambda, + // which in turn is within the root graph. + // the expected behavior is: + // - internal graph will naturally append to the Address + // - internal graph interrupts, where the Address includes steps for both the root graph and the internal graph + // - lambda extracts InterruptInfo, then GetInterruptContexts + // - lambda then acts as a composite node, uses CompositeInterrupt to pass up the + // internal interrupt points + // - the root graph interrupts + // - end-user extracts the interrupt ID and related info + // - end-user uses ResumeWithData to resume the ID + // - lambda node resumes, invokes the inner graph as usual + // - the internal graph resumes the interrupted node + // To implement this test, within the internal graph you can define another lambda node that can interrupt resume. + + // 1. Define the innermost lambda that actually interrupts + interruptingLambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + wasInterrupted, hasState, state := GetInterruptState[*myInterruptState](ctx) + if !wasInterrupted { + return "", StatefulInterrupt(ctx, "inner interrupt", &myInterruptState{OriginalInput: input}) + } + + assert.True(t, hasState) + assert.Equal(t, "top level input", state.OriginalInput) + + isResume, hasData, data := GetResumeContext[*myResumeData](ctx) + assert.True(t, isResume) + assert.True(t, hasData) + assert.Equal(t, "resume inner", data.Message) + + return "inner lambda resumed successfully", nil + }) + + // 2. Define the internal graph that contains the interrupting lambda + innerGraph := NewGraph[string, string]() + _ = innerGraph.AddLambdaNode("inner_lambda", interruptingLambda) + _ = innerGraph.AddEdge(START, "inner_lambda") + _ = innerGraph.AddEdge("inner_lambda", END) + // Give the inner graph a name so it can create its "runnable" addr step. + compiledInnerGraph, err := innerGraph.Compile(context.Background(), WithGraphName("inner"), WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + // 3. Define the outer lambda that acts as a composite node + compositeLambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + // The lambda invokes the inner graph. If the inner graph interrupts, this lambda + // must act as a proper composite node and wrap the error. + output, err := compiledInnerGraph.Invoke(ctx, input, WithCheckPointID("inner-cp")) + if err != nil { + _, isInterrupt := ExtractInterruptInfo(err) + if !isInterrupt { + return "", err // Not an interrupt, just fail + } + + // The composite interrupt itself can be stateless, as it's just a wrapper. + // It signals to the framework to look inside the subErrs and correctly + // prepend the current addr to the paths of the inner interrupts. + return "", CompositeInterrupt(ctx, "composite interrupt from lambda", nil, err) + } + return output, nil + }) + + // 4. Define the root graph + rootGraph := NewGraph[string, string]() + _ = rootGraph.AddLambdaNode("composite_lambda", compositeLambda) + _ = rootGraph.AddEdge(START, "composite_lambda") + _ = rootGraph.AddEdge("composite_lambda", END) + // Give the root graph a name for its "runnable" addr step. + compiledRootGraph, err := rootGraph.Compile(context.Background(), WithGraphName("root"), WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + // 5. First invocation - should interrupt + checkPointID := "graph-in-lambda-test" + _, err = compiledRootGraph.Invoke(context.Background(), "top level input", WithCheckPointID(checkPointID)) + + // 6. Verify the interrupt + assert.Error(t, err) + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + interruptContexts := interruptInfo.InterruptContexts + assert.Len(t, interruptContexts, 1) // Only the root cause is returned + + // The addr is now fully qualified, including the runnable steps from both graphs. + rootCause := interruptContexts[0] + expectedPath := "runnable:root;node:composite_lambda;runnable:inner;node:inner_lambda" + assert.Equal(t, expectedPath, rootCause.Address.String()) + assert.Equal(t, "inner interrupt", rootCause.Info) + assert.True(t, rootCause.IsRootCause) + + // Check parent hierarchy + assert.NotNil(t, rootCause.Parent) + assert.Equal(t, "runnable:root;node:composite_lambda;runnable:inner", rootCause.Parent.Address.String()) + assert.Nil(t, rootCause.Parent.Info) // The inner runnable doesn't have its own info + assert.False(t, rootCause.Parent.IsRootCause) + + // Check grandparent + assert.NotNil(t, rootCause.Parent.Parent) + assert.Equal(t, "runnable:root;node:composite_lambda", rootCause.Parent.Parent.Address.String()) + assert.Equal(t, "composite interrupt from lambda", rootCause.Parent.Parent.Info) + assert.False(t, rootCause.Parent.Parent.IsRootCause) + + // 7. Resume execution using the complete, fully-qualified ID + resumeCtx := ResumeWithData(context.Background(), rootCause.ID, &myResumeData{Message: "resume inner"}) + finalOutput, err := compiledRootGraph.Invoke(resumeCtx, "top level input", WithCheckPointID(checkPointID)) + + // 8. Verify final result + assert.NoError(t, err) + assert.Equal(t, "inner lambda resumed successfully", finalOutput) +} + +func TestLegacyInterrupt(t *testing.T) { + // this test case aims to test the behavior of the deprecated InterruptAndRerun, + // NewInterruptAndRerunErr within CompositeInterrupt. + // Define two sub-processes(functions), one interrupts with InterruptAndRerun, + // the other interrupts with NewInterruptAndRerunErr. + // create a lambda as a composite node, within the lambda invokes the two sub-processes. + // create the graph, add lambda node and invoke it. + // after verifying the interrupt points, just invokes again without explicit resume. + // verify the same interrupt IDs again. + // then finally Resume() the graph. + + // 1. Define the sub-processes that use legacy and modern interrupts + subProcess1 := func(ctx context.Context) (string, error) { + isResume, _, data := GetResumeContext[string](ctx) + if isResume { + return data, nil + } + return "", deprecatedInterruptAndRerun + } + subProcess2 := func(ctx context.Context) (string, error) { + isResume, _, data := GetResumeContext[string](ctx) + if isResume { + return data, nil + } + return "", deprecatedInterruptAndRerunErr("legacy info") + } + subProcess3 := func(ctx context.Context) (string, error) { + isResume, _, data := GetResumeContext[string](ctx) + if isResume { + return data, nil + } + // Use the modern, addr-aware interrupt function + return "", Interrupt(ctx, "modern info") + } + + // 2. Define the composite lambda + compositeLambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + // If the lambda itself is being resumed, it means the whole process is done. + isResume, _, data := GetResumeContext[string](ctx) + + // Run sub-processes and collect their errors + var ( + errs []error + outStr string + ) + + const PathStepCustom AddressSegmentType = "custom" + subCtx1 := AppendAddressSegment(ctx, PathStepCustom, "1") + out1, err1 := subProcess1(subCtx1) + if err1 != nil { + // Wrap the legacy error to give it a addr + wrappedErr := WrapInterruptAndRerunIfNeeded(ctx, AddressSegment{Type: PathStepCustom, ID: "1"}, err1) + errs = append(errs, wrappedErr) + } else { + outStr += out1 + } + subCtx2 := AppendAddressSegment(ctx, PathStepCustom, "2") + out2, err2 := subProcess2(subCtx2) + if err2 != nil { + // Wrap the legacy error to give it a addr + wrappedErr := WrapInterruptAndRerunIfNeeded(ctx, AddressSegment{Type: PathStepCustom, ID: "2"}, err2) + errs = append(errs, wrappedErr) + } else { + outStr += out2 + } + subCtx3 := AppendAddressSegment(ctx, PathStepCustom, "3") + out3, err3 := subProcess3(subCtx3) + if err3 != nil { + // The error from Interrupt() is already addr-aware. WrapInterruptAndRerunIfNeeded + // should handle this gracefully and return the error as-is. + wrappedErr := WrapInterruptAndRerunIfNeeded(ctx, AddressSegment{Type: PathStepCustom, ID: "3"}, err3) + errs = append(errs, wrappedErr) + } else { + outStr += out3 + } + + if len(errs) > 0 { + // Return a composite interrupt containing the wrapped legacy errors + return "", CompositeInterrupt(ctx, "legacy composite", nil, errs...) + } + + if isResume { + outStr = outStr + " " + data + } + + return outStr, nil + }) + + // 3. Create and compile the graph + rootGraph := NewGraph[string, string]() + _ = rootGraph.AddLambdaNode("legacy_composite", compositeLambda) + _ = rootGraph.AddEdge(START, "legacy_composite") + _ = rootGraph.AddEdge("legacy_composite", END) + compiledGraph, err := rootGraph.Compile(context.Background(), WithGraphName("root"), WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + // 4. First invocation - should interrupt + checkPointID := "legacy-interrupt-test" + _, err = compiledGraph.Invoke(context.Background(), "input", WithCheckPointID(checkPointID)) + + // 5. Verify the three interrupt points + assert.Error(t, err) + info, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + assert.Len(t, info.InterruptContexts, 3) // Only the 3 root causes + + found := make(map[string]any) + addrToID := make(map[string]string) + var parentCtx *InterruptCtx + for _, iCtx := range info.InterruptContexts { + addrStr := iCtx.Address.String() + found[addrStr] = iCtx.Info + addrToID[addrStr] = iCtx.ID + assert.True(t, iCtx.IsRootCause) + // Check parent + assert.NotNil(t, iCtx.Parent) + if parentCtx == nil { + parentCtx = iCtx.Parent + assert.Equal(t, "runnable:root;node:legacy_composite", parentCtx.Address.String()) + assert.Equal(t, "legacy composite", parentCtx.Info) + assert.False(t, parentCtx.IsRootCause) + } else { + assert.Same(t, parentCtx, iCtx.Parent) + } + } + expectedID1 := "runnable:root;node:legacy_composite;custom:1" + expectedID2 := "runnable:root;node:legacy_composite;custom:2" + expectedID3 := "runnable:root;node:legacy_composite;custom:3" + assert.Contains(t, found, expectedID1) + assert.Nil(t, found[expectedID1]) // From InterruptAndRerun + assert.Contains(t, found, expectedID2) + assert.Equal(t, "legacy info", found[expectedID2]) // From NewInterruptAndRerunErr + assert.Contains(t, found, expectedID3) + assert.Equal(t, "modern info", found[expectedID3]) // From Interrupt + + // 6. Second invocation (re-run without resume) - should yield the same interrupts + _, err = compiledGraph.Invoke(context.Background(), "input", WithCheckPointID(checkPointID)) + assert.Error(t, err) + info2, isInterrupt2 := ExtractInterruptInfo(err) + assert.True(t, isInterrupt2) + assert.Len(t, info2.InterruptContexts, 3, "Should have the same number of interrupts on re-run") + + // 7. Third invocation - Resume all three interrupt points with specific data + resumeData := map[string]any{ + addrToID[expectedID1]: "output1", + addrToID[expectedID2]: "output2", + addrToID[expectedID3]: "output3", + } + resumeCtx := BatchResumeWithData(context.Background(), resumeData) + // TODO: The legacy interrupt wrapping does not currently work correctly with BatchResumeWithData. + // The graph re-interrupts instead of completing. This should be fixed in the core framework. + _, err = compiledGraph.Invoke(resumeCtx, "input", WithCheckPointID(checkPointID)) + assert.Error(t, err) +} + +type wrapperToolForTest struct { + compiledGraph Runnable[string, string] + isResumeTargetLog []bool +} + +func (w *wrapperToolForTest) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "wrapperTool", + Desc: "A tool that wraps a nested graph", + }, nil +} + +func (w *wrapperToolForTest) InvokableRun(ctx context.Context, input string, opts ...tool.Option) (string, error) { + isResumeTarget, _, _ := tool.GetResumeContext[any](ctx) + w.isResumeTargetLog = append(w.isResumeTargetLog, isResumeTarget) + + result, err := w.compiledGraph.Invoke(ctx, input) + if err != nil { + if _, ok := ExtractInterruptInfo(err); ok { + return "", tool.CompositeInterrupt(ctx, "wrapper tool interrupt", nil, err) + } + return "", err + } + return result, nil +} + +func TestToolCompositeInterruptWithNestedGraphInterrupt(t *testing.T) { + ctx := context.Background() + + var innerNodeIsResumeTarget bool + subSubGraph := NewGraph[string, string]() + err := subSubGraph.AddLambdaNode("interruptNode", InvokableLambda(func(ctx context.Context, input string) (string, error) { + wasInterrupted, _, _ := GetInterruptState[any](ctx) + if !wasInterrupted { + return "", Interrupt(ctx, "sub-sub graph interrupt info") + } + isResumeTarget, _, _ := GetResumeContext[any](ctx) + innerNodeIsResumeTarget = isResumeTarget + return "resumed successfully", nil + })) + assert.NoError(t, err) + assert.NoError(t, subSubGraph.AddEdge(START, "interruptNode")) + assert.NoError(t, subSubGraph.AddEdge("interruptNode", END)) + + nestedGraph := NewGraph[string, string]() + err = nestedGraph.AddGraphNode("subSubGraph", subSubGraph) + assert.NoError(t, err) + assert.NoError(t, nestedGraph.AddEdge(START, "subSubGraph")) + assert.NoError(t, nestedGraph.AddEdge("subSubGraph", END)) + compiledNestedGraph, err := nestedGraph.Compile(ctx) + assert.NoError(t, err) + + wrapperTool := &wrapperToolForTest{compiledGraph: compiledNestedGraph.(Runnable[string, string])} + + toolsNode, err := NewToolNode(ctx, &ToolsNodeConfig{Tools: []tool.BaseTool{wrapperTool}}) + assert.NoError(t, err) + + outerGraph := NewGraph[*schema.Message, []*schema.Message]() + err = outerGraph.AddToolsNode("tools", toolsNode) + assert.NoError(t, err) + assert.NoError(t, outerGraph.AddEdge(START, "tools")) + assert.NoError(t, outerGraph.AddEdge("tools", END)) + + compiledOuterGraph, err := outerGraph.Compile(ctx, WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + checkpointID := "test-wrapper-tool-resume" + inputMsg := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + {ID: "call_1", Function: schema.FunctionCall{Name: "wrapperTool", Arguments: `"test"`}}, + }, + } + + _, err = compiledOuterGraph.Invoke(ctx, inputMsg, WithCheckPointID(checkpointID)) + assert.Error(t, err) + + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok, "should be an interrupt error") + assert.NotNil(t, info) + assert.NotEmpty(t, info.InterruptContexts) + + rootCause := info.InterruptContexts[0] + assert.Equal(t, "sub-sub graph interrupt info", rootCause.Info) + assert.True(t, rootCause.IsRootCause) + + var wrapperToolParent *InterruptCtx + for p := rootCause.Parent; p != nil; p = p.Parent { + if p.Info == "wrapper tool interrupt" { + wrapperToolParent = p + break + } + } + assert.NotNil(t, wrapperToolParent, "should have parent from wrapper tool with info 'wrapper tool interrupt'") + + assert.Len(t, wrapperTool.isResumeTargetLog, 1) + assert.False(t, wrapperTool.isResumeTargetLog[0], "first invocation: wrapper tool should not be resume target") + + resumeCtx := Resume(ctx, rootCause.ID) + _, err = compiledOuterGraph.Invoke(resumeCtx, inputMsg, WithCheckPointID(checkpointID)) + assert.NoError(t, err) + + assert.True(t, innerNodeIsResumeTarget, "inner node should be resume target") + + assert.Len(t, wrapperTool.isResumeTargetLog, 2) + assert.True(t, wrapperTool.isResumeTargetLog[1], "second invocation: wrapper tool should be resume target because its child is targeted") +} diff --git a/compose/runnable.go b/compose/runnable.go new file mode 100644 index 0000000..731484b --- /dev/null +++ b/compose/runnable.go @@ -0,0 +1,539 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +// Runnable is the interface for an executable object. Graph, Chain can be compiled into Runnable. +// runnable is the core conception of eino, we do downgrade compatibility for four data flow patterns, +// and can automatically connect components that only implement one or more methods. +// eg, if a component only implements Stream() method, you can still call Invoke() to convert stream output to invoke output. +type Runnable[I, O any] interface { + Invoke(ctx context.Context, input I, opts ...Option) (output O, err error) + Stream(ctx context.Context, input I, opts ...Option) (output *schema.StreamReader[O], err error) + Collect(ctx context.Context, input *schema.StreamReader[I], opts ...Option) (output O, err error) + Transform(ctx context.Context, input *schema.StreamReader[I], opts ...Option) (output *schema.StreamReader[O], err error) +} + +type invoke func(ctx context.Context, input any, opts ...any) (output any, err error) +type transform func(ctx context.Context, input streamReader, opts ...any) (output streamReader, err error) + +// composableRunnable the wrapper for all executable object directly provided by the user. +// one instance corresponds to one instance of the executable object. +// all information comes from executable object without any other dimensions of information. +// for the graphNode, ChainBranch, StatePreHandler, StatePostHandler etc. +type composableRunnable struct { + i invoke + t transform + + inputType reflect.Type + outputType reflect.Type + optionType reflect.Type + + *genericHelper + + isPassthrough bool + + meta *executorMeta + + // only available when in Graph node + // if composableRunnable not in Graph node, this field would be nil + nodeInfo *nodeInfo +} + +func runnableLambda[I, O, TOption any](i Invoke[I, O, TOption], s Stream[I, O, TOption], c Collect[I, O, TOption], + t Transform[I, O, TOption], enableCallback bool) *composableRunnable { + rp := newRunnablePacker(i, s, c, t, enableCallback) + + return rp.toComposableRunnable() +} + +func isNilAssignableType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return true + default: + return false + } +} + +type runnablePacker[I, O, TOption any] struct { + i Invoke[I, O, TOption] + s Stream[I, O, TOption] + c Collect[I, O, TOption] + t Transform[I, O, TOption] +} + +func (rp *runnablePacker[I, O, TOption]) wrapRunnableCtx(ctxWrapper func(ctx context.Context, opts ...TOption) context.Context) { + i, s, c, t := rp.i, rp.s, rp.c, rp.t + rp.i = func(ctx context.Context, input I, opts ...TOption) (output O, err error) { + ctx = ctxWrapper(ctx, opts...) + return i(ctx, input, opts...) + } + rp.s = func(ctx context.Context, input I, opts ...TOption) (output *schema.StreamReader[O], err error) { + ctx = ctxWrapper(ctx, opts...) + return s(ctx, input, opts...) + } + rp.c = func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (output O, err error) { + ctx = ctxWrapper(ctx, opts...) + return c(ctx, input, opts...) + } + + rp.t = func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (output *schema.StreamReader[O], err error) { + ctx = ctxWrapper(ctx, opts...) + return t(ctx, input, opts...) + } +} + +func (rp *runnablePacker[I, O, TOption]) toComposableRunnable() *composableRunnable { + inputType := generic.TypeOf[I]() + outputType := generic.TypeOf[O]() + optionType := generic.TypeOf[TOption]() + c := &composableRunnable{ + genericHelper: newGenericHelper[I, O](), + inputType: inputType, + outputType: outputType, + optionType: optionType, + } + + i := func(ctx context.Context, input any, opts ...any) (output any, err error) { + in, ok := input.(I) + if !ok { + // When a nil is passed as an 'any' type, its original type information is lost, + // becoming an untyped nil. This would cause type assertions to fail for nil-able + // input types, so recreate the zero value of I when nil is valid for I. + if input == nil && isNilAssignableType(reflect.TypeOf((*I)(nil)).Elem()) { + var i I + in = i + } else { + panic(newUnexpectedInputTypeErr(inputType, reflect.TypeOf(input))) + } + } + + tos, err := convertOption[TOption](opts...) + if err != nil { + return nil, err + } + return rp.Invoke(ctx, in, tos...) + } + + t := func(ctx context.Context, input streamReader, opts ...any) (output streamReader, err error) { + in, ok := unpackStreamReader[I](input) + if !ok { + panic(newUnexpectedInputTypeErr(reflect.TypeOf(in), input.getType())) + } + + tos, err := convertOption[TOption](opts...) + if err != nil { + return nil, err + } + + out, err := rp.Transform(ctx, in, tos...) + if err != nil { + return nil, err + } + + return packStreamReader(out), nil + } + + c.i = i + c.t = t + + return c +} + +// Invoke works like `ping => pong`. +func (rp *runnablePacker[I, O, TOption]) Invoke(ctx context.Context, + input I, opts ...TOption) (output O, err error) { + return rp.i(ctx, input, opts...) +} + +// Stream works like `ping => stream output`. +func (rp *runnablePacker[I, O, TOption]) Stream(ctx context.Context, + input I, opts ...TOption) (output *schema.StreamReader[O], err error) { + + return rp.s(ctx, input, opts...) +} + +// Collect works like `stream input => pong`. +func (rp *runnablePacker[I, O, TOption]) Collect(ctx context.Context, + input *schema.StreamReader[I], opts ...TOption) (output O, err error) { + return rp.c(ctx, input, opts...) +} + +// Transform works like `stream input => stream output`. +func (rp *runnablePacker[I, O, TOption]) Transform(ctx context.Context, + input *schema.StreamReader[I], opts ...TOption) (output *schema.StreamReader[O], err error) { + return rp.t(ctx, input, opts...) +} + +func defaultImplConcatStreamReader[T any]( + sr *schema.StreamReader[T]) (T, error) { + + c, err := concatStreamReader(sr) + if err != nil { + var t T + return t, fmt.Errorf("concat stream reader fail: %w", err) + } + + return c, nil +} + +func invokeByStream[I, O, TOption any](s Stream[I, O, TOption]) Invoke[I, O, TOption] { + return func(ctx context.Context, input I, opts ...TOption) (output O, err error) { + sr, err := s(ctx, input, opts...) + if err != nil { + return output, err + } + + return defaultImplConcatStreamReader(sr) + } +} + +func invokeByCollect[I, O, TOption any](c Collect[I, O, TOption]) Invoke[I, O, TOption] { + return func(ctx context.Context, input I, opts ...TOption) (output O, err error) { + sr := schema.StreamReaderFromArray([]I{input}) + + return c(ctx, sr, opts...) + } +} + +func invokeByTransform[I, O, TOption any](t Transform[I, O, TOption]) Invoke[I, O, TOption] { + return func(ctx context.Context, input I, opts ...TOption) (output O, err error) { + srInput := schema.StreamReaderFromArray([]I{input}) + + srOutput, err := t(ctx, srInput, opts...) + if err != nil { + return output, err + } + + return defaultImplConcatStreamReader(srOutput) + } +} + +func streamByTransform[I, O, TOption any](t Transform[I, O, TOption]) Stream[I, O, TOption] { + return func(ctx context.Context, input I, opts ...TOption) (output *schema.StreamReader[O], err error) { + srInput := schema.StreamReaderFromArray([]I{input}) + + return t(ctx, srInput, opts...) + } +} + +func streamByInvoke[I, O, TOption any](i Invoke[I, O, TOption]) Stream[I, O, TOption] { + return func(ctx context.Context, input I, opts ...TOption) (output *schema.StreamReader[O], err error) { + out, err := i(ctx, input, opts...) + if err != nil { + return nil, err + } + + return schema.StreamReaderFromArray([]O{out}), nil + } +} + +func streamByCollect[I, O, TOption any](c Collect[I, O, TOption]) Stream[I, O, TOption] { + return func(ctx context.Context, input I, opts ...TOption) (output *schema.StreamReader[O], err error) { + srInput := schema.StreamReaderFromArray([]I{input}) + out, err := c(ctx, srInput, opts...) + if err != nil { + return nil, err + } + + return schema.StreamReaderFromArray([]O{out}), nil + } +} + +func collectByTransform[I, O, TOption any](t Transform[I, O, TOption]) Collect[I, O, TOption] { + return func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (output O, err error) { + srOutput, err := t(ctx, input, opts...) + if err != nil { + return output, err + } + + return defaultImplConcatStreamReader(srOutput) + } +} + +func collectByInvoke[I, O, TOption any](i Invoke[I, O, TOption]) Collect[I, O, TOption] { + return func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (output O, err error) { + in, err := defaultImplConcatStreamReader(input) + if err != nil { + return output, err + } + + return i(ctx, in, opts...) + } +} + +func collectByStream[I, O, TOption any](s Stream[I, O, TOption]) Collect[I, O, TOption] { + return func(ctx context.Context, input *schema.StreamReader[I], opts ...TOption) (output O, err error) { + in, err := defaultImplConcatStreamReader(input) + if err != nil { + return output, err + } + + srOutput, err := s(ctx, in, opts...) + if err != nil { + return output, err + } + + return defaultImplConcatStreamReader(srOutput) + } +} + +func transformByStream[I, O, TOption any](s Stream[I, O, TOption]) Transform[I, O, TOption] { + return func(ctx context.Context, input *schema.StreamReader[I], + opts ...TOption) (output *schema.StreamReader[O], err error) { + in, err := defaultImplConcatStreamReader(input) + if err != nil { + return output, err + } + + return s(ctx, in, opts...) + } +} + +func transformByCollect[I, O, TOption any](c Collect[I, O, TOption]) Transform[I, O, TOption] { + return func(ctx context.Context, input *schema.StreamReader[I], + opts ...TOption) (output *schema.StreamReader[O], err error) { + out, err := c(ctx, input, opts...) + if err != nil { + return output, err + } + + return schema.StreamReaderFromArray([]O{out}), nil + } +} + +func transformByInvoke[I, O, TOption any](i Invoke[I, O, TOption]) Transform[I, O, TOption] { + return func(ctx context.Context, input *schema.StreamReader[I], + opts ...TOption) (output *schema.StreamReader[O], err error) { + in, err := defaultImplConcatStreamReader(input) + if err != nil { + return output, err + } + + out, err := i(ctx, in, opts...) + if err != nil { + return output, err + } + + return schema.StreamReaderFromArray([]O{out}), nil + } +} + +func newRunnablePacker[I, O, TOption any](i Invoke[I, O, TOption], s Stream[I, O, TOption], + c Collect[I, O, TOption], t Transform[I, O, TOption], enableCallback bool) *runnablePacker[I, O, TOption] { + + r := &runnablePacker[I, O, TOption]{} + + if enableCallback { + if i != nil { + i = invokeWithCallbacks(i) + } + + if s != nil { + s = streamWithCallbacks(s) + } + + if c != nil { + c = collectWithCallbacks(c) + } + + if t != nil { + t = transformWithCallbacks(t) + } + } + + if i != nil { + r.i = i + } else if s != nil { + r.i = invokeByStream(s) + } else if c != nil { + r.i = invokeByCollect(c) + } else { + r.i = invokeByTransform(t) + } + + if s != nil { + r.s = s + } else if t != nil { + r.s = streamByTransform(t) + } else if i != nil { + r.s = streamByInvoke(i) + } else { + r.s = streamByCollect(c) + } + + if c != nil { + r.c = c + } else if t != nil { + r.c = collectByTransform(t) + } else if i != nil { + r.c = collectByInvoke(i) + } else { + r.c = collectByStream(s) + } + + if t != nil { + r.t = t + } else if s != nil { + r.t = transformByStream(s) + } else if c != nil { + r.t = transformByCollect(c) + } else { + r.t = transformByInvoke(i) + } + + return r +} + +func toGenericRunnable[I, O any](cr *composableRunnable, ctxWrapper func(ctx context.Context, opts ...Option) context.Context) ( + *runnablePacker[I, O, Option], error) { + i := func(ctx context.Context, input I, opts ...Option) (output O, err error) { + out, err := cr.i(ctx, input, toAnyList(opts)...) + if err != nil { + return output, err + } + + to, ok := out.(O) + if !ok { + // When a nil is passed as an 'any' type, its original type information is lost, + // becoming an untyped nil. This would cause type assertions to fail. + // So if the output is nil and the target type O is an interface, we need to explicitly create a nil of type O. + if out == nil && generic.TypeOf[O]().Kind() == reflect.Interface { + var o O + to = o + } else { + panic(newUnexpectedInputTypeErr(generic.TypeOf[O](), reflect.TypeOf(out))) + } + } + return to, nil + } + + t := func(ctx context.Context, input *schema.StreamReader[I], + opts ...Option) (output *schema.StreamReader[O], err error) { + in := packStreamReader(input) + out, err := cr.t(ctx, in, toAnyList(opts)...) + + if err != nil { + return nil, err + } + + output, ok := unpackStreamReader[O](out) + if !ok { + panic("impossible") + } + + return output, nil + } + + r := newRunnablePacker(i, nil, nil, t, false) + r.wrapRunnableCtx(ctxWrapper) + + return r, nil +} + +func inputKeyedComposableRunnable(key string, r *composableRunnable) *composableRunnable { + wrapper := *r + wrapper.genericHelper = wrapper.genericHelper.forMapInput() + i := r.i + wrapper.i = func(ctx context.Context, input any, opts ...any) (output any, err error) { + v, ok := input.(map[string]any)[key] + if !ok { + return nil, fmt.Errorf("cannot find input key: %s", key) + } + out, err := i(ctx, v, opts...) + if err != nil { + return nil, err + } + + return out, nil + } + + t := r.t + wrapper.t = func(ctx context.Context, input streamReader, opts ...any) (output streamReader, err error) { + nInput, ok := r.inputStreamFilter(key, input) + if !ok { + return nil, fmt.Errorf("inputStreamFilter failed, key= %s, node name= %s, err= %w", key, r.nodeInfo.name, err) + } + out, err := t(ctx, nInput, opts...) + if err != nil { + return nil, err + } + + return out, nil + } + + wrapper.inputType = generic.TypeOf[map[string]any]() + return &wrapper +} + +func outputKeyedComposableRunnable(key string, r *composableRunnable) *composableRunnable { + wrapper := *r + wrapper.genericHelper = wrapper.genericHelper.forMapOutput() + i := r.i + wrapper.i = func(ctx context.Context, input any, opts ...any) (output any, err error) { + out, err := i(ctx, input, opts...) + if err != nil { + return nil, err + } + + return map[string]any{key: out}, nil + } + + t := r.t + wrapper.t = func(ctx context.Context, input streamReader, opts ...any) (output streamReader, err error) { + out, err := t(ctx, input, opts...) + if err != nil { + return nil, err + } + + return out.withKey(key), nil + } + + wrapper.outputType = generic.TypeOf[map[string]any]() + + return &wrapper +} + +// composablePassthrough special runnable that passthrough input to output +func composablePassthrough() *composableRunnable { + r := &composableRunnable{isPassthrough: true, nodeInfo: &nodeInfo{}} + + r.i = func(ctx context.Context, input any, opts ...any) (output any, err error) { + return input, nil + } + + r.t = func(ctx context.Context, input streamReader, opts ...any) (output streamReader, err error) { + return input, nil + } + + r.meta = &executorMeta{ + component: ComponentOfPassthrough, + isComponentCallbackEnabled: false, + componentImplType: "Passthrough", + } + + return r +} diff --git a/compose/runnable_test.go b/compose/runnable_test.go new file mode 100644 index 0000000..95744b7 --- /dev/null +++ b/compose/runnable_test.go @@ -0,0 +1,210 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + "io" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestRunnableLambda(t *testing.T) { + ctx := context.Background() + + t.Run("invoke_to_runnable", func(t *testing.T) { + rl := runnableLambda( + func(ctx context.Context, input int, opts ...Option) (output string, err error) { + return strconv.Itoa(input) + "+" + opts[0].options[0].(string), nil + }, + nil, nil, nil, false) + + ctxWrapper := func(ctx context.Context, opts ...Option) context.Context { + return ctx + } + gr, err := toGenericRunnable[int, string](rl, ctxWrapper) + assert.NoError(t, err) + out, err := gr.Invoke(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err := gr.Stream(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sri, swi := schema.Pipe[int](1) + _ = swi.Send(10, nil) + swi.Close() + sriArr := sri.Copy(2) + + out, err = gr.Collect(ctx, sriArr[0], WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err = gr.Transform(ctx, sriArr[1], WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + }) + + t.Run("stream_to_runnable", func(t *testing.T) { + rl := runnableLambda(nil, + func(ctx context.Context, input int, opts ...Option) (output *schema.StreamReader[string], err error) { + sro, swo := schema.Pipe[string](3) + _ = swo.Send(strconv.Itoa(input), nil) + _ = swo.Send("+", nil) + _ = swo.Send(opts[0].options[0].(string), nil) + swo.Close() + return sro, nil + }, nil, nil, false) + + ctxWrapper := func(ctx context.Context, opts ...Option) context.Context { + return ctx + } + gr, err := toGenericRunnable[int, string](rl, ctxWrapper) + assert.NoError(t, err) + out, err := gr.Invoke(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err := gr.Stream(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sri, swi := schema.Pipe[int](1) + _ = swi.Send(10, nil) + swi.Close() + sriArr := sri.Copy(2) + + out, err = gr.Collect(ctx, sriArr[0], WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err = gr.Transform(ctx, sriArr[1], WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + }) + + t.Run("transform_to_runnable", func(t *testing.T) { + rl := runnableLambda( + nil, nil, nil, + func(ctx context.Context, input *schema.StreamReader[int], opts ...Option) (output *schema.StreamReader[string], err error) { + + in, e := input.Recv() + if errors.Is(e, io.EOF) { + return nil, fmt.Errorf("unpected EOF") + } + input.Close() + + sro, swo := schema.Pipe[string](3) + _ = swo.Send(strconv.Itoa(in), nil) + _ = swo.Send("+", nil) + _ = swo.Send(opts[0].options[0].(string), nil) + swo.Close() + return sro, nil + }, + false) + + ctxWrapper := func(ctx context.Context, opts ...Option) context.Context { + return ctx + } + gr, err := toGenericRunnable[int, string](rl, ctxWrapper) + assert.NoError(t, err) + out, err := gr.Invoke(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err := gr.Stream(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sri, swi := schema.Pipe[int](1) + _ = swi.Send(10, nil) + swi.Close() + sriArr := sri.Copy(2) + + out, err = gr.Collect(ctx, sriArr[0], WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err = gr.Transform(ctx, sriArr[1], WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + }) + + t.Run("collect_to_runnable", func(t *testing.T) { + rl := runnableLambda(nil, nil, + func(ctx context.Context, input *schema.StreamReader[int], opts ...Option) (output string, err error) { + in, e := input.Recv() + if errors.Is(e, io.EOF) { + return "", fmt.Errorf("unpected EOF") + } + input.Close() + + return strconv.Itoa(in) + "+" + opts[0].options[0].(string), nil + }, + nil, false) + + ctxWrapper := func(ctx context.Context, opts ...Option) context.Context { + return ctx + } + + gr, err := toGenericRunnable[int, string](rl, ctxWrapper) + assert.NoError(t, err) + out, err := gr.Invoke(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err := gr.Stream(ctx, 10, WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sri, swi := schema.Pipe[int](1) + _ = swi.Send(10, nil) + swi.Close() + sriArr := sri.Copy(2) + + out, err = gr.Collect(ctx, sriArr[0], WithLambdaOption("100")) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + + sr, err = gr.Transform(ctx, sriArr[1], WithLambdaOption("100")) + assert.NoError(t, err) + out, err = concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "10+100", out) + }) +} diff --git a/compose/state.go b/compose/state.go new file mode 100644 index 0000000..f350601 --- /dev/null +++ b/compose/state.go @@ -0,0 +1,196 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + "sync" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +// GenLocalState is a function that generates the state. +type GenLocalState[S any] func(ctx context.Context) (state S) + +type stateKey struct{} + +type internalState struct { + state any + mu sync.Mutex + parent *internalState +} + +// StatePreHandler is a function called before the node is executed. +// Notice: if user called Stream but with StatePreHandler, the StatePreHandler will read all stream chunks and merge them into a single object. +type StatePreHandler[I, S any] func(ctx context.Context, in I, state S) (I, error) + +// StatePostHandler is a function called after the node is executed. +// Notice: if user called Stream but with StatePostHandler, the StatePostHandler will read all stream chunks and merge them into a single object. +type StatePostHandler[O, S any] func(ctx context.Context, out O, state S) (O, error) + +// StreamStatePreHandler is a function that is called before the node is executed with stream input and output. +type StreamStatePreHandler[I, S any] func(ctx context.Context, in *schema.StreamReader[I], state S) (*schema.StreamReader[I], error) + +// StreamStatePostHandler is a function that is called after the node is executed with stream input and output. +type StreamStatePostHandler[O, S any] func(ctx context.Context, out *schema.StreamReader[O], state S) (*schema.StreamReader[O], error) + +func convertPreHandler[I, S any](handler StatePreHandler[I, S]) *composableRunnable { + rf := func(ctx context.Context, in I, opts ...any) (I, error) { + cState, pMu, err := getState[S](ctx) + if err != nil { + return in, err + } + pMu.Lock() + defer pMu.Unlock() + + return handler(ctx, in, cState) + } + + return runnableLambda[I, I](rf, nil, nil, nil, false) +} + +func convertPostHandler[O, S any](handler StatePostHandler[O, S]) *composableRunnable { + rf := func(ctx context.Context, out O, opts ...any) (O, error) { + cState, pMu, err := getState[S](ctx) + if err != nil { + return out, err + } + pMu.Lock() + defer pMu.Unlock() + + return handler(ctx, out, cState) + } + + return runnableLambda[O, O](rf, nil, nil, nil, false) +} + +func streamConvertPreHandler[I, S any](handler StreamStatePreHandler[I, S]) *composableRunnable { + rf := func(ctx context.Context, in *schema.StreamReader[I], opts ...any) (*schema.StreamReader[I], error) { + cState, pMu, err := getState[S](ctx) + if err != nil { + return in, err + } + pMu.Lock() + defer pMu.Unlock() + + return handler(ctx, in, cState) + } + + return runnableLambda[I, I](nil, nil, nil, rf, false) +} + +func streamConvertPostHandler[O, S any](handler StreamStatePostHandler[O, S]) *composableRunnable { + rf := func(ctx context.Context, out *schema.StreamReader[O], opts ...any) (*schema.StreamReader[O], error) { + cState, pMu, err := getState[S](ctx) + if err != nil { + return out, err + } + pMu.Lock() + defer pMu.Unlock() + + return handler(ctx, out, cState) + } + + return runnableLambda[O, O](nil, nil, nil, rf, false) +} + +// ProcessState processes the state from the context in a concurrency-safe way. +// This is the recommended way to access and modify state in custom nodes. +// The provided function handler will be executed with exclusive access to the state (protected by mutex). +// +// State Lookup Behavior: +// - If the requested state type exists in the current graph, it will be returned +// - If not found in current graph, ProcessState will search in parent graph states (for nested graphs) +// - This enables nested graphs to access state from their parent graphs +// - Follows lexical scoping: inner state of the same type shadows outer state +// +// Concurrency Safety: +// - ProcessState automatically locks the mutex of the state being accessed (current or parent level) +// - Each state level has its own mutex, allowing concurrent access to different levels +// - The lock is held for the entire duration of the handler function +// +// Note: This method will report an error if the state type doesn't match or state is not found in the context chain. +// +// Example - Basic usage in a single graph: +// +// lambdaFunc := func(ctx context.Context, in string, opts ...any) (string, error) { +// err := compose.ProcessState[*MyState](ctx, func(ctx context.Context, state *MyState) error { +// // Safely modify state +// state.Count++ +// return nil +// }) +// if err != nil { +// return "", err +// } +// return in, nil +// } +// +// Example - Nested graph accessing parent state: +// +// // In an inner graph node +// innerNode := func(ctx context.Context, input string) (string, error) { +// // Access parent graph's state +// err := compose.ProcessState[*OuterState](ctx, func(ctx context.Context, s *OuterState) error { +// s.Counter++ // Safely modify parent state +// return nil +// }) +// if err != nil { +// return "", err +// } +// +// // Also access inner graph's own state +// err = compose.ProcessState[*InnerState](ctx, func(ctx context.Context, s *InnerState) error { +// s.Data = "processed" +// return nil +// }) +// return input, nil +// } +func ProcessState[S any](ctx context.Context, handler func(context.Context, S) error) error { + s, pMu, err := getState[S](ctx) + if err != nil { + return fmt.Errorf("get state from context fail: %w", err) + } + pMu.Lock() + defer pMu.Unlock() + return handler(ctx, s) +} + +func getState[S any](ctx context.Context) (S, *sync.Mutex, error) { + state := ctx.Value(stateKey{}) + + if state == nil { + var s S + return s, nil, fmt.Errorf("have not set state") + } + + interState := state.(*internalState) + + for interState != nil { + if cState, ok := interState.state.(S); ok { + return cState, &interState.mu, nil + } + interState = interState.parent + } + + var s S + return s, nil, fmt.Errorf("cannot find state with type: %v in states chain, "+ + "current state type: %v", + generic.TypeOf[S](), reflect.TypeOf(state.(*internalState).state)) +} diff --git a/compose/state_test.go b/compose/state_test.go new file mode 100644 index 0000000..247b4b6 --- /dev/null +++ b/compose/state_test.go @@ -0,0 +1,765 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "io" + "strings" + "sync" + "testing" + "time" + "unicode/utf8" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +type midStr string + +func TestStateGraphWithEdge(t *testing.T) { + + ctx := context.Background() + + const ( + nodeOfL1 = "invokable" + nodeOfL2 = "streamable" + nodeOfL3 = "transformable" + ) + + type testState struct { + ms []string + } + + gen := func(ctx context.Context) *testState { + return &testState{} + } + + sg := NewGraph[string, string](WithGenLocalState(gen)) + + l1 := InvokableLambda(func(ctx context.Context, in string) (out midStr, err error) { + return midStr("InvokableLambda: " + in), nil + }) + + l1StateToInput := func(ctx context.Context, in string, state *testState) (string, error) { + state.ms = append(state.ms, in) + return in, nil + } + + l1StateToOutput := func(ctx context.Context, out midStr, state *testState) (midStr, error) { + state.ms = append(state.ms, string(out)) + return out, nil + } + + err := sg.AddLambdaNode(nodeOfL1, l1, + WithStatePreHandler(l1StateToInput), WithStatePostHandler(l1StateToOutput)) + assert.NoError(t, err) + + l2 := StreamableLambda(func(ctx context.Context, input midStr) (output *schema.StreamReader[string], err error) { + outStr := "StreamableLambda: " + string(input) + + sr, sw := schema.Pipe[string](utf8.RuneCountInString(outStr)) + + go func() { + for _, field := range strings.Fields(outStr) { + sw.Send(field+" ", nil) + } + sw.Close() + }() + + return sr, nil + }) + + l2StateToOutput := func(ctx context.Context, out string, state *testState) (string, error) { + state.ms = append(state.ms, out) + return out, nil + } + + err = sg.AddLambdaNode(nodeOfL2, l2, WithStatePostHandler(l2StateToOutput)) + assert.NoError(t, err) + + l3 := TransformableLambda(func(ctx context.Context, input *schema.StreamReader[string]) ( + output *schema.StreamReader[string], err error) { + + prefix := "TransformableLambda: " + sr, sw := schema.Pipe[string](20) + + go func() { + for _, field := range strings.Fields(prefix) { + sw.Send(field+" ", nil) + } + defer input.Close() + + for { + chunk, err := input.Recv() + if err != nil { + if err == io.EOF { + break + } + // TODO: how to trace this kind of error in the goroutine of processing stream + sw.Send(chunk, err) + break + } + + sw.Send(chunk, nil) + + } + sw.Close() + }() + + return sr, nil + }) + + l3StateToOutput := func(ctx context.Context, out string, state *testState) (string, error) { + state.ms = append(state.ms, out) + assert.Len(t, state.ms, 4) + return out, nil + } + + err = sg.AddLambdaNode(nodeOfL3, l3, WithStatePostHandler(l3StateToOutput)) + assert.NoError(t, err) + + err = sg.AddEdge(START, nodeOfL1) + assert.NoError(t, err) + + err = sg.AddEdge(nodeOfL1, nodeOfL2) + assert.NoError(t, err) + + err = sg.AddEdge(nodeOfL2, nodeOfL3) + assert.NoError(t, err) + + err = sg.AddEdge(nodeOfL3, END) + assert.NoError(t, err) + + run, err := sg.Compile(ctx) + assert.NoError(t, err) + + out, err := run.Invoke(ctx, "how are you") + assert.NoError(t, err) + assert.Equal(t, "TransformableLambda: StreamableLambda: InvokableLambda: how are you ", out) + + stream, err := run.Stream(ctx, "how are you") + assert.NoError(t, err) + out, err = concatStreamReader(stream) + assert.NoError(t, err) + assert.Equal(t, "TransformableLambda: StreamableLambda: InvokableLambda: how are you ", out) + + sr, sw := schema.Pipe[string](1) + sw.Send("how are you", nil) + sw.Close() + + stream, err = run.Transform(ctx, sr) + assert.NoError(t, err) + out, err = concatStreamReader(stream) + assert.NoError(t, err) + assert.Equal(t, "TransformableLambda: StreamableLambda: InvokableLambda: how are you ", out) +} + +func TestStateGraphUtils(t *testing.T) { + t.Run("getState_success", func(t *testing.T) { + type testStruct struct { + UserID int64 + } + + ctx := context.Background() + + ctx = context.WithValue(ctx, stateKey{}, &internalState{ + state: &testStruct{UserID: 10}, + }) + + var userID int64 + err := ProcessState[*testStruct](ctx, func(_ context.Context, state *testStruct) error { + userID = state.UserID + return nil + }) + assert.NoError(t, err) + assert.Equal(t, int64(10), userID) + }) + + t.Run("getState_nil", func(t *testing.T) { + type testStruct struct { + UserID int64 + } + + ctx := context.Background() + ctx = context.WithValue(ctx, stateKey{}, &internalState{}) + + err := ProcessState[*testStruct](ctx, func(_ context.Context, state *testStruct) error { + return nil + }) + assert.ErrorContains(t, err, "cannot find state with type: *compose.testStruct in states chain, "+ + "current state type: ") + }) + + t.Run("getState_type_error", func(t *testing.T) { + type testStruct struct { + UserID int64 + } + + ctx := context.Background() + ctx = context.WithValue(ctx, stateKey{}, &internalState{ + state: &testStruct{UserID: 10}, + }) + + err := ProcessState[string](ctx, func(_ context.Context, state string) error { + return nil + }) + assert.ErrorContains(t, err, "cannot find state with type: string in states chain, "+ + "current state type: *compose.testStruct") + + }) +} + +func TestStateChain(t *testing.T) { + ctx := context.Background() + type testState struct { + Field1 string + Field2 string + } + sc := NewChain[string, string](WithGenLocalState(func(ctx context.Context) (state *testState) { + return &testState{} + })) + + r, err := sc.AppendLambda(InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + err = ProcessState[*testState](ctx, func(_ context.Context, state *testState) error { + state.Field1 = "node1" + return nil + }) + if err != nil { + return "", err + } + return input, nil + }), WithStatePostHandler(func(ctx context.Context, out string, state *testState) (string, error) { + state.Field2 = "node2" + return out, nil + })). + AppendLambda(InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + }), WithStatePreHandler(func(ctx context.Context, in string, state *testState) (string, error) { + return in + state.Field1 + state.Field2, nil + })).Compile(ctx) + if err != nil { + t.Fatal(err) + } + result, err := r.Invoke(ctx, "start") + if err != nil { + t.Fatal(err) + } + if result != "startnode1node2" { + t.Fatal("result is unexpected") + } +} + +func TestStreamState(t *testing.T) { + type testState struct { + Field1 string + } + ctx := context.Background() + s := &testState{Field1: "1"} + g := NewGraph[string, string](WithGenLocalState(func(ctx context.Context) (state *testState) { return s })) + err := g.AddLambdaNode("1", TransformableLambda(func(ctx context.Context, input *schema.StreamReader[string]) (output *schema.StreamReader[string], err error) { + return input, nil + }), WithStreamStatePreHandler(func(ctx context.Context, in *schema.StreamReader[string], state *testState) (*schema.StreamReader[string], error) { + sr, sw := schema.Pipe[string](5) + for i := 0; i < 5; i++ { + sw.Send(state.Field1, nil) + } + sw.Close() + return sr, nil + }), WithStreamStatePostHandler(func(ctx context.Context, in *schema.StreamReader[string], state *testState) (*schema.StreamReader[string], error) { + ss := in.Copy(2) + for { + chunk, err := ss[0].Recv() + if err == io.EOF { + return ss[1], nil + } + if err != nil { + return nil, err + } + state.Field1 += chunk + } + })) + if err != nil { + t.Fatal(err) + } + err = g.AddEdge(START, "1") + if err != nil { + t.Fatal(err) + } + err = g.AddEdge("1", END) + if err != nil { + t.Fatal(err) + } + r, err := g.Compile(ctx) + if err != nil { + t.Fatal(err) + } + sr, _ := schema.Pipe[string](1) + streamResult, err := r.Transform(ctx, sr) + if err != nil { + t.Fatal(err) + } + if s.Field1 != "111111" { + t.Fatal("state is unexpected") + } + for i := 0; i < 5; i++ { + chunk, err := streamResult.Recv() + if err != nil { + t.Fatal(err) + } + if chunk != "1" { + t.Fatal("result is unexpected") + } + } + _, err = streamResult.Recv() + if err != io.EOF { + t.Fatal("result is unexpected") + } +} + +// Nested Graph State Tests + +type NestedOuterState struct { + Value string + Counter int +} + +type NestedInnerState struct { + Value string +} + +func init() { + schema.RegisterName[*NestedOuterState]("NestedOuterState") + schema.RegisterName[*NestedInnerState]("NestedInnerState") +} + +func TestNestedGraphStateAccess(t *testing.T) { + // Test that inner graph can access outer graph's state + genOuterState := func(ctx context.Context) *NestedOuterState { + return &NestedOuterState{Value: "outer", Counter: 0} + } + + genInnerState := func(ctx context.Context) *NestedInnerState { + return &NestedInnerState{Value: "inner"} + } + + innerNode := func(ctx context.Context, input string) (string, error) { + // Access both inner and outer state + var outerValue string + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + outerValue = s.Value + return nil + }) + if err != nil { + return "", err + } + + var innerValue string + err = ProcessState(ctx, func(ctx context.Context, s *NestedInnerState) error { + innerValue = s.Value + return nil + }) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s_inner=%s_outer=%s", input, innerValue, outerValue), nil + } + + innerGraph := NewGraph[string, string](WithGenLocalState(genInnerState)) + _ = innerGraph.AddLambdaNode("inner_node", InvokableLambda(innerNode)) + _ = innerGraph.AddEdge(START, "inner_node") + _ = innerGraph.AddEdge("inner_node", END) + + outerGraph := NewGraph[string, string](WithGenLocalState(genOuterState)) + _ = outerGraph.AddGraphNode("inner_graph", innerGraph) + _ = outerGraph.AddEdge(START, "inner_graph") + _ = outerGraph.AddEdge("inner_graph", END) + + r, err := outerGraph.Compile(context.Background()) + assert.NoError(t, err) + + out, err := r.Invoke(context.Background(), "start") + assert.NoError(t, err) + assert.Equal(t, "start_inner=inner_outer=outer", out) +} + +func TestNestedGraphStateShadowing(t *testing.T) { + // Test that inner state shadows outer state of the same type (lexical scoping) + type CommonState struct { + Value string + } + + genOuterState := func(ctx context.Context) *CommonState { + return &CommonState{Value: "outer"} + } + + genInnerState := func(ctx context.Context) *CommonState { + return &CommonState{Value: "inner"} + } + + innerNode := func(ctx context.Context, input string) (string, error) { + var value string + err := ProcessState(ctx, func(ctx context.Context, s *CommonState) error { + // Should see "inner" because inner state shadows outer state + value = s.Value + return nil + }) + if err != nil { + return "", err + } + return input + "_" + value, nil + } + + innerGraph := NewGraph[string, string](WithGenLocalState(genInnerState)) + _ = innerGraph.AddLambdaNode("inner_node", InvokableLambda(innerNode)) + _ = innerGraph.AddEdge(START, "inner_node") + _ = innerGraph.AddEdge("inner_node", END) + + outerGraph := NewGraph[string, string](WithGenLocalState(genOuterState)) + _ = outerGraph.AddGraphNode("inner_graph", innerGraph) + _ = outerGraph.AddEdge(START, "inner_graph") + _ = outerGraph.AddEdge("inner_graph", END) + + r, err := outerGraph.Compile(context.Background()) + assert.NoError(t, err) + + out, err := r.Invoke(context.Background(), "start") + assert.NoError(t, err) + assert.Equal(t, "start_inner", out) +} + +func TestNestedGraphStateAfterResume(t *testing.T) { + // Test that state parent linking works correctly after resume + // when the outer state is restored from checkpoint (new instance) + genOuterState := func(ctx context.Context) *NestedOuterState { + return &NestedOuterState{Value: "outer", Counter: 0} + } + + genInnerState := func(ctx context.Context) *NestedInnerState { + return &NestedInnerState{Value: "inner"} + } + + // Node that modifies outer state + outerNode := func(ctx context.Context, input string) (string, error) { + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + s.Counter = 42 + return nil + }) + if err != nil { + return "", err + } + return input, nil + } + + // Inner node that reads outer state + innerNode := func(ctx context.Context, input string) (string, error) { + var outerCounter int + var outerValue string + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + // Should see the modified counter value from the restored state + outerCounter = s.Counter + outerValue = s.Value + return nil + }) + if err != nil { + return "", err + } + return fmt.Sprintf("%s_counter=%d_value=%s", input, outerCounter, outerValue), nil + } + + innerGraph := NewGraph[string, string](WithGenLocalState(genInnerState)) + _ = innerGraph.AddLambdaNode("inner_node", InvokableLambda(innerNode)) + _ = innerGraph.AddEdge(START, "inner_node") + _ = innerGraph.AddEdge("inner_node", END) + + outerGraph := NewGraph[string, string](WithGenLocalState(genOuterState)) + _ = outerGraph.AddLambdaNode("outer_node", InvokableLambda(outerNode)) + _ = outerGraph.AddGraphNode("inner_graph", innerGraph, WithGraphCompileOptions(WithInterruptBeforeNodes([]string{"inner_node"}))) + _ = outerGraph.AddEdge(START, "outer_node") + _ = outerGraph.AddEdge("outer_node", "inner_graph") + _ = outerGraph.AddEdge("inner_graph", END) + + store := newInMemoryStore() + r, err := outerGraph.Compile(context.Background(), WithCheckPointStore(store)) + assert.NoError(t, err) + + // First run - should interrupt after modifying outer state + _, err = r.Invoke(context.Background(), "start", WithCheckPointID("state_resume_test")) + assert.Error(t, err) + + // Resume - outer state should be restored with Counter=42 + // Inner graph should link to this restored outer state + out, err := r.Invoke(context.Background(), "start", WithCheckPointID("state_resume_test")) + assert.NoError(t, err) + assert.Equal(t, "start_counter=42_value=outer", out) +} + +func TestLambdaNestedGraphStateAccess(t *testing.T) { + // Test that inner graph invoked from a lambda can access outer graph's state + // This tests the case: outer graph -> lambda node -> inner graph (using CompositeInterrupt) + genOuterState := func(ctx context.Context) *NestedOuterState { + return &NestedOuterState{Value: "outer", Counter: 100} + } + + genInnerState := func(ctx context.Context) *NestedInnerState { + return &NestedInnerState{Value: "inner"} + } + + // Inner node that accesses outer state + innerNode := func(ctx context.Context, input string) (string, error) { + var outerValue string + var outerCounter int + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + outerValue = s.Value + outerCounter = s.Counter + return nil + }) + if err != nil { + return "", err + } + + var innerValue string + err = ProcessState(ctx, func(ctx context.Context, s *NestedInnerState) error { + innerValue = s.Value + return nil + }) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s_inner=%s_outer=%s_%d", input, innerValue, outerValue, outerCounter), nil + } + + // Build inner graph + innerGraph := NewGraph[string, string](WithGenLocalState(genInnerState)) + _ = innerGraph.AddLambdaNode("inner_node", InvokableLambda(innerNode)) + _ = innerGraph.AddEdge(START, "inner_node") + _ = innerGraph.AddEdge("inner_node", END) + + // Compile inner graph as a standalone runnable + innerRunnable, err := innerGraph.Compile(context.Background()) + assert.NoError(t, err) + + // Lambda that invokes the inner graph + lambdaNode := InvokableLambda(func(ctx context.Context, input string) (string, error) { + // Simply invoke the inner graph - state context is passed through + return innerRunnable.Invoke(ctx, input) + }) + + // Build outer graph + outerGraph := NewGraph[string, string](WithGenLocalState(genOuterState)) + _ = outerGraph.AddLambdaNode("lambda_with_graph", lambdaNode) + _ = outerGraph.AddEdge(START, "lambda_with_graph") + _ = outerGraph.AddEdge("lambda_with_graph", END) + + r, err := outerGraph.Compile(context.Background()) + assert.NoError(t, err) + + out, err := r.Invoke(context.Background(), "start") + assert.NoError(t, err) + assert.Equal(t, "start_inner=inner_outer=outer_100", out) +} + +func TestLambdaNestedGraphStateAfterResume(t *testing.T) { + // Test that state parent linking works correctly after resume + // in the lambda-nested case (outer graph -> lambda -> inner graph) + genOuterState := func(ctx context.Context) *NestedOuterState { + return &NestedOuterState{Value: "outer", Counter: 0} + } + + genInnerState := func(ctx context.Context) *NestedInnerState { + return &NestedInnerState{Value: "inner"} + } + + // Outer node that modifies state + outerNode := func(ctx context.Context, input string) (string, error) { + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + s.Counter = 99 + return nil + }) + if err != nil { + return "", err + } + return input, nil + } + + // Inner lambda that interrupts on first run, reads outer state on resume + innerLambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + wasInterrupted, _, _ := GetInterruptState[*NestedInnerState](ctx) + if !wasInterrupted { + // First run: interrupt + return "", StatefulInterrupt(ctx, "inner interrupt", &NestedInnerState{Value: "inner"}) + } + + // Resumed: read outer state + var outerCounter int + var outerValue string + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + // Should see the modified counter from the restored state + outerCounter = s.Counter + outerValue = s.Value + return nil + }) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s_counter=%d_value=%s", input, outerCounter, outerValue), nil + }) + + // Build inner graph + innerGraph := NewGraph[string, string](WithGenLocalState(genInnerState)) + _ = innerGraph.AddLambdaNode("inner_lambda", innerLambda) + _ = innerGraph.AddEdge(START, "inner_lambda") + _ = innerGraph.AddEdge("inner_lambda", END) + + // Compile inner graph as standalone runnable with checkpoint support + innerRunnable, err := innerGraph.Compile(context.Background(), + WithGraphName("inner"), + WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + // Composite lambda that invokes the inner graph and handles interrupts + compositeLambda := InvokableLambda(func(ctx context.Context, input string) (string, error) { + output, err := innerRunnable.Invoke(ctx, input, WithCheckPointID("inner-cp")) + if err != nil { + _, isInterrupt := ExtractInterruptInfo(err) + if !isInterrupt { + return "", err + } + // Wrap the interrupt using CompositeInterrupt + return "", CompositeInterrupt(ctx, "composite interrupt", nil, err) + } + return output, nil + }) + + // Build outer graph + outerGraph := NewGraph[string, string](WithGenLocalState(genOuterState)) + _ = outerGraph.AddLambdaNode("outer_node", InvokableLambda(outerNode)) + _ = outerGraph.AddLambdaNode("composite_lambda", compositeLambda) + _ = outerGraph.AddEdge(START, "outer_node") + _ = outerGraph.AddEdge("outer_node", "composite_lambda") + _ = outerGraph.AddEdge("composite_lambda", END) + + // Compile outer graph + outerRunnable, err := outerGraph.Compile(context.Background(), + WithGraphName("root"), + WithCheckPointStore(newInMemoryStore())) + assert.NoError(t, err) + + // First run - should interrupt after modifying outer state + checkPointID := "lambda_state_resume_test" + _, err = outerRunnable.Invoke(context.Background(), "start", WithCheckPointID(checkPointID)) + assert.Error(t, err) + + interruptInfo, isInterrupt := ExtractInterruptInfo(err) + assert.True(t, isInterrupt) + + // Resume - outer state should be restored with Counter=99 + // Inner lambda should link to this restored outer state + ctx := ResumeWithData(context.Background(), interruptInfo.InterruptContexts[0].ID, nil) + out, err := outerRunnable.Invoke(ctx, "start", WithCheckPointID(checkPointID)) + assert.NoError(t, err) + + // Verify the inner lambda saw the modified counter from the restored outer state + assert.Contains(t, out, "counter=99") + assert.Contains(t, out, "value=outer") +} + +func TestNestedGraphStateConcurrency(t *testing.T) { + // Test that concurrent access to parent and child states uses correct locks + // This verifies that ProcessState properly locks the parent state's mutex when accessing it + genOuterState := func(ctx context.Context) *NestedOuterState { + return &NestedOuterState{Value: "outer", Counter: 0} + } + + genInnerState := func(ctx context.Context) *NestedInnerState { + return &NestedInnerState{Value: "inner"} + } + + // Inner node that concurrently modifies both outer and inner state + innerNode := func(ctx context.Context, input string) (string, error) { + var wg sync.WaitGroup + errors := make(chan error, 20) + + // Launch 10 goroutines that modify outer state + // If locks don't work correctly, we'll see race conditions + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := ProcessState(ctx, func(ctx context.Context, s *NestedOuterState) error { + // ProcessState should hold the parent's lock during this entire function + current := s.Counter + time.Sleep(time.Millisecond) // Simulate work + s.Counter = current + 1 + return nil + }) + if err != nil { + errors <- err + } + }() + } + + // Launch 10 goroutines that modify inner state + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := ProcessState(ctx, func(ctx context.Context, s *NestedInnerState) error { + // This uses the inner state's own lock + return nil + }) + if err != nil { + errors <- err + } + }() + } + + wg.Wait() + close(errors) + + // Check for errors + for err := range errors { + return "", err + } + + return input, nil + } + + innerGraph := NewGraph[string, string](WithGenLocalState(genInnerState)) + _ = innerGraph.AddLambdaNode("inner_node", InvokableLambda(innerNode)) + _ = innerGraph.AddEdge(START, "inner_node") + _ = innerGraph.AddEdge("inner_node", END) + + outerGraph := NewGraph[string, string](WithGenLocalState(genOuterState)) + _ = outerGraph.AddGraphNode("inner_graph", innerGraph) + _ = outerGraph.AddEdge(START, "inner_graph") + _ = outerGraph.AddEdge("inner_graph", END) + + r, err := outerGraph.Compile(context.Background()) + assert.NoError(t, err) + + _, err = r.Invoke(context.Background(), "start") + assert.NoError(t, err) + + // Note: This test is primarily validated by running with -race flag + // If locks don't work correctly, the race detector will catch it +} diff --git a/compose/stream_concat.go b/compose/stream_concat.go new file mode 100644 index 0000000..4feeba8 --- /dev/null +++ b/compose/stream_concat.go @@ -0,0 +1,88 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "errors" + "io" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/schema" +) + +// RegisterStreamChunkConcatFunc registers a function to concat stream chunks. +// It's required when you want to concat stream chunks of a specific type. +// for example you call Invoke() but node only implements Stream(). +// call at process init +// not thread safe +// eg. +// +// type testStruct struct { +// field1 string +// field2 int +// } +// compose.RegisterStreamChunkConcatFunc(func(items []testStruct) (testStruct, error) { +// return testStruct{ +// field1: items[1].field1, // may implement inplace logic by your scenario +// field2: items[0].field2 + items[1].field2, +// }, nil +// }) +func RegisterStreamChunkConcatFunc[T any](fn func([]T) (T, error)) { + internal.RegisterStreamChunkConcatFunc(fn) +} + +var emptyStreamConcatErr = errors.New("stream reader is empty, concat fail") + +func concatStreamReader[T any](sr *schema.StreamReader[T]) (T, error) { + defer sr.Close() + + var items []T + + for { + chunk, err := sr.Recv() + if err != nil { + if err == io.EOF { + break + } + + if _, ok := schema.GetSourceName(err); ok { + continue + } + + var t T + return t, newStreamReadError(err) + } + + items = append(items, chunk) + } + + if len(items) == 0 { + var t T + return t, emptyStreamConcatErr + } + + if len(items) == 1 { + return items[0], nil + } + + res, err := internal.ConcatItems(items) + if err != nil { + var t T + return t, err + } + return res, nil +} diff --git a/compose/stream_concat_test.go b/compose/stream_concat_test.go new file mode 100644 index 0000000..ce0210c --- /dev/null +++ b/compose/stream_concat_test.go @@ -0,0 +1,218 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "errors" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/schema" +) + +type tStreamConcatItemForTest struct { + s string +} + +func concatTStreamForTest(items []tStreamConcatItemForTest) (tStreamConcatItemForTest, error) { + var s string + for _, item := range items { + s += item.s + } + + return tStreamConcatItemForTest{s: s}, nil +} + +func concatIntForTest(items []int) (int, error) { + var i int + for _, item := range items { + i += item + } + + return i, nil +} + +type tConcatErrForTest struct{} + +func concatTStreamError(_ []tConcatErrForTest) (tConcatErrForTest, error) { + return tConcatErrForTest{}, errors.New("test error") +} + +func TestConcatRegistry(t *testing.T) { + + RegisterStreamChunkConcatFunc(concatTStreamForTest) + + sr, sw := schema.Pipe[tStreamConcatItemForTest](10) + go func() { + for i := 0; i < 10; i++ { + sw.Send(tStreamConcatItemForTest{s: strconv.Itoa(i)}, nil) + } + sw.Close() + }() + + lastVal, err := concatStreamReader(sr) + assert.Nil(t, err) + + assert.Equal(t, "0123456789", lastVal.s) +} + +func TestStringConcat(t *testing.T) { + sr, sw := schema.Pipe[string](10) + go func() { + for i := 0; i < 10; i++ { + sw.Send(strconv.Itoa(i), nil) + } + + sw.Close() + }() + + lastVal, err := concatStreamReader(sr) + assert.Nil(t, err) + + assert.Equal(t, "0123456789", lastVal) +} + +func TestMessageConcat(t *testing.T) { + sr, sw := schema.Pipe[*schema.Message](10) + go func() { + for i := 0; i < 10; i++ { + content := schema.UserMessage(strconv.Itoa(i)) + if i%4 == 0 { + content.Extra = map[string]any{ + "key_1": strconv.Itoa(i), + strconv.Itoa(i): strconv.Itoa(i), + } + } + sw.Send(content, nil) + } + sw.Close() + }() + + lastVal, err := concatStreamReader(sr) + assert.Nil(t, err) + assert.Equal(t, "0123456789", lastVal.Content) + assert.Len(t, lastVal.Extra, 4) + assert.Equal(t, map[string]any{ + "key_1": "048", + "0": "0", + "4": "4", + "8": "8", + }, lastVal.Extra) + +} + +func TestMapConcat(t *testing.T) { + RegisterStreamChunkConcatFunc(concatTStreamForTest) + RegisterStreamChunkConcatFunc(concatIntForTest) + + t.Run("simple map", func(t *testing.T) { + sr, sw := schema.Pipe[map[string]any](10) + + go func() { + for i := 0; i < 10; i++ { + sw.Send(map[string]any{ + "string": strconv.Itoa(i), + "custom_concat": tStreamConcatItemForTest{s: strconv.Itoa(9 - i)}, + "count": i, + }, nil) + } + sw.Close() + }() + + lastVal, err := concatStreamReader(sr) + assert.Nil(t, err) + + assert.Equal(t, "0123456789", lastVal["string"]) + assert.Equal(t, "9876543210", lastVal["custom_concat"].(tStreamConcatItemForTest).s) + assert.Equal(t, 45, lastVal["count"]) + + }) + + t.Run("complex map", func(t *testing.T) { + sr, sw := schema.Pipe[map[string]any](10) + + go func() { + for i := 0; i < 10; i++ { + // 嵌套 map, 仅允许第一层做类型合并,第二层直接覆盖 + sw.Send(map[string]any{ // 嵌套 map + "string": strconv.Itoa(i), + "deep_map": map[string]any{ + "message": &schema.Message{ + Content: strconv.Itoa(i), + }, + "custom_concat_deep": tStreamConcatItemForTest{s: strconv.Itoa(9 - i)}, + "count": i, + }, + "custom_concat": tStreamConcatItemForTest{s: strconv.Itoa(9 - i)}, + "count": i, + }, nil) + } + sw.Close() + }() + + lastVal, err := concatStreamReader(sr) + assert.Nil(t, err) + + assert.Equal(t, "0123456789", lastVal["string"]) + assert.Equal(t, 45, lastVal["count"]) + assert.Equal(t, "0123456789", lastVal["deep_map"].(map[string]any)["message"].(*schema.Message).Content) + assert.Equal(t, "9876543210", lastVal["deep_map"].(map[string]any)["custom_concat_deep"].(tStreamConcatItemForTest).s) + assert.Equal(t, 45, lastVal["deep_map"].(map[string]any)["count"]) + }) +} + +func TestConcatError(t *testing.T) { + t.Run("map type not equal", func(t *testing.T) { + a := map[string]any{ + "str": "string_01", + "x": "string_in_a", + } + + b := map[string]any{ + "str": "string_02", + "x": 123, + } + _, err := internal.ConcatItems([]map[string]any{a, b}) + assert.NotNil(t, err) + }) + + t.Run("merge error", func(t *testing.T) { + RegisterStreamChunkConcatFunc(concatTStreamError) + + _, err := internal.ConcatItems([]tConcatErrForTest{{}, {}}) + assert.NotNil(t, err) + }) +} + +func TestConcatSliceValue(t *testing.T) { + type testStruct struct { + A string + } + + s := []testStruct{{}, {A: "123"}, {}} + result, err := internal.ConcatItems(s) + assert.Nil(t, err) + assert.Equal(t, testStruct{A: "123"}, result) + + s = []testStruct{{}, {}, {}} + result, err = internal.ConcatItems(s) + assert.Nil(t, err) + assert.Equal(t, testStruct{}, result) +} diff --git a/compose/stream_reader.go b/compose/stream_reader.go new file mode 100644 index 0000000..07acbb3 --- /dev/null +++ b/compose/stream_reader.go @@ -0,0 +1,129 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "reflect" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +type streamReader interface { + copy(n int) []streamReader + getType() reflect.Type + getChunkType() reflect.Type + merge([]streamReader) streamReader + withKey(string) streamReader + close() + toAnyStreamReader() *schema.StreamReader[any] + mergeWithNames([]streamReader, []string) streamReader +} + +type streamReaderPacker[T any] struct { + sr *schema.StreamReader[T] +} + +func (srp streamReaderPacker[T]) close() { + srp.sr.Close() +} + +func (srp streamReaderPacker[T]) copy(n int) []streamReader { + ret := make([]streamReader, n) + srs := srp.sr.Copy(n) + + for i := 0; i < n; i++ { + ret[i] = streamReaderPacker[T]{srs[i]} + } + + return ret +} + +func (srp streamReaderPacker[T]) getType() reflect.Type { + return reflect.TypeOf(srp.sr) +} + +func (srp streamReaderPacker[T]) getChunkType() reflect.Type { + return generic.TypeOf[T]() +} + +func (srp streamReaderPacker[T]) toStreamReaders(srs []streamReader) []*schema.StreamReader[T] { + ret := make([]*schema.StreamReader[T], len(srs)+1) + ret[0] = srp.sr + for i := 1; i < len(ret); i++ { + sr, ok := unpackStreamReader[T](srs[i-1]) + if !ok { + return nil + } + + ret[i] = sr + } + + return ret +} + +func (srp streamReaderPacker[T]) merge(isrs []streamReader) streamReader { + srs := srp.toStreamReaders(isrs) + + sr := schema.MergeStreamReaders(srs) + + return packStreamReader(sr) +} + +func (srp streamReaderPacker[T]) mergeWithNames(isrs []streamReader, names []string) streamReader { + srs := srp.toStreamReaders(isrs) + + sr := schema.InternalMergeNamedStreamReaders(srs, names) + + return packStreamReader(sr) +} + +func (srp streamReaderPacker[T]) withKey(key string) streamReader { + cvt := func(v T) (map[string]any, error) { + return map[string]any{key: v}, nil + } + + ret := schema.StreamReaderWithConvert[T, map[string]any](srp.sr, cvt) + + return packStreamReader(ret) +} + +func (srp streamReaderPacker[T]) toAnyStreamReader() *schema.StreamReader[any] { + return schema.StreamReaderWithConvert(srp.sr, func(t T) (any, error) { + return t, nil + }) +} + +func packStreamReader[T any](sr *schema.StreamReader[T]) streamReader { + return streamReaderPacker[T]{sr} +} + +func unpackStreamReader[T any](isr streamReader) (*schema.StreamReader[T], bool) { + c, ok := isr.(streamReaderPacker[T]) + if ok { + return c.sr, true + } + + typ := generic.TypeOf[T]() + if typ.Kind() == reflect.Interface { + return schema.StreamReaderWithConvert(isr.toAnyStreamReader(), func(t any) (T, error) { + return t.(T), nil + }), true + } + + return nil, false +} diff --git a/compose/stream_reader_test.go b/compose/stream_reader_test.go new file mode 100644 index 0000000..d104a50 --- /dev/null +++ b/compose/stream_reader_test.go @@ -0,0 +1,102 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "io" + "reflect" + "testing" + + "github.com/cloudwego/eino/schema" + "github.com/stretchr/testify/assert" +) + +func TestArrayStreamMerge(t *testing.T) { + + t.Run("unpack_to_equal_type", func(t *testing.T) { + a1 := []int{1, 2, 3} + a2 := []int{4, 5, 6} + a3 := []int{7, 8, 9} + s1 := schema.StreamReaderFromArray(a1) + s2 := schema.StreamReaderFromArray(a2) + s3 := schema.StreamReaderFromArray(a3) + + sp1 := streamReaderPacker[int]{sr: s1} + sp2 := streamReaderPacker[int]{sr: s2} + sp3 := streamReaderPacker[int]{sr: s3} + + sp := sp1.merge([]streamReader{sp2, sp3}) + + sr, ok := unpackStreamReader[int](sp) + if !ok { + t.Fatal("unexpected") + } + + defer sr.Close() + + var result []int + for { + chunk, err := sr.Recv() + if err == io.EOF { + break + } + assert.Nil(t, err) + result = append(result, chunk) + } + if !reflect.DeepEqual(result, append(append(a1, a2...), a3...)) { + t.Fatalf("result: %v error", result) + } + }) + + t.Run("unpack_to_father_type", func(t *testing.T) { + a1 := []*doctor{{say: "a"}, {say: "b"}, {say: "c"}} + a2 := []*doctor{{say: "d"}, {say: "e"}, {say: "f"}} + a3 := []*doctor{{say: "g"}, {say: "h"}, {say: "i"}} + s1 := schema.StreamReaderFromArray(a1) + s2 := schema.StreamReaderFromArray(a2) + s3 := schema.StreamReaderFromArray(a3) + + sp1 := streamReaderPacker[*doctor]{sr: s1} + sp2 := streamReaderPacker[*doctor]{sr: s2} + sp3 := streamReaderPacker[*doctor]{sr: s3} + + sp := sp1.merge([]streamReader{sp2, sp3}) + + sr, ok := unpackStreamReader[person](sp) + assert.True(t, ok) + + defer sr.Close() + + var result []person + for { + chunk, err := sr.Recv() + if err == io.EOF { + break + } + assert.Nil(t, err) + result = append(result, chunk) + } + + baseline := append(append(a1, a2...), a3...) + + assert.Len(t, result, len(baseline)) + + for idx := range result { + assert.Equal(t, baseline[idx].say, result[idx].Say()) + } + }) +} diff --git a/compose/tool_alias_test.go b/compose/tool_alias_test.go new file mode 100644 index 0000000..487132c --- /dev/null +++ b/compose/tool_alias_test.go @@ -0,0 +1,1178 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" +) + +type searchArgs struct { + Query string `json:"query"` +} + +func TestToolNameAliases(t *testing.T) { + ctx := context.Background() + + // Create test tool + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search for information", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string", Desc: "Search query"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "search result", nil + }) + + // Configure aliases + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"search_v1", "query", "find"}, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Test calling tool with alias + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "search_v1", // Using alias + Arguments: `{"query": "test"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Equal(t, "call_1", output[0].ToolCallID) + assert.Contains(t, output[0].Content, "search result") +} + +type searchArgsWithLimit struct { + Query string `json:"query"` + Limit int `json:"limit"` +} + +func TestArgumentsAliases(t *testing.T) { + ctx := context.Background() + + receivedArgs := "" + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search for information", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + "limit": {Type: "integer"}, + }), + }, func(ctx context.Context, args *searchArgsWithLimit) (string, error) { + b, _ := json.Marshal(args) + receivedArgs = string(b) + return "result", nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + ArgumentsAliases: map[string][]string{ + "query": {"q", "search_term"}, + "limit": {"max_results", "count"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Use alias parameters + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "search", + Arguments: `{"q": "test", "max_results": 10}`, // Using aliases + }, + }, + }) + + _, err = node.Invoke(ctx, input) + require.NoError(t, err) + + // Verify tool received canonical parameter names + var args map[string]any + err = json.Unmarshal([]byte(receivedArgs), &args) + require.NoError(t, err) + assert.Equal(t, "test", args["query"]) + assert.Equal(t, float64(10), args["limit"]) + assert.NotContains(t, args, "q") + assert.NotContains(t, args, "max_results") +} + +type emptyArgs struct{} + +func TestAliasConflict(t *testing.T) { + ctx := context.Background() + + tool1 := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { + return "result", nil + }) + tool2 := newTool(&schema.ToolInfo{Name: "query", Desc: "Query"}, func(ctx context.Context, args *emptyArgs) (string, error) { + return "result", nil + }) + + t.Run("tool name alias conflict", func(t *testing.T) { + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1, tool2}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + }, + "query": { + NameAliases: []string{"find"}, // Conflict: find already used by search + }, + }, + } + + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "conflicts with an alias already registered for") + }) + + t.Run("tool name alias conflicts with canonical name", func(t *testing.T) { + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1, tool2}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"query"}, // Conflict: "query" is tool2's canonical name + }, + }, + } + + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "conflicts with existing tool's canonical name") + }) + + t.Run("argument alias conflict", func(t *testing.T) { + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + "limit": {"q"}, // Conflict: q maps to multiple parameters + }, + }, + }, + } + + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "conflicting arg alias") + }) + + t.Run("arg alias conflicts with existing schema property", func(t *testing.T) { + searchWithParams := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + "limit": {Type: "integer"}, + }), + }, func(ctx context.Context, args *emptyArgs) (string, error) { + return "result", nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchWithParams}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + ArgumentsAliases: map[string][]string{ + "limit": {"query"}, // "query" is already a schema property + }, + }, + }, + } + + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "conflicts with existing schema property") + }) +} + +func TestArgumentsAliasesWithHandler(t *testing.T) { + ctx := context.Background() + + executionOrder := []string{} + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + executionOrder = append(executionOrder, "tool_invoke") + return "result", nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + }, + ToolArgumentsHandler: func(ctx context.Context, name, args string) (string, error) { + executionOrder = append(executionOrder, "args_handler") + // Handler receives the original model-returned name (alias) + assert.Equal(t, "search", name) + // Verify alias remapping has already been done + var m map[string]any + err := json.Unmarshal([]byte(args), &m) + require.NoError(t, err) + assert.Contains(t, m, "query") + assert.NotContains(t, m, "q") + return args, nil + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Call with alias name "find" and alias arg "q" + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "test"}`, + }, + }, + }) + + _, err = node.Invoke(ctx, input) + require.NoError(t, err) + + // Verify execution order: alias remapping → ToolArgumentsHandler → tool execution + assert.Equal(t, []string{"args_handler", "tool_invoke"}, executionOrder) +} + +func TestNonExistentToolInAliasConfig(t *testing.T) { + ctx := context.Background() + + tool1 := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { + return "result", nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1}, + ToolAliases: map[string]ToolAliasConfig{ + "non_existent_tool": { // Non-existent tool + NameAliases: []string{"alias1"}, + }, + }, + } + + // Should not error — non-existent tool alias configs are silently skipped + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // The existing tool should still work normally + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "search", + Arguments: `{}`, + }, + }, + }) + output, err := node.Invoke(ctx, input) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "result") +} + +type weatherArgs struct { + Location string `json:"location"` +} + +func TestToolAliasesE2E(t *testing.T) { + ctx := context.Background() + + // Create multiple tools + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search for information", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + "limit": {Type: "integer"}, + }), + }, func(ctx context.Context, args *searchArgsWithLimit) (string, error) { + return "search result", nil + }) + + weatherTool := newTool(&schema.ToolInfo{ + Name: "weather", + Desc: "Get weather information", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "location": {Type: "string"}, + }), + }, func(ctx context.Context, args *weatherArgs) (string, error) { + return "weather result", nil + }) + + // Configure aliases for multiple tools + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool, weatherTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"search_v1", "query"}, + ArgumentsAliases: map[string][]string{ + "query": {"q", "search_term"}, + "limit": {"max_results"}, + }, + }, + "weather": { + NameAliases: []string{"get_weather"}, + ArgumentsAliases: map[string][]string{ + "location": {"loc", "city"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Construct message with multiple tool calls using different aliases + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "search_v1", // Tool name alias + Arguments: `{"q": "test", "max_results": 5}`, // Parameter aliases + }, + }, + { + ID: "call_2", + Function: schema.FunctionCall{ + Name: "get_weather", // Tool name alias + Arguments: `{"city": "Beijing"}`, // Parameter alias + }, + }, + }) + + output, err := node.Invoke(ctx, input) + require.NoError(t, err) + require.Len(t, output, 2) + + // Verify both tools executed successfully + assert.Equal(t, "call_1", output[0].ToolCallID) + assert.Equal(t, "call_2", output[1].ToolCallID) + assert.Contains(t, output[0].Content, "search result") + assert.Contains(t, output[1].Content, "weather result") +} + +func TestRemapArgsEdgeCases(t *testing.T) { + aliasMap := map[string]string{"q": "query"} + + t.Run("empty string", func(t *testing.T) { + result, err := remapArgs("", aliasMap) + assert.NoError(t, err) + assert.Equal(t, "", result) + }) + + t.Run("whitespace only", func(t *testing.T) { + result, err := remapArgs(" ", aliasMap) + assert.NoError(t, err) + assert.Equal(t, " ", result) + }) + + t.Run("non-object JSON", func(t *testing.T) { + result, err := remapArgs(`"hello"`, aliasMap) + assert.NoError(t, err) + assert.Equal(t, `"hello"`, result) + }) + + t.Run("JSON array", func(t *testing.T) { + result, err := remapArgs(`[1,2,3]`, aliasMap) + assert.NoError(t, err) + assert.Equal(t, `[1,2,3]`, result) + }) + + t.Run("invalid JSON", func(t *testing.T) { + result, err := remapArgs(`{invalid`, aliasMap) + assert.NoError(t, err) + assert.Equal(t, `{invalid`, result) + }) + + t.Run("alias and canonical both present", func(t *testing.T) { + // When both alias "q" and canonical "query" exist, alias is kept as-is (not deleted, not overwritten) + result, err := remapArgs(`{"q": "alias_val", "query": "canonical_val"}`, aliasMap) + assert.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal([]byte(result), &m)) + assert.Equal(t, "canonical_val", m["query"]) + assert.Equal(t, "alias_val", m["q"]) + }) + + t.Run("unknown fields preserved", func(t *testing.T) { + result, err := remapArgs(`{"q": "test", "unknown_field": 42}`, aliasMap) + assert.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal([]byte(result), &m)) + assert.Equal(t, "test", m["query"]) + assert.NotContains(t, m, "q") + assert.Equal(t, float64(42), m["unknown_field"]) + }) +} + +func TestCanonicalNameCallWithAliasConfigured(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "result: " + args.Query, nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Call with canonical name and canonical arg — should work normally + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "search", + Arguments: `{"query": "hello"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "result: hello") +} + +func TestEmptyAliasValidation(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { + return "result", nil + }) + + t.Run("empty name alias", func(t *testing.T) { + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{""}, + }, + }, + } + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "empty name alias") + }) + + t.Run("empty arg alias", func(t *testing.T) { + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + ArgumentsAliases: map[string][]string{ + "query": {""}, + }, + }, + }, + } + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "empty argument alias") + }) + + t.Run("empty canonical arg key", func(t *testing.T) { + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + ArgumentsAliases: map[string][]string{ + "": {"q"}, + }, + }, + }, + } + _, err := NewToolNode(ctx, config) + require.Error(t, err) + assert.Contains(t, err.Error(), "empty canonical argument key") + }) +} + +func TestNameAliasSameAsCanonical(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{Name: "search", Desc: "Search"}, func(ctx context.Context, args *emptyArgs) (string, error) { + return "result", nil + }) + + // Alias same as canonical name — should be tolerated (skip, no error) + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"search", "find"}, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Both canonical and alias should work + for _, name := range []string{"search", "find"} { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: name, + Arguments: `{}`, + }, + }, + }) + output, err := node.Invoke(ctx, input) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "result") + } +} + +func TestToolAliasesWithDynamicToolList(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "search result: " + args.Query, nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Use dynamic ToolList via option — alias should still work + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "dynamic"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolList(searchTool)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "search result: dynamic") +} + +func TestToolNameAliasesStream(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search for information", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "stream result: " + args.Query, nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "hello"}`, + }, + }, + }) + + reader, err := node.Stream(ctx, input) + require.NoError(t, err) + + var chunks [][]*schema.Message + for { + chunk, err := reader.Recv() + if err != nil { + break + } + chunks = append(chunks, chunk) + } + + msgs, err := schema.ConcatMessageArray(chunks) + require.NoError(t, err) + require.Len(t, msgs, 1) + assert.Equal(t, "call_1", msgs[0].ToolCallID) + assert.Contains(t, msgs[0].Content, "stream result: hello") +} + +func TestEnhancedToolWithAliases(t *testing.T) { + ctx := context.Background() + + enhancedTool := &enhancedInvokableTool{ + info: &schema.ToolInfo{ + Name: "search", + Desc: "Enhanced search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, + fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "enhanced: " + input.Text}, + }, + }, nil + }, + } + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{enhancedTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Call with alias name and alias arg + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "test"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Equal(t, "call_1", output[0].ToolCallID) + // Verify arg alias was remapped: "q" → "query" in the JSON passed to enhanced tool + assert.Contains(t, output[0].UserInputMultiContent[0].Text, "enhanced:") +} + +func TestDynamicToolListAliasRemoved(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "search result", nil + }) + + weatherTool := newTool(&schema.ToolInfo{ + Name: "weather", + Desc: "Weather", + }, func(ctx context.Context, args *emptyArgs) (string, error) { + return "weather result", nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool, weatherTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + // Dynamic tool list only contains weatherTool — "search" and its alias "find" should not be available + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{}`, + }, + }, + }) + + _, err = node.Invoke(ctx, input, WithToolList(weatherTool)) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestToolAliasesOptionOverridesGlobal(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "search result: " + args.Query, nil + }) + + weatherTool := newTool(&schema.ToolInfo{ + Name: "weather", + Desc: "Weather", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "location": {Type: "string"}, + }), + }, func(ctx context.Context, args *weatherArgs) (string, error) { + return "weather result: " + args.Location, nil + }) + + // Global aliases: search has alias "find" + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool, weatherTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + t.Run("opt ToolAliases overrides global in Invoke", func(t *testing.T) { + // opt.ToolAliases defines "lookup" as alias for search (not "find") + optAliases := map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"lookup"}, + ArgumentsAliases: map[string][]string{ + "query": {"keyword"}, + }, + }, + } + + // "lookup" should work with opt aliases + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "lookup", + Arguments: `{"keyword": "test"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolList(searchTool), WithToolAliases(optAliases)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "search result: test") + + // "find" (global alias) should NOT work when opt.ToolAliases is set + input2 := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_2", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "test"}`, + }, + }, + }) + + _, err = node.Invoke(ctx, input2, WithToolList(searchTool), WithToolAliases(optAliases)) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) + + t.Run("opt ToolAliases overrides global in Stream", func(t *testing.T) { + optAliases := map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"lookup"}, + ArgumentsAliases: map[string][]string{ + "query": {"keyword"}, + }, + }, + } + + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "lookup", + Arguments: `{"keyword": "stream_test"}`, + }, + }, + }) + + reader, err := node.Stream(ctx, input, WithToolList(searchTool), WithToolAliases(optAliases)) + require.NoError(t, err) + + var chunks [][]*schema.Message + for { + chunk, err := reader.Recv() + if err != nil { + break + } + chunks = append(chunks, chunk) + } + + msgs, err := schema.ConcatMessageArray(chunks) + require.NoError(t, err) + require.Len(t, msgs, 1) + assert.Contains(t, msgs[0].Content, "search result: stream_test") + }) + + t.Run("nil opt ToolAliases falls back to global filtered", func(t *testing.T) { + // No WithToolAliases — should use global "find" alias, filtered by ToolList + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "fallback"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolList(searchTool)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "search result: fallback") + }) + + t.Run("opt ToolAliases only without ToolList replaces global", func(t *testing.T) { + // Only WithToolAliases, no WithToolList — should use global tools with opt aliases + optAliases := map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"lookup"}, + ArgumentsAliases: map[string][]string{ + "query": {"keyword"}, + }, + }, + } + + // "lookup" (opt alias) should work + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "lookup", + Arguments: `{"keyword": "only_alias"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolAliases(optAliases)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "search result: only_alias") + + // "find" (global alias) should NOT work when opt.ToolAliases replaces global + input2 := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_2", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "test"}`, + }, + }, + }) + + _, err = node.Invoke(ctx, input2, WithToolAliases(optAliases)) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) + + t.Run("opt ToolAliases only without ToolList in Stream", func(t *testing.T) { + optAliases := map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"lookup"}, + }, + } + + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "lookup", + Arguments: `{"query": "stream_only_alias"}`, + }, + }, + }) + + reader, err := node.Stream(ctx, input, WithToolAliases(optAliases)) + require.NoError(t, err) + + var chunks [][]*schema.Message + for { + chunk, err := reader.Recv() + if err != nil { + break + } + chunks = append(chunks, chunk) + } + + msgs, err := schema.ConcatMessageArray(chunks) + require.NoError(t, err) + require.Len(t, msgs, 1) + assert.Contains(t, msgs[0].Content, "search result: stream_only_alias") + }) +} + +func TestAliasConfigForToolAddedViaOption(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "search result: " + args.Query, nil + }) + + weatherTool := newTool(&schema.ToolInfo{ + Name: "weather", + Desc: "Weather", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "location": {Type: "string"}, + }), + }, func(ctx context.Context, args *weatherArgs) (string, error) { + return "weather result: " + args.Location, nil + }) + + // New with only searchTool, but alias config includes weather tool + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + ArgumentsAliases: map[string][]string{ + "query": {"q"}, + }, + }, + "weather": { + NameAliases: []string{"forecast"}, + ArgumentsAliases: map[string][]string{ + "location": {"loc"}, + }, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + t.Run("weather alias works when tool passed via option", func(t *testing.T) { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "forecast", + Arguments: `{"loc": "Beijing"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolList(searchTool, weatherTool)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "weather result: Beijing") + }) + + t.Run("search alias still works with option tool list", func(t *testing.T) { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"q": "test"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolList(searchTool, weatherTool)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "search result: test") + }) +} + +func TestOptionWithToolListAndToolAliases(t *testing.T) { + ctx := context.Background() + + searchTool := newTool(&schema.ToolInfo{ + Name: "search", + Desc: "Search", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": {Type: "string"}, + }), + }, func(ctx context.Context, args *searchArgs) (string, error) { + return "search result: " + args.Query, nil + }) + + weatherTool := newTool(&schema.ToolInfo{ + Name: "weather", + Desc: "Weather", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "location": {Type: "string"}, + }), + }, func(ctx context.Context, args *weatherArgs) (string, error) { + return "weather result: " + args.Location, nil + }) + + config := &ToolsNodeConfig{ + Tools: []tool.BaseTool{searchTool}, + ToolAliases: map[string]ToolAliasConfig{ + "search": { + NameAliases: []string{"find"}, + }, + }, + } + + node, err := NewToolNode(ctx, config) + require.NoError(t, err) + + t.Run("opt aliases override global when both tool list and aliases provided", func(t *testing.T) { + optAliases := map[string]ToolAliasConfig{ + "weather": { + NameAliases: []string{"forecast"}, + ArgumentsAliases: map[string][]string{ + "location": {"loc"}, + }, + }, + } + + // "forecast" should work via opt aliases + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_1", + Function: schema.FunctionCall{ + Name: "forecast", + Arguments: `{"loc": "Shanghai"}`, + }, + }, + }) + + output, err := node.Invoke(ctx, input, WithToolList(searchTool, weatherTool), WithToolAliases(optAliases)) + require.NoError(t, err) + require.Len(t, output, 1) + assert.Contains(t, output[0].Content, "weather result: Shanghai") + + // "find" (global alias) should NOT work when opt aliases override + input2 := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call_2", + Function: schema.FunctionCall{ + Name: "find", + Arguments: `{"query": "test"}`, + }, + }, + }) + + _, err = node.Invoke(ctx, input2, WithToolList(searchTool, weatherTool), WithToolAliases(optAliases)) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} diff --git a/compose/tool_node.go b/compose/tool_node.go new file mode 100644 index 0000000..f65037e --- /dev/null +++ b/compose/tool_node.go @@ -0,0 +1,1310 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "runtime/debug" + "sort" + "strings" + "sync" + + "github.com/bytedance/sonic" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/internal/safe" + "github.com/cloudwego/eino/schema" +) + +type toolsNodeOptions struct { + ToolOptions []tool.Option + ToolList []tool.BaseTool + + ToolAliases map[string]ToolAliasConfig +} + +// ToolsNodeOption is the option func type for ToolsNode. +type ToolsNodeOption func(o *toolsNodeOptions) + +// WithToolOption adds tool options to the ToolsNode. +func WithToolOption(opts ...tool.Option) ToolsNodeOption { + return func(o *toolsNodeOptions) { + o.ToolOptions = append(o.ToolOptions, opts...) + } +} + +// WithToolList sets the tool list for the ToolsNode. +func WithToolList(tool ...tool.BaseTool) ToolsNodeOption { + return func(o *toolsNodeOptions) { + o.ToolList = tool + } +} + +// WithToolAliases sets the tool aliases for the ToolsNode call option. +// When used with WithToolList, it overrides the global alias configuration for the dynamic tool list. +// When used alone (without WithToolList), it replaces the global alias configuration while keeping the original tool list. +func WithToolAliases(toolAliases map[string]ToolAliasConfig) ToolsNodeOption { + return func(o *toolsNodeOptions) { + o.ToolAliases = toolAliases + } +} + +// ToolsNode represents a node capable of executing tools within a graph. +// The Graph Node interface is defined as follows: +// +// Invoke(ctx context.Context, input *schema.Message, opts ...ToolsNodeOption) ([]*schema.Message, error) +// Stream(ctx context.Context, input *schema.Message, opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.Message], error) +// +// Input: An AssistantMessage containing ToolCalls +// Output: An array of ToolMessage where the order of elements corresponds to the order of ToolCalls in the input +type ToolsNode struct { + tuple *toolsTuple + tools []tool.BaseTool + unknownToolHandler func(ctx context.Context, name, input string) (string, error) + executeSequentially bool + toolArgumentsHandler func(ctx context.Context, name, input string) (string, error) + toolCallMiddlewares []InvokableToolMiddleware + streamToolCallMiddlewares []StreamableToolMiddleware + enhancedToolCallMiddlewares []EnhancedInvokableToolMiddleware + enhancedStreamToolCallMiddlewares []EnhancedStreamableToolMiddleware + toolAliasConfigs map[string]ToolAliasConfig +} + +// ToolInput represents the input parameters for a tool call execution. +type ToolInput struct { + // Name is the name of the tool to be executed. + Name string + // Arguments contains the arguments for the tool call. + Arguments string + // CallID is the unique identifier for this tool call. + CallID string + // CallOptions contains tool options for the execution. + CallOptions []tool.Option +} + +// ToolOutput represents the result of a non-streaming tool call execution. +type ToolOutput struct { + // Result contains the string output from the tool execution. + Result string +} + +// StreamToolOutput represents the result of a streaming tool call execution. +type StreamToolOutput struct { + // Result is a stream reader that provides access to the tool's streaming output. + Result *schema.StreamReader[string] +} + +// EnhancedInvokableToolOutput represents the result of a non-streaming enhanced tool call execution. +// It supports returning structured multimodal content (text, images, audio, video, files) from tools. +type EnhancedInvokableToolOutput struct { + // Result contains the structured multimodal output from the tool execution. + Result *schema.ToolResult +} + +// EnhancedStreamableToolOutput represents the result of a streaming enhanced tool call execution. +// It provides a stream reader for accessing multimodal content progressively. +type EnhancedStreamableToolOutput struct { + // Result is a stream reader that provides access to the tool's streaming multimodal output. + Result *schema.StreamReader[*schema.ToolResult] +} + +// InvokableToolEndpoint is the function signature for non-streaming tool calls. +type InvokableToolEndpoint func(ctx context.Context, input *ToolInput) (*ToolOutput, error) + +// StreamableToolEndpoint is the function signature for streaming tool calls. +type StreamableToolEndpoint func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) + +type EnhancedInvokableToolEndpoint func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) + +type EnhancedStreamableToolEndpoint func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) + +// InvokableToolMiddleware is a function that wraps InvokableToolEndpoint to add custom processing logic. +// It can be used to intercept, modify, or enhance tool call execution for non-streaming tools. +type InvokableToolMiddleware func(InvokableToolEndpoint) InvokableToolEndpoint + +// StreamableToolMiddleware is a function that wraps StreamableToolEndpoint to add custom processing logic. +// It can be used to intercept, modify, or enhance tool call execution for streaming tools. +type StreamableToolMiddleware func(StreamableToolEndpoint) StreamableToolEndpoint + +type EnhancedInvokableToolMiddleware func(EnhancedInvokableToolEndpoint) EnhancedInvokableToolEndpoint + +type EnhancedStreamableToolMiddleware func(EnhancedStreamableToolEndpoint) EnhancedStreamableToolEndpoint + +// ToolMiddleware groups middleware hooks for invokable and streamable tool calls. +type ToolMiddleware struct { + // Invokable contains middleware function for non-streaming tool calls. + // Note: This middleware only applies to tools that implement the InvokableTool interface. + Invokable InvokableToolMiddleware + + // Streamable contains middleware function for streaming tool calls. + // Note: This middleware only applies to tools that implement the StreamableTool interface. + Streamable StreamableToolMiddleware + + // EnhancedInvokable contains middleware function for non-streaming enhanced tool calls. + // Note: This middleware only applies to tools that implement the EnhancedInvokableTool interface. + EnhancedInvokable EnhancedInvokableToolMiddleware + + // EnhancedStreamable contains middleware function for streaming enhanced tool calls. + // Note: This middleware only applies to tools that implement the EnhancedStreamableTool interface. + EnhancedStreamable EnhancedStreamableToolMiddleware +} + +// ToolAliasConfig configures name and argument aliases for a single tool. +type ToolAliasConfig struct { + // NameAliases are alternative names for this tool. + // If the model returns any of these names, it will be resolved to the canonical tool name. + NameAliases []string + + // ArgumentsAliases maps canonical argument keys to their alias lists. + // key=canonical, value=[]alias. Applied to top-level JSON keys before tool execution. + // Example: {"query": ["q", "search_term"], "limit": ["max_results", "count"]} + ArgumentsAliases map[string][]string +} + +// ToolsNodeConfig is the config for ToolsNode. +type ToolsNodeConfig struct { + // Tools specify the list of tools can be called which are BaseTool but must implement InvokableTool or StreamableTool. + Tools []tool.BaseTool + + // ToolAliases configures name and argument aliases for tools. + // Key is the canonical tool name, value defines its aliases. + // This field is optional. When provided, tool name aliases will be resolved during tool dispatch, + // and argument aliases will be remapped before ToolArgumentsHandler (if configured) and tool execution. + // Execution order: ArgumentsAliases remapping → ToolArgumentsHandler → tool execution + ToolAliases map[string]ToolAliasConfig + + // UnknownToolsHandler handles tool calls for non-existent tools when LLM hallucinates. + // This field is optional. When not set, calling a non-existent tool will result in an error. + // When provided, if the LLM attempts to call a tool that doesn't exist in the Tools list, + // this handler will be invoked instead of returning an error, allowing graceful handling of hallucinated tools. + // Parameters: + // - ctx: The context for the tool call + // - name: The name of the non-existent tool + // - input: The tool call input generated by llm + // Returns: + // - string: The response to be returned as if the tool was executed + // - error: Any error that occurred during handling + UnknownToolsHandler func(ctx context.Context, name, input string) (string, error) + + // ExecuteSequentially determines whether tool calls should be executed sequentially (in order) or in parallel. + // When set to true, tool calls will be executed one after another in the order they appear in the input message. + // When set to false (default), tool calls will be executed in parallel. + ExecuteSequentially bool + + // ToolArgumentsHandler allows handling of tool arguments before execution. + // When provided, this function will be called for each tool call to process the arguments. + // Parameters: + // - ctx: The context for the tool call + // - name: The name of the tool being called + // - arguments: The original arguments string for the tool + // Returns: + // - string: The processed arguments string to be used for tool execution + // - error: Any error that occurred during preprocessing + ToolArgumentsHandler func(ctx context.Context, name, arguments string) (string, error) + + // ToolCallMiddlewares configures middleware for tool calls. + // Each element can contain Invokable and/or Streamable middleware. + // Invokable middleware only applies to tools implementing InvokableTool interface. + // Streamable middleware only applies to tools implementing StreamableTool interface. + ToolCallMiddlewares []ToolMiddleware +} + +// NewToolNode creates a new ToolsNode. +// e.g. +// +// conf := &ToolsNodeConfig{ +// Tools: []tool.BaseTool{invokableTool1, streamableTool2}, +// } +// toolsNode, err := NewToolNode(ctx, conf) +func NewToolNode(ctx context.Context, conf *ToolsNodeConfig) (*ToolsNode, error) { + var middlewares []InvokableToolMiddleware + var streamMiddlewares []StreamableToolMiddleware + var enhancedInvokableMiddlewares []EnhancedInvokableToolMiddleware + var enhancedStreamableMiddlewares []EnhancedStreamableToolMiddleware + + for _, m := range conf.ToolCallMiddlewares { + if m.Invokable != nil { + middlewares = append(middlewares, m.Invokable) + } + if m.Streamable != nil { + streamMiddlewares = append(streamMiddlewares, m.Streamable) + } + if m.EnhancedInvokable != nil { + enhancedInvokableMiddlewares = append(enhancedInvokableMiddlewares, m.EnhancedInvokable) + } + if m.EnhancedStreamable != nil { + enhancedStreamableMiddlewares = append(enhancedStreamableMiddlewares, m.EnhancedStreamable) + } + } + + params := convToolsParams{ + tools: conf.Tools, + aliasConfigs: conf.ToolAliases, + } + params.middlewares.invokable = middlewares + params.middlewares.streamable = streamMiddlewares + params.middlewares.enhancedInvokable = enhancedInvokableMiddlewares + params.middlewares.enhancedStreamable = enhancedStreamableMiddlewares + tuple, err := convTools(ctx, params) + if err != nil { + return nil, err + } + + return &ToolsNode{ + tuple: tuple, + tools: conf.Tools, + unknownToolHandler: conf.UnknownToolsHandler, + executeSequentially: conf.ExecuteSequentially, + toolArgumentsHandler: conf.ToolArgumentsHandler, + toolCallMiddlewares: middlewares, + streamToolCallMiddlewares: streamMiddlewares, + enhancedToolCallMiddlewares: enhancedInvokableMiddlewares, + enhancedStreamToolCallMiddlewares: enhancedStreamableMiddlewares, + toolAliasConfigs: conf.ToolAliases, + }, nil +} + +// ToolsInterruptAndRerunExtra carries interrupt metadata for ToolsNode reruns. +type ToolsInterruptAndRerunExtra struct { + // ToolCalls contains all tool calls from the original assistant message. + ToolCalls []schema.ToolCall + + // ExecutedTools maps tool call IDs to their string output for successfully executed standard tools. + ExecutedTools map[string]string + + // ExecutedEnhancedTools maps tool call IDs to their structured multimodal output for successfully executed enhanced tools. + ExecutedEnhancedTools map[string]*schema.ToolResult + + // RerunTools contains the IDs of tool calls that need to be re-executed. + RerunTools []string + + // RerunExtraMap stores additional metadata for each tool call that needs rerun, keyed by tool call ID. + RerunExtraMap map[string]any +} + +func init() { + schema.RegisterName[*ToolsInterruptAndRerunExtra]("_eino_compose_tools_interrupt_and_rerun_extra") + schema.RegisterName[*toolsInterruptAndRerunState]("_eino_compose_tools_interrupt_and_rerun_state") +} + +type toolsInterruptAndRerunState struct { + Input *schema.Message + ExecutedTools map[string]string + ExecutedEnhancedTools map[string]*schema.ToolResult + RerunTools []string +} + +type toolsTuple struct { + indexes map[string]int + meta []*executorMeta + endpoints []InvokableToolEndpoint + streamEndpoints []StreamableToolEndpoint + enhancedInvokableEndpoints []EnhancedInvokableToolEndpoint + enhancedStreamableEndpoints []EnhancedStreamableToolEndpoint + // argsAliasMap stores reverse argument alias mappings for each tool. + // key: canonical tool name, value: map[aliasKey]canonicalKey (alias → canonical direction) + argsAliasMap map[string]map[string]string + // canonicalNames stores the canonical name for each tool index + canonicalNames []string + // toolInfos stores the ToolInfo for each tool index, used for alias validation + toolInfos []*schema.ToolInfo +} + +// remapArgs replaces alias keys in the JSON arguments string with canonical keys. +// aliasMap: alias → canonical mapping +func remapArgs(args string, aliasMap map[string]string) (string, error) { + if len(aliasMap) == 0 { + return args, nil + } + + trimmed := strings.TrimSpace(args) + if trimmed == "" || trimmed[0] != '{' { + return args, nil + } + + var m map[string]json.RawMessage + if err := sonic.Unmarshal([]byte(args), &m); err != nil { + return args, nil + } + + changed := false + for alias, canonical := range aliasMap { + if v, ok := m[alias]; ok { + // Only replace if canonical key doesn't exist. + // If both alias and canonical are present (e.g. {"q":"a","query":"b"}), + // the alias key is kept as-is and passed through as an unknown field. + if _, exists := m[canonical]; !exists { + m[canonical] = v + delete(m, alias) + changed = true + } + } + } + + if !changed { + return args, nil + } + + b, err := sonic.Marshal(m) + return string(b), err +} + +type convToolsParams struct { + tools []tool.BaseTool + middlewares struct { + invokable []InvokableToolMiddleware + streamable []StreamableToolMiddleware + enhancedInvokable []EnhancedInvokableToolMiddleware + enhancedStreamable []EnhancedStreamableToolMiddleware + } + aliasConfigs map[string]ToolAliasConfig +} + +func (t *toolsTuple) applyAliasConfigs(aliasConfigs map[string]ToolAliasConfig) error { + t.argsAliasMap = make(map[string]map[string]string) + + sortedToolNames := make([]string, 0, len(aliasConfigs)) + for toolName := range aliasConfigs { + sortedToolNames = append(sortedToolNames, toolName) + } + sort.Strings(sortedToolNames) + + for _, toolName := range sortedToolNames { + aliasConfig := aliasConfigs[toolName] + var ( + toolIdx int + exists bool + ) + if toolIdx, exists = t.indexes[toolName]; !exists { + continue + } + + if err := t.applyNameAliases(toolName, toolIdx, aliasConfig.NameAliases); err != nil { + return err + } + + if err := t.applyArgsAliases(toolName, toolIdx, aliasConfig.ArgumentsAliases); err != nil { + return err + } + } + + return nil +} + +// applyNameAliases validates and registers name aliases for a single tool into the indexes map. +func (t *toolsTuple) applyNameAliases(toolName string, toolIdx int, nameAliases []string) error { + for _, alias := range nameAliases { + if strings.TrimSpace(alias) == "" { + return fmt.Errorf("tool '%s' has empty name alias", toolName) + } + if existingIdx, conflict := t.indexes[alias]; conflict { + if existingIdx != toolIdx { + conflictToolName := t.canonicalNames[existingIdx] + if alias == conflictToolName { + return fmt.Errorf("tool '%s': name alias '%s' conflicts with existing tool's canonical name", toolName, alias) + } + return fmt.Errorf("tool '%s': name alias '%s' conflicts with an alias already registered for tool '%s'", toolName, alias, conflictToolName) + } + continue + } + t.indexes[alias] = toolIdx + } + return nil +} + +// applyArgsAliases validates argument aliases against the tool schema and builds a reverse alias map for a single tool. +func (t *toolsTuple) applyArgsAliases(toolName string, toolIdx int, argumentsAliases map[string][]string) error { + if len(argumentsAliases) == 0 { + return nil + } + + schemaKeys := make(map[string]bool) + if info := t.toolInfos[toolIdx]; info != nil && info.ParamsOneOf != nil { + js, err := info.ParamsOneOf.ToJSONSchema() + if err != nil { + return fmt.Errorf("tool '%s': failed to parse JSON schema for alias validation: %w", toolName, err) + } + if js != nil && js.Properties != nil { + for pair := js.Properties.Oldest(); pair != nil; pair = pair.Next() { + schemaKeys[pair.Key] = true + } + } + } + + reverseMap := make(map[string]string) + sortedCanonicals := make([]string, 0, len(argumentsAliases)) + for canonical := range argumentsAliases { + sortedCanonicals = append(sortedCanonicals, canonical) + } + sort.Strings(sortedCanonicals) + + for _, canonical := range sortedCanonicals { + aliases := argumentsAliases[canonical] + if strings.TrimSpace(canonical) == "" { + return fmt.Errorf("tool '%s' has empty canonical argument key", toolName) + } + if strings.Contains(canonical, ".") { + return fmt.Errorf("tool '%s' has unsupported '.' in canonical argument key '%s': nested field matching is not yet supported", + toolName, canonical) + } + for _, alias := range aliases { + if strings.TrimSpace(alias) == "" { + return fmt.Errorf("tool '%s' has empty argument alias for canonical key '%s'", toolName, canonical) + } + if schemaKeys[alias] { + return fmt.Errorf("tool '%s' has arg alias '%s' that conflicts with existing schema property '%s'", + toolName, alias, alias) + } + if existingCanonical, conflict := reverseMap[alias]; conflict { + return fmt.Errorf("tool '%s' has conflicting arg alias '%s' mapped to both '%s' and '%s'", + toolName, alias, existingCanonical, canonical) + } + reverseMap[alias] = canonical + } + } + t.argsAliasMap[toolName] = reverseMap + + return nil +} + +func convTools(ctx context.Context, params convToolsParams) (*toolsTuple, error) { + ret := &toolsTuple{ + indexes: make(map[string]int), + meta: make([]*executorMeta, len(params.tools)), + endpoints: make([]InvokableToolEndpoint, len(params.tools)), + streamEndpoints: make([]StreamableToolEndpoint, len(params.tools)), + enhancedInvokableEndpoints: make([]EnhancedInvokableToolEndpoint, len(params.tools)), + enhancedStreamableEndpoints: make([]EnhancedStreamableToolEndpoint, len(params.tools)), + canonicalNames: make([]string, len(params.tools)), + toolInfos: make([]*schema.ToolInfo, len(params.tools)), + } + for idx, bt := range params.tools { + tl, err := bt.Info(ctx) + if err != nil { + return nil, fmt.Errorf("(NewToolNode) failed to get tool info at idx= %d: %w", idx, err) + } + + toolName := tl.Name + var ( + st tool.StreamableTool + it tool.InvokableTool + eiTool tool.EnhancedInvokableTool + esTool tool.EnhancedStreamableTool + + invokable InvokableToolEndpoint + streamable StreamableToolEndpoint + enhancedInvokable EnhancedInvokableToolEndpoint + enhancedStreamable EnhancedStreamableToolEndpoint + + ok bool + meta *executorMeta + ) + + meta = parseExecutorInfoFromComponent(components.ComponentOfTool, bt) + + if st, ok = bt.(tool.StreamableTool); ok { + streamable = wrapStreamToolCall(st, params.middlewares.streamable, !meta.isComponentCallbackEnabled) + } + + if it, ok = bt.(tool.InvokableTool); ok { + invokable = wrapToolCall(it, params.middlewares.invokable, !meta.isComponentCallbackEnabled) + } + + if eiTool, ok = bt.(tool.EnhancedInvokableTool); ok { + enhancedInvokable = wrapEnhancedInvokableToolCall(eiTool, params.middlewares.enhancedInvokable, !meta.isComponentCallbackEnabled) + } + + if esTool, ok = bt.(tool.EnhancedStreamableTool); ok { + enhancedStreamable = wrapEnhancedStreamableToolCall(esTool, params.middlewares.enhancedStreamable, !meta.isComponentCallbackEnabled) + } + + if st == nil && it == nil && eiTool == nil && esTool == nil { + return nil, fmt.Errorf("tool %s is not invokable, streamable, enhanced invokable or enhanced streamable", toolName) + } + if streamable == nil && invokable != nil { + streamable = invokableToStreamable(invokable) + } + if invokable == nil && streamable != nil { + invokable = streamableToInvokable(streamable) + } + + if enhancedStreamable == nil && enhancedInvokable != nil { + enhancedStreamable = enhancedInvokableToEnhancedStreamable(enhancedInvokable) + } + if enhancedInvokable == nil && enhancedStreamable != nil { + enhancedInvokable = enhancedStreamableToEnhancedInvokable(enhancedStreamable) + } + + ret.indexes[toolName] = idx + ret.meta[idx] = meta + ret.endpoints[idx] = invokable + ret.streamEndpoints[idx] = streamable + ret.enhancedInvokableEndpoints[idx] = enhancedInvokable + ret.enhancedStreamableEndpoints[idx] = enhancedStreamable + ret.canonicalNames[idx] = toolName + ret.toolInfos[idx] = tl + } + + if len(params.aliasConfigs) > 0 { + if err := ret.applyAliasConfigs(params.aliasConfigs); err != nil { + return nil, err + } + } + + return ret, nil +} + +func wrapToolCall(it tool.InvokableTool, middlewares []InvokableToolMiddleware, needCallback bool) InvokableToolEndpoint { + middleware := func(next InvokableToolEndpoint) InvokableToolEndpoint { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } + if needCallback { + it = &invokableToolWithCallback{it: it} + } + return middleware(func(ctx context.Context, input *ToolInput) (*ToolOutput, error) { + result, err := it.InvokableRun(ctx, input.Arguments, input.CallOptions...) + if err != nil { + return nil, err + } + return &ToolOutput{Result: result}, nil + }) +} + +func wrapStreamToolCall(st tool.StreamableTool, middlewares []StreamableToolMiddleware, needCallback bool) StreamableToolEndpoint { + middleware := func(next StreamableToolEndpoint) StreamableToolEndpoint { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } + if needCallback { + st = &streamableToolWithCallback{st: st} + } + return middleware(func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) { + result, err := st.StreamableRun(ctx, input.Arguments, input.CallOptions...) + if err != nil { + return nil, err + } + return &StreamToolOutput{Result: result}, nil + }) +} + +func wrapEnhancedInvokableToolCall(eiTool tool.EnhancedInvokableTool, middlewares []EnhancedInvokableToolMiddleware, needCallback bool) EnhancedInvokableToolEndpoint { + middleware := func(next EnhancedInvokableToolEndpoint) EnhancedInvokableToolEndpoint { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } + if needCallback { + eiTool = &enhancedInvokableToolWithCallback{eiTool: eiTool} + } + return middleware(func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) { + result, err := eiTool.InvokableRun(ctx, &schema.ToolArgument{Text: input.Arguments}, input.CallOptions...) + if err != nil { + return nil, err + } + return &EnhancedInvokableToolOutput{Result: result}, nil + }) +} + +func wrapEnhancedStreamableToolCall(est tool.EnhancedStreamableTool, middlewares []EnhancedStreamableToolMiddleware, needCallback bool) EnhancedStreamableToolEndpoint { + middleware := func(next EnhancedStreamableToolEndpoint) EnhancedStreamableToolEndpoint { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } + if needCallback { + est = &enhancedStreamableToolWithCallback{est: est} + } + return middleware(func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) { + result, err := est.StreamableRun(ctx, &schema.ToolArgument{Text: input.Arguments}, input.CallOptions...) + if err != nil { + return nil, err + } + return &EnhancedStreamableToolOutput{Result: result}, nil + }) +} + +type invokableToolWithCallback struct { + it tool.InvokableTool +} + +func (i *invokableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) { + return i.it.Info(ctx) +} + +func (i *invokableToolWithCallback) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + return invokeWithCallbacks(i.it.InvokableRun)(ctx, argumentsInJSON, opts...) +} + +type streamableToolWithCallback struct { + st tool.StreamableTool +} + +func (s *streamableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) { + return s.st.Info(ctx) +} + +func (s *streamableToolWithCallback) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + return streamWithCallbacks(s.st.StreamableRun)(ctx, argumentsInJSON, opts...) +} + +type enhancedInvokableToolWithCallback struct { + eiTool tool.EnhancedInvokableTool +} + +func (e *enhancedInvokableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) { + return e.eiTool.Info(ctx) +} + +func (e *enhancedInvokableToolWithCallback) InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + return invokeEnhancedWithCallbacks(e.eiTool.InvokableRun)(ctx, toolArgument, opts...) +} + +type enhancedStreamableToolWithCallback struct { + est tool.EnhancedStreamableTool +} + +func (e *enhancedStreamableToolWithCallback) Info(ctx context.Context) (*schema.ToolInfo, error) { + return e.est.Info(ctx) +} + +func (e *enhancedStreamableToolWithCallback) StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return streamEnhancedWithCallbacks(e.est.StreamableRun)(ctx, toolArgument, opts...) +} + +func streamableToInvokable(e StreamableToolEndpoint) InvokableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*ToolOutput, error) { + so, err := e(ctx, input) + if err != nil { + return nil, err + } + o, err := concatStreamReader(so.Result) + if err != nil { + return nil, fmt.Errorf("failed to concat StreamableTool output message stream: %w", err) + } + return &ToolOutput{Result: o}, nil + } +} + +func invokableToStreamable(e InvokableToolEndpoint) StreamableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) { + o, err := e(ctx, input) + if err != nil { + return nil, err + } + return &StreamToolOutput{Result: schema.StreamReaderFromArray([]string{o.Result})}, nil + } +} + +func enhancedStreamableToEnhancedInvokable(e EnhancedStreamableToolEndpoint) EnhancedInvokableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) { + so, err := e(ctx, input) + if err != nil { + return nil, err + } + o, err := concatStreamReader(so.Result) + if err != nil { + return nil, fmt.Errorf("failed to concat EnhancedStreamableTool output message stream: %w", err) + } + return &EnhancedInvokableToolOutput{Result: o}, nil + } +} + +func enhancedInvokableToEnhancedStreamable(e EnhancedInvokableToolEndpoint) EnhancedStreamableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) { + o, err := e(ctx, input) + if err != nil { + return nil, err + } + return &EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{o.Result})}, nil + } +} + +func invokeEnhancedWithCallbacks(i func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error)) func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) { + return runWithCallbacks(i, onStart[*schema.ToolArgument], onEnd[*schema.ToolResult], onError) +} + +func streamEnhancedWithCallbacks(s func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error)) func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return runWithCallbacks(s, onStart[*schema.ToolArgument], onEndWithStreamOutput[*schema.ToolResult], onError) +} + +type toolCallTask struct { + // in + endpoint InvokableToolEndpoint + streamEndpoint StreamableToolEndpoint + enhancedInvokableEndpoint EnhancedInvokableToolEndpoint + enhancedStreamableEndpoint EnhancedStreamableToolEndpoint + meta *executorMeta + name string + arg string + callID string + useEnhanced bool + + // out + executed bool + output string + sOutput *schema.StreamReader[string] + enhancedOutput *schema.ToolResult + enhancedSOutput *schema.StreamReader[*schema.ToolResult] + err error +} + +func (tn *ToolsNode) genToolCallTasks(ctx context.Context, tuple *toolsTuple, + input *schema.Message, executedTools map[string]string, executedEnhancedTools map[string]*schema.ToolResult, isStream bool) ([]toolCallTask, error) { + + if input.Role != schema.Assistant { + return nil, fmt.Errorf("expected message role is Assistant, got %s", input.Role) + } + + n := len(input.ToolCalls) + if n == 0 { + return nil, errors.New("no tool call found in input message") + } + + toolCallTasks := make([]toolCallTask, n) + + for i := 0; i < n; i++ { + toolCall := input.ToolCalls[i] + if enhancedResult, executed := executedEnhancedTools[toolCall.ID]; executed { + toolCallTasks[i].name = toolCall.Function.Name + toolCallTasks[i].arg = toolCall.Function.Arguments + toolCallTasks[i].callID = toolCall.ID + toolCallTasks[i].executed = true + toolCallTasks[i].useEnhanced = true + if isStream { + toolCallTasks[i].enhancedSOutput = schema.StreamReaderFromArray([]*schema.ToolResult{enhancedResult}) + } else { + toolCallTasks[i].enhancedOutput = enhancedResult + } + continue + } + if result, executed := executedTools[toolCall.ID]; executed { + toolCallTasks[i].name = toolCall.Function.Name + toolCallTasks[i].arg = toolCall.Function.Arguments + toolCallTasks[i].callID = toolCall.ID + toolCallTasks[i].executed = true + toolCallTasks[i].useEnhanced = false + if isStream { + toolCallTasks[i].sOutput = schema.StreamReaderFromArray([]string{result}) + } else { + toolCallTasks[i].output = result + } + continue + } + index, ok := tuple.indexes[toolCall.Function.Name] + if !ok { + if tn.unknownToolHandler == nil { + return nil, fmt.Errorf("tool %s not found in toolsNode indexes", toolCall.Function.Name) + } + toolCallTasks[i] = newUnknownToolTask(toolCall.Function.Name, toolCall.Function.Arguments, toolCall.ID, tn.unknownToolHandler) + } else { + toolCallTasks[i].meta = tuple.meta[index] + toolCallTasks[i].name = toolCall.Function.Name + toolCallTasks[i].callID = toolCall.ID + + if tuple.enhancedInvokableEndpoints[index] != nil && tuple.enhancedStreamableEndpoints[index] != nil { + toolCallTasks[i].enhancedInvokableEndpoint = tuple.enhancedInvokableEndpoints[index] + toolCallTasks[i].enhancedStreamableEndpoint = tuple.enhancedStreamableEndpoints[index] + toolCallTasks[i].useEnhanced = true + } else { + toolCallTasks[i].endpoint = tuple.endpoints[index] + toolCallTasks[i].streamEndpoint = tuple.streamEndpoints[index] + toolCallTasks[i].useEnhanced = false + } + + // Get canonical tool name for looking up argument aliases + canonicalToolName := tuple.canonicalNames[index] + + // Process argument aliases remapping + args := toolCall.Function.Arguments + if aliasMap, hasAliases := tuple.argsAliasMap[canonicalToolName]; hasAliases { + remappedArgs, err := remapArgs(args, aliasMap) + if err != nil { + return nil, fmt.Errorf("failed to remap args for tool[name:%s]: %w", canonicalToolName, err) + } + args = remappedArgs + } + + if tn.toolArgumentsHandler != nil { + arg, err := tn.toolArgumentsHandler(ctx, canonicalToolName, args) + if err != nil { + return nil, fmt.Errorf("failed to executed tool[name:%s arguments:%s] arguments handler: %w", toolCall.Function.Name, args, err) + } + toolCallTasks[i].arg = arg + } else { + toolCallTasks[i].arg = args + } + } + } + + return toolCallTasks, nil +} + +func newUnknownToolTask(name, arg, callID string, unknownToolHandler func(ctx context.Context, name, input string) (string, error)) toolCallTask { + endpoint := func(ctx context.Context, input *ToolInput) (*ToolOutput, error) { + result, err := unknownToolHandler(ctx, input.Name, input.Arguments) + if err != nil { + return nil, err + } + return &ToolOutput{ + Result: result, + }, nil + } + return toolCallTask{ + endpoint: endpoint, + streamEndpoint: invokableToStreamable(endpoint), + meta: &executorMeta{ + component: components.ComponentOfTool, + isComponentCallbackEnabled: false, + componentImplType: "UnknownTool", + }, + name: name, + arg: arg, + callID: callID, + } +} + +func runToolCallTaskByInvoke(ctx context.Context, task *toolCallTask, opts ...tool.Option) { + if task.executed { + return + } + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{ + Name: task.name, + Type: task.meta.componentImplType, + Component: task.meta.component, + }) + + ctx = setToolCallInfo(ctx, &toolCallInfo{toolCallID: task.callID}) + ctx = appendToolAddressSegment(ctx, task.name, task.callID) + + if task.useEnhanced { + enhancedOutput, err := task.enhancedInvokableEndpoint(ctx, &ToolInput{ + Name: task.name, + Arguments: task.arg, + CallID: task.callID, + CallOptions: opts, + }) + if err != nil { + task.err = err + } else { + task.enhancedOutput = enhancedOutput.Result + task.executed = true + } + } else { + output, err := task.endpoint(ctx, &ToolInput{ + Name: task.name, + Arguments: task.arg, + CallID: task.callID, + CallOptions: opts, + }) + if err != nil { + task.err = err + } else { + task.output = output.Result + task.executed = true + } + } +} + +func runToolCallTaskByStream(ctx context.Context, task *toolCallTask, opts ...tool.Option) { + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{ + Name: task.name, + Type: task.meta.componentImplType, + Component: task.meta.component, + }) + + ctx = setToolCallInfo(ctx, &toolCallInfo{toolCallID: task.callID}) + ctx = appendToolAddressSegment(ctx, task.name, task.callID) + + if task.useEnhanced { + enhancedOutput, err := task.enhancedStreamableEndpoint(ctx, &ToolInput{ + Name: task.name, + Arguments: task.arg, + CallID: task.callID, + CallOptions: opts, + }) + if err != nil { + task.err = err + } else { + task.enhancedSOutput = enhancedOutput.Result + task.executed = true + } + } else { + output, err := task.streamEndpoint(ctx, &ToolInput{ + Name: task.name, + Arguments: task.arg, + CallID: task.callID, + CallOptions: opts, + }) + if err != nil { + task.err = err + } else { + task.sOutput = output.Result + task.executed = true + } + } +} + +func sequentialRunToolCall(ctx context.Context, + run func(ctx2 context.Context, callTask *toolCallTask, opts ...tool.Option), + tasks []toolCallTask, opts ...tool.Option) { + + for i := range tasks { + if tasks[i].executed { + continue + } + run(ctx, &tasks[i], opts...) + } +} + +func parallelRunToolCall(ctx context.Context, + run func(ctx2 context.Context, callTask *toolCallTask, opts ...tool.Option), + tasks []toolCallTask, opts ...tool.Option) { + + if len(tasks) == 1 { + run(ctx, &tasks[0], opts...) + return + } + + var wg sync.WaitGroup + for i := 1; i < len(tasks); i++ { + if tasks[i].executed { + continue + } + wg.Add(1) + go func(ctx_ context.Context, t *toolCallTask, opts ...tool.Option) { + defer wg.Done() + defer func() { + panicErr := recover() + if panicErr != nil { + t.err = safe.NewPanicErr(panicErr, debug.Stack()) + } + }() + run(ctx_, t, opts...) + }(ctx, &tasks[i], opts...) + } + + if !tasks[0].executed { + run(ctx, &tasks[0], opts...) + } + + wg.Wait() +} + +// buildTupleFromOpts rebuilds a toolsTuple when call options override tools or aliases. +func (tn *ToolsNode) buildTupleFromOpts(ctx context.Context, opt *toolsNodeOptions) (*toolsTuple, error) { + tools := opt.ToolList + if tools == nil { + tools = tn.tools + } + aliasConfigs := opt.ToolAliases + if aliasConfigs == nil { + aliasConfigs = tn.toolAliasConfigs + } + p := convToolsParams{ + tools: tools, + aliasConfigs: aliasConfigs, + } + p.middlewares.invokable = tn.toolCallMiddlewares + p.middlewares.streamable = tn.streamToolCallMiddlewares + p.middlewares.enhancedInvokable = tn.enhancedToolCallMiddlewares + p.middlewares.enhancedStreamable = tn.enhancedStreamToolCallMiddlewares + tuple, err := convTools(ctx, p) + if err != nil { + return nil, fmt.Errorf("failed to convert tool list from call option: %w", err) + } + return tuple, nil +} + +// Invoke calls the tools and collects the results of invokable tools. +// it's parallel if there are multiple tool calls in the input message. +func (tn *ToolsNode) Invoke(ctx context.Context, input *schema.Message, + opts ...ToolsNodeOption) ([]*schema.Message, error) { + + opt := getToolsNodeOptions(opts...) + tuple := tn.tuple + if opt.ToolList != nil || opt.ToolAliases != nil { + var err error + tuple, err = tn.buildTupleFromOpts(ctx, opt) + if err != nil { + return nil, err + } + } + + var executedTools map[string]string + var executedEnhancedTools map[string]*schema.ToolResult + if wasInterrupted, hasState, tnState := GetInterruptState[*toolsInterruptAndRerunState](ctx); wasInterrupted && hasState { + input = tnState.Input + if tnState.ExecutedTools != nil { + executedTools = tnState.ExecutedTools + } + if tnState.ExecutedEnhancedTools != nil { + executedEnhancedTools = tnState.ExecutedEnhancedTools + } + } + + tasks, err := tn.genToolCallTasks(ctx, tuple, input, executedTools, executedEnhancedTools, false) + if err != nil { + return nil, err + } + + if tn.executeSequentially { + sequentialRunToolCall(ctx, runToolCallTaskByInvoke, tasks, opt.ToolOptions...) + } else { + parallelRunToolCall(ctx, runToolCallTaskByInvoke, tasks, opt.ToolOptions...) + } + + n := len(tasks) + output := make([]*schema.Message, n) + + rerunExtra := &ToolsInterruptAndRerunExtra{ + ToolCalls: input.ToolCalls, + ExecutedTools: make(map[string]string), + ExecutedEnhancedTools: make(map[string]*schema.ToolResult), + RerunExtraMap: make(map[string]any), + } + rerunState := &toolsInterruptAndRerunState{ + Input: input, + ExecutedTools: make(map[string]string), + ExecutedEnhancedTools: make(map[string]*schema.ToolResult), + } + + var errs []error + for i := 0; i < n; i++ { + if tasks[i].err != nil { + info, ok := IsInterruptRerunError(tasks[i].err) + if !ok { + return nil, fmt.Errorf("failed to invoke tool[name:%s id:%s]: %w", tasks[i].name, tasks[i].callID, tasks[i].err) + } + + rerunExtra.RerunTools = append(rerunExtra.RerunTools, tasks[i].callID) + rerunState.RerunTools = append(rerunState.RerunTools, tasks[i].callID) + if info != nil { + rerunExtra.RerunExtraMap[tasks[i].callID] = info + } + + iErr := WrapInterruptAndRerunIfNeeded(ctx, + AddressSegment{ID: tasks[i].callID, Type: AddressSegmentTool}, tasks[i].err) + errs = append(errs, iErr) + continue + } + if tasks[i].executed { + if tasks[i].useEnhanced { + rerunExtra.ExecutedEnhancedTools[tasks[i].callID] = tasks[i].enhancedOutput + rerunState.ExecutedEnhancedTools[tasks[i].callID] = tasks[i].enhancedOutput + } else { + rerunExtra.ExecutedTools[tasks[i].callID] = tasks[i].output + rerunState.ExecutedTools[tasks[i].callID] = tasks[i].output + } + } + + if len(errs) == 0 { + if tasks[i].useEnhanced { + output[i] = schema.ToolMessage("", tasks[i].callID, schema.WithToolName(tasks[i].name)) + output[i].UserInputMultiContent, err = tasks[i].enhancedOutput.ToMessageInputParts() + if err != nil { + return nil, err + } + } else { + output[i] = schema.ToolMessage(tasks[i].output, tasks[i].callID, schema.WithToolName(tasks[i].name)) + } + + } + } + if len(errs) > 0 { + return nil, CompositeInterrupt(ctx, rerunExtra, rerunState, errs...) + } + + return output, nil +} + +// Stream calls the tools and collects the results of stream readers. +// it's parallel if there are multiple tool calls in the input message. +func (tn *ToolsNode) Stream(ctx context.Context, input *schema.Message, + opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.Message], error) { + + opt := getToolsNodeOptions(opts...) + tuple := tn.tuple + if opt.ToolList != nil || opt.ToolAliases != nil { + var err error + tuple, err = tn.buildTupleFromOpts(ctx, opt) + if err != nil { + return nil, err + } + } + + var executedTools map[string]string + var executedEnhancedTools map[string]*schema.ToolResult + if wasInterrupted, hasState, tnState := GetInterruptState[*toolsInterruptAndRerunState](ctx); wasInterrupted && hasState { + input = tnState.Input + if tnState.ExecutedTools != nil { + executedTools = tnState.ExecutedTools + } + if tnState.ExecutedEnhancedTools != nil { + executedEnhancedTools = tnState.ExecutedEnhancedTools + } + } + + tasks, err := tn.genToolCallTasks(ctx, tuple, input, executedTools, executedEnhancedTools, true) + if err != nil { + return nil, err + } + + if tn.executeSequentially { + sequentialRunToolCall(ctx, runToolCallTaskByStream, tasks, opt.ToolOptions...) + } else { + parallelRunToolCall(ctx, runToolCallTaskByStream, tasks, opt.ToolOptions...) + } + + n := len(tasks) + + rerunExtra := &ToolsInterruptAndRerunExtra{ + ToolCalls: input.ToolCalls, + ExecutedTools: make(map[string]string), + ExecutedEnhancedTools: make(map[string]*schema.ToolResult), + RerunExtraMap: make(map[string]any), + } + rerunState := &toolsInterruptAndRerunState{ + Input: input, + ExecutedTools: make(map[string]string), + ExecutedEnhancedTools: make(map[string]*schema.ToolResult), + } + var errs []error + // check rerun + for i := 0; i < n; i++ { + if tasks[i].err != nil { + info, ok := IsInterruptRerunError(tasks[i].err) + if !ok { + return nil, fmt.Errorf("failed to stream tool call %s: %w", tasks[i].callID, tasks[i].err) + } + + rerunExtra.RerunTools = append(rerunExtra.RerunTools, tasks[i].callID) + rerunState.RerunTools = append(rerunState.RerunTools, tasks[i].callID) + if info != nil { + rerunExtra.RerunExtraMap[tasks[i].callID] = info + } + iErr := WrapInterruptAndRerunIfNeeded(ctx, + AddressSegment{ID: tasks[i].callID, Type: AddressSegmentTool}, tasks[i].err) + errs = append(errs, iErr) + continue + } + } + + if len(errs) > 0 { + // concat and save tool output + for _, t := range tasks { + if t.executed { + if t.useEnhanced { + eo, err_ := concatStreamReader(t.enhancedSOutput) + if err_ != nil { + return nil, fmt.Errorf("failed to concat enhanced tool[name:%s id:%s]'s stream output: %w", t.name, t.callID, err_) + } + rerunExtra.ExecutedEnhancedTools[t.callID] = eo + rerunState.ExecutedEnhancedTools[t.callID] = eo + + } else { + o, err_ := concatStreamReader(t.sOutput) + if err_ != nil { + return nil, fmt.Errorf("failed to concat tool[name:%s id:%s]'s stream output: %w", t.name, t.callID, err_) + } + rerunExtra.ExecutedTools[t.callID] = o + rerunState.ExecutedTools[t.callID] = o + } + } + } + return nil, CompositeInterrupt(ctx, rerunExtra, rerunState, errs...) + } + + // common return + sOutput := make([]*schema.StreamReader[[]*schema.Message], n) + for i := 0; i < n; i++ { + index := i + callID := tasks[i].callID + callName := tasks[i].name + if tasks[i].useEnhanced { + cvt := func(tr *schema.ToolResult) ([]*schema.Message, error) { + ret := make([]*schema.Message, n) + ret[index] = schema.ToolMessage("", callID, schema.WithToolName(callName)) + ret[index].UserInputMultiContent, err = tr.ToMessageInputParts() + if err != nil { + return nil, err + } + return ret, nil + } + sOutput[i] = schema.StreamReaderWithConvert(tasks[i].enhancedSOutput, cvt) + } else { + cvt := func(s string) ([]*schema.Message, error) { + ret := make([]*schema.Message, n) + ret[index] = schema.ToolMessage(s, callID, schema.WithToolName(callName)) + return ret, nil + } + sOutput[i] = schema.StreamReaderWithConvert(tasks[i].sOutput, cvt) + } + + } + return schema.MergeStreamReaders(sOutput), nil +} + +// GetType returns the component type string for the Tools node. +func (tn *ToolsNode) GetType() string { + return "" +} + +func getToolsNodeOptions(opts ...ToolsNodeOption) *toolsNodeOptions { + o := &toolsNodeOptions{ + ToolOptions: make([]tool.Option, 0), + } + for _, opt := range opts { + opt(o) + } + return o +} + +type toolCallInfoKey struct{} +type toolCallInfo struct { + toolCallID string +} + +func setToolCallInfo(ctx context.Context, toolCallInfo *toolCallInfo) context.Context { + return context.WithValue(ctx, toolCallInfoKey{}, toolCallInfo) +} + +// GetToolCallID gets the current tool call id from the context. +func GetToolCallID(ctx context.Context) string { + v := ctx.Value(toolCallInfoKey{}) + if v == nil { + return "" + } + + info, ok := v.(*toolCallInfo) + if !ok { + return "" + } + + return info.toolCallID +} diff --git a/compose/tool_node_test.go b/compose/tool_node_test.go new file mode 100644 index 0000000..e24b42b --- /dev/null +++ b/compose/tool_node_test.go @@ -0,0 +1,1310 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "io" + "strconv" + "strings" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +const ( + toolNameOfUserCompany = "user_company" + toolIDOfUserCompany = "call_TRZhlagwBS0LpWbWPeZOvIXc" + + toolNameOfUserSalary = "user_salary" + toolIDOfUserSalary = "call_AqfoRW6fuF98k0o7696k2nzm" +) + +func TestToolsNode(t *testing.T) { + var err error + ctx := context.Background() + + userCompanyToolInfo := &schema.ToolInfo{ + Name: toolNameOfUserCompany, + Desc: "Query user's company and position information based on user's name and email", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Type: "string", + Desc: "User's name", + }, + "email": { + Type: "string", + Desc: "User's email", + }, + }), + } + + userSalaryToolInfo := &schema.ToolInfo{ + Name: toolNameOfUserSalary, + Desc: "Query user's salary information based on user's name and email", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Type: "string", + Desc: "User's name", + }, + "email": { + Type: "string", + Desc: "User's email", + }, + }), + } + + t.Run("success", func(t *testing.T) { + const ( + nodeOfTools = "tools" + nodeOfModel = "model" + ) + g := NewGraph[[]*schema.Message, []*schema.Message]() + + err = g.AddChatModelNode(nodeOfModel, &mockIntentChatModel{}) + assert.NoError(t, err) + + ui := newTool(userCompanyToolInfo, queryUserCompany) + us := newStreamableTool(userSalaryToolInfo, queryUserSalary) + + toolsNode, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{ui, us}, + }) + assert.NoError(t, err) + + err = g.AddToolsNode(nodeOfTools, toolsNode) + assert.NoError(t, err) + + err = g.AddEdge(START, nodeOfModel) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfModel, nodeOfTools) + assert.NoError(t, err) + + err = g.AddEdge(nodeOfTools, END) + assert.NoError(t, err) + + r, err := g.Compile(ctx) + assert.NoError(t, err) + + out, err := r.Invoke(ctx, []*schema.Message{}) + assert.NoError(t, err) + + msg := findMsgByToolCallID(out, toolIDOfUserCompany) + assert.Equal(t, toolIDOfUserCompany, msg.ToolCallID) + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","gender":"male","company":"bytedance","position":"CEO"}`, + msg.Content) + + msg = findMsgByToolCallID(out, toolIDOfUserSalary) + assert.Equal(t, toolIDOfUserSalary, msg.ToolCallID) + assert.Contains(t, msg.Content, + `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":5000}{"user_id":"zhangsan-zhangsan@bytedance.com","salary":3000}{"user_id":"zhangsan-zhangsan@bytedance.com","salary":2000}`) + + // 测试流式调用 + reader, err := r.Stream(ctx, []*schema.Message{}) + assert.NoError(t, err) + loops := 0 + userSalaryTimes := 0 + + defer reader.Close() + + var arrMsgs [][]*schema.Message + for ; loops < 10; loops++ { + msgs, err := reader.Recv() + if err == io.EOF { + break + } + + arrMsgs = append(arrMsgs, msgs) + + assert.NoError(t, err) + + assert.Len(t, msgs, 2) + if msg := findMsgByToolCallID(out, toolIDOfUserCompany); msg != nil { + assert.Equal(t, schema.Tool, msg.Role) + assert.Equal(t, toolIDOfUserCompany, msg.ToolCallID) + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","gender":"male","company":"bytedance","position":"CEO"}`, + msg.Content) + } else if msg := findMsgByToolCallID(out, toolIDOfUserSalary); msg != nil { + assert.Equal(t, schema.Tool, msg.Role) + assert.Equal(t, toolIDOfUserSalary, msg.ToolCallID) + + switch userSalaryTimes { + case 0: + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":5000}`, + msg.Content) + case 1: + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":3000}`, + msg.Content) + case 2: + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":2000}`, + msg.Content) + } + + userSalaryTimes++ + } else { + assert.Fail(t, "unexpected tool name") + } + } + + assert.Equal(t, 4, loops) + + msgs, err_ := schema.ConcatMessageArray(arrMsgs) + assert.NoError(t, err_) + msg = findMsgByToolCallID(msgs, toolIDOfUserCompany) + msg = findMsgByToolCallID(msgs, toolIDOfUserSalary) + + sr, sw := schema.Pipe[[]*schema.Message](2) + sw.Send([]*schema.Message{ + { + Role: schema.User, + Content: `hi, how are you`, + }, + }, nil) + sw.Send([]*schema.Message{ + { + Role: schema.User, + Content: `i'm fine'`, + }, + }, nil) + sw.Close() + + reader, err = r.Transform(ctx, sr) + assert.NoError(t, err) + + defer reader.Close() + + loops = 0 + userSalaryTimes = 0 + + for ; loops < 10; loops++ { + msgs, err := reader.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + + assert.Len(t, msgs, 2) + if msg := findMsgByToolCallID(out, toolIDOfUserCompany); msg != nil { + assert.Equal(t, schema.Tool, msg.Role) + assert.Equal(t, toolIDOfUserCompany, msg.ToolCallID) + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","gender":"male","company":"bytedance","position":"CEO"}`, + msg.Content) + } else if msg := findMsgByToolCallID(out, toolIDOfUserSalary); msg != nil { + assert.Equal(t, schema.Tool, msg.Role) + assert.Equal(t, toolIDOfUserSalary, msg.ToolCallID) + + switch userSalaryTimes { + case 0: + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":5000}`, + msg.Content) + case 1: + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":3000}`, + msg.Content) + case 2: + assert.JSONEq(t, `{"user_id":"zhangsan-zhangsan@bytedance.com","salary":2000}`, + msg.Content) + } + + userSalaryTimes++ + } else { + assert.Fail(t, "unexpected tool name") + } + } + + assert.Equal(t, 4, loops) + }) + + t.Run("order_consistency", func(t *testing.T) { + // Create a ToolsNode with multiple tools + ui := newTool(userCompanyToolInfo, queryUserCompany) + us := newTool(userSalaryToolInfo, queryUserSalary) + + toolsNode, err_ := NewToolNode(context.Background(), &ToolsNodeConfig{ + Tools: []tool.BaseTool{ui, us}, + }) + assert.NoError(t, err_) + + // Create an input message with multiple tool calls in a specific order + input := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserSalary, + Function: schema.FunctionCall{ + Name: toolNameOfUserSalary, + Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`, + }, + }, + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: toolNameOfUserCompany, + Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`, + }, + }, + }, + } + + // Invoke the ToolsNode + output, err_ := toolsNode.Invoke(context.Background(), input) + assert.NoError(t, err_) + + // Verify the order of output messages matches the order of input tool calls + assert.Equal(t, 2, len(output)) + assert.Equal(t, toolIDOfUserSalary, output[0].ToolCallID) + assert.Equal(t, toolIDOfUserCompany, output[1].ToolCallID) + + // Test with Stream method as well + streamer, err_ := toolsNode.Stream(context.Background(), input) + assert.NoError(t, err_) + defer streamer.Close() + + // Collect all stream outputs + var streamOutputs [][]*schema.Message + for { + chunk, err__ := streamer.Recv() + if err__ == io.EOF { + break + } + assert.NoError(t, err__) + streamOutputs = append(streamOutputs, chunk) + } + + // Verify each chunk maintains the correct order + for _, chunk := range streamOutputs { + if chunk[0] != nil { + assert.Equal(t, toolIDOfUserSalary, chunk[0].ToolCallID) + } + if chunk[1] != nil { + assert.Equal(t, toolIDOfUserCompany, chunk[1].ToolCallID) + } + } + + // Concatenate all stream outputs and verify final result + concatenated, err_ := schema.ConcatMessageArray(streamOutputs) + assert.NoError(t, err_) + assert.Equal(t, 2, len(concatenated)) + assert.Equal(t, toolIDOfUserSalary, concatenated[0].ToolCallID) + assert.Equal(t, toolIDOfUserCompany, concatenated[1].ToolCallID) + }) +} + +type userCompanyRequest struct { + Name string `json:"name"` + Email string `json:"email"` +} + +type userCompanyResponse struct { + UserID string `json:"user_id"` + Gender string `json:"gender"` + Company string `json:"company"` + Position string `json:"position"` +} + +func queryUserCompany(ctx context.Context, req *userCompanyRequest) (resp *userCompanyResponse, err error) { + callID := GetToolCallID(ctx) + if callID != toolIDOfUserCompany { + return nil, fmt.Errorf("invalid tool call id= %s", callID) + } + + return &userCompanyResponse{ + UserID: fmt.Sprintf("%v-%v", req.Name, req.Email), + Gender: "male", + Company: "bytedance", + Position: "CEO", + }, nil +} + +type userSalaryRequest struct { + Name string `json:"name"` + Email string `json:"email"` +} + +type userSalaryResponse struct { + UserID string `json:"user_id"` + Salary int `json:"salary"` +} + +func queryUserSalary(ctx context.Context, req *userSalaryRequest) (resp *schema.StreamReader[*userSalaryResponse], err error) { + callID := GetToolCallID(ctx) + if callID != toolIDOfUserSalary { + return nil, fmt.Errorf("invalid tool call id= %s", callID) + } + + sr, sw := schema.Pipe[*userSalaryResponse](10) + sw.Send(&userSalaryResponse{ + UserID: fmt.Sprintf("%v-%v", req.Name, req.Email), + Salary: 5000, + }, nil) + + sw.Send(&userSalaryResponse{ + UserID: fmt.Sprintf("%v-%v", req.Name, req.Email), + Salary: 3000, + }, nil) + + sw.Send(&userSalaryResponse{ + UserID: fmt.Sprintf("%v-%v", req.Name, req.Email), + Salary: 2000, + }, nil) + sw.Close() + return sr, nil +} + +type mockIntentChatModel struct{} + +func (m *mockIntentChatModel) BindTools(tools []*schema.ToolInfo) error { + return nil +} + +func (m *mockIntentChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return &schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: toolNameOfUserCompany, + Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`, + }, + }, + { + ID: toolIDOfUserSalary, + Function: schema.FunctionCall{ + Name: toolNameOfUserSalary, + Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`, + }, + }, + }, + }, nil +} + +func (m *mockIntentChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](2) + sw.Send(&schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: toolNameOfUserCompany, + Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`, + }, + }, + }, + }, nil) + + sw.Send(&schema.Message{ + Role: schema.Assistant, + Content: "", + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserSalary, + Function: schema.FunctionCall{ + Name: toolNameOfUserSalary, + Arguments: `{"name": "zhangsan", "email": "zhangsan@bytedance.com"}`, + }, + }, + }, + }, nil) + + sw.Close() + + return sr, nil +} + +func TestToolsNodeOptions(t *testing.T) { + ctx := context.Background() + + t.Run("tool_option", func(t *testing.T) { + + g := NewGraph[*schema.Message, []*schema.Message]() + + mt := &mockTool{} + + tn, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{mt}, + }) + assert.NoError(t, err) + + err = g.AddToolsNode("tools", tn) + assert.NoError(t, err) + + err = g.AddEdge(START, "tools") + assert.NoError(t, err) + err = g.AddEdge("tools", END) + assert.NoError(t, err) + + r, err := g.Compile(ctx) + assert.NoError(t, err) + + out, err := r.Invoke(ctx, &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: "mock_tool", + Arguments: `{"name": "jack"}`, + }, + }, + }, + }, WithToolsNodeOption(WithToolOption(WithAge(10)))) + assert.NoError(t, err) + assert.Len(t, out, 1) + assert.JSONEq(t, `{"echo": "jack: 10"}`, out[0].Content) + + outMessages := make([][]*schema.Message, 0) + outStream, err := r.Stream(ctx, &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: "mock_tool", + Arguments: `{"name": "jack"}`, + }, + }, + }, + }, WithToolsNodeOption(WithToolOption(WithAge(10)))) + + assert.NoError(t, err) + + for { + msgs, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + outMessages = append(outMessages, msgs) + } + outStream.Close() + + msgs, err := internal.ConcatItems(outMessages) + assert.NoError(t, err) + + assert.Len(t, msgs, 1) + assert.JSONEq(t, `{"echo":"jack: 10"}`, msgs[0].Content) + }) + t.Run("tool_list", func(t *testing.T) { + + g := NewGraph[*schema.Message, []*schema.Message]() + + mt := &mockTool{} + + tn, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{}, + }) + assert.NoError(t, err) + + err = g.AddToolsNode("tools", tn) + assert.NoError(t, err) + + err = g.AddEdge(START, "tools") + assert.NoError(t, err) + err = g.AddEdge("tools", END) + assert.NoError(t, err) + + r, err := g.Compile(ctx) + assert.NoError(t, err) + + out, err := r.Invoke(ctx, &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: "mock_tool", + Arguments: `{"name": "jack"}`, + }, + }, + }, + }, WithToolsNodeOption(WithToolList(mt), WithToolOption(WithAge(10)))) + assert.NoError(t, err) + assert.Len(t, out, 1) + assert.JSONEq(t, `{"echo": "jack: 10"}`, out[0].Content) + + outMessages := make([][]*schema.Message, 0) + outStream, err := r.Stream(ctx, &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: toolIDOfUserCompany, + Function: schema.FunctionCall{ + Name: "mock_tool", + Arguments: `{"name": "jack"}`, + }, + }, + }, + }, WithToolsNodeOption(WithToolList(mt), WithToolOption(WithAge(10)))) + + assert.NoError(t, err) + + for { + msgs, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + outMessages = append(outMessages, msgs) + } + outStream.Close() + + msgs, err := internal.ConcatItems(outMessages) + assert.NoError(t, err) + + assert.Len(t, msgs, 1) + assert.JSONEq(t, `{"echo":"jack: 10"}`, msgs[0].Content) + }) + +} + +func findMsgByToolCallID(msgs []*schema.Message, toolCallID string) *schema.Message { + for _, msg := range msgs { + if msg.ToolCallID == toolCallID { + return msg + } + } + + return nil +} + +type mockToolOptions struct { + Age int +} + +func WithAge(age int) tool.Option { + return tool.WrapImplSpecificOptFn(func(o *mockToolOptions) { + o.Age = age + }) +} + +type mockToolRequest struct { + Name string `json:"name"` +} + +type mockToolResponse struct { + Echo string `json:"echo"` +} + +type mockTool struct{} + +func (m *mockTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "mock_tool", + Desc: "mock tool", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Type: "string", + Desc: "name", + Required: true, + }, + }), + }, nil +} + +func (m *mockTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + opt := tool.GetImplSpecificOptions(&mockToolOptions{}, opts...) + + req := &mockToolRequest{} + + if e := sonic.UnmarshalString(argumentsInJSON, req); e != nil { + return "", e + } + + resp := &mockToolResponse{ + Echo: fmt.Sprintf("%v: %v", req.Name, opt.Age), + } + + return sonic.MarshalString(resp) +} + +func (m *mockTool) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + sr, sw := schema.Pipe[string](1) + go func() { + defer sw.Close() + + opt := tool.GetImplSpecificOptions(&mockToolOptions{}, opts...) + + req := &mockToolRequest{} + + if e := sonic.UnmarshalString(argumentsInJSON, req); e != nil { + sw.Send("", e) + return + } + + resp := mockToolResponse{ + Echo: fmt.Sprintf("%v: %v", req.Name, opt.Age), + } + + output, err := sonic.MarshalString(resp) + if err != nil { + sw.Send("", err) + return + } + + for i := 0; i < len(output); i++ { + sw.Send(string(output[i]), nil) + } + }() + + return sr, nil +} + +func TestUnknownTool(t *testing.T) { + ctx := context.Background() + tn, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: nil, + UnknownToolsHandler: func(ctx context.Context, name, input string) (string, error) { + return "unknown", nil + }, + }) + assert.NoError(t, err) + + input := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "1", + Function: schema.FunctionCall{ + Name: "unknown1", + Arguments: `arg1`, + }, + }, + { + ID: "2", + Function: schema.FunctionCall{ + Name: "unknown2", + Arguments: `arg2`, + }, + }, + }, + } + + expected := []*schema.Message{ + { + Role: schema.Tool, + Content: "unknown", + ToolCallID: "1", + ToolName: "unknown1", + }, + { + Role: schema.Tool, + Content: "unknown", + ToolCallID: "2", + ToolName: "unknown2", + }, + } + + result, err := tn.Invoke(ctx, input) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + streamResult, err := tn.Stream(ctx, input) + assert.NoError(t, err) + result = make([]*schema.Message, 2) + for { + chunk, err := streamResult.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + for i := range chunk { + if chunk[i] != nil { + result[i] = chunk[i] + } + } + } + assert.Equal(t, expected, result) +} + +func TestToolRerun(t *testing.T) { + type myToolRerunState struct { + In *schema.Message + } + + schema.Register[myToolRerunState]() + + tc := []schema.ToolCall{ + { + ID: "3", + Function: schema.FunctionCall{ + Name: "tool3", + Arguments: "input", + }, + }, + { + ID: "4", + Function: schema.FunctionCall{ + Name: "tool4", + Arguments: "input", + }, + }, + { + ID: "1", + Function: schema.FunctionCall{ + Name: "tool1", + Arguments: "input", + }, + }, + { + ID: "2", + Function: schema.FunctionCall{ + Name: "tool2", + Arguments: "input", + }, + }, + } + g := NewGraph[*schema.Message, string](WithGenLocalState(func(ctx context.Context) (state *myToolRerunState) { + return &myToolRerunState{In: &schema.Message{Role: schema.Assistant, ToolCalls: tc}} + })) + ctx := context.Background() + tn, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{&myTool1{}, &myTool2{}, &myTool3{t: t}, &myTool4{t: t}}, + }) + assert.NoError(t, err) + assert.NoError(t, g.AddToolsNode("tool node", tn)) + assert.NoError(t, g.AddLambdaNode("lambda", InvokableLambda(func(ctx context.Context, input []*schema.Message) (output string, err error) { + contents := make([]string, len(input)) + for _, m := range input { + callID := m.ToolCallID + callIDInt, err := strconv.Atoi(callID) + if err != nil { + return "", err + } + contents[callIDInt-1] = m.Content + } + sb := strings.Builder{} + for _, m := range contents { + sb.WriteString(m) + } + return sb.String(), nil + }))) + assert.NoError(t, g.AddEdge(START, "tool node")) + assert.NoError(t, g.AddEdge("tool node", "lambda")) + assert.NoError(t, g.AddEdge("lambda", END)) + + r, err := g.Compile(ctx, WithCheckPointStore(&inMemoryStore{m: map[string][]byte{}})) + assert.NoError(t, err) + + _, err = r.Stream(ctx, &schema.Message{Role: schema.Assistant, ToolCalls: tc}, WithCheckPointID("1")) + info, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + assert.Equal(t, []string{"tool node"}, info.RerunNodes) + assert.Equal(t, &ToolsInterruptAndRerunExtra{ + ToolCalls: tc, + RerunTools: []string{"1", "2"}, + RerunExtraMap: map[string]any{"1": "tool1 rerun extra", "2": "tool2 rerun extra"}, + ExecutedTools: map[string]string{ + "3": "tool3 input: input", + "4": "tool4 input: input", + }, + ExecutedEnhancedTools: make(map[string]*schema.ToolResult), + }, info.RerunNodesExtra["tool node"]) + + sr, err := r.Stream(ctx, nil, WithCheckPointID("1")) + assert.NoError(t, err) + result, err := concatStreamReader(sr) + assert.NoError(t, err) + assert.Equal(t, "tool1 input: inputtool2 input: inputtool3 input: inputtool4 input: input", result) +} + +func TestToolMiddleware(t *testing.T) { + ctx := context.Background() + t3 := &myTool3{t: t} + t4 := &myTool4{t: t} + tn, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{t3, t4}, + ToolCallMiddlewares: []ToolMiddleware{ + { + Invokable: func(endpoint InvokableToolEndpoint) InvokableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*ToolOutput, error) { + _, err := endpoint(ctx, input) + if err != nil { + return nil, err + } + return &ToolOutput{Result: "middleware1"}, nil + } + }, + }, + { + Streamable: func(endpoint StreamableToolEndpoint) StreamableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*StreamToolOutput, error) { + _, err := endpoint(ctx, input) + if err != nil { + return nil, err + } + return &StreamToolOutput{Result: schema.StreamReaderFromArray([]string{"middleware2"})}, nil + } + }, + }, + }, + }) + assert.NoError(t, err) + + messages, err := tn.Invoke(ctx, schema.AssistantMessage("", []schema.ToolCall{ + {ID: "1", Function: schema.FunctionCall{Name: "tool3", Arguments: ""}}, + {ID: "2", Function: schema.FunctionCall{Name: "tool4", Arguments: ""}}, + })) + assert.NoError(t, err) + assert.Len(t, messages, 2) + assert.Equal(t, "middleware1", messages[0].Content) + assert.Equal(t, "middleware2", messages[1].Content) + + t3.times, t4.times = 0, 0 // reset t3 t4 + messageStreams, err := tn.Stream(ctx, schema.AssistantMessage("", []schema.ToolCall{ + {ID: "1", Function: schema.FunctionCall{Name: "tool3", Arguments: ""}}, + {ID: "2", Function: schema.FunctionCall{Name: "tool4", Arguments: ""}}, + })) + assert.NoError(t, err) + var messageArray [][]*schema.Message + for { + chunk, err := messageStreams.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + messageArray = append(messageArray, chunk) + } + messages, err = schema.ConcatMessageArray(messageArray) + assert.Len(t, messages, 2) + assert.Equal(t, "middleware1", messages[0].Content) + assert.Equal(t, "middleware2", messages[1].Content) +} + +type myTool1 struct { + times uint +} + +func (m *myTool1) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "tool1"}, nil +} + +func (m *myTool1) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + if m.times == 0 { + m.times++ + return "", tool.Interrupt(ctx, "tool1 rerun extra") + } + return "tool1 input: " + argumentsInJSON, nil +} + +type myTool2 struct { + times uint +} + +func (m *myTool2) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "tool2"}, nil +} + +func (m *myTool2) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + if m.times == 0 { + m.times++ + return nil, tool.Interrupt(ctx, "tool2 rerun extra") + } + return schema.StreamReaderFromArray([]string{"tool2 input: ", argumentsInJSON}), nil +} + +type myTool3 struct { + t *testing.T + times int +} + +func (m *myTool3) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "tool3"}, nil +} + +func (m *myTool3) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + assert.Equal(m.t, 0, m.times) + m.times++ + return "tool3 input: " + argumentsInJSON, nil +} + +type myTool4 struct { + t *testing.T + times int +} + +func (m *myTool4) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "tool4"}, nil +} + +func (m *myTool4) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) { + assert.Equal(m.t, 0, m.times) + m.times++ + return schema.StreamReaderFromArray([]string{"tool4 input: ", argumentsInJSON}), nil +} + +func newTool[I, O any](info *schema.ToolInfo, f func(ctx context.Context, in I) (O, error)) tool.InvokableTool { + return &invokableTool[I, O]{ + info: info, + fn: f, + } +} + +type invokableTool[I, O any] struct { + info *schema.ToolInfo + fn func(ctx context.Context, in I) (O, error) +} + +func (f *invokableTool[I, O]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return f.info, nil +} + +func (f *invokableTool[I, O]) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + t := generic.NewInstance[I]() + err := sonic.UnmarshalString(argumentsInJSON, t) + if err != nil { + return "", err + } + o, err := f.fn(ctx, t) + if err != nil { + return "", err + } + return sonic.MarshalString(o) +} + +func newStreamableTool[I, O any](info *schema.ToolInfo, f func(ctx context.Context, in I) (*schema.StreamReader[O], error)) tool.StreamableTool { + return &streamableTool[I, O]{ + info: info, + fn: f, + } +} + +type streamableTool[I, O any] struct { + info *schema.ToolInfo + fn func(ctx context.Context, in I) (*schema.StreamReader[O], error) +} + +func (f *streamableTool[I, O]) Info(ctx context.Context) (*schema.ToolInfo, error) { + return f.info, nil +} +func (f *streamableTool[I, O]) StreamableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (*schema.StreamReader[string], error) { + t := generic.NewInstance[I]() + err := sonic.UnmarshalString(argumentsInJSON, t) + if err != nil { + return nil, err + } + sr, err := f.fn(ctx, t) + if err != nil { + return nil, err + } + return schema.StreamReaderWithConvert(sr, func(o O) (string, error) { + return sonic.MarshalString(o) + }), nil +} + +type enhancedInvokableTool struct { + info *schema.ToolInfo + fn func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) +} + +func (e *enhancedInvokableTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return e.info, nil +} + +func (e *enhancedInvokableTool) InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) { + return e.fn(ctx, toolArgument) +} + +type enhancedStreamableTool struct { + info *schema.ToolInfo + fn func(ctx context.Context, input *schema.ToolArgument) (*schema.StreamReader[*schema.ToolResult], error) +} + +func (e *enhancedStreamableTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return e.info, nil +} + +func (e *enhancedStreamableTool) StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) { + return e.fn(ctx, toolArgument) +} + +func TestEnhancedToolNode(t *testing.T) { + ctx := context.Background() + + enhancedInvokable := &enhancedInvokableTool{ + info: &schema.ToolInfo{ + Name: "enhanced_invokable_tool", + Desc: "test enhanced invokable tool", + }, + fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "invokable result: " + input.Text}, + }, + }, nil + }, + } + + enhancedStreamable := &enhancedStreamableTool{ + info: &schema.ToolInfo{ + Name: "enhanced_streamable_tool", + Desc: "test enhanced streamable tool", + }, + fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.StreamReader[*schema.ToolResult], error) { + results := []*schema.ToolResult{ + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "stream part 1: " + input.Text}}}, + {Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: " stream part 2"}}}, + } + return schema.StreamReaderFromArray(results), nil + }, + } + + toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{enhancedInvokable, enhancedStreamable}, + }) + assert.NoError(t, err) + assert.NotNil(t, toolNode) + + t.Run("enhanced invokable tool", func(t *testing.T) { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call1", + Function: schema.FunctionCall{ + Name: "enhanced_invokable_tool", + Arguments: "test input", + }, + }, + }) + + output, err := toolNode.Invoke(ctx, input) + assert.NoError(t, err) + assert.Len(t, output, 1) + assert.Equal(t, schema.Tool, output[0].Role) + assert.Equal(t, "call1", output[0].ToolCallID) + }) + + t.Run("enhanced streamable tool", func(t *testing.T) { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call2", + Function: schema.FunctionCall{ + Name: "enhanced_streamable_tool", + Arguments: "test stream", + }, + }, + }) + + streamReader, err := toolNode.Stream(ctx, input) + assert.NoError(t, err) + assert.NotNil(t, streamReader) + + var messages []*schema.Message + for { + chunk, err := streamReader.Recv() + if err != nil { + break + } + if chunk != nil { + messages = append(messages, chunk...) + } + } + message, err := schema.ConcatMessages(messages) + assert.NoError(t, err) + assert.Len(t, messages, 2) + assert.Equal(t, schema.Tool, messages[0].Role) + assert.Equal(t, "call2", messages[0].ToolCallID) + assert.Contains(t, message.UserInputMultiContent[0].Text, "stream part") + }) +} + +func TestEnhancedToolConversion(t *testing.T) { + ctx := context.Background() + + enhancedInvokable := &enhancedInvokableTool{ + info: &schema.ToolInfo{ + Name: "enhanced_only_invokable", + Desc: "test enhanced invokable only", + }, + fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Type: schema.ToolPartTypeText, Text: "enhanced: " + input.Text}, + }, + }, nil + }, + } + + toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{enhancedInvokable}, + }) + assert.NoError(t, err) + + t.Run("enhanced invokable auto-converts to streamable", func(t *testing.T) { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call1", + Function: schema.FunctionCall{ + Name: "enhanced_only_invokable", + Arguments: "test", + }, + }, + }) + + streamReader, err := toolNode.Stream(ctx, input) + assert.NoError(t, err) + assert.NotNil(t, streamReader) + + var messages []*schema.Message + for { + chunk, err := streamReader.Recv() + if err != nil { + break + } + if chunk != nil { + messages = append(messages, chunk...) + } + } + assert.Len(t, messages, 1) + }) +} + +func TestEnhancedToolMiddleware(t *testing.T) { + ctx := context.Background() + + var invokableMiddlewareCalled bool + var streamableMiddlewareCalled bool + + enhancedInvokable := &enhancedInvokableTool{ + info: &schema.ToolInfo{ + Name: "enhanced_tool_with_middleware", + Desc: "test enhanced tool with middleware", + }, + fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Text: "result", Type: schema.ToolPartTypeText}, + }, + }, nil + }, + } + + toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{enhancedInvokable}, + ToolCallMiddlewares: []ToolMiddleware{ + { + EnhancedInvokable: func(next EnhancedInvokableToolEndpoint) EnhancedInvokableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*EnhancedInvokableToolOutput, error) { + invokableMiddlewareCalled = true + return next(ctx, input) + } + }, + EnhancedStreamable: func(next EnhancedStreamableToolEndpoint) EnhancedStreamableToolEndpoint { + return func(ctx context.Context, input *ToolInput) (*EnhancedStreamableToolOutput, error) { + streamableMiddlewareCalled = true + return next(ctx, input) + } + }, + }, + }, + }) + assert.NoError(t, err) + + t.Run("enhanced invokable middleware", func(t *testing.T) { + invokableMiddlewareCalled = false + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call1", + Function: schema.FunctionCall{ + Name: "enhanced_tool_with_middleware", + Arguments: "test", + }, + }, + }) + + _, err := toolNode.Invoke(ctx, input) + assert.NoError(t, err) + assert.True(t, invokableMiddlewareCalled) + }) + + t.Run("enhanced streamable middleware", func(t *testing.T) { + streamableMiddlewareCalled = false + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call2", + Function: schema.FunctionCall{ + Name: "enhanced_tool_with_middleware", + Arguments: "test", + }, + }, + }) + + streamReader, err := toolNode.Stream(ctx, input) + assert.NoError(t, err) + for { + _, err := streamReader.Recv() + if err != nil { + break + } + } + assert.False(t, streamableMiddlewareCalled) + }) +} + +func TestEnhancedToolPriority(t *testing.T) { + ctx := context.Background() + + enhancedInvokable := &enhancedInvokableTool{ + info: &schema.ToolInfo{ + Name: "test_tool", + Desc: "test tool with both enhanced and regular", + }, + fn: func(ctx context.Context, input *schema.ToolArgument) (*schema.ToolResult, error) { + return &schema.ToolResult{ + Parts: []schema.ToolOutputPart{ + {Text: "enhanced result", Type: schema.ToolPartTypeText}, + }, + }, nil + }, + } + + toolNode, err := NewToolNode(ctx, &ToolsNodeConfig{ + Tools: []tool.BaseTool{enhancedInvokable}, + }) + assert.NoError(t, err) + + t.Run("enhanced tool is used when available", func(t *testing.T) { + input := schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "call1", + Function: schema.FunctionCall{ + Name: "test_tool", + Arguments: "test", + }, + }, + }) + + output, err := toolNode.Invoke(ctx, input) + assert.NoError(t, err) + assert.Len(t, output, 1) + assert.Contains(t, output[0].UserInputMultiContent[0].Text, "enhanced result") + }) +} diff --git a/compose/types.go b/compose/types.go new file mode 100644 index 0000000..54f8e2b --- /dev/null +++ b/compose/types.go @@ -0,0 +1,47 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "github.com/cloudwego/eino/components" +) + +type component = components.Component + +// built-in component types in graph node. +// it represents the type of the most primitive executable object provided by the user. +const ( + ComponentOfUnknown component = "Unknown" + ComponentOfGraph component = "Graph" + ComponentOfWorkflow component = "Workflow" + ComponentOfChain component = "Chain" + ComponentOfPassthrough component = "Passthrough" + ComponentOfToolsNode component = "ToolsNode" + ComponentOfAgenticToolsNode component = "AgenticToolsNode" + ComponentOfLambda component = "Lambda" +) + +// NodeTriggerMode controls the triggering mode of graph nodes. +type NodeTriggerMode string + +const ( + // AnyPredecessor means that the node will be triggered when any of its predecessors is included in the previous completed super step. + // Ref:https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/orchestration_design_principles/#runtime-engine + AnyPredecessor NodeTriggerMode = "any_predecessor" + // AllPredecessor means that the current node will only be triggered when all of its predecessor nodes have finished running. + AllPredecessor NodeTriggerMode = "all_predecessor" +) diff --git a/compose/types_composable.go b/compose/types_composable.go new file mode 100644 index 0000000..bfd3618 --- /dev/null +++ b/compose/types_composable.go @@ -0,0 +1,31 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "reflect" +) + +// AnyGraph the identifiers for composable and compilable Graph[I, O]、Chain[I, O] in Eino. +type AnyGraph interface { + getGenericHelper() *genericHelper + compile(ctx context.Context, options *graphCompileOptions) (*composableRunnable, error) + inputType() reflect.Type + outputType() reflect.Type + component() component +} diff --git a/compose/types_lambda.go b/compose/types_lambda.go new file mode 100644 index 0000000..05bbab3 --- /dev/null +++ b/compose/types_lambda.go @@ -0,0 +1,265 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/schema" +) + +// Invoke is the type of the invokable lambda function. +type Invoke[I, O, TOption any] func(ctx context.Context, input I, opts ...TOption) (output O, err error) + +// Stream is the type of the streamable lambda function. +type Stream[I, O, TOption any] func(ctx context.Context, + input I, opts ...TOption) (output *schema.StreamReader[O], err error) + +// Collect is the type of the collectable lambda function. +type Collect[I, O, TOption any] func(ctx context.Context, + input *schema.StreamReader[I], opts ...TOption) (output O, err error) + +// Transform is the type of the transformable lambda function. +type Transform[I, O, TOption any] func(ctx context.Context, + input *schema.StreamReader[I], opts ...TOption) (output *schema.StreamReader[O], err error) + +// InvokeWOOpt is the type of the invokable lambda function without options. +type InvokeWOOpt[I, O any] func(ctx context.Context, input I) (output O, err error) + +// StreamWOOpt is the type of the streamable lambda function without options. +type StreamWOOpt[I, O any] func(ctx context.Context, + input I) (output *schema.StreamReader[O], err error) + +// CollectWOOpt is the type of the collectable lambda function without options. +type CollectWOOpt[I, O any] func(ctx context.Context, + input *schema.StreamReader[I]) (output O, err error) + +// TransformWOOpts is the type of the transformable lambda function without options. +type TransformWOOpts[I, O any] func(ctx context.Context, + input *schema.StreamReader[I]) (output *schema.StreamReader[O], err error) + +// Lambda is the node that wraps the user provided lambda function. +// It can be used as a node in Graph or Chain (include Parallel and Branch). +// Create a Lambda by using AnyLambda/InvokableLambda/StreamableLambda/CollectableLambda/TransformableLambda. +// eg. +// +// lambda := compose.InvokableLambda(func(ctx context.Context, input string) (output string, err error) { +// return input, nil +// }) +type Lambda struct { + executor *composableRunnable +} + +type lambdaOpts struct { + // same as executorMeta.isComponentCallbackEnabled + // indicates whether the executable lambda user provided could execute the callback aspect itself. + // if it could, the callback in the corresponding graph node won't be executed anymore + enableComponentCallback bool + + // same as executorMeta.componentImplType + // for AnyLambda, the value comes from the user's explicit config + // if componentImplType is empty, then the class name or func name in the instance will be inferred, but no guarantee. + componentImplType string +} + +// LambdaOpt is the option for creating a Lambda. +type LambdaOpt func(o *lambdaOpts) + +// WithLambdaCallbackEnable enables the callback aspect of the lambda function. +func WithLambdaCallbackEnable(y bool) LambdaOpt { + return func(o *lambdaOpts) { + o.enableComponentCallback = y + } +} + +// WithLambdaType sets the type of the lambda function. +func WithLambdaType(t string) LambdaOpt { + return func(o *lambdaOpts) { + o.componentImplType = t + } +} + +type unreachableOption struct{} + +// InvokableLambdaWithOption creates a Lambda with invokable lambda function and options. +func InvokableLambdaWithOption[I, O, TOption any](i Invoke[I, O, TOption], opts ...LambdaOpt) *Lambda { + return anyLambda(i, nil, nil, nil, opts...) +} + +// InvokableLambda creates a Lambda with invokable lambda function without options. +func InvokableLambda[I, O any](i InvokeWOOpt[I, O], opts ...LambdaOpt) *Lambda { + f := func(ctx context.Context, input I, opts_ ...unreachableOption) (output O, err error) { + return i(ctx, input) + } + + return anyLambda(f, nil, nil, nil, opts...) +} + +// StreamableLambdaWithOption creates a Lambda with streamable lambda function and options. +func StreamableLambdaWithOption[I, O, TOption any](s Stream[I, O, TOption], opts ...LambdaOpt) *Lambda { + return anyLambda(nil, s, nil, nil, opts...) +} + +// StreamableLambda creates a Lambda with streamable lambda function without options. +func StreamableLambda[I, O any](s StreamWOOpt[I, O], opts ...LambdaOpt) *Lambda { + f := func(ctx context.Context, input I, opts_ ...unreachableOption) ( + output *schema.StreamReader[O], err error) { + + return s(ctx, input) + } + + return anyLambda(nil, f, nil, nil, opts...) +} + +// CollectableLambdaWithOption creates a Lambda with collectable lambda function and options. +func CollectableLambdaWithOption[I, O, TOption any](c Collect[I, O, TOption], opts ...LambdaOpt) *Lambda { + return anyLambda(nil, nil, c, nil, opts...) +} + +// CollectableLambda creates a Lambda with collectable lambda function without options. +func CollectableLambda[I, O any](c CollectWOOpt[I, O], opts ...LambdaOpt) *Lambda { + f := func(ctx context.Context, input *schema.StreamReader[I], + opts_ ...unreachableOption) (output O, err error) { + + return c(ctx, input) + } + + return anyLambda(nil, nil, f, nil, opts...) +} + +// TransformableLambdaWithOption creates a Lambda with transformable lambda function and options. +func TransformableLambdaWithOption[I, O, TOption any](t Transform[I, O, TOption], opts ...LambdaOpt) *Lambda { + return anyLambda(nil, nil, nil, t, opts...) +} + +// TransformableLambda creates a Lambda with transformable lambda function without options. +func TransformableLambda[I, O any](t TransformWOOpts[I, O], opts ...LambdaOpt) *Lambda { + + f := func(ctx context.Context, input *schema.StreamReader[I], + opts_ ...unreachableOption) (output *schema.StreamReader[O], err error) { + + return t(ctx, input) + } + + return anyLambda(nil, nil, nil, f, opts...) +} + +// AnyLambda creates a Lambda with any lambda function. +// you can only implement one or more of the four lambda functions, and the rest use nil. +// eg. +// +// invokeFunc := func(ctx context.Context, input string, opts ...myOption) (output string, err error) { +// // ... +// } +// streamFunc := func(ctx context.Context, input string, opts ...myOption) (output *schema.StreamReader[string], err error) { +// // ... +// } +// +// lambda := compose.AnyLambda(invokeFunc, streamFunc, nil, nil) +func AnyLambda[I, O, TOption any](i Invoke[I, O, TOption], s Stream[I, O, TOption], + c Collect[I, O, TOption], t Transform[I, O, TOption], opts ...LambdaOpt) (*Lambda, error) { + + if i == nil && s == nil && c == nil && t == nil { + return nil, fmt.Errorf("needs to have at least one of four lambda types: invoke/stream/collect/transform, got none") + } + + return anyLambda(i, s, c, t, opts...), nil +} + +func anyLambda[I, O, TOption any](i Invoke[I, O, TOption], s Stream[I, O, TOption], + c Collect[I, O, TOption], t Transform[I, O, TOption], opts ...LambdaOpt) *Lambda { + + opt := getLambdaOpt(opts...) + + executor := runnableLambda(i, s, c, t, + !opt.enableComponentCallback, + ) + executor.meta = &executorMeta{ + component: ComponentOfLambda, + isComponentCallbackEnabled: opt.enableComponentCallback, + componentImplType: opt.componentImplType, + } + + return &Lambda{ + executor: executor, + } +} + +func getLambdaOpt(opts ...LambdaOpt) *lambdaOpts { + opt := &lambdaOpts{ + enableComponentCallback: false, + componentImplType: "", + } + + for _, optFn := range opts { + optFn(opt) + } + return opt +} + +// ToList creates a Lambda that converts input I to a []I. +// It's useful when you want to convert a single input to a list of inputs. +// eg. +// +// lambda := compose.ToList[*schema.Message]() +// chain := compose.NewChain[[]*schema.Message, []*schema.Message]() +// +// chain.AddChatModel(chatModel) // chatModel returns *schema.Message, but we need []*schema.Message +// chain.AddLambda(lambda) // convert *schema.Message to []*schema.Message +func ToList[I any](opts ...LambdaOpt) *Lambda { + i := func(ctx context.Context, input I, opts_ ...unreachableOption) (output []I, err error) { + return []I{input}, nil + } + + f := func(ctx context.Context, inputS *schema.StreamReader[I], opts_ ...unreachableOption) (outputS *schema.StreamReader[[]I], err error) { + return schema.StreamReaderWithConvert(inputS, func(i I) ([]I, error) { + return []I{i}, nil + }), nil + } + + return anyLambda(i, nil, nil, f, opts...) +} + +// MessageParser creates a lambda that parses a message into an object T, usually used after a chatmodel. +// usage: +// +// parser := schema.NewMessageJSONParser[MyStruct](&schema.MessageJSONParseConfig{ +// ParseFrom: schema.MessageParseFromContent, +// }) +// parserLambda := MessageParser(parser) +// +// chain := NewChain[*schema.Message, MyStruct]() +// chain.AppendChatModel(chatModel) +// chain.AppendLambda(parserLambda) +// +// r, err := chain.Compile(context.Background()) +// +// // parsed is a MyStruct object +// parsed, err := r.Invoke(context.Background(), &schema.Message{ +// Role: schema.MessageRoleUser, +// Content: "return a json string for my struct", +// }) +func MessageParser[T any](p schema.MessageParser[T], opts ...LambdaOpt) *Lambda { + i := func(ctx context.Context, input *schema.Message, opts_ ...unreachableOption) (output T, err error) { + return p.Parse(ctx, input) + } + + opts = append([]LambdaOpt{WithLambdaType("MessageParse")}, opts...) + + return anyLambda(i, nil, nil, nil, opts...) +} diff --git a/compose/types_lambda_test.go b/compose/types_lambda_test.go new file mode 100644 index 0000000..96afc0b --- /dev/null +++ b/compose/types_lambda_test.go @@ -0,0 +1,212 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/schema" +) + +func TestLambda(t *testing.T) { + t.Run("InvokableLambda", func(t *testing.T) { + ld := InvokableLambdaWithOption( + func(ctx context.Context, input string, opts ...any) (output string, err error) { + return "good", nil + }, + WithLambdaCallbackEnable(false), + WithLambdaType("ForTest"), + ) + + assert.Equal(t, false, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "ForTest", ld.executor.meta.componentImplType) + + ld = InvokableLambda( + func(ctx context.Context, input string) (output string, err error) { + return "good", nil + }, + WithLambdaCallbackEnable(false), + WithLambdaType("ForTest"), + ) + + assert.Equal(t, false, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "ForTest", ld.executor.meta.componentImplType) + }) + + t.Run("StreamableLambda", func(t *testing.T) { + ld := StreamableLambdaWithOption( + func(ctx context.Context, input string, opts ...any) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](1) + sw.Close() + return sr, nil + }, + WithLambdaCallbackEnable(false), + WithLambdaType("ForTest"), + ) + + assert.Equal(t, false, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "ForTest", ld.executor.meta.componentImplType) + + ld = StreamableLambda( + func(ctx context.Context, input string) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](1) + sw.Close() + return sr, nil + }, + WithLambdaCallbackEnable(false), + WithLambdaType("ForTest"), + ) + + assert.Equal(t, false, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "ForTest", ld.executor.meta.componentImplType) + }) + + t.Run("CollectableLambda", func(t *testing.T) { + ld := CollectableLambdaWithOption( + func(ctx context.Context, input *schema.StreamReader[string], opts ...any) (output string, err error) { + return "good", nil + }, + WithLambdaCallbackEnable(true), + ) + + assert.Equal(t, true, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "", ld.executor.meta.componentImplType) + + ld = CollectableLambda( + func(ctx context.Context, input *schema.StreamReader[string]) (output string, err error) { + return "good", nil + }, + WithLambdaCallbackEnable(true), + ) + + assert.Equal(t, true, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "", ld.executor.meta.componentImplType) + }) + + t.Run("TransformableLambda", func(t *testing.T) { + ld := TransformableLambdaWithOption( + func(ctx context.Context, input *schema.StreamReader[string], opts ...any) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](1) + sw.Close() + return sr, nil + }, + WithLambdaCallbackEnable(true), + ) + + assert.Equal(t, true, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "", ld.executor.meta.componentImplType) + + ld = TransformableLambda( + func(ctx context.Context, input *schema.StreamReader[string]) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](1) + sw.Close() + return sr, nil + }, + WithLambdaCallbackEnable(true), + ) + + assert.Equal(t, true, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "", ld.executor.meta.componentImplType) + }) + + t.Run("AnyLambda", func(t *testing.T) { + ld, err := AnyLambda[string, string]( + func(ctx context.Context, input string, opts ...any) (output string, err error) { + return "good", nil + }, + func(ctx context.Context, input string, opts ...any) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](1) + sw.Close() + return sr, nil + }, + func(ctx context.Context, input *schema.StreamReader[string], opts ...any) (output string, err error) { + return "good", nil + }, + func(ctx context.Context, input *schema.StreamReader[string], opts ...any) (output *schema.StreamReader[string], err error) { + sr, sw := schema.Pipe[string](1) + sw.Close() + return sr, nil + }, + WithLambdaCallbackEnable(true), + WithLambdaType("ForTest"), + ) + assert.NoError(t, err) + + assert.Equal(t, true, ld.executor.meta.isComponentCallbackEnabled) + assert.Equal(t, ComponentOfLambda, ld.executor.meta.component) + assert.Equal(t, "ForTest", ld.executor.meta.componentImplType) + }) +} + +type TestStructForParse struct { + ID int `json:"id"` +} + +func TestMessageParser(t *testing.T) { + t.Run("parse from content", func(t *testing.T) { + parser := schema.NewMessageJSONParser[TestStructForParse](&schema.MessageJSONParseConfig{ + ParseFrom: schema.MessageParseFromContent, + }) + + parserLambda := MessageParser(parser) + + chain := NewChain[*schema.Message, TestStructForParse]() + chain.AppendLambda(parserLambda) + + r, err := chain.Compile(context.Background()) + assert.Nil(t, err) + + parsed, err := r.Invoke(context.Background(), &schema.Message{ + Content: `{"id": 1}`, + }) + assert.Nil(t, err) + assert.Equal(t, 1, parsed.ID) + }) + + t.Run("parse from tool call", func(t *testing.T) { + parser := schema.NewMessageJSONParser[*TestStructForParse](&schema.MessageJSONParseConfig{ + ParseFrom: schema.MessageParseFromToolCall, + }) + + parserLambda := MessageParser(parser) + + chain := NewChain[*schema.Message, *TestStructForParse]() + chain.AppendLambda(parserLambda) + + r, err := chain.Compile(context.Background()) + assert.Nil(t, err) + + parsed, err := r.Invoke(context.Background(), &schema.Message{ + ToolCalls: []schema.ToolCall{ + {Function: schema.FunctionCall{Arguments: `{"id": 1}`}}, + }, + }) + assert.Nil(t, err) + assert.Equal(t, 1, parsed.ID) + }) +} diff --git a/compose/utils.go b/compose/utils.go new file mode 100644 index 0000000..c348c95 --- /dev/null +++ b/compose/utils.go @@ -0,0 +1,324 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + + "github.com/cloudwego/eino/callbacks" + icb "github.com/cloudwego/eino/internal/callbacks" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +type on[T any] func(context.Context, T) (context.Context, T) + +func onStart[T any](ctx context.Context, input T) (context.Context, T) { + return icb.On(ctx, input, icb.OnStartHandle[T], callbacks.TimingOnStart, true) +} + +func onEnd[T any](ctx context.Context, output T) (context.Context, T) { + return icb.On(ctx, output, icb.OnEndHandle[T], callbacks.TimingOnEnd, false) +} + +func onStartWithStreamInput[T any](ctx context.Context, input *schema.StreamReader[T]) ( + context.Context, *schema.StreamReader[T]) { + + return icb.On(ctx, input, icb.OnStartWithStreamInputHandle[T], callbacks.TimingOnStartWithStreamInput, true) +} + +func genericOnStartWithStreamInputHandle(ctx context.Context, input streamReader, + runInfo *icb.RunInfo, handlers []icb.Handler) (context.Context, streamReader) { + + handlers = generic.Reverse(handlers) + + cpy := input.copy + + handle := func(ctx context.Context, handler icb.Handler, in streamReader) context.Context { + in_, ok := unpackStreamReader[icb.CallbackInput](in) + if !ok { + panic("impossible") + } + + return handler.OnStartWithStreamInput(ctx, runInfo, in_) + } + + return icb.OnWithStreamHandle(ctx, input, handlers, cpy, handle) +} + +func genericOnStartWithStreamInput(ctx context.Context, input streamReader) (context.Context, streamReader) { + return icb.On(ctx, input, genericOnStartWithStreamInputHandle, callbacks.TimingOnStartWithStreamInput, true) +} + +func onEndWithStreamOutput[T any](ctx context.Context, output *schema.StreamReader[T]) ( + context.Context, *schema.StreamReader[T]) { + + return icb.On(ctx, output, icb.OnEndWithStreamOutputHandle[T], callbacks.TimingOnEndWithStreamOutput, false) +} + +func genericOnEndWithStreamOutputHandle(ctx context.Context, output streamReader, + runInfo *icb.RunInfo, handlers []icb.Handler) (context.Context, streamReader) { + + cpy := output.copy + + handle := func(ctx context.Context, handler icb.Handler, out streamReader) context.Context { + out_, ok := unpackStreamReader[icb.CallbackOutput](out) + if !ok { + panic("impossible") + } + + return handler.OnEndWithStreamOutput(ctx, runInfo, out_) + } + + return icb.OnWithStreamHandle(ctx, output, handlers, cpy, handle) +} + +func genericOnEndWithStreamOutput(ctx context.Context, output streamReader) (context.Context, streamReader) { + return icb.On(ctx, output, genericOnEndWithStreamOutputHandle, callbacks.TimingOnEndWithStreamOutput, false) +} + +func onError(ctx context.Context, err error) (context.Context, error) { + return icb.On(ctx, err, icb.OnErrorHandle, callbacks.TimingOnError, false) +} + +func runWithCallbacks[I, O, TOption any](r func(context.Context, I, ...TOption) (O, error), + onStart on[I], onEnd on[O], onError on[error]) func(context.Context, I, ...TOption) (O, error) { + + return func(ctx context.Context, input I, opts ...TOption) (output O, err error) { + ctx, input = onStart(ctx, input) + + output, err = r(ctx, input, opts...) + if err != nil { + ctx, err = onError(ctx, err) + return output, err + } + + ctx, output = onEnd(ctx, output) + + return output, nil + } +} + +func invokeWithCallbacks[I, O, TOption any](i Invoke[I, O, TOption]) Invoke[I, O, TOption] { + return runWithCallbacks(i, onStart[I], onEnd[O], onError) +} + +func onGraphStart(ctx context.Context, input any, isStream bool) (context.Context, any) { + if isStream { + return genericOnStartWithStreamInput(ctx, input.(streamReader)) + } + return onStart(ctx, input) +} + +func onGraphEnd(ctx context.Context, output any, isStream bool) (context.Context, any) { + if isStream { + return genericOnEndWithStreamOutput(ctx, output.(streamReader)) + } + return onEnd(ctx, output) +} + +func onGraphError(ctx context.Context, err error) (context.Context, error) { + return onError(ctx, err) +} + +func streamWithCallbacks[I, O, TOption any](s Stream[I, O, TOption]) Stream[I, O, TOption] { + return runWithCallbacks(s, onStart[I], onEndWithStreamOutput[O], onError) +} + +func collectWithCallbacks[I, O, TOption any](c Collect[I, O, TOption]) Collect[I, O, TOption] { + return runWithCallbacks(c, onStartWithStreamInput[I], onEnd[O], onError) +} + +func transformWithCallbacks[I, O, TOption any](t Transform[I, O, TOption]) Transform[I, O, TOption] { + return runWithCallbacks(t, onStartWithStreamInput[I], onEndWithStreamOutput[O], onError) +} + +func initGraphCallbacks(ctx context.Context, info *nodeInfo, meta *executorMeta, opts ...Option) context.Context { + ri := &callbacks.RunInfo{} + if meta != nil { + ri.Component = meta.component + ri.Type = meta.componentImplType + } + + if info != nil { + ri.Name = info.name + } + + var cbs []callbacks.Handler + for i := range opts { + if len(opts[i].handler) != 0 && len(opts[i].paths) == 0 { + cbs = append(cbs, opts[i].handler...) + } + } + + if len(cbs) == 0 { + return icb.ReuseHandlers(ctx, ri) + } + + return icb.AppendHandlers(ctx, ri, cbs...) +} + +func initNodeCallbacks(ctx context.Context, key string, info *nodeInfo, meta *executorMeta, opts ...Option) context.Context { + ri := &callbacks.RunInfo{} + if meta != nil { + ri.Component = meta.component + ri.Type = meta.componentImplType + } + + if info != nil { + ri.Name = info.name + } + + var cbs []callbacks.Handler + for i := range opts { + if len(opts[i].handler) != 0 { + if len(opts[i].paths) != 0 { + for _, k := range opts[i].paths { + if len(k.path) == 1 && k.path[0] == key { + cbs = append(cbs, opts[i].handler...) + break + } + } + } + } + } + + if len(cbs) == 0 { + return icb.ReuseHandlers(ctx, ri) + } + + return icb.AppendHandlers(ctx, ri, cbs...) +} + +func streamChunkConvertForCBOutput[O any](o O) (callbacks.CallbackOutput, error) { + return o, nil +} + +func streamChunkConvertForCBInput[I any](i I) (callbacks.CallbackInput, error) { + return i, nil +} + +func toAnyList[T any](in []T) []any { + ret := make([]any, len(in)) + for i := range in { + ret[i] = in[i] + } + return ret +} + +type assignableType uint8 + +const ( + assignableTypeMustNot assignableType = iota + assignableTypeMust + assignableTypeMay +) + +func checkAssignable(input, arg reflect.Type) assignableType { + if arg == nil || input == nil { + return assignableTypeMustNot + } + + if arg == input { + return assignableTypeMust + } + + if arg.Kind() == reflect.Interface && input.Implements(arg) { + return assignableTypeMust + } + if input.Kind() == reflect.Interface { + if arg.Implements(input) { + return assignableTypeMay + } + return assignableTypeMustNot + } + + return assignableTypeMustNot +} + +func extractOption(nodes map[string]*chanCall, opts ...Option) (map[string][]any, error) { + optMap := map[string][]any{} + for _, opt := range opts { + if len(opt.paths) == 0 { + // common, discard callback, filter option by type + if len(opt.options) == 0 { + continue + } + for name, c := range nodes { + if c.action.optionType == nil { + // subgraph + optMap[name] = append(optMap[name], opt) + } else if reflect.TypeOf(opt.options[0]) == c.action.optionType { // assume that types of options are the same + optMap[name] = append(optMap[name], opt.options...) + } + } + } + for _, path := range opt.paths { + if len(path.path) == 0 { + return nil, fmt.Errorf("call option has designated an empty path") + } + + var curNode *chanCall + var ok bool + if curNode, ok = nodes[path.path[0]]; !ok { + return nil, fmt.Errorf("option has designated an unknown node: %s", path) + } + curNodeKey := path.path[0] + + if len(path.path) == 1 { + if len(opt.options) == 0 { + // sub graph common callbacks has been added to ctx in initNodeCallback and won't be passed to subgraph only pass options + // node callback also won't be passed + continue + } + if curNode.action.optionType == nil { + nOpt := opt.deepCopy() + nOpt.paths = []*NodePath{} + optMap[curNodeKey] = append(optMap[curNodeKey], nOpt) + } else { + // designate to component + if curNode.action.optionType != reflect.TypeOf(opt.options[0]) { // assume that types of options are the same + return nil, fmt.Errorf("option type[%s] is different from which the designated node[%s] expects[%s]", + reflect.TypeOf(opt.options[0]).String(), path, curNode.action.optionType.String()) + } + optMap[curNodeKey] = append(optMap[curNodeKey], opt.options...) + } + } else { + if curNode.action.optionType != nil { + // component + return nil, fmt.Errorf("cannot designate sub path of a component, path:%s", path) + } + // designate to sub graph's nodes + nOpt := opt.deepCopy() + nOpt.paths = []*NodePath{NewNodePath(path.path[1:]...)} + optMap[curNodeKey] = append(optMap[curNodeKey], nOpt) + } + } + } + + return optMap, nil +} + +func mapToList(m map[string]any) []any { + ret := make([]any, 0, len(m)) + for _, v := range m { + ret = append(ret, v) + } + return ret +} diff --git a/compose/utils_test.go b/compose/utils_test.go new file mode 100644 index 0000000..07a781c --- /dev/null +++ b/compose/utils_test.go @@ -0,0 +1,114 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/generic" +) + +type good interface { + ThisIsGood() bool +} + +type good2 interface { + ThisIsGood2() bool +} + +type good3 interface { + ThisIsGood() bool +} + +type goodImpl struct{} + +func (g *goodImpl) ThisIsGood() bool { + return true +} + +type goodNotImpl struct{} + +func TestValidateType(t *testing.T) { + + t.Run("equal_type", func(t *testing.T) { + arg := generic.TypeOf[int]() + input := generic.TypeOf[int]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMust, result) + }) + + t.Run("unequal_type", func(t *testing.T) { + arg := generic.TypeOf[int]() + input := generic.TypeOf[string]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMustNot, result) + }) + + t.Run("implement_interface", func(t *testing.T) { + arg := generic.TypeOf[good]() + input := generic.TypeOf[*goodImpl]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMust, result) + }) + + t.Run("may_implement_interface", func(t *testing.T) { + arg := generic.TypeOf[*goodImpl]() + input := generic.TypeOf[good]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMay, result) + }) + + t.Run("not_implement_interface", func(t *testing.T) { + arg := generic.TypeOf[good]() + input := generic.TypeOf[*goodNotImpl]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMustNot, result) + }) + + t.Run("interface_unequal_interface", func(t *testing.T) { + arg := generic.TypeOf[good]() + input := generic.TypeOf[good2]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMustNot, result) + }) + + t.Run("interface_equal_interface", func(t *testing.T) { + arg := generic.TypeOf[good]() + input := generic.TypeOf[good3]() + + result := checkAssignable(input, arg) + assert.Equal(t, assignableTypeMust, result) + }) +} + +func TestStreamChunkConvert(t *testing.T) { + o, err := streamChunkConvertForCBOutput(1) + assert.Nil(t, err) + assert.Equal(t, o, 1) + + i, err := streamChunkConvertForCBInput(1) + assert.Nil(t, err) + assert.Equal(t, i, 1) +} diff --git a/compose/values_merge.go b/compose/values_merge.go new file mode 100644 index 0000000..c08a12e --- /dev/null +++ b/compose/values_merge.go @@ -0,0 +1,82 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "fmt" + "reflect" + + "github.com/cloudwego/eino/internal" +) + +// RegisterValuesMergeFunc registers a function to merge outputs from multiple nodes when fan-in. +// It's used to define how to merge for a specific type. +// For maps that already have a default merge function, you don't need to register a new one unless you want to customize the merge logic. +func RegisterValuesMergeFunc[T any](fn func([]T) (T, error)) { + internal.RegisterValuesMergeFunc(fn) +} + +type mergeOptions struct { + streamMergeWithSourceEOF bool + names []string +} + +// the caller should ensure len(vs) > 1 +func mergeValues(vs []any, opts *mergeOptions) (any, error) { + v0 := reflect.ValueOf(vs[0]) + t0 := v0.Type() + + if fn := internal.GetMergeFunc(t0); fn != nil { + return fn(vs) + } + + // merge StreamReaders + if s, ok := vs[0].(streamReader); ok { + t := s.getChunkType() + if internal.GetMergeFunc(t) == nil { + return nil, fmt.Errorf("(mergeValues | stream type)"+ + " unsupported chunk type: %v", t) + } + + ss := make([]streamReader, len(vs)-1) + for i := 0; i < len(ss); i++ { + sri, ok_ := vs[i+1].(streamReader) + if !ok_ { + return nil, fmt.Errorf("(mergeStream) unexpected type. "+ + "expect: %v, got: %v", t0, reflect.TypeOf(vs[i])) + } + + if st := sri.getChunkType(); st != t { + return nil, fmt.Errorf("(mergeStream) chunk type mismatch. "+ + "expect: %v, got: %v", t, st) + } + + ss[i] = sri + } + + if opts != nil && opts.streamMergeWithSourceEOF { + ms := s.mergeWithNames(ss, opts.names) + return ms, nil + } + + ms := s.merge(ss) + + return ms, nil + } + + return nil, fmt.Errorf("(mergeValues) unsupported type: %v", t0) +} diff --git a/compose/values_merge_test.go b/compose/values_merge_test.go new file mode 100644 index 0000000..a848397 --- /dev/null +++ b/compose/values_merge_test.go @@ -0,0 +1,256 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "fmt" + "io" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/schema" +) + +func Test_mergeValues(t *testing.T) { + t.Run("merge maps", func(t *testing.T) { + m1 := map[int]int{1: 1, 2: 2, 3: 3, 4: 4} + m2 := map[int]int{5: 5, 6: 6, 7: 7, 8: 8} + m3 := map[int]int{9: 9, 10: 10, 11: 11} + + t.Run("regular", func(t *testing.T) { + mergedM, err := mergeValues([]any{m1, m2, m3}, nil) + assert.NoError(t, err) + + m := mergedM.(map[int]int) + + // len(m) == len(m1) + len(m2) + len(m3) + assert.Equal(t, len(m), len(m1)+len(m2)+len(m3)) + }) + + t.Run("duplicated key", func(t *testing.T) { + _, err := mergeValues([]any{m1, m2, m3, map[int]int{1: 1}}, nil) + assert.ErrorContains(t, err, "duplicated key") + }) + + t.Run("type mismatch", func(t *testing.T) { + _, err := mergeValues([]any{m1, m2, m3, map[int]string{1: "1"}}, nil) + assert.ErrorContains(t, err, "type mismatch") + }) + }) + + t.Run("merge stream", func(t *testing.T) { + ass := []any{ + packStreamReader(schema.StreamReaderFromArray[map[int]string]([]map[int]string{{1: "1"}})), + packStreamReader(schema.StreamReaderFromArray[map[int]string]([]map[int]string{{2: "2"}})), + packStreamReader(schema.StreamReaderFromArray[map[int]string]([]map[int]string{{3: "3", 4: "4"}})), + } + isr, err := mergeValues(ass, nil) + require.NoError(t, err) + ret, ok := unpackStreamReader[map[int]string](isr.(streamReader)) + require.True(t, ok) + defer ret.Close() + + got := make(map[int]string) + for i := 0; i < 3; i++ { + m, err := ret.Recv() + require.NoError(t, err) + for k, v := range m { + got[k] = v + } + } + _, err = ret.Recv() + require.ErrorIs(t, err, io.EOF) + + assert.Equal(t, map[int]string{ + 1: "1", + 2: "2", + 3: "3", + 4: "4", + }, got) + }) + + t.Run("merge stream with source EOF", func(t *testing.T) { + ass := []any{ + packStreamReader(schema.StreamReaderFromArray[map[int]string]([]map[int]string{{1: "1"}})), + packStreamReader(schema.StreamReaderFromArray[map[int]string]([]map[int]string{{2: "2"}})), + packStreamReader(schema.StreamReaderFromArray[map[int]string]([]map[int]string{{3: "3", 4: "4"}})), + } + opts := &mergeOptions{ + streamMergeWithSourceEOF: true, + names: []string{ + "source0", + "source1", + "source2", + }, + } + isr, err := mergeValues(ass, opts) + require.NoError(t, err) + ret, ok := unpackStreamReader[map[int]string](isr.(streamReader)) + require.True(t, ok) + defer ret.Close() + + got := make(map[int]string) + endedSources := make(map[string]bool) + + for { + m, e := ret.Recv() + if e != nil { + if sourceName, ok_ := schema.GetSourceName(e); ok_ { + t.Logf("Source '%s' ended", sourceName) + endedSources[sourceName] = true + continue + } + if e == io.EOF { + // This EOF means all chunks from all sources that were not SourceEOF have been merged and sent. + // Or, if all sources send SourceEOF first, this io.EOF means the merged stream itself is now empty. + break + } + + require.NoError(t, e) // Fail on any other error + } + // If streamMergeWithSourceEOF is true, the final merged result comes as a single map chunk + // after all SourceEOFs (if any non-empty streams existed) or directly if all streams were empty. + for k, v := range m { + got[k] = v + } + } + + // Check that all expected sources have ended if they were part of opts.names + for i := 0; i < len(ass); i++ { + expectedSourceName := opts.names[i] + assert.True(t, endedSources[expectedSourceName], "Expected source %s to have sent SourceEOF", expectedSourceName) + } + + // The final 'got' map should contain all items because streamMergeWithSourceEOF merges them at the end. + assert.Equal(t, map[int]string{ + 1: "1", + 2: "2", + 3: "3", + 4: "4", + }, got) + }) + + type TestType struct { + A int + B []string + } + + RegisterValuesMergeFunc(func(vs []*TestType) (*TestType, error) { + ret := &TestType{} + for _, v := range vs { + if v == nil { + continue + } + if ret.A < 0 { + return nil, fmt.Errorf("test error: %v", ret.A) + } + ret.A += v.A + ret.B = append(ret.B, v.B...) + } + sort.Strings(ret.B) + return ret, nil + }) + + t.Run("custom merge", func(t *testing.T) { + t.Run("regular", func(t *testing.T) { + vs := []any{ + &TestType{A: 0, B: []string{}}, + &TestType{A: 1, B: []string{"1"}}, + &TestType{A: 2, B: []string{"2", "22"}}, + &TestType{A: 3, B: []string{"3", "33", "333"}}, + } + ret, err := mergeValues(vs, nil) + require.NoError(t, err) + assert.Equal(t, &TestType{ + A: 6, + B: []string{"1", "2", "22", "3", "33", "333"}, + }, ret) + }) + + t.Run("custom error", func(t *testing.T) { + vs := []any{ + &TestType{A: 0, B: []string{}}, + &TestType{A: 1, B: []string{"1"}}, + &TestType{A: -2, B: []string{"2", "22"}}, + &TestType{A: 3, B: []string{"3", "33", "333"}}, + } + _, err := mergeValues(vs, nil) + require.ErrorContains(t, err, "test error") + }) + + t.Run("type mismatch", func(t *testing.T) { + vs := []any{ + &TestType{A: 0, B: []string{}}, + &TestType{A: 1, B: []string{"1"}}, + &TestType{A: 2, B: []string{"2", "22"}}, + "test3", + } + _, err := mergeValues(vs, nil) + require.ErrorContains(t, err, "type mismatch") + }) + + t.Run("stream", func(t *testing.T) { + ass := []any{ + packStreamReader(schema.StreamReaderFromArray([]*TestType{ + {A: 0, B: []string{}}, + })), + packStreamReader(schema.StreamReaderFromArray([]*TestType{ + {A: 1, B: []string{"1"}}, + })), + packStreamReader(schema.StreamReaderFromArray([]*TestType{ + {A: 2, B: []string{"2", "22"}}, + })), + packStreamReader(schema.StreamReaderFromArray([]*TestType{ + {A: 3, B: []string{"3", "33", "333"}}, + })), + } + isr, err := mergeValues(ass, nil) + require.NoError(t, err) + ret, ok := unpackStreamReader[*TestType](isr.(streamReader)) + require.True(t, ok) + defer ret.Close() + + var vs []any + for i := 0; i < 4; i++ { + v, err := ret.Recv() + require.NoError(t, err) + vs = append(vs, v) + } + + _, err = ret.Recv() + require.ErrorIs(t, err, io.EOF) + + merged, err := mergeValues(vs, nil) + require.NoError(t, err) + + assert.Equal(t, &TestType{ + A: 6, + B: []string{"1", "2", "22", "3", "33", "333"}, + }, merged) + }) + }) + + t.Run("unregistered type", func(t *testing.T) { + type Unregistered TestType + _, err := mergeValues([]any{&Unregistered{}}, nil) + assert.ErrorContains(t, err, "unsupported type") + }) + +} diff --git a/compose/workflow.go b/compose/workflow.go new file mode 100644 index 0000000..6b50962 --- /dev/null +++ b/compose/workflow.go @@ -0,0 +1,545 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "fmt" + "reflect" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/schema" +) + +// WorkflowNode is the node of the Workflow. +type WorkflowNode struct { + g *graph + key string + addInputs []func() error + staticValues map[string]any + dependencySetter func(fromNodeKey string, typ dependencyType) + mappedFieldPath map[string]any +} + +// Workflow is wrapper of graph, replacing AddEdge with declaring dependencies and field mappings between nodes. +// Under the hood it uses NodeTriggerMode(AllPredecessor), so does not support cycles. +type Workflow[I, O any] struct { + g *graph + workflowNodes map[string]*WorkflowNode + workflowBranches []*WorkflowBranch + dependencies map[string]map[string]dependencyType +} + +type dependencyType int + +const ( + normalDependency dependencyType = iota + noDirectDependency + branchDependency +) + +// NewWorkflow creates a new Workflow. +func NewWorkflow[I, O any](opts ...NewGraphOption) *Workflow[I, O] { + options := &newGraphOptions{} + for _, opt := range opts { + opt(options) + } + + wf := &Workflow[I, O]{ + g: newGraphFromGeneric[I, O]( + ComponentOfWorkflow, + options.withState, + options.stateType, + opts, + ), + workflowNodes: make(map[string]*WorkflowNode), + dependencies: make(map[string]map[string]dependencyType), + } + + return wf +} + +// Compile builds the workflow into a runnable graph. +func (wf *Workflow[I, O]) Compile(ctx context.Context, opts ...GraphCompileOption) (Runnable[I, O], error) { + return compileAnyGraph[I, O](ctx, wf, opts...) +} + +// AddChatModelNode adds a chat model node and returns it. +func (wf *Workflow[I, O]) AddChatModelNode(key string, chatModel model.BaseChatModel, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddChatModelNode(key, chatModel, opts...) + return wf.initNode(key) +} + +// AddAgenticModelNode adds an agentic model node and returns it. +func (wf *Workflow[I, O]) AddAgenticModelNode(key string, agenticModel model.AgenticModel, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddAgenticModelNode(key, agenticModel, opts...) + return wf.initNode(key) +} + +// AddChatTemplateNode adds a chat template node and returns it. +func (wf *Workflow[I, O]) AddChatTemplateNode(key string, chatTemplate prompt.ChatTemplate, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddChatTemplateNode(key, chatTemplate, opts...) + return wf.initNode(key) +} + +// AddAgenticChatTemplateNode adds an agentic chat template node and returns it. +func (wf *Workflow[I, O]) AddAgenticChatTemplateNode(key string, chatTemplate prompt.AgenticChatTemplate, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddAgenticChatTemplateNode(key, chatTemplate, opts...) + return wf.initNode(key) +} + +// AddToolsNode adds a tools node and returns it. +func (wf *Workflow[I, O]) AddToolsNode(key string, tools *ToolsNode, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddToolsNode(key, tools, opts...) + return wf.initNode(key) +} + +// AddAgenticToolsNode adds an agentic tools node and returns it. +func (wf *Workflow[I, O]) AddAgenticToolsNode(key string, tools *AgenticToolsNode, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddAgenticToolsNode(key, tools, opts...) + return wf.initNode(key) +} + +// AddRetrieverNode adds a retriever node and returns it. +func (wf *Workflow[I, O]) AddRetrieverNode(key string, retriever retriever.Retriever, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddRetrieverNode(key, retriever, opts...) + return wf.initNode(key) +} + +// AddEmbeddingNode adds an embedding node and returns it. +func (wf *Workflow[I, O]) AddEmbeddingNode(key string, embedding embedding.Embedder, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddEmbeddingNode(key, embedding, opts...) + return wf.initNode(key) +} + +// AddIndexerNode adds an indexer node to the workflow and returns it. +func (wf *Workflow[I, O]) AddIndexerNode(key string, indexer indexer.Indexer, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddIndexerNode(key, indexer, opts...) + return wf.initNode(key) +} + +// AddLoaderNode adds a document loader node to the workflow and returns it. +func (wf *Workflow[I, O]) AddLoaderNode(key string, loader document.Loader, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddLoaderNode(key, loader, opts...) + return wf.initNode(key) +} + +// AddDocumentTransformerNode adds a document transformer node and returns it. +func (wf *Workflow[I, O]) AddDocumentTransformerNode(key string, transformer document.Transformer, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddDocumentTransformerNode(key, transformer, opts...) + return wf.initNode(key) +} + +// AddGraphNode adds a nested graph node to the workflow and returns it. +func (wf *Workflow[I, O]) AddGraphNode(key string, graph AnyGraph, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddGraphNode(key, graph, opts...) + return wf.initNode(key) +} + +// AddLambdaNode adds a lambda node to the workflow and returns it. +func (wf *Workflow[I, O]) AddLambdaNode(key string, lambda *Lambda, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddLambdaNode(key, lambda, opts...) + return wf.initNode(key) +} + +// End returns the WorkflowNode representing END node. +func (wf *Workflow[I, O]) End() *WorkflowNode { + if node, ok := wf.workflowNodes[END]; ok { + return node + } + return wf.initNode(END) +} + +// AddPassthroughNode adds a passthrough node to the workflow and returns it. +func (wf *Workflow[I, O]) AddPassthroughNode(key string, opts ...GraphAddNodeOpt) *WorkflowNode { + _ = wf.g.AddPassthroughNode(key, opts...) + return wf.initNode(key) +} + +// AddInput creates both data and execution dependencies between nodes. +// It configures how data flows from the predecessor node (fromNodeKey) to the current node, +// and ensures the current node only executes after the predecessor completes. +// +// Parameters: +// - fromNodeKey: the key of the predecessor node +// - inputs: field mappings that specify how data should flow from the predecessor +// to the current node. If no mappings are provided, the entire output of the +// predecessor will be used as input. +// +// Example: +// +// // Map between specific field +// node.AddInput("userNode", MapFields("user.name", "displayName")) +// +// // Use entire output +// node.AddInput("dataNode") +// +// Returns the current node for method chaining. +func (n *WorkflowNode) AddInput(fromNodeKey string, inputs ...*FieldMapping) *WorkflowNode { + return n.addDependencyRelation(fromNodeKey, inputs, &workflowAddInputOpts{}) +} + +type workflowAddInputOpts struct { + // noDirectDependency indicates whether to create a data mapping without establishing + // a direct execution dependency. When true, the current node can access data from + // the predecessor node but its execution is not directly blocked by it. + noDirectDependency bool + // dependencyWithoutInput indicates whether to create an execution dependency + // without any data mapping. When true, the current node will wait for the + // predecessor node to complete but won't receive any data from it. + dependencyWithoutInput bool +} + +// WorkflowAddInputOpt configures behavior of AddInputWithOptions. +type WorkflowAddInputOpt func(*workflowAddInputOpts) + +func getAddInputOpts(opts []WorkflowAddInputOpt) *workflowAddInputOpts { + opt := &workflowAddInputOpts{} + for _, o := range opts { + o(opt) + } + return opt +} + +// WithNoDirectDependency creates a data mapping without establishing a direct execution dependency. +// The predecessor node will still complete before the current node executes, but through indirect +// execution paths rather than a direct dependency. +// +// In a workflow graph, node dependencies typically serve two purposes: +// 1. Execution order: determining when nodes should run +// 2. Data flow: specifying how data passes between nodes +// +// This option separates these concerns by: +// - Creating data mapping from the predecessor to the current node +// - Relying on the predecessor's path to reach the current node through other nodes +// that have direct execution dependencies +// +// Example: +// +// node.AddInputWithOptions("dataNode", mappings, WithNoDirectDependency()) +// +// Important: +// +// 1. Branch scenarios: When connecting nodes on different sides of a branch, +// WithNoDirectDependency MUST be used to let the branch itself handle the +// execution order, preventing incorrect dependencies that could bypass the branch. +// +// 2. Execution guarantee: The predecessor will still complete before the current +// node executes because the predecessor must have a path (through other nodes) +// that eventually reaches the current node. +// +// 3. Graph validity: There MUST be a path from the predecessor that eventually +// reaches the current node through other nodes with direct dependencies. +// This ensures the execution order while avoiding redundant direct dependencies. +// +// Common use cases: +// - Cross-branch data access where the branch handles execution order +// - Avoiding redundant dependencies when a path already exists +func WithNoDirectDependency() WorkflowAddInputOpt { + return func(opt *workflowAddInputOpts) { + opt.noDirectDependency = true + } +} + +// AddInputWithOptions creates a dependency between nodes with custom configuration options. +// It allows fine-grained control over both data flow and execution dependencies. +// +// Parameters: +// - fromNodeKey: the key of the predecessor node +// - inputs: field mappings that specify how data flows from the predecessor to the current node. +// If no mappings are provided, the entire output of the predecessor will be used as input. +// - opts: configuration options that control how the dependency is established +// +// Example: +// +// // Create data mapping without direct execution dependency +// node.AddInputWithOptions("dataNode", mappings, WithNoDirectDependency()) +// +// Returns the current node for method chaining. +func (n *WorkflowNode) AddInputWithOptions(fromNodeKey string, inputs []*FieldMapping, opts ...WorkflowAddInputOpt) *WorkflowNode { + return n.addDependencyRelation(fromNodeKey, inputs, getAddInputOpts(opts)) +} + +// AddDependency creates an execution-only dependency between nodes. +// The current node will wait for the predecessor node to complete before executing, +// but no data will be passed between them. +// +// Parameters: +// - fromNodeKey: the key of the predecessor node that must complete before this node starts +// +// Example: +// +// // Wait for "setupNode" to complete before executing +// node.AddDependency("setupNode") +// +// This is useful when: +// - You need to ensure execution order without data transfer +// - The predecessor performs setup or initialization that must complete first +// - You want to explicitly separate execution dependencies from data flow +// +// Returns the current node for method chaining. +func (n *WorkflowNode) AddDependency(fromNodeKey string) *WorkflowNode { + return n.addDependencyRelation(fromNodeKey, nil, &workflowAddInputOpts{dependencyWithoutInput: true}) +} + +// SetStaticValue sets a static value for a field path that will be available +// during workflow execution. These values are determined at compile time and +// remain constant throughout the workflow's lifecycle. +// +// Example: +// +// node.SetStaticValue(FieldPath{"query"}, "static query") +func (n *WorkflowNode) SetStaticValue(path FieldPath, value any) *WorkflowNode { + n.staticValues[path.join()] = value + return n +} + +func (n *WorkflowNode) addDependencyRelation(fromNodeKey string, inputs []*FieldMapping, options *workflowAddInputOpts) *WorkflowNode { + for _, input := range inputs { + input.fromNodeKey = fromNodeKey + } + + if options.noDirectDependency { + n.addInputs = append(n.addInputs, func() error { + var paths []FieldPath + for _, input := range inputs { + paths = append(paths, input.targetPath()) + } + if err := n.checkAndAddMappedPath(paths); err != nil { + return err + } + + if err := n.g.addEdgeWithMappings(fromNodeKey, n.key, true, false, inputs...); err != nil { + return err + } + n.dependencySetter(fromNodeKey, noDirectDependency) + return nil + }) + } else if options.dependencyWithoutInput { + n.addInputs = append(n.addInputs, func() error { + if len(inputs) > 0 { + return fmt.Errorf("dependency without input should not have inputs. node: %s, fromNode: %s, inputs: %v", n.key, fromNodeKey, inputs) + } + if err := n.g.addEdgeWithMappings(fromNodeKey, n.key, false, true); err != nil { + return err + } + n.dependencySetter(fromNodeKey, normalDependency) + return nil + }) + } else { + n.addInputs = append(n.addInputs, func() error { + var paths []FieldPath + for _, input := range inputs { + paths = append(paths, input.targetPath()) + } + if err := n.checkAndAddMappedPath(paths); err != nil { + return err + } + + if err := n.g.addEdgeWithMappings(fromNodeKey, n.key, false, false, inputs...); err != nil { + return err + } + n.dependencySetter(fromNodeKey, normalDependency) + return nil + }) + } + + return n +} + +func (n *WorkflowNode) checkAndAddMappedPath(paths []FieldPath) error { + if v, ok := n.mappedFieldPath[""]; ok { + if _, ok = v.(struct{}); ok { + return fmt.Errorf("entire output has already been mapped for node: %s", n.key) + } + } else { + if len(paths) == 0 { + n.mappedFieldPath[""] = struct{}{} + return nil + } else { + n.mappedFieldPath[""] = map[string]any{} + } + } + + for _, targetPath := range paths { + m := n.mappedFieldPath[""].(map[string]any) + var traversed FieldPath + for i, path := range targetPath { + traversed = append(traversed, path) + if v, ok := m[path]; ok { + if _, ok = v.(struct{}); ok { + return fmt.Errorf("two terminal field paths conflict for node %s: %v, %v", n.key, traversed, targetPath) + } + } + + if i < len(targetPath)-1 { + m[path] = make(map[string]any) + m = m[path].(map[string]any) + } else { + m[path] = struct{}{} + } + } + } + + return nil +} + +// WorkflowBranch represents a branch added to a workflow. +// Each branch may define its own end nodes and mappings. +type WorkflowBranch struct { + fromNodeKey string + *GraphBranch +} + +// AddBranch adds a branch to the workflow. +// +// End Nodes Field Mappings: +// End nodes of the branch are required to define their own field mappings. +// This is a key distinction between Graph's Branch and Workflow's Branch: +// - Graph's Branch: Automatically passes its input to the selected node. +// - Workflow's Branch: Does not pass its input to the selected node. +func (wf *Workflow[I, O]) AddBranch(fromNodeKey string, branch *GraphBranch) *WorkflowBranch { + wb := &WorkflowBranch{ + fromNodeKey: fromNodeKey, + GraphBranch: branch, + } + + wf.workflowBranches = append(wf.workflowBranches, wb) + return wb +} + +// AddEnd connects a node to END with optional field mappings. +// Deprecated: use *Workflow[I,O].End() to obtain a WorkflowNode instance for END, then work with it just like a normal WorkflowNode. +func (wf *Workflow[I, O]) AddEnd(fromNodeKey string, inputs ...*FieldMapping) *Workflow[I, O] { + for _, input := range inputs { + input.fromNodeKey = fromNodeKey + } + _ = wf.g.addEdgeWithMappings(fromNodeKey, END, false, false, inputs...) + return wf +} + +func (wf *Workflow[I, O]) compile(ctx context.Context, options *graphCompileOptions) (*composableRunnable, error) { + if wf.g.buildError != nil { + return nil, wf.g.buildError + } + + for _, wb := range wf.workflowBranches { + for endNode := range wb.endNodes { + if endNode == END { + if _, ok := wf.dependencies[END]; !ok { + wf.dependencies[END] = make(map[string]dependencyType) + } + wf.dependencies[END][wb.fromNodeKey] = branchDependency + } else { + n := wf.workflowNodes[endNode] + n.dependencySetter(wb.fromNodeKey, branchDependency) + } + } + _ = wf.g.addBranch(wb.fromNodeKey, wb.GraphBranch, true) + } + + for _, n := range wf.workflowNodes { + for _, addInput := range n.addInputs { + if err := addInput(); err != nil { + return nil, err + } + } + n.addInputs = nil + } + + for _, n := range wf.workflowNodes { + if len(n.staticValues) > 0 { + value := make(map[string]any, len(n.staticValues)) + var paths []FieldPath + for path, v := range n.staticValues { + value[path] = v + paths = append(paths, splitFieldPath(path)) + } + + if err := n.checkAndAddMappedPath(paths); err != nil { + return nil, err + } + + pair := handlerPair{ + invoke: func(in any) (any, error) { + values := []any{in, value} + return mergeValues(values, nil) + }, + transform: func(in streamReader) streamReader { + sr := schema.StreamReaderFromArray([]map[string]any{value}) + newS, err := mergeValues([]any{in, packStreamReader(sr)}, nil) + if err != nil { + errSR, errSW := schema.Pipe[map[string]any](1) + errSW.Send(nil, err) + errSW.Close() + return packStreamReader(errSR) + } + + return newS.(streamReader) + }, + } + + for i := range paths { + wf.g.fieldMappingRecords[n.key] = append(wf.g.fieldMappingRecords[n.key], ToFieldPath(paths[i])) + } + + wf.g.handlerPreNode[n.key] = []handlerPair{pair} + } + } + + // TODO: check indirect edges are legal + + return wf.g.compile(ctx, options) +} + +func (wf *Workflow[I, O]) initNode(key string) *WorkflowNode { + n := &WorkflowNode{ + g: wf.g, + key: key, + staticValues: make(map[string]any), + dependencySetter: func(fromNodeKey string, typ dependencyType) { + if _, ok := wf.dependencies[key]; !ok { + wf.dependencies[key] = make(map[string]dependencyType) + } + wf.dependencies[key][fromNodeKey] = typ + }, + mappedFieldPath: make(map[string]any), + } + wf.workflowNodes[key] = n + return n +} + +func (wf *Workflow[I, O]) getGenericHelper() *genericHelper { + return wf.g.getGenericHelper() +} + +func (wf *Workflow[I, O]) inputType() reflect.Type { + return wf.g.inputType() +} + +func (wf *Workflow[I, O]) outputType() reflect.Type { + return wf.g.outputType() +} + +func (wf *Workflow[I, O]) component() component { + return wf.g.component() +} diff --git a/compose/workflow_test.go b/compose/workflow_test.go new file mode 100644 index 0000000..d0d4293 --- /dev/null +++ b/compose/workflow_test.go @@ -0,0 +1,1603 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compose + +import ( + "context" + "errors" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/internal/mock/components/embedding" + "github.com/cloudwego/eino/internal/mock/components/indexer" + "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +func TestWorkflow(t *testing.T) { + ctx := context.Background() + + type structA struct { + Field1 string + Field2 int + Field3 []any + } + + type structB struct { + Field1 string + Field2 int + } + + type structC struct { + Field1 string + } + + type structE struct { + Field1 string + Field2 string + Field3 []any + } + + type structF struct { + Field1 string + Field2 string + Field3 []any + B int + StateTemp string + } + RegisterStreamChunkConcatFunc(func(ts []*structF) (*structF, error) { + ret := &structF{} + for _, tt := range ts { + ret.Field1 += tt.Field1 + ret.Field2 += tt.Field2 + ret.Field3 = append(ret.Field3, tt.Field3...) + ret.B += tt.B + ret.StateTemp += tt.StateTemp + } + return ret, nil + }) + + type state struct { + temp string + } + + type structEnd struct { + Field1 string + } + + subGraph := NewGraph[string, *structB]() + _ = subGraph.AddLambdaNode( + "1", + InvokableLambda(func(ctx context.Context, input string) (*structB, error) { + return &structB{Field1: input, Field2: 33}, nil + }), + ) + _ = subGraph.AddEdge(START, "1") + _ = subGraph.AddEdge("1", END) + + subChain := NewChain[any, *structC](). + AppendLambda(InvokableLambda(func(_ context.Context, in any) (*structC, error) { + return &structC{Field1: fmt.Sprintf("%d", in)}, nil + })) + + type struct2 struct { + F map[string]any + } + subWorkflow := NewWorkflow[[]any, []any]() + subWorkflow.AddLambdaNode( + "1", + InvokableLambda(func(_ context.Context, in []any) ([]any, error) { + return in, nil + }), + WithOutputKey("key")). + AddInput(START) // []any -> map["key"][]any + subWorkflow.AddLambdaNode( + "2", + InvokableLambda(func(_ context.Context, in []any) ([]any, error) { + return in, nil + }), + WithInputKey("key"), + WithOutputKey("key1")). + AddInput("1") // map["key"][]any -> []any -> map["key1"][]any + subWorkflow.AddLambdaNode( + "3", + InvokableLambda(func(_ context.Context, in struct2) (map[string]any, error) { + return in.F, nil + }), + ). + AddInput("2", ToField("F")) // map["key1"][]any -> map["F"]map["key1"][]any -> struct2{F: map["key1"]any} -> map["key1"][]any + subWorkflow.AddLambdaNode( + "4", + InvokableLambda(func(_ context.Context, in []any) ([]any, error) { + return in, nil + }), + WithInputKey("key1"), + ). + AddInput("3") // map["key1"][]any -> []any + subWorkflow.End().AddInput("4") + + w := NewWorkflow[*structA, *structEnd](WithGenLocalState(func(context.Context) *state { return &state{} })) + + w. + AddGraphNode("B", subGraph, + WithStatePostHandler(func(ctx context.Context, out *structB, state *state) (*structB, error) { + state.temp = out.Field1 + return out, nil + })). + AddInput(START, FromField("Field1")) + + w. + AddGraphNode("C", subChain). + AddInput(START, FromField("Field2")) + + w. + AddGraphNode("D", subWorkflow). + AddInput(START, FromField("Field3")) + + w. + AddLambdaNode( + "E", + TransformableLambda(func(_ context.Context, in *schema.StreamReader[structE]) (*schema.StreamReader[structE], error) { + return schema.StreamReaderWithConvert(in, func(in structE) (structE, error) { + if len(in.Field1) > 0 { + in.Field1 = "E:" + in.Field1 + } + if len(in.Field2) > 0 { + in.Field2 = "E:" + in.Field2 + } + + return in, nil + }), nil + }), + WithStreamStatePreHandler(func(ctx context.Context, in *schema.StreamReader[structE], state *state) (*schema.StreamReader[structE], error) { + temp := state.temp + return schema.StreamReaderWithConvert(in, func(v structE) (structE, error) { + if len(v.Field3) > 0 { + v.Field3 = append(v.Field3, "Pre:"+temp) + } + + return v, nil + }), nil + }), + WithStreamStatePostHandler(func(ctx context.Context, out *schema.StreamReader[structE], state *state) (*schema.StreamReader[structE], error) { + return schema.StreamReaderWithConvert(out, func(v structE) (structE, error) { + if len(v.Field1) > 0 { + v.Field1 = v.Field1 + "+Post" + } + return v, nil + }), nil + })). + AddInput("B", MapFields("Field1", "Field1")). + AddInput("C", MapFields("Field1", "Field2")). + AddInput("D", ToField("Field3")) + + w. + AddLambdaNode( + "F", + InvokableLambda(func(ctx context.Context, in *structF) (string, error) { + return fmt.Sprintf("%v_%v_%v_%v_%v", in.Field1, in.Field2, in.Field3, in.B, in.StateTemp), nil + }), + WithStatePreHandler(func(ctx context.Context, in *structF, state *state) (*structF, error) { + in.StateTemp = state.temp + return in, nil + }), + ). + AddInput("B", MapFields("Field2", "B")). + AddInput("E", + MapFields("Field1", "Field1"), + MapFields("Field2", "Field2"), + MapFields("Field3", "Field3"), + ) + + w.End().AddInput("F", ToField("Field1")) + + compiled, err := w.Compile(ctx) + assert.NoError(t, err) + + input := &structA{ + Field1: "1", + Field2: 2, + Field3: []any{ + 1, "good", + }, + } + out, err := compiled.Invoke(ctx, input) + assert.NoError(t, err) + assert.Equal(t, &structEnd{"E:1+Post_E:2_[1 good Pre:1]_33_1"}, out) + + outStream, err := compiled.Stream(ctx, input) + assert.NoError(t, err) + defer outStream.Close() + for { + chunk, err := outStream.Recv() + if err != nil { + if err == io.EOF { + break + } + + t.Error(err) + return + } + + assert.Equal(t, &structEnd{"E:1+Post_E:2_[1 good Pre:1]_33_1"}, chunk) + } +} + +func TestWorkflowWithMap(t *testing.T) { + ctx := context.Background() + + type structA struct { + F1 any + } + + wf := NewWorkflow[map[string]any, map[string]any]() + wf.AddLambdaNode("lambda1", InvokableLambda(func(ctx context.Context, in map[string]any) (map[string]any, error) { + return in, nil + })).AddInput(START, MapFields("map_key", "lambda1_key")) + wf.AddLambdaNode("lambda2", InvokableLambda(func(ctx context.Context, in *structA) (*structA, error) { + return in, nil + })).AddInput(START, MapFields("map_key", "F1")) + wf.End().AddInput("lambda1", MapFields("lambda1_key", "end_lambda1")) + wf.End().AddInput("lambda2", MapFields("F1", "end_lambda2")) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[string]any{"map_key": "value"}) + assert.NoError(t, err) + assert.Equal(t, map[string]any{"end_lambda1": "value", "end_lambda2": "value"}, out) +} + +func TestWorkflowWithNestedFieldMappings(t *testing.T) { + ctx := context.Background() + + type structA struct { + F1 string + } + + type structB struct { + F1 *structA + F2 map[string]any + F3 int + F4 any + F5 map[string]structA + F6 structA + } + + t.Run("from struct.struct.field", func(t *testing.T) { + wf := NewWorkflow[*structB, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, &structB{ + F1: &structA{ + F1: "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + + wf = NewWorkflow[*structB, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F1", "F2"})) + _, err = wf.Compile(ctx) + assert.ErrorContains(t, err, "has no field[F2]") + }) + + t.Run("to struct.(non-ptr)struct.field", func(t *testing.T) { + wf := NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F6", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, &structB{ + F6: structA{ + F1: "hello", + }, + }, out) + }) + + t.Run("to map.(non-ptr)struct.field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]structA]() + wf.End().AddInput(START, ToFieldPath([]string{"key", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]structA{ + "key": { + F1: "hello", + }, + }, out) + }) + + t.Run("from map.map.field", func(t *testing.T) { + wf := NewWorkflow[map[string]map[string]string, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[string]map[string]string{ + "F1": { + "F1": "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + + _, err = r.Invoke(ctx, map[string]map[string]string{ + "F1": { + "F2": "hello", + }, + }) + + var ie *internalError + assert.True(t, errors.As(err, &ie)) + var myErr *errMapKeyNotFound + assert.True(t, errors.As(ie.origError, &myErr)) + }) + + t.Run("from struct.map.field", func(t *testing.T) { + wf := NewWorkflow[*structB, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F2", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, &structB{ + F2: map[string]any{ + "F1": "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + + _, err = r.Invoke(ctx, &structB{ + F2: map[string]any{ + "F2": "hello", + }, + }) + var ie *internalError + assert.True(t, errors.As(err, &ie)) + var myErr *errMapKeyNotFound + assert.True(t, errors.As(ie.origError, &myErr)) + }) + + t.Run("from map.struct.field", func(t *testing.T) { + wf := NewWorkflow[map[string]*structA, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[string]*structA{ + "F1": { + F1: "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + + wf = NewWorkflow[map[string]*structA, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F1", "F2"})) + _, err = wf.Compile(ctx) + assert.ErrorContains(t, err, "has no field[F2]") + }) + + t.Run("from map[string]any.field", func(t *testing.T) { + wf := NewWorkflow[map[string]any, string]() + wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[string]any{ + "F1": &structA{ + F1: "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + + out, err = r.Invoke(ctx, map[string]any{ + "F1": map[string]any{ + "F1": "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + + _, err = r.Invoke(ctx, map[string]any{ + "F1": 1, + }) + + var ie *internalError + assert.True(t, errors.As(err, &ie)) + var myErr *errInterfaceNotValidForFieldMapping + assert.True(t, errors.As(ie.origError, &myErr)) + }) + + t.Run("to struct.struct.field", func(t *testing.T) { + wf := NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, &structB{ + F1: &structA{ + F1: "hello", + }, + }, out) + + wf = NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F1", "F2"})) + _, err = wf.Compile(ctx) + assert.ErrorContains(t, err, "has no field[F2]") + }) + + t.Run("to map.map.field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]map[string]string]() + wf.End().AddInput(START, ToFieldPath([]string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]map[string]string{ + "F1": { + "F1": "hello", + }, + }, out) + + wf1 := NewWorkflow[string, map[string]map[string]int]() + wf1.End().AddInput(START, ToFieldPath([]string{"F1", "F1"})) + _, err = wf1.Compile(ctx) + assert.ErrorContains(t, err, "field[string]-[int] is absolutely not assignable") + }) + + t.Run("to struct.map.field", func(t *testing.T) { + wf := NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F2", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, &structB{ + F2: map[string]any{ + "F1": "hello", + }, + }, out) + }) + + t.Run("to map.struct.struct.field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]*structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F1", "F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]*structB{ + "F1": { + F1: &structA{ + F1: "hello", + }, + }, + }, out) + }) + + t.Run("to struct.map.struct.field", func(t *testing.T) { + wf := NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F5", "key", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, &structB{ + F5: map[string]structA{ + "key": { + F1: "hello", + }, + }, + }, out) + }) + + t.Run("to map.map.struct(non-ptr).field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]map[string]structA]() + wf.End().AddInput(START, ToFieldPath([]string{"key1", "key2", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]map[string]structA{ + "key1": { + "key2": { + F1: "hello", + }, + }, + }, out) + }) + + t.Run("to struct.int.field", func(t *testing.T) { + wf := NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F3", "F1", "F1"})) + _, err := wf.Compile(ctx) + assert.ErrorContains(t, err, "type[int] is not valid") + }) + + t.Run("to struct.any.field", func(t *testing.T) { + wf := NewWorkflow[string, *structB]() + wf.End().AddInput(START, ToFieldPath([]string{"F4", "F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, &structB{ + F4: map[string]any{ + "F1": map[string]any{ + "F1": "hello", + }, + }, + }, out) + }) + + t.Run("to map.any.any.field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.End().AddInput(START, ToFieldPath([]string{"Key1", "Key2", "Key3"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "Key1": map[string]any{ + "Key2": map[string]any{ + "Key3": "hello", + }, + }, + }, out) + }) + + t.Run("to any", func(t *testing.T) { + wf := NewWorkflow[string, any]() + wf.End().AddInput(START) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, "hello", out) + }) + + t.Run("to any.field", func(t *testing.T) { + wf := NewWorkflow[string, any]() + wf.End().AddInput(START, ToFieldPath([]string{"Key1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "Key1": "hello", + }, out) + }) + + t.Run("to interface.field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]fmt.Stringer]() + wf.End().AddInput(START, ToFieldPath([]string{"Key1", "A"})) + _, err := wf.Compile(ctx) + assert.ErrorContains(t, err, "static check failed for mapping [from start to Key1\u001FA(field)], "+ + "the successor has intermediate interface type fmt.Stringer") + }) + + t.Run("both to map.any, and to map.any.field", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.End().AddInput(START, ToFieldPath([]string{"Key1"}), ToFieldPath([]string{"Key1", "Key2"})) + _, err := wf.Compile(ctx) + assert.ErrorContains(t, err, "two terminal field paths conflict") + }) + + t.Run("to map.any.any.field1, and to map.any.any.field2", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.End().AddInput(START, ToFieldPath([]string{"Key1", "Key2", "key3"}), ToFieldPath([]string{"Key1", "Key2", "key4"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "Key1": map[string]any{ + "Key2": map[string]any{ + "key3": "hello", + "key4": "hello", + }, + }, + }, out) + }) + + t.Run("from nested to nested", func(t *testing.T) { + wf := NewWorkflow[map[string]any, *structB]() + wf.End().AddInput(START, MapFieldPaths([]string{"key1", "key2"}, []string{"F1", "F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[string]any{ + "key1": map[string]any{ + "key2": "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, &structB{ + F1: &structA{ + F1: "hello", + }, + }, out) + }) + + t.Run("from nested to normal", func(t *testing.T) { + wf := NewWorkflow[map[string]any, *structA]() + wf.End().AddInput(START, MapFieldPaths(FieldPath{"key1", "key2"}, FieldPath{"F1"})) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[string]any{ + "key1": map[string]any{ + "key2": "hello", + }, + }) + assert.NoError(t, err) + assert.Equal(t, &structA{ + F1: "hello", + }, out) + }) +} + +func TestWorkflowCompile(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + + t.Run("compile without add end", func(t *testing.T) { + w := NewWorkflow[*schema.Message, []*schema.Message]() + w.AddToolsNode("1", &ToolsNode{}).AddInput(START) + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "end node not set") + }) + + t.Run("type mismatch", func(t *testing.T) { + w := NewWorkflow[string, string]() + w.AddToolsNode("1", &ToolsNode{}).AddInput(START) + w.End().AddInput("1") + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, " mismatch") + }) + + t.Run("predecessor's output not struct/struct ptr/map, mapping has FromField", func(t *testing.T) { + w := NewWorkflow[[]*schema.Document, []string]() + + w.AddIndexerNode("indexer", indexer.NewMockIndexer(ctrl)).AddInput(START, FromField("F1")) + w.End().AddInput("indexer") + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "predecessor output type should be struct") + }) + + t.Run("successor's input not struct/struct ptr/map, mapping has ToField", func(t *testing.T) { + w := NewWorkflow[[]string, [][]float64]() + w.AddEmbeddingNode("embedder", embedding.NewMockEmbedder(ctrl)).AddInput(START, ToField("F1")) + w.End().AddInput("embedder") + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "successor input type should be struct") + }) + + t.Run("map to non existing field in predecessor", func(t *testing.T) { + w := NewWorkflow[*schema.Message, []*schema.Message]() + w.AddToolsNode("tools_node", &ToolsNode{}).AddInput(START, FromField("non_exist")) + w.End().AddInput("tools_node") + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "type[schema.Message] has no field[non_exist]") + }) + + t.Run("map to not exported field in successor", func(t *testing.T) { + w := NewWorkflow[string, *FieldMapping]() + w.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })).AddInput(START) + w.End().AddInput("1", ToField("to")) + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "has an unexported field[to]") + }) + + t.Run("map from not exported field in predecessor", func(t *testing.T) { + w := NewWorkflow[*FieldMapping, string]() + w.End().AddInput(START, FromField("from")) + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "has an unexported field[from]") + }) + + t.Run("duplicate node key", func(t *testing.T) { + w := NewWorkflow[[]*schema.Message, []*schema.Message]() + w.AddChatModelNode("1", model.NewMockChatModel(ctrl)).AddInput(START) + w.AddToolsNode("1", &ToolsNode{}).AddInput("1") + w.End().AddInput("1") + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "node '1' already present") + }) + + t.Run("from non-existing node", func(t *testing.T) { + w := NewWorkflow[*schema.Message, []*schema.Message]() + w.AddToolsNode("1", &ToolsNode{}).AddInput(START) + w.End().AddInput("2") + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "edge start node '2' needs to be added to graph first") + }) + + t.Run("to map with non-string key type", func(t *testing.T) { + w := NewWorkflow[string, map[int]any]() + w.End().AddInput(START, ToField("1")) + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "type[map[int]interface {}] is not a map with string or string alias key") + + type stringAlias string + w1 := NewWorkflow[string, map[stringAlias]any]() + w1.End().AddInput(START, ToField("1")) + r, err := w1.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[stringAlias]any{ + "1": "hello", + }, out) + }) + + t.Run("from map with non-string key type", func(t *testing.T) { + w := NewWorkflow[map[int]any, string]() + w.End().AddInput(START, FromField("1")) + _, err := w.Compile(ctx) + assert.ErrorContains(t, err, "type[map[int]interface {}] is not a map with string or string alias key") + type stringAlias string + w1 := NewWorkflow[map[stringAlias]any, string]() + w1.End().AddInput(START, FromField("1")) + r, err := w1.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, map[stringAlias]any{ + "1": "hello", + }) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + }) +} + +func TestFanInToSameDest(t *testing.T) { + t.Run("traditional outputKey fan-in with map[string]any", func(t *testing.T) { + wf := NewWorkflow[string, []*schema.Message]() + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in, nil + }), WithOutputKey("q1")).AddInput(START) + wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in, nil + }), WithOutputKey("q2")).AddInput(START) + wf.AddChatTemplateNode("prompt", prompt.FromMessages(schema.Jinja2, schema.UserMessage("{{q1}}_{{q2}}"))). + AddInput("1", MapFields("q1", "q1")). + AddInput("2", MapFields("q2", "q2")) + wf.End().AddInput("prompt") + c, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := c.Invoke(context.Background(), "query") + assert.NoError(t, err) + assert.Equal(t, []*schema.Message{{Role: schema.User, Content: "query_query"}}, out) + }) + + t.Run("fan-in to a field of map", func(t *testing.T) { + type dest struct { + F map[string]any + } + + type in struct { + A string + B int + } + + wf := NewWorkflow[in, dest]() + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in, nil + }), WithOutputKey("A")).AddInput(START, FromField("A")) + wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in int) (output int, err error) { + return in, nil + }), WithOutputKey("B")).AddInput(START, FromField("B")) + wf.End().AddInput("1", ToField("F")).AddInput("2", ToField("F")) + _, err := wf.Compile(context.Background()) + assert.ErrorContains(t, err, "two terminal field paths conflict for node end: [F], [F]") + }) +} + +func TestIndirectEdge(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_" + in, nil + })).AddInput(START) + + wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in map[string]string) (output string, err error) { + return in["1"] + "_" + in[START], nil + })).AddInput("1", ToField("1")). + AddInputWithOptions(START, []*FieldMapping{ToField(START)}, WithNoDirectDependency()) + + wf.End().AddInput("2", ToField("2")). + AddInputWithOptions("1", []*FieldMapping{ToField("1")}, WithNoDirectDependency()) + + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := r.Invoke(context.Background(), "query") + assert.NoError(t, err) + assert.Equal(t, map[string]any{"1": "query_query", "2": "query_query_query"}, out) +} + +func TestDependencyWithNoInput(t *testing.T) { + t.Run("simple case", func(t *testing.T) { + wf := NewWorkflow[string, string]() + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return "useless", nil + })).AddInput(START) + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_done", nil + })).AddDependency("0").AddInputWithOptions(START, nil, WithNoDirectDependency()) + wf.End().AddInput("1") + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := r.Invoke(context.Background(), "hello") + assert.NoError(t, err) + assert.Equal(t, "hello_done", out) + }) + + t.Run("simple control flow: [Start] --> [Node '0'] --> [End]", func(t *testing.T) { + // [Start] --> [Node "0"] --> [End] + wf := NewWorkflow[map[string]any, map[string]any]() + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { + return map[string]any{ + "result": "result from node 0", + }, nil + })).AddDependency(START) + wf.End().AddInput("0", ToField("final_result")). + AddInputWithOptions(START, []*FieldMapping{ToField("final_from_start")}, WithNoDirectDependency()) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + ret, err := r.Invoke(context.Background(), map[string]any{ + "input": "hello", + }) + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "final_result": map[string]any{ + "result": "result from node 0", + }, + "final_from_start": map[string]any{ + "input": "hello", + }, + }, ret) + + sRet, err := r.Stream(context.Background(), map[string]any{ + "input": "hello", + }) + assert.NoError(t, err) + ret, err = concatStreamReader(sRet) + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "final_result": map[string]any{ + "result": "result from node 0", + }, + "final_from_start": map[string]any{ + "input": "hello", + }, + }, ret) + }) +} + +func TestStaticValue(t *testing.T) { + t.Run("prefill map", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { + return in, nil + })). + AddInput(START, ToField(START)). + SetStaticValue(FieldPath{"prefilled"}, "yo-ho") + wf.End().AddInput("0") + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := r.Invoke(context.Background(), "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{"prefilled": "yo-ho", START: "hello"}, out) + streamOut, err := r.Stream(context.Background(), "hello") + assert.NoError(t, err) + out = map[string]any{} + for { + chunk, err := streamOut.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + for k, v := range chunk { + out[k] = v + } + } + assert.Equal(t, map[string]any{"prefilled": "yo-ho", START: "hello"}, out) + }) + + t.Run("static value and to-all mapping conflict", func(t *testing.T) { + wf := NewWorkflow[map[string]any, map[string]any]() + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { + return in, nil + })). + AddInput(START). + SetStaticValue( + FieldPath{"prefilled"}, + "yo-ho", + ) + wf.End().AddInput("0") + _, err := wf.Compile(context.Background()) + assert.ErrorContains(t, err, "entire output has already been mapped for node: 0") + }) + + t.Run("static value and dynamic mapping conflict", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { + return in, nil + })). + AddInput(START, ToField("prefilled")). + SetStaticValue(FieldPath{"prefilled"}, "yo-ho") + wf.End().AddInput("0") + _, err := wf.Compile(context.Background()) + assert.ErrorContains(t, err, "two terminal field paths conflict for node 0: [prefilled], [prefilled]") + }) + + t.Run("all inputs are static values", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { + return in, nil + })). + AddDependency(START). + SetStaticValue(FieldPath{"a", "b"}, "a_b"). + SetStaticValue(FieldPath{"c", "d"}, "c_d"). + SetStaticValue(FieldPath{"a", "d"}, "a_d") + wf.End().AddInput("0") + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := r.Invoke(context.Background(), "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "a": map[string]any{ + "b": "a_b", + "d": "a_d", + }, + "c": map[string]any{ + "d": "c_d", + }, + }, out) + + type a struct { + B string + D string + } + + type s struct { + A a + C map[string]any + } + + wf1 := NewWorkflow[string, *s]() + wf1.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { + return in, nil + })). + AddDependency(START). + SetStaticValue(FieldPath{"A", "B"}, "a_b"). + SetStaticValue(FieldPath{"C", "D"}, "c_d"). + SetStaticValue(FieldPath{"A", "D"}, "a_d") + wf1.End().AddInput("0", MapFieldPaths(FieldPath{"A", "B"}, FieldPath{"A", "B"}), + MapFieldPaths(FieldPath{"A", "D"}, FieldPath{"A", "D"}), + MapFields("C", "C")) + r1, err := wf1.Compile(context.Background()) + assert.NoError(t, err) + out1, err := r1.Stream(context.Background(), "hello") + assert.NoError(t, err) + outChunk, err := out1.Recv() + out1.Close() + assert.Equal(t, &s{ + A: a{ + B: "a_b", + D: "a_d", + }, + C: map[string]any{ + "D": "c_d", + }, + }, outChunk) + }) +} + +func TestBranch(t *testing.T) { + ctx := context.Background() + t.Run("simple branch: one predecessor, two successor, one of them is END", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_" + in, nil + })).AddInputWithOptions(START, nil, WithNoDirectDependency()) + + wf.AddPassthroughNode("branch_1").AddInput(START, ToField(START)) + + branch := NewGraphBranch(func(ctx context.Context, in map[string]any) (string, error) { + if in[START] == "hello" { + return "1", nil + } + return END, nil + }, map[string]bool{ + "1": true, + END: true, + }) + wf.AddBranch("branch_1", branch) + wf.End().AddInput("1", ToField("1")).AddInputWithOptions(START, []*FieldMapping{ToField(START)}, WithNoDirectDependency()) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "1": "hello_hello", + START: "hello", + }, out) + out, err = r.Invoke(ctx, "world") + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + START: "world", + }, out) + }) + + t.Run("multiple predecessors", func(t *testing.T) { + wf := NewWorkflow[string, map[string]any]() + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_" + in, nil + })).AddInput(START) + wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_" + in, nil + })).AddInputWithOptions("1", nil, WithNoDirectDependency()) + wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_" + in, nil + })).AddInput(START) + + wf.AddPassthroughNode("branch_1").AddInput(START, ToField(START)).AddInput("1", ToField("1")).AddDependency("0") + wf.AddBranch("branch_1", NewGraphBranch(func(ctx context.Context, in map[string]any) (string, error) { + if in[START].(string) == "hello" { + return "2", nil + } + return END, nil + }, map[string]bool{ + "2": true, + END: true, + })) + wf.End().AddInput("2", ToField("2")).AddInputWithOptions(START, []*FieldMapping{ToField(START)}, WithNoDirectDependency()) + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, map[string]any{"2": "hello_hello_hello_hello", START: "hello"}, out) + out, err = r.Invoke(ctx, "world") + assert.NoError(t, err) + assert.Equal(t, map[string]any{START: "world"}, out) + }) + t.Run("empty input for node after branch", func(t *testing.T) { + wf := NewWorkflow[map[string]any, map[string]any]() + wf.AddLambdaNode("start_1", InvokableLambda(func(ctx context.Context, input map[string]any) (map[string]any, error) { + return map[string]any{}, nil + })).AddInput("start") + wf.AddLambdaNode("branch_1", InvokableLambda(func(ctx context.Context, input map[string]any) (map[string]any, error) { + return map[string]any{}, nil + })) + wf.AddPassthroughNode("my_branch").AddInput("start_1") + wf.AddBranch("my_branch", NewGraphBranch(func(ctx context.Context, input map[string]any) (string, error) { + return END, nil + }, map[string]bool{ + "branch_1": true, + END: true, + })) + wf.End().AddInput("branch_1") + runner, err := wf.Compile(context.Background()) + assert.NoError(t, err) + resp, err := runner.Invoke(context.Background(), map[string]any{}) + assert.NoError(t, err) + assert.Equal(t, resp, (map[string]any)(nil)) + }) +} + +type goodInterface interface { + GOOD() +} +type goodStruct struct{} + +func (g *goodStruct) GOOD() {} + +func TestMayAssignableFieldMapping(t *testing.T) { + type in struct { + A goodInterface + } + wf := NewWorkflow[in, *goodStruct]() + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input *goodStruct) (output goodInterface, err error) { return input, nil })). + AddInput(START, FromField("A")) + wf.End().AddInput("1") + ctx := context.Background() + r, err := wf.Compile(ctx) + assert.NoError(t, err) + result, err := r.Invoke(ctx, in{A: &goodStruct{}}) + assert.NoError(t, err) + result.GOOD() +} + +func TestNilValue(t *testing.T) { + t.Run("from map key with a nil value to map key", func(t *testing.T) { + wf := NewWorkflow[map[string]any, map[string]any]() + wf.End().AddInput(START, MapFields("a", "a")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + + result, err := r.Invoke(context.Background(), map[string]any{"a": nil}) + assert.NoError(t, err) + assert.Equal(t, map[string]any{"a": nil}, result) + }) + + t.Run("from nil struct field to map key", func(t *testing.T) { + type in struct { + A *string + } + wf := NewWorkflow[in, map[string]any]() + wf.End().AddInput(START, MapFields("A", "A")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + result, err := r.Invoke(context.Background(), in{A: nil}) + assert.NoError(t, err) + assert.Equal(t, map[string]any{"A": (*string)(nil)}, result) + }) + + t.Run("from map key with a nil value to struct field", func(t *testing.T) { + type out struct { + A *string + } + wf := NewWorkflow[map[string]any, out]() + wf.End().AddInput(START, MapFields("A", "A")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + result, err := r.Invoke(context.Background(), map[string]any{"A": nil}) + assert.NoError(t, err) + assert.Equal(t, out{A: (*string)(nil)}, result) + }) + + t.Run("from nil struct field to struct field", func(t *testing.T) { + type inOut struct { + A *string + } + wf := NewWorkflow[inOut, inOut]() + wf.End().AddInput(START, MapFields("A", "A")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + result, err := r.Invoke(context.Background(), inOut{A: nil}) + assert.NoError(t, err) + assert.Equal(t, inOut{A: (*string)(nil)}, result) + }) + + t.Run("from nil to a type that can't be nil", func(t *testing.T) { + wf := NewWorkflow[map[string]any, int]() + wf.End().AddInput(START, FromField("a")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]any{"a": nil}) + assert.ErrorContains(t, err, "runtime check failed for mapping [from a(field) of start], field[]-[int] is absolutely not assignable") + }) + + t.Run("from nil to a map other than map[string]any", func(t *testing.T) { + wf := NewWorkflow[map[string]any, map[string]fmt.Stringer]() + wf.End().AddInput(START, MapFields("a", "a")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := r.Invoke(context.Background(), map[string]any{"a": nil}) + assert.Equal(t, map[string]fmt.Stringer{ + "a": nil, + }, out) + }) +} + +func TestStreamFieldMap(t *testing.T) { + t.Run("multiple incomplete chunks in source stream", func(t *testing.T) { + wf := NewWorkflow[map[string]any, map[string]any]() + wf.End().AddInput(START, MapFields("a", "a"), MapFields("b", "b")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + + sr, sw := schema.Pipe[map[string]any](2) + sw.Send(map[string]any{"a": 1}, nil) + sw.Send(map[string]any{"b": 2}, nil) + sw.Close() + outputS, err := r.Transform(context.Background(), sr) + assert.NoError(t, err) + result, err := concatStreamReader(outputS) + assert.NoError(t, err) + assert.Equal(t, map[string]any{"a": 1, "b": 2}, result) + }) +} + +func TestRuntimeTypeCheck(t *testing.T) { + g := NewWorkflow[map[string]any, any]() + + _ = g. + AddLambdaNode("A", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })). + AddInput(START, FromField("A")) + + _ = g.AddLambdaNode("B", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { + return input, nil + })). + AddInput(START, FromField("B")) + + _ = g.AddLambdaNode("MergeA", InvokableLambda(func(ctx context.Context, input map[string]any) (output map[string]any, err error) { + return input, nil + })). + AddInput("A", ToField("a")). + AddInput("B", ToField("b")) + + g.End().AddInput("MergeA") + + ctx := context.Background() + r, err := g.Compile(ctx) + assert.NoError(t, err) + result, err := r.Stream(ctx, map[string]any{"A": "1", "B": "2"}) + assert.NoError(t, err) + chunk, err := result.Recv() + assert.NoError(t, err) + assert.Equal(t, map[string]any{"a": "1", "b": "2"}, chunk) + chunk, err = result.Recv() + assert.True(t, errors.Is(err, io.EOF)) +} + +func TestIntermediateMappingSource(t *testing.T) { + t.Run("intermediate any source is nil", func(t *testing.T) { + wf := NewWorkflow[map[string]any, any]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "b"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]any{ + "a": nil, + }) + assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [interface {}]") + outStream, err := r.Transform(context.Background(), schema.StreamReaderFromArray([]map[string]any{ + { + "a": nil, + }, + { + "b": "ok", + }, + })) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [interface {}]") + outStream.Close() + }) + + t.Run("intermediate map source is nil", func(t *testing.T) { + wf := NewWorkflow[map[string]any, any]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), nil) + assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for map type [map[string]interface {}]") + outStream, err := r.Stream(context.Background(), nil) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for map type [map[string]interface {}]") + outStream.Close() + }) + + t.Run("intermediate map ptr source is nil", func(t *testing.T) { + wf := NewWorkflow[*map[string]any, any]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), nil) + assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for type [*map[string]interface {}]") + outStream, err := r.Stream(context.Background(), nil) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for type [*map[string]interface {}]") + outStream.Close() + }) + + t.Run("intermediate struct ptr source is nil", func(t *testing.T) { + type inner struct { + A string + } + + wf := NewWorkflow[map[string]*inner, string]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"I", "A"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]*inner{"I": nil}) + assert.ErrorContains(t, err, "intermediate source value on path=[I A] is nil") + outStream, err := r.Stream(context.Background(), map[string]*inner{"I": nil}) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "intermediate source value on path=[I A] is nil") + outStream.Close() + }) + + t.Run("intermediate interface source is nil", func(t *testing.T) { + wf := NewWorkflow[map[string]fmt.Stringer, string]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "b"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": nil}) + assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [fmt.Stringer]") + outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": nil}) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [fmt.Stringer]") + outStream.Close() + }) + + t.Run("intermediate interface source valid", func(t *testing.T) { + wf := NewWorkflow[map[string]fmt.Stringer, string]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "A"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + out, err := r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) + assert.NoError(t, err) + assert.Equal(t, "hello", out) + outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) + assert.NoError(t, err) + out, err = outStream.Recv() + assert.NoError(t, err) + assert.Equal(t, "hello", out) + outStream.Close() + }) + + t.Run("intermediate interface source, source field not found at request time", func(t *testing.T) { + wf := NewWorkflow[map[string]fmt.Stringer, string]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "B"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) + assert.ErrorContains(t, err, "field mapping from a struct field, but field not found. field=B") + outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "field mapping from a struct field, but field not found. field=B") + outStream.Close() + }) + + t.Run("intermediate interface source, source field not exported at request time", func(t *testing.T) { + wf := NewWorkflow[map[string]fmt.Stringer, string]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "c"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello", c: "c"}}) + assert.ErrorContains(t, err, "field mapping from a struct field, but field not exported.") + outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello", c: "c"}}) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "field mapping from a struct field, but field not exported.") + outStream.Close() + }) + + t.Run("intermediate interface source, type mismatch at request time", func(t *testing.T) { + wf := NewWorkflow[map[string]fmt.Stringer, int]() + wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "A"})) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) + assert.ErrorContains(t, err, "runtime check failed for mapping [from a\x1fA(field) of start], field[string]-[int] is absolutely not assignable") + outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) + assert.NoError(t, err) + _, err = outStream.Recv() + assert.ErrorContains(t, err, "runtime check failed for mapping [from a\u001FA(field) of start], field[string]-[int] is absolutely not assignable") + outStream.Close() + }) +} + +type goodStruct2 struct { + A string + c string +} + +func (g *goodStruct2) String() string { + return g.A +} + +func TestSetFanInMergeConfig_RealStreamNode_Workflow(t *testing.T) { + wf := NewWorkflow[int, map[string]int]() + + wf.AddLambdaNode("s1", StreamableLambda(func(ctx context.Context, input int) (*schema.StreamReader[int], error) { + sr, sw := schema.Pipe[int](2) + sw.Send(input+1, nil) + sw.Send(input+2, nil) + sw.Close() + return sr, nil + })).AddInput(START) + + wf.AddLambdaNode("s2", StreamableLambda(func(ctx context.Context, input int) (*schema.StreamReader[int], error) { + sr, sw := schema.Pipe[int](2) + sw.Send(input+10, nil) + sw.Send(input+20, nil) + sw.Close() + return sr, nil + })).AddInput(START) + + wf.End().AddInput("s1", ToField("s1")).AddInput("s2", ToField("s2")) + + r, err := wf.Compile(context.Background(), + WithFanInMergeConfig(map[string]FanInMergeConfig{END: {StreamMergeWithSourceEOF: true}})) + assert.NoError(t, err) + + sr, err := r.Stream(context.Background(), 1) + assert.NoError(t, err) + + merged := make(map[string]map[int]bool) + var sourceEOFCount int + sourceNames := make(map[string]bool) + for { + m, e := sr.Recv() + if e != nil { + if name, ok := schema.GetSourceName(e); ok { + sourceEOFCount++ + sourceNames[name] = true + continue + } + if e == io.EOF { + break + } + assert.NoError(t, e) + } + + for k, v := range m { + if merged[k] == nil { + merged[k] = make(map[int]bool) + } + merged[k][v] = true + } + } + + assert.Equal(t, map[string]map[int]bool{"s1": {2: true, 3: true}, "s2": {11: true, 21: true}}, merged) + assert.Equal(t, 2, sourceEOFCount, "should receive SourceEOF for each input stream when StreamMergeWithSourceEOF is true") + assert.True(t, sourceNames["s1"], "should receive SourceEOF from s1") + assert.True(t, sourceNames["s2"], "should receive SourceEOF from s2") +} + +func TestCustomExtractor(t *testing.T) { + t.Run("custom extract from array element", func(t *testing.T) { + wf := NewWorkflow[[]int, map[string]int]() + wf.End().AddInput(START, ToField("a", WithCustomExtractor(func(input any) (any, error) { + return input.([]int)[0], nil + }))) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + result, err := r.Invoke(context.Background(), []int{1, 2}) + assert.NoError(t, err) + assert.Equal(t, map[string]int{"a": 1}, result) + }) + + t.Run("mix custom extract with normal mapping", func(t *testing.T) { + wf := NewWorkflow[map[string]any, map[string]int]() + wf.End().AddInput(START, ToField("a", WithCustomExtractor(func(input any) (any, error) { + return input.(map[string]any)["a"].([]any)[0].(map[string]any)["c"], nil + })), MapFields("b", "b")) + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + result, err := r.Invoke(context.Background(), map[string]any{ + "a": []any{ + map[string]any{ + "c": 1, + }, + }, + "b": 2, + }) + assert.NoError(t, err) + assert.Equal(t, map[string]int{"a": 1, "b": 2}, result) + }) +} + +func TestAddDependency(t *testing.T) { + ctx := context.Background() + + wf := NewWorkflow[string, any]() + + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { + return in + "_" + in, nil + })).AddDependency(START) + + wf.End().AddDependency("1") + + r, err := wf.Compile(ctx) + assert.NoError(t, err) + out, err := r.Invoke(ctx, "input") + assert.NoError(t, err) + assert.Equal(t, nil, out) +} + +func TestIndirectDependencyWithBranch(t *testing.T) { + t.Run("data only mapping across branch", func(t *testing.T) { + wf := NewWorkflow[[]int, map[string]any]() + + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input int) (output int, err error) { + return input + 1, nil + })). + AddInputWithOptions(START, []*FieldMapping{ToField("", WithCustomExtractor(func(input any) (any, error) { + inputList := input.([]int) + if len(inputList) == 0 { + return nil, fmt.Errorf("input list is empty") + } + return input.([]int)[0], nil + }))}, WithNoDirectDependency()) + + wf.AddBranch(START, NewGraphBranch(func(ctx context.Context, in []int) (endNode string, err error) { + if len(in) > 0 { + return "1", nil + } + + return END, nil + }, map[string]bool{"1": true, END: true})) + + wf.End(). + AddInput("1", ToField("output")). + SetStaticValue(FieldPath{"static"}, 2) + + r, err := wf.Compile(context.Background()) + assert.NoError(t, err) + + // skip lambda node "1" + out, err := r.Invoke(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, out, map[string]any{"static": 2}) + + // choose lambda node "1" + out, err = r.Invoke(context.Background(), []int{1}) + assert.NoError(t, err) + assert.Equal(t, out, map[string]any{"output": 2, "static": 2}) + }) + + t.Run("data only mapping across branch, with interrupt after branch", func(t *testing.T) { + wf := NewWorkflow[[]int, map[string]any]() + + wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input int) (output int, err error) { + return input + 1, nil + })). + AddInputWithOptions(START, []*FieldMapping{ToField("", WithCustomExtractor(func(input any) (any, error) { + inputList := input.([]int) + if len(inputList) == 0 { + return nil, fmt.Errorf("input list is empty") + } + return input.([]int)[0], nil + }))}, WithNoDirectDependency()) + + wf.AddBranch(START, NewGraphBranch(func(ctx context.Context, in []int) (endNode string, err error) { + if len(in) > 0 { + return "1", nil + } + + return END, nil + }, map[string]bool{"1": true, END: true})) + + wf.End(). + AddInput("1", ToField("output")). + SetStaticValue(FieldPath{"static"}, 2) + + r, err := wf.Compile(context.Background(), WithCheckPointStore(newInMemoryStore()), + WithInterruptBeforeNodes([]string{"1"})) + assert.NoError(t, err) + + // skip lambda node "1" + out, err := r.Invoke(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, out, map[string]any{"static": 2}) + + // choose lambda node "1" + _, err = r.Invoke(context.Background(), []int{1}, WithCheckPointID("123")) + _, ok := ExtractInterruptInfo(err) + assert.True(t, ok) + + out, err = r.Invoke(context.Background(), nil, WithCheckPointID("123")) + assert.NoError(t, err) + assert.Equal(t, out, map[string]any{"output": 2, "static": 2}) + }) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..9c53751 --- /dev/null +++ b/doc.go @@ -0,0 +1,19 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package eino provides building blocks for agent workflows, +// tools, and composable graph utilities. +package eino diff --git a/flow/agent/agent_option.go b/flow/agent/agent_option.go new file mode 100644 index 0000000..9d16fbe --- /dev/null +++ b/flow/agent/agent_option.go @@ -0,0 +1,71 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package agent defines common option types used by agents and multi-agents. +package agent + +import "github.com/cloudwego/eino/compose" + +// AgentOption is the common option type for various agent and multi-agent implementations. +// For options intended to use with underlying graph or components, use WithComposeOptions to specify. +// For options intended to use with particular agent/multi-agent implementations, use WrapImplSpecificOptFn to specify. +type AgentOption struct { + implSpecificOptFn any + composeOptions []compose.Option +} + +// GetComposeOptions returns all compose options from the given agent options. +func GetComposeOptions(opts ...AgentOption) []compose.Option { + var result []compose.Option + for _, opt := range opts { + result = append(result, opt.composeOptions...) + } + + return result +} + +// WithComposeOptions returns an agent option that specifies compose options. +func WithComposeOptions(opts ...compose.Option) AgentOption { + return AgentOption{ + composeOptions: opts, + } +} + +// WrapImplSpecificOptFn returns an agent option that specifies a function to modify the implementation-specific options. +func WrapImplSpecificOptFn[T any](optFn func(*T)) AgentOption { + return AgentOption{ + implSpecificOptFn: optFn, + } +} + +// GetImplSpecificOptions returns the implementation-specific options from the given agent options. +func GetImplSpecificOptions[T any](base *T, opts ...AgentOption) *T { + if base == nil { + base = new(T) + } + + for i := range opts { + opt := opts[i] + if opt.implSpecificOptFn != nil { + optFn, ok := opt.implSpecificOptFn.(func(*T)) + if ok { + optFn(base) + } + } + } + + return base +} diff --git a/flow/agent/multiagent/host/callback.go b/flow/agent/multiagent/host/callback.go new file mode 100644 index 0000000..e5bcc63 --- /dev/null +++ b/flow/agent/multiagent/host/callback.go @@ -0,0 +1,100 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package host + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/schema" + template "github.com/cloudwego/eino/utils/callbacks" +) + +// MultiAgentCallback is the callback interface for host multi-agent. +type MultiAgentCallback interface { + OnHandOff(ctx context.Context, info *HandOffInfo) context.Context +} + +// HandOffInfo is the info which will be passed to MultiAgentCallback.OnHandOff, representing a hand off event. +type HandOffInfo struct { + ToAgentName string + Argument string +} + +// ConvertCallbackHandlers converts []host.MultiAgentCallback to callbacks.Handler. +func ConvertCallbackHandlers(handlers ...MultiAgentCallback) callbacks.Handler { + onChatModelEnd := func(ctx context.Context, info *callbacks.RunInfo, output *model.CallbackOutput) context.Context { + msg := output.Message + if msg == nil || msg.Role != schema.Assistant || len(msg.ToolCalls) == 0 { + return ctx + } + + for _, cb := range handlers { + for _, toolCall := range msg.ToolCalls { + ctx = cb.OnHandOff(ctx, &HandOffInfo{ + ToAgentName: toolCall.Function.Name, + Argument: toolCall.Function.Arguments, + }) + } + } + + return ctx + } + + onChatModelEndWithStreamOutput := func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[*model.CallbackOutput]) context.Context { + go func() { + msg, err := schema.ConcatMessageStream(schema.StreamReaderWithConvert(output, + func(m *model.CallbackOutput) (*schema.Message, error) { + return m.Message, nil + })) + if err != nil { + fmt.Printf("concat message stream for host multi-agent failed: %v", err) + return + } + + for _, cb := range handlers { + for _, tc := range msg.ToolCalls { + _ = cb.OnHandOff(ctx, &HandOffInfo{ + ToAgentName: tc.Function.Name, + Argument: tc.Function.Arguments, + }) + } + } + }() + + return ctx + } + + return template.NewHandlerHelper().ChatModel(&template.ModelCallbackHandler{ + OnEnd: onChatModelEnd, + OnEndWithStreamOutput: onChatModelEndWithStreamOutput, + }).Handler() +} + +// convertCallbacks reads graph call options, extract host.MultiAgentCallback and convert it to callbacks.Handler. +func convertCallbacks(opts ...agent.AgentOption) callbacks.Handler { + agentOptions := agent.GetImplSpecificOptions(&options{}, opts...) + if len(agentOptions.agentCallbacks) == 0 { + return nil + } + + handlers := agentOptions.agentCallbacks + return ConvertCallbackHandlers(handlers...) +} diff --git a/flow/agent/multiagent/host/compose.go b/flow/agent/multiagent/host/compose.go new file mode 100644 index 0000000..6a65cfb --- /dev/null +++ b/flow/agent/multiagent/host/compose.go @@ -0,0 +1,335 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package host + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/schema" +) + +const ( + defaultHostNodeKey = "host" // the key of the host node in the graph + defaultHostPrompt = "decide which tool is best for the task and call only the best tool." + specialistsAnswersCollectorNodeKey = "specialist_answers_collect" + singleIntentAnswerNodeKey = "single_intent_answer" + multiIntentSummarizeNodeKey = "multi_intents_summarize" + defaultSummarizerPrompt = "summarize the answers from the specialists into a single answer." + map2ListConverterNodeKey = "map_to_list" +) + +type state struct { + msgs []*schema.Message + isMultipleIntents bool +} + +// NewMultiAgent creates a new host multi-agent system. +// +// IMPORTANT!! For models that don't output tool calls in the first streaming chunk (e.g. Claude) +// the default StreamToolCallChecker may not work properly since it only checks the first chunk for tool calls. +// In such cases, you need to implement a custom StreamToolCallChecker that can properly detect tool calls. +func NewMultiAgent(ctx context.Context, config *MultiAgentConfig) (*MultiAgent, error) { + if err := config.validate(); err != nil { + return nil, err + } + + hostKeyName := defaultHostNodeKey + if config.HostNodeName != "" { + hostKeyName = config.HostNodeName + } + + var ( + hostPrompt = config.Host.SystemPrompt + name = config.Name + toolCallChecker = config.StreamToolCallChecker + ) + + if len(hostPrompt) == 0 { + hostPrompt = defaultHostPrompt + } + + if len(name) == 0 { + name = "host multi agent" + } + + if toolCallChecker == nil { + toolCallChecker = firstChunkStreamToolCallChecker + } + + g := compose.NewGraph[[]*schema.Message, *schema.Message]( + compose.WithGenLocalState(func(context.Context) *state { return &state{} })) + + if err := g.AddPassthroughNode(specialistsAnswersCollectorNodeKey); err != nil { + return nil, err + } + + agentTools := make([]*schema.ToolInfo, 0, len(config.Specialists)) + agentMap := make(map[string]bool, len(config.Specialists)+1) + for i := range config.Specialists { + specialist := config.Specialists[i] + + agentTools = append(agentTools, &schema.ToolInfo{ + Name: specialist.Name, + Desc: specialist.IntendedUse, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "reason": { + Type: schema.String, + Desc: "the reason to call this tool", + }, + }), + }) + + if err := addSpecialistAgent(specialist, g); err != nil { + return nil, err + } + + agentMap[specialist.Name] = true + } + + chatModel, err := agent.ChatModelWithTools(config.Host.ChatModel, config.Host.ToolCallingModel, agentTools) + if err != nil { + return nil, err + } + + if err = addHostAgent(chatModel, hostPrompt, g, hostKeyName); err != nil { + return nil, err + } + + const convertorName = "msg2MsgList" + if err = g.AddLambdaNode(convertorName, compose.ToList[*schema.Message](), compose.WithNodeName("converter")); err != nil { + return nil, err + } + + if err = addDirectAnswerBranch(convertorName, g, toolCallChecker); err != nil { + return nil, err + } + + if err = addMultiSpecialistsBranch(convertorName, agentMap, g); err != nil { + return nil, err + } + + if err = addSingleIntentAnswerNode(g); err != nil { + return nil, err + } + + if err = addMultiIntentsSummarizeNode(config.Summarizer, g); err != nil { + return nil, err + } + + if err = addAfterSpecialistsBranch(g); err != nil { + return nil, err + } + + compileOpts := []compose.GraphCompileOption{compose.WithNodeTriggerMode(compose.AnyPredecessor), compose.WithGraphName(name)} + r, err := g.Compile(ctx, compileOpts...) + if err != nil { + return nil, err + } + + return &MultiAgent{ + runnable: r, + graph: g, + graphAddNodeOpts: []compose.GraphAddNodeOpt{compose.WithGraphCompileOptions(compileOpts...)}, + }, nil +} + +func addSpecialistAgent(specialist *Specialist, g *compose.Graph[[]*schema.Message, *schema.Message]) error { + if specialist.Invokable != nil || specialist.Streamable != nil { + lambda, err := compose.AnyLambda(specialist.Invokable, specialist.Streamable, nil, nil, compose.WithLambdaType("Specialist")) + if err != nil { + return err + } + preHandler := func(_ context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) { + return state.msgs, nil // replace the tool call message with input msgs stored in state + } + if err := g.AddLambdaNode(specialist.Name, lambda, compose.WithStatePreHandler(preHandler), + compose.WithNodeName(specialist.Name), compose.WithOutputKey(specialist.Name)); err != nil { + return err + } + } else if specialist.ChatModel != nil { + preHandler := func(_ context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) { + if len(specialist.SystemPrompt) > 0 { + return append([]*schema.Message{{ + Role: schema.System, + Content: specialist.SystemPrompt, + }}, state.msgs...), nil + } + + return state.msgs, nil // replace the tool call message with input msgs stored in state + } + + if err := g.AddChatModelNode(specialist.Name, specialist.ChatModel, compose.WithStatePreHandler(preHandler), compose.WithNodeName(specialist.Name), compose.WithOutputKey(specialist.Name)); err != nil { + return err + } + } + + return g.AddEdge(specialist.Name, specialistsAnswersCollectorNodeKey) +} + +func addHostAgent(model model.BaseChatModel, prompt string, g *compose.Graph[[]*schema.Message, *schema.Message], hostNodeName string) error { + preHandler := func(_ context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) { + state.msgs = input + if len(prompt) == 0 { + return input, nil + } + return append([]*schema.Message{{ + Role: schema.System, + Content: prompt, + }}, input...), nil + } + if err := g.AddChatModelNode(defaultHostNodeKey, model, compose.WithStatePreHandler(preHandler), compose.WithNodeName(hostNodeName)); err != nil { + return err + } + + return g.AddEdge(compose.START, defaultHostNodeKey) +} + +func addDirectAnswerBranch(convertorName string, g *compose.Graph[[]*schema.Message, *schema.Message], + toolCallChecker func(ctx context.Context, modelOutput *schema.StreamReader[*schema.Message]) (bool, error)) error { + // handles the case where the host agent returns a direct answer, instead of handling off to any specialist + branch := compose.NewStreamGraphBranch(func(ctx context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) { + isToolCall, err := toolCallChecker(ctx, sr) + if err != nil { + return "", err + } + if isToolCall { + return convertorName, nil + } + return compose.END, nil + }, map[string]bool{convertorName: true, compose.END: true}) + + return g.AddBranch(defaultHostNodeKey, branch) +} + +func addMultiSpecialistsBranch(convertorName string, agentMap map[string]bool, g *compose.Graph[[]*schema.Message, *schema.Message]) error { + branch := compose.NewGraphMultiBranch(func(ctx context.Context, input []*schema.Message) (map[string]bool, error) { + if len(input) != 1 { + return nil, fmt.Errorf("host agent output %d messages, but expected 1", len(input)) + } + + results := map[string]bool{} + for _, toolCall := range input[0].ToolCalls { + results[toolCall.Function.Name] = true + } + + if len(results) > 1 { + _ = compose.ProcessState(ctx, func(_ context.Context, state *state) error { + state.isMultipleIntents = true + return nil + }) + } + + return results, nil + }, agentMap) + + return g.AddBranch(convertorName, branch) +} + +func addSingleIntentAnswerNode(g *compose.Graph[[]*schema.Message, *schema.Message]) error { + rc := func(ctx context.Context, input *schema.StreamReader[map[string]any]) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderWithConvert(input, func(msgs map[string]any) (*schema.Message, error) { + if len(msgs) != 1 { + return nil, fmt.Errorf("host agent output %d messages, but expected 1", len(msgs)) + } + for _, msg := range msgs { + return msg.(*schema.Message), nil + } + return nil, schema.ErrNoValue + }), nil + } + + _ = g.AddLambdaNode(singleIntentAnswerNodeKey, compose.TransformableLambda(rc)) + return g.AddEdge(singleIntentAnswerNodeKey, compose.END) +} + +func addAfterSpecialistsBranch(g *compose.Graph[[]*schema.Message, *schema.Message]) error { + ab := func(ctx context.Context, _ *schema.StreamReader[map[string]any]) (string, error) { + var isMultipleIntents bool + _ = compose.ProcessState(ctx, func(_ context.Context, state *state) error { + isMultipleIntents = state.isMultipleIntents + return nil + }) + + if !isMultipleIntents { + return singleIntentAnswerNodeKey, nil + } + + return map2ListConverterNodeKey, nil + } + + b := compose.NewStreamGraphBranch(ab, map[string]bool{ + singleIntentAnswerNodeKey: true, + map2ListConverterNodeKey: true, + }) + + return g.AddBranch(specialistsAnswersCollectorNodeKey, b) +} + +func addMultiIntentsSummarizeNode(summarizer *Summarizer, g *compose.Graph[[]*schema.Message, *schema.Message]) error { + map2list := func(ctx context.Context, input map[string]any) ([]*schema.Message, error) { + var output []*schema.Message + for k := range input { + output = append(output, input[k].(*schema.Message)) + } + return output, nil + } + + _ = g.AddLambdaNode(map2ListConverterNodeKey, compose.InvokableLambda(map2list)) + + if summarizer != nil { + _ = g.AddChatModelNode(multiIntentSummarizeNodeKey, summarizer.ChatModel, + compose.WithStatePreHandler(func(ctx context.Context, in []*schema.Message, state *state) ([]*schema.Message, error) { + var ( + out []*schema.Message + systemPrompt = defaultSummarizerPrompt + ) + if summarizer.SystemPrompt != "" { + systemPrompt = summarizer.SystemPrompt + } + out = append(out, &schema.Message{ + Role: schema.System, + Content: systemPrompt, + }) + + out = append(out, state.msgs...) + out = append(out, in...) + return out, nil + })) + _ = g.AddEdge(map2ListConverterNodeKey, multiIntentSummarizeNodeKey) + return g.AddEdge(multiIntentSummarizeNodeKey, compose.END) + } + + s := func(ctx context.Context, input []*schema.Message) (*schema.Message, error) { + output := &schema.Message{ + Role: schema.Assistant, + } + + for _, msg := range input { + output.Content += msg.Content + "\n" + } + + return output, nil + } + + _ = g.AddLambdaNode(multiIntentSummarizeNodeKey, compose.InvokableLambda(s)) + _ = g.AddEdge(map2ListConverterNodeKey, multiIntentSummarizeNodeKey) + return g.AddEdge(multiIntentSummarizeNodeKey, compose.END) +} diff --git a/flow/agent/multiagent/host/compose_test.go b/flow/agent/multiagent/host/compose_test.go new file mode 100644 index 0000000..c0afef4 --- /dev/null +++ b/flow/agent/multiagent/host/compose_test.go @@ -0,0 +1,691 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package host + +import ( + "context" + "io" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +func TestHostMultiAgent(t *testing.T) { + ctrl := gomock.NewController(t) + mockHostLLM := model.NewMockToolCallingChatModel(ctrl) + mockSpecialistLLM1 := model.NewMockChatModel(ctrl) + + specialist1 := &Specialist{ + ChatModel: mockSpecialistLLM1, + SystemPrompt: "You are a helpful assistant.", + AgentMeta: AgentMeta{ + Name: "specialist 1", + IntendedUse: "do stuff that works", + }, + } + + specialist2Msg1 := &schema.Message{ + Role: schema.Assistant, + Content: "specialist2", + } + specialist2Msg2 := &schema.Message{ + Role: schema.Assistant, + Content: " stream answer", + } + + specialist2 := &Specialist{ + Invokable: func(ctx context.Context, input []*schema.Message, opts ...agent.AgentOption) (*schema.Message, error) { + return &schema.Message{ + Role: schema.Assistant, + Content: "specialist2 invoke answer", + }, nil + }, + Streamable: func(ctx context.Context, input []*schema.Message, opts ...agent.AgentOption) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{specialist2Msg1, specialist2Msg2}), nil + }, + AgentMeta: AgentMeta{ + Name: "specialist 2", + IntendedUse: "do stuff that works too", + }, + } + + ctx := context.Background() + + mockHostLLM.EXPECT().WithTools(gomock.Any()).Return(mockHostLLM, nil).AnyTimes() + + hostMA, err := NewMultiAgent(ctx, &MultiAgentConfig{ + Host: Host{ + ToolCallingModel: mockHostLLM, + }, + Specialists: []*Specialist{ + specialist1, + specialist2, + }, + }) + + assert.NoError(t, err) + + t.Run("generate direct answer from host", func(t *testing.T) { + directAnswerMsg := &schema.Message{ + Role: schema.Assistant, + Content: "direct answer", + } + + mockHostLLM.EXPECT().Generate(gomock.Any(), gomock.Any()).Return(directAnswerMsg, nil).Times(1) + + mockCallback := newMockAgentCallback(0) + + out, err := hostMA.Generate(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + assert.Equal(t, "direct answer", out.Content) + assert.Empty(t, mockCallback.infos) + }) + + t.Run("stream direct answer from host", func(t *testing.T) { + directAnswerMsg1 := &schema.Message{ + Role: schema.Assistant, + Content: "direct ", + } + + directAnswerMsg2 := &schema.Message{ + Role: schema.Assistant, + Content: "answer", + } + + sr, sw := schema.Pipe[*schema.Message](0) + go func() { + sw.Send(directAnswerMsg1, nil) + sw.Send(directAnswerMsg2, nil) + sw.Close() + }() + + mockHostLLM.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + + mockCallback := newMockAgentCallback(0) + outStream, err := hostMA.Stream(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + assert.Empty(t, mockCallback.infos) + + var msgs []*schema.Message + for { + msg, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + msgs = append(msgs, msg) + } + + outStream.Close() + + assert.Equal(t, directAnswerMsg1, msgs[0]) + assert.Equal(t, directAnswerMsg2, msgs[1]) + }) + + t.Run("generate hand off", func(t *testing.T) { + handOffMsg := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{ + Name: specialist1.Name, + Arguments: `{"reason": "specialist 1 is the best"}`, + }, + }, + }, + } + + specialistMsg := &schema.Message{ + Role: schema.Assistant, + Content: "specialist 1 answer", + } + + mockHostLLM.EXPECT().Generate(gomock.Any(), gomock.Any()).Return(handOffMsg, nil).Times(1) + mockSpecialistLLM1.EXPECT().Generate(gomock.Any(), gomock.Any()).Return(specialistMsg, nil).Times(1) + + mockCallback := newMockAgentCallback(1) + + out, err := hostMA.Generate(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + assert.Equal(t, "specialist 1 answer", out.Content) + mockCallback.wg.Wait() + assert.Equal(t, []*HandOffInfo{ + { + ToAgentName: specialist1.Name, + Argument: `{"reason": "specialist 1 is the best"}`, + }, + }, mockCallback.infos) + + handOffMsg.ToolCalls[0].Function.Name = specialist2.Name + handOffMsg.ToolCalls[0].Function.Arguments = `{"reason": "specialist 2 is even better"}` + mockHostLLM.EXPECT().Generate(gomock.Any(), gomock.Any()).Return(handOffMsg, nil).Times(1) + + mockCallback = newMockAgentCallback(1) + + out, err = hostMA.Generate(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + assert.Equal(t, "specialist2 invoke answer", out.Content) + mockCallback.wg.Wait() + assert.Equal(t, []*HandOffInfo{ + { + ToAgentName: specialist2.Name, + Argument: `{"reason": "specialist 2 is even better"}`, + }, + }, mockCallback.infos) + }) + + t.Run("stream hand off to chat model", func(t *testing.T) { + handOffMsg1 := &schema.Message{ + Role: schema.Assistant, + Content: "need to call function", + } + + handOffMsg2 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + }, + }, + } + + handOffMsg3 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{}, + }, + }, + } + + handOffMsg4 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{ + Name: specialist1.Name, + Arguments: `{"reason": "specialist 1 is the best"}`, + }, + }, + }, + } + + sr, sw := schema.Pipe[*schema.Message](0) + go func() { + sw.Send(handOffMsg1, nil) + sw.Send(handOffMsg2, nil) + sw.Send(handOffMsg3, nil) + sw.Send(handOffMsg4, nil) + sw.Close() + }() + + specialistMsg1 := &schema.Message{ + Role: schema.Assistant, + Content: "specialist ", + } + + specialistMsg2 := &schema.Message{ + Role: schema.Assistant, + Content: "1 answer", + } + + sr1, sw1 := schema.Pipe[*schema.Message](0) + go func() { + sw1.Send(specialistMsg1, nil) + sw1.Send(specialistMsg2, nil) + sw1.Close() + }() + + streamToolCallChecker := func(ctx context.Context, modelOutput *schema.StreamReader[*schema.Message]) (bool, error) { + defer modelOutput.Close() + + for { + msg, err := modelOutput.Recv() + if err != nil { + if err == io.EOF { + return false, nil + } + + return false, err + } + + if len(msg.ToolCalls) == 0 { + continue + } + + if len(msg.ToolCalls) > 0 { + return true, nil + } + } + } + + hostMA, err = NewMultiAgent(ctx, &MultiAgentConfig{ + Host: Host{ + ToolCallingModel: mockHostLLM, + }, + Specialists: []*Specialist{ + specialist1, + specialist2, + }, + StreamToolCallChecker: streamToolCallChecker, + }) + assert.NoError(t, err) + + mockHostLLM.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + mockSpecialistLLM1.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr1, nil).Times(1) + + mockCallback := newMockAgentCallback(1) + outStream, err := hostMA.Stream(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + + var msgs []*schema.Message + for { + msg, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + msgs = append(msgs, msg) + } + + outStream.Close() + + assert.Equal(t, specialistMsg1, msgs[0]) + assert.Equal(t, specialistMsg2, msgs[1]) + + mockCallback.wg.Wait() + + assert.Equal(t, []*HandOffInfo{ + { + ToAgentName: specialist1.Name, + Argument: `{"reason": "specialist 1 is the best"}`, + }, + }, mockCallback.infos) + + handOffMsg4.ToolCalls[0].Function.Name = specialist2.Name + handOffMsg4.ToolCalls[0].Function.Arguments = `{"reason": "specialist 2 is even better"}` + sr, sw = schema.Pipe[*schema.Message](0) + go func() { + sw.Send(handOffMsg1, nil) + sw.Send(handOffMsg2, nil) + sw.Send(handOffMsg3, nil) + sw.Send(handOffMsg4, nil) + sw.Close() + }() + + mockHostLLM.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + + mockCallback = newMockAgentCallback(1) + outStream, err = hostMA.Stream(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + + msgs = nil + for { + msg, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + msgs = append(msgs, msg) + } + + outStream.Close() + + assert.Equal(t, specialist2Msg1, msgs[0]) + assert.Equal(t, specialist2Msg2, msgs[1]) + + mockCallback.wg.Wait() + + assert.Equal(t, []*HandOffInfo{ + { + ToAgentName: specialist2.Name, + Argument: `{"reason": "specialist 2 is even better"}`, + }, + }, mockCallback.infos) + }) + + t.Run("multi-agent within graph", func(t *testing.T) { + handOffMsg := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{ + Name: specialist1.Name, + Arguments: `{"reason": "specialist 1 is the best"}`, + }, + }, + }, + } + + specialistMsg := &schema.Message{ + Role: schema.Assistant, + Content: "Beijing", + } + + mockHostLLM.EXPECT().Generate(gomock.Any(), gomock.Any()).Return(handOffMsg, nil).Times(1) + mockSpecialistLLM1.EXPECT().Generate(gomock.Any(), gomock.Any()).Return(specialistMsg, nil).Times(1) + + mockCallback := newMockAgentCallback(1) + + hostMA, err := NewMultiAgent(ctx, &MultiAgentConfig{ + Host: Host{ + ToolCallingModel: mockHostLLM, + }, + Specialists: []*Specialist{ + specialist1, + specialist2, + }, + }) + + assert.NoError(t, err) + + maGraph, opts := hostMA.ExportGraph() + + fullGraph, err := compose.NewChain[map[string]any, *schema.Message](). + AppendChatTemplate(prompt.FromMessages(schema.FString, schema.UserMessage("what's the capital city of {country_name}"))). + AppendGraph(maGraph, append(opts, compose.WithNodeKey("host_ma_node"))...). + Compile(ctx) + assert.NoError(t, err) + + out, err := fullGraph.Invoke(ctx, map[string]any{"country_name": "China"}, compose.WithCallbacks(ConvertCallbackHandlers(mockCallback)).DesignateNodeWithPath(compose.NewNodePath("host_ma_node", hostMA.HostNodeKey()))) + assert.NoError(t, err) + assert.Equal(t, "Beijing", out.Content) + + mockCallback.wg.Wait() + assert.Equal(t, []*HandOffInfo{ + { + ToAgentName: specialist1.Name, + Argument: `{"reason": "specialist 1 is the best"}`, + }, + }, mockCallback.infos) + }) + + t.Run("multiple intents", func(t *testing.T) { + handOffMsg1 := &schema.Message{ + Role: schema.Assistant, + Content: "need to call function", + } + + handOffMsg2 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + }, + }, + } + + handOffMsg3 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{}, + }, + }, + } + + handOffMsg4 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{ + Name: specialist1.Name, + Arguments: `{"reason": "specialist 1 is good"}`, + }, + }, { + Index: generic.PtrOf(1), + Function: schema.FunctionCall{ + Name: specialist2.Name, + Arguments: `{"reason": "specialist 2`, + }, + }, + }, + } + + handOffMsg5 := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(1), + Function: schema.FunctionCall{ + Arguments: ` is also good"}`, + }, + }, + }, + } + + sr := schema.StreamReaderFromArray([]*schema.Message{ + handOffMsg1, + handOffMsg2, + handOffMsg3, + handOffMsg4, + handOffMsg5, + }) + + specialist1Msg1 := &schema.Message{ + Role: schema.Assistant, + Content: "specialist ", + } + + specialist1Msg2 := &schema.Message{ + Role: schema.Assistant, + Content: "1 answer", + } + + sr1 := schema.StreamReaderFromArray([]*schema.Message{ + specialist1Msg1, + specialist1Msg2, + }) + + streamToolCallChecker := func(ctx context.Context, modelOutput *schema.StreamReader[*schema.Message]) (bool, error) { + defer modelOutput.Close() + + for { + msg, err := modelOutput.Recv() + if err != nil { + if err == io.EOF { + return false, nil + } + + return false, err + } + + if len(msg.ToolCalls) == 0 { + continue + } + + if len(msg.ToolCalls) > 0 { + return true, nil + } + } + } + + hostMA, err = NewMultiAgent(ctx, &MultiAgentConfig{ + Host: Host{ + ToolCallingModel: mockHostLLM, + }, + Specialists: []*Specialist{ + specialist1, + specialist2, + }, + StreamToolCallChecker: streamToolCallChecker, + }) + assert.NoError(t, err) + + mockHostLLM.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + mockSpecialistLLM1.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr1, nil).Times(1) + + mockCallback := newMockAgentCallback(2) + outStream, err := hostMA.Stream(ctx, nil, WithAgentCallbacks(mockCallback)) + assert.NoError(t, err) + + var msgs []*schema.Message + for { + msg, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + msgs = append(msgs, msg) + } + + outStream.Close() + + msg, err := schema.ConcatMessages(msgs) + assert.NoError(t, err) + if msg.Content != "specialist2 stream answer\nspecialist 1 answer\n" && + msg.Content != "specialist 1 answer\nspecialist2 stream answer\n" { + t.Errorf("Unexpected message content: %s", msg.Content) + } + + mockCallback.wg.Wait() + assert.Equal(t, []*HandOffInfo{ + { + ToAgentName: specialist1.Name, + Argument: `{"reason": "specialist 1 is good"}`, + }, + { + ToAgentName: specialist2.Name, + Argument: `{"reason": "specialist 2 is also good"}`, + }, + }, mockCallback.infos) + }) + + t.Run("summarize multiple intents", func(t *testing.T) { + + handOffMsg := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + Index: generic.PtrOf(0), + Function: schema.FunctionCall{ + Name: specialist1.Name, + Arguments: `{"reason": "specialist 1 is good"}`, + }, + }, { + Index: generic.PtrOf(1), + Function: schema.FunctionCall{ + Name: specialist2.Name, + Arguments: `{"reason": "specialist 2 is also good"}`, + }, + }, + }, + } + sr := schema.StreamReaderFromArray([]*schema.Message{ + handOffMsg, + }) + + specialist1Msg1 := &schema.Message{ + Role: schema.Assistant, + Content: "specialist 1 answer", + } + sr1 := schema.StreamReaderFromArray([]*schema.Message{ + specialist1Msg1, + }) + + const summaryContent = "summarized answer" + sr2 := schema.StreamReaderFromArray([]*schema.Message{ + { + Role: schema.Assistant, + Content: summaryContent, + }, + }) + + mockSumChatModel := model.NewMockChatModel(ctrl) + hostMA, err = NewMultiAgent(ctx, &MultiAgentConfig{ + Host: Host{ + ToolCallingModel: mockHostLLM, + }, + Specialists: []*Specialist{ + specialist1, + specialist2, + }, + Summarizer: &Summarizer{ + ChatModel: mockSumChatModel, + }, + }) + assert.NoError(t, err) + + mockHostLLM.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr, nil).Times(1) + mockSpecialistLLM1.EXPECT().Stream(gomock.Any(), gomock.Any()).Return(sr1, nil).Times(1) + mockSumChatModel.EXPECT().Stream(gomock.Any(), gomock.Cond(func(x any) bool { + return assert.Equal(t, defaultSummarizerPrompt, func() string { + if input := x.([]*schema.Message); len(input) > 0 { + return input[0].Content + } + return "" + }()) + })).Return(sr2, nil).Times(1) + + outStream, err := hostMA.Stream(ctx, nil) + assert.NoError(t, err) + + var msgs []*schema.Message + for { + msg, err := outStream.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + msgs = append(msgs, msg) + } + outStream.Close() + + msg, err := schema.ConcatMessages(msgs) + assert.NoError(t, err) + if msg.Content != summaryContent { + t.Errorf("Unexpected message content: %s", msg.Content) + } + }, + ) +} + +type mockAgentCallback struct { + infos []*HandOffInfo + wg sync.WaitGroup +} + +func (m *mockAgentCallback) OnHandOff(ctx context.Context, info *HandOffInfo) context.Context { + m.infos = append(m.infos, info) + m.wg.Done() + return ctx +} + +func newMockAgentCallback(expects int) *mockAgentCallback { + m := &mockAgentCallback{ + infos: make([]*HandOffInfo, 0), + wg: sync.WaitGroup{}, + } + + m.wg.Add(expects) + return m +} diff --git a/flow/agent/multiagent/host/doc.go b/flow/agent/multiagent/host/doc.go new file mode 100644 index 0000000..3d205ce --- /dev/null +++ b/flow/agent/multiagent/host/doc.go @@ -0,0 +1,17 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package host diff --git a/flow/agent/multiagent/host/options.go b/flow/agent/multiagent/host/options.go new file mode 100644 index 0000000..55b5395 --- /dev/null +++ b/flow/agent/multiagent/host/options.go @@ -0,0 +1,30 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package host + +import "github.com/cloudwego/eino/flow/agent" + +type options struct { + agentCallbacks []MultiAgentCallback +} + +// WithAgentCallbacks registers callbacks to be invoked by the host multi-agent. +func WithAgentCallbacks(agentCallbacks ...MultiAgentCallback) agent.AgentOption { + return agent.WrapImplSpecificOptFn(func(opts *options) { + opts.agentCallbacks = append(opts.agentCallbacks, agentCallbacks...) + }) +} diff --git a/flow/agent/multiagent/host/types.go b/flow/agent/multiagent/host/types.go new file mode 100644 index 0000000..19d417a --- /dev/null +++ b/flow/agent/multiagent/host/types.go @@ -0,0 +1,204 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package host implements the host pattern for multi-agent system. +package host + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/schema" +) + +// MultiAgent is a host multi-agent system. +// A host agent is responsible for deciding which specialist to 'hand off' the task to. +// One or more specialist agents are responsible for completing the task. +type MultiAgent struct { + runnable compose.Runnable[[]*schema.Message, *schema.Message] + graph *compose.Graph[[]*schema.Message, *schema.Message] + graphAddNodeOpts []compose.GraphAddNodeOpt +} + +// Generate runs the multi-agent synchronously and returns the final message. +func (ma *MultiAgent) Generate(ctx context.Context, input []*schema.Message, opts ...agent.AgentOption) (*schema.Message, error) { + composeOptions := agent.GetComposeOptions(opts...) + + handler := convertCallbacks(opts...) + if handler != nil { + composeOptions = append(composeOptions, compose.WithCallbacks(handler).DesignateNode(ma.HostNodeKey())) + } + + return ma.runnable.Invoke(ctx, input, composeOptions...) +} + +// Stream runs the multi-agent in streaming mode and returns a message stream. +func (ma *MultiAgent) Stream(ctx context.Context, input []*schema.Message, opts ...agent.AgentOption) (*schema.StreamReader[*schema.Message], error) { + composeOptions := agent.GetComposeOptions(opts...) + + handler := convertCallbacks(opts...) + if handler != nil { + composeOptions = append(composeOptions, compose.WithCallbacks(handler).DesignateNode(ma.HostNodeKey())) + } + + return ma.runnable.Stream(ctx, input, composeOptions...) +} + +// ExportGraph exports the underlying graph from MultiAgent, along with the []compose.GraphAddNodeOpt to be used when adding this graph to another graph. +func (ma *MultiAgent) ExportGraph() (compose.AnyGraph, []compose.GraphAddNodeOpt) { + return ma.graph, ma.graphAddNodeOpts +} + +// HostNodeKey returns the graph node key used for the host agent. +func (ma *MultiAgent) HostNodeKey() string { + return defaultHostNodeKey +} + +// MultiAgentConfig is the config for host multi-agent system. +type MultiAgentConfig struct { + Host Host + Specialists []*Specialist + + Name string // the name of the host multi-agent + HostNodeName string // the name of the host node in the graph, default is "host" + // StreamToolCallChecker is a function to determine whether the model's streaming output contains tool calls. + // Different models have different ways of outputting tool calls in streaming mode: + // - Some models (like OpenAI) output tool calls directly + // - Others (like Claude) output text first, then tool calls + // This handler allows custom logic to check for tool calls in the stream. + // It should return: + // - true if the output contains tool calls and agent should continue processing + // - false if no tool calls and agent should stop + // Note: This field only needs to be configured when using streaming mode + // Note: The handler MUST close the modelOutput stream before returning + // Optional. By default, it checks if the first chunk contains tool calls. + // Note: The default implementation does not work well with Claude, which typically outputs tool calls after text content. + // Note: If your ChatModel doesn't output tool calls first, you can try adding prompts to constrain the model from generating extra text during the tool call. + StreamToolCallChecker func(ctx context.Context, modelOutput *schema.StreamReader[*schema.Message]) (bool, error) + + // Summarizer is the summarizer agent that will summarize the outputs of all the chosen specialist agents. + // Only when the Host agent picks multiple Specialist will this be called. + // If you do not provide a summarizer, a default summarizer that simply concatenates all the output messages into one message will be used. + // Note: the default summarizer do not support streaming. + Summarizer *Summarizer +} + +func (conf *MultiAgentConfig) validate() error { + if conf == nil { + return errors.New("host multi agent config is nil") + } + + if conf.Host.ChatModel == nil && conf.Host.ToolCallingModel == nil { + return errors.New("host multi agent host ChatModel is nil") + } + + if len(conf.Specialists) == 0 { + return errors.New("host multi agent specialists are empty") + } + + for _, s := range conf.Specialists { + if s.ChatModel == nil && s.Invokable == nil && s.Streamable == nil { + return fmt.Errorf("specialist %s has no chat model or Invokable or Streamable", s.Name) + } + + if err := s.AgentMeta.validate(); err != nil { + return err + } + } + + return nil +} + +// AgentMeta is the meta information of an agent within a multi-agent system. +type AgentMeta struct { + Name string // the name of the agent, should be unique within multi-agent system + IntendedUse string // the intended use-case of the agent, used as the reason for the multi-agent system to hand over control to this agent +} + +func (am AgentMeta) validate() error { + if len(am.Name) == 0 { + return errors.New("agent meta name is empty") + } + + if len(am.IntendedUse) == 0 { + return errors.New("agent meta intended use is empty") + } + + return nil +} + +// Host is the host agent within a multi-agent system. +// Currently, it can only be a model.ChatModel. +type Host struct { + ToolCallingModel model.ToolCallingChatModel + // Deprecated: ChatModel is deprecated, please use ToolCallingModel instead. + // This field will be removed in a future release. + ChatModel model.ChatModel + SystemPrompt string +} + +// Specialist is a specialist agent within a host multi-agent system. +// It can be a model.ChatModel or any Invokable and/or Streamable, such as react.Agent. +// ChatModel and (Invokable / Streamable) are mutually exclusive, only one should be provided. +// notice: SystemPrompt only effects when ChatModel has been set. +// If Invokable is provided but not Streamable, then the Specialist will be 'compose.InvokableLambda'. +// If Streamable is provided but not Invokable, then the Specialist will be 'compose.StreamableLambda'. +// if Both Invokable and Streamable is provided, then the Specialist will be 'compose.AnyLambda'. +type Specialist struct { + AgentMeta + + ChatModel model.BaseChatModel + SystemPrompt string + + Invokable compose.Invoke[[]*schema.Message, *schema.Message, agent.AgentOption] + Streamable compose.Stream[[]*schema.Message, *schema.Message, agent.AgentOption] +} + +// Summarizer defines a lightweight agent used to summarize +// conversations or tool outputs using a chat model and prompt. +type Summarizer struct { + ChatModel model.BaseChatModel + SystemPrompt string +} + +func firstChunkStreamToolCallChecker(_ context.Context, sr *schema.StreamReader[*schema.Message]) (bool, error) { + defer sr.Close() + + for { + msg, err := sr.Recv() + if err == io.EOF { + return false, nil + } + if err != nil { + return false, err + } + + if len(msg.ToolCalls) > 0 { + return true, nil + } + + if len(msg.Content) == 0 { // skip empty chunks at the front + continue + } + + return false, nil + } +} diff --git a/flow/agent/react/callback.go b/flow/agent/react/callback.go new file mode 100644 index 0000000..d2e4c76 --- /dev/null +++ b/flow/agent/react/callback.go @@ -0,0 +1,33 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package react provides helpers to build callback handlers for React agents. +package react + +import ( + "github.com/cloudwego/eino/callbacks" + template "github.com/cloudwego/eino/utils/callbacks" +) + +// BuildAgentCallback builds a callback handler for agent. +// e.g. +// +// callback := BuildAgentCallback(modelHandler, toolHandler) +// agent, err := react.NewAgent(ctx, &AgentConfig{}) +// agent.Generate(ctx, input, agent.WithComposeOptions(compose.WithCallbacks(callback))) +func BuildAgentCallback(modelHandler *template.ModelCallbackHandler, toolHandler *template.ToolCallbackHandler) callbacks.Handler { + return template.NewHandlerHelper().ChatModel(modelHandler).Tool(toolHandler).Handler() +} diff --git a/flow/agent/react/doc.go b/flow/agent/react/doc.go new file mode 100644 index 0000000..1e012c6 --- /dev/null +++ b/flow/agent/react/doc.go @@ -0,0 +1,17 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package react diff --git a/flow/agent/react/option.go b/flow/agent/react/option.go new file mode 100644 index 0000000..e6c9fb4 --- /dev/null +++ b/flow/agent/react/option.go @@ -0,0 +1,401 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package react + +import ( + "context" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/schema" + ub "github.com/cloudwego/eino/utils/callbacks" +) + +// WithToolOptions returns an agent option that specifies tool.Option for the tools in agent. +func WithToolOptions(opts ...tool.Option) agent.AgentOption { + return agent.WithComposeOptions(compose.WithToolsNodeOption(compose.WithToolOption(opts...))) +} + +// WithChatModelOptions returns an agent option that specifies model.Option for the chat model in agent. +func WithChatModelOptions(opts ...model.Option) agent.AgentOption { + return agent.WithComposeOptions(compose.WithChatModelOption(opts...)) +} + +// WithToolList returns an agent option that specifies compose.ToolsNodeOption for ToolsNode in agent. +// If you also need to pass ToolInfo to the chat model, use WithTools instead. +// Deprecated: This changes tool list for ToolsNode ONLY. +func WithToolList(tools ...tool.BaseTool) agent.AgentOption { + return agent.WithComposeOptions(compose.WithToolsNodeOption(compose.WithToolList(tools...))) +} + +// WithTools is a convenience function that configures a React agent with a list of tools. +// It performs two essential operations: +// 1. Extracts tool information for the chat model to understand available tools +// 2. Registers the actual tool implementations for execution +// +// Parameters: +// - ctx: The context for the operation, used when calling Info() on each tool +// - tools: A variadic list of tools that must implement either InvokableTool or StreamableTool interfaces +// +// Returns: +// - []agent.AgentOption: A slice containing exactly 2 agent options: +// - Option 1: Configures the chat model with tool schemas via model.WithTools(toolInfos) +// - Option 2: Registers the tool implementations via compose.WithToolList(tools...) +// - error: Returns an error if any tool's Info() method fails +// +// Usage Example: +// +// ctx := context.Background() +// agentOptions, err := WithTools(ctx, myTool1, myTool2, myTool3) +// if err != nil { +// return fmt.Errorf("failed to configure tools: %w", err) +// } +// +// agent, err := react.NewAgent(ctx, &react.AgentConfig{ +// ToolCallingModel: myModel, +// // other config... +// }) +// if err != nil { +// return fmt.Errorf("failed to create agent: %w", err) +// } +// +// // Use the tool options with Generate or Stream methods +// msg, err := agent.Generate(ctx, messages, agentOptions...) +// // or +// stream, err := agent.Stream(ctx, messages, agentOptions...) +// +// Comparison with Related Functions: +// - WithToolList: Only registers tool implementations, doesn't configure the chat model +// - WithTools: Comprehensive setup that handles both chat model configuration and tool registration +// +// Notes: +// - The function always returns exactly 2 options when successful +// - Both returned options should be applied to the agent for proper tool functionality +func WithTools(ctx context.Context, tools ...tool.BaseTool) ([]agent.AgentOption, error) { + toolInfos := make([]*schema.ToolInfo, 0, len(tools)) + for _, tl := range tools { + info, err := tl.Info(ctx) + if err != nil { + return nil, err + } + + toolInfos = append(toolInfos, info) + } + + opts := make([]agent.AgentOption, 2) + opts[0] = agent.WithComposeOptions(compose.WithChatModelOption(model.WithTools(toolInfos))) + opts[1] = agent.WithComposeOptions(compose.WithToolsNodeOption(compose.WithToolList(tools...))) + return opts, nil +} + +// Iterator provides a lightweight FIFO stream of values and errors +// produced during agent execution. +type Iterator[T any] struct { + ch *internal.UnboundedChan[item[T]] +} + +// Next retrieves the next value from the iterator. +// It returns the zero value and false when the stream is exhausted. +func (iter *Iterator[T]) Next() (T, bool, error) { + ch := iter.ch + if ch == nil { + var zero T + return zero, false, nil + } + + i, ok := ch.Receive() + if !ok { + var zero T + return zero, false, nil + } + + return i.v, true, i.err +} + +// MessageFuture exposes asynchronous accessors for messages produced +// by Generate and Stream calls. +type MessageFuture interface { + // GetMessages returns an iterator for retrieving messages generated during "agent.Generate" calls. + GetMessages() *Iterator[*schema.Message] + + // GetMessageStreams returns an iterator for retrieving streaming messages generated during "agent.Stream" calls. + GetMessageStreams() *Iterator[*schema.StreamReader[*schema.Message]] +} + +// WithMessageFuture returns an agent option and a MessageFuture interface instance. +// The option configures the agent to collect messages generated during execution, +// while the MessageFuture interface allows users to asynchronously retrieve these messages. +// +// This function works correctly both when the agent is used directly and when it is +// embedded as a subgraph within another graph. Graph callbacks are filtered using +// the address in the context: only callbacks whose address contains a runnable segment +// matching the agent's configured graph name are processed. +func WithMessageFuture() (agent.AgentOption, MessageFuture) { + h := &cbHandler{started: make(chan struct{}), graphName: GraphName} + + cmHandler := &ub.ModelCallbackHandler{ + OnEnd: h.onChatModelEnd, + OnEndWithStreamOutput: h.onChatModelEndWithStreamOutput, + } + createToolResultSender := func() toolResultSender { + return func(toolName, callID, result string) { + msg := schema.ToolMessage(result, callID, schema.WithToolName(toolName)) + h.sendMessage(msg) + } + } + createStreamToolResultSender := func() streamToolResultSender { + return func(toolName, callID string, resultStream *schema.StreamReader[string]) { + cvt := func(in string) (*schema.Message, error) { + return schema.ToolMessage(in, callID, schema.WithToolName(toolName)), nil + } + msgStream := schema.StreamReaderWithConvert(resultStream, cvt) + h.sendMessageStream(msgStream) + } + } + + createEnhancedToolResultSender := func() enhancedToolResultSender { + return func(toolName, callID string, result *schema.ToolResult) { + var err error + msg := schema.ToolMessage("", callID, schema.WithToolName(toolName)) + msg.UserInputMultiContent, err = result.ToMessageInputParts() + if err != nil { + return + } + h.sendMessage(msg) + } + } + + createEnhancedStreamToolResultSender := func() enhancedStreamToolResultSender { + return func(toolName, callID string, resultStream *schema.StreamReader[*schema.ToolResult]) { + cvt := func(result *schema.ToolResult) (*schema.Message, error) { + var err error + msg := schema.ToolMessage("", callID, schema.WithToolName(toolName)) + msg.UserInputMultiContent, err = result.ToMessageInputParts() + if err != nil { + return nil, err + } + return msg, nil + } + msgStream := schema.StreamReaderWithConvert(resultStream, cvt) + h.sendMessageStream(msgStream) + } + } + + graphHandler := callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + ctx = h.onGraphStart(ctx, info, input) + return setToolResultSendersToCtx(ctx, &toolResultSenders{ + sender: createToolResultSender(), + streamSender: createStreamToolResultSender(), + enhancedResultSender: createEnhancedToolResultSender(), + enhancedStreamToolResultSender: createEnhancedStreamToolResultSender(), + }) + }). + OnStartWithStreamInputFn(func(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + ctx = h.onGraphStartWithStreamInput(ctx, info, input) + return setToolResultSendersToCtx(ctx, &toolResultSenders{ + sender: createToolResultSender(), + streamSender: createStreamToolResultSender(), + enhancedResultSender: createEnhancedToolResultSender(), + enhancedStreamToolResultSender: createEnhancedStreamToolResultSender(), + }) + }). + OnEndFn(h.onGraphEnd). + OnEndWithStreamOutputFn(h.onGraphEndWithStreamOutput). + OnErrorFn(h.onGraphError).Build() + cb := ub.NewHandlerHelper().ChatModel(cmHandler).Graph(graphHandler).Handler() + option := agent.WithComposeOptions(compose.WithCallbacks(cb)) + + return option, h +} + +type item[T any] struct { + v T + err error +} + +type cbHandler struct { + graphName string + + ownAddress compose.Address + ownClaimed bool + + msgs *internal.UnboundedChan[item[*schema.Message]] + sMsgs *internal.UnboundedChan[item[*schema.StreamReader[*schema.Message]]] + + started chan struct{} +} + +func (h *cbHandler) GetMessages() *Iterator[*schema.Message] { + <-h.started + + return &Iterator[*schema.Message]{ch: h.msgs} +} + +func (h *cbHandler) GetMessageStreams() *Iterator[*schema.StreamReader[*schema.Message]] { + <-h.started + + return &Iterator[*schema.StreamReader[*schema.Message]]{ch: h.sMsgs} +} + +// isOwnGraph reports whether the callback is being invoked for the React agent's own graph. +// +// After the first onGraphStart call records h.ownAddress, subsequent calls compare the +// current context address against that exact recorded address. This handles nested React +// agents correctly: each cbHandler instance records its own unique full address path, so +// two React agents at different nesting depths (e.g., an outer agent whose tool invokes +// another React agent) will never interfere with each other even if they share the same +// graph name. +func (h *cbHandler) isOwnGraph(ctx context.Context) bool { + if !h.ownClaimed { + return false + } + return compose.GetCurrentAddress(ctx).Equals(h.ownAddress) +} + +// claimOwnership is called exclusively from onGraphStart / onGraphStartWithStreamInput +// to record this handler's address on first invocation. It returns true if the handler +// has not yet been initialised, records the current address, and returns false on all +// subsequent calls. +func (h *cbHandler) claimOwnership(ctx context.Context) bool { + if h.ownClaimed { + return false + } + h.ownAddress = compose.GetCurrentAddress(ctx) + h.ownClaimed = true + return true +} + +func (h *cbHandler) onChatModelEnd(ctx context.Context, + _ *callbacks.RunInfo, input *model.CallbackOutput) context.Context { + + h.sendMessage(input.Message) + + return ctx +} + +func (h *cbHandler) onChatModelEndWithStreamOutput(ctx context.Context, + _ *callbacks.RunInfo, input *schema.StreamReader[*model.CallbackOutput]) context.Context { + + c := func(output *model.CallbackOutput) (*schema.Message, error) { + return output.Message, nil + } + s := schema.StreamReaderWithConvert(input, c) + + h.sendMessageStream(s) + + return ctx +} + +func (h *cbHandler) onGraphError(ctx context.Context, + _ *callbacks.RunInfo, err error) context.Context { + + if !h.isOwnGraph(ctx) { + return ctx + } + + if h.msgs != nil { + h.msgs.Send(item[*schema.Message]{err: err}) + } else if h.sMsgs != nil { + h.sMsgs.Send(item[*schema.StreamReader[*schema.Message]]{err: err}) + } + + return ctx +} + +func (h *cbHandler) onGraphEnd(ctx context.Context, + _ *callbacks.RunInfo, _ callbacks.CallbackOutput) context.Context { + + if !h.isOwnGraph(ctx) { + return ctx + } + + if h.msgs != nil { + h.msgs.Close() + } + + return ctx +} + +func (h *cbHandler) onGraphEndWithStreamOutput(ctx context.Context, + _ *callbacks.RunInfo, _ *schema.StreamReader[callbacks.CallbackOutput]) context.Context { + + if !h.isOwnGraph(ctx) { + return ctx + } + + if h.sMsgs != nil { + h.sMsgs.Close() + } + + return ctx +} + +func (h *cbHandler) onGraphStart(ctx context.Context, + _ *callbacks.RunInfo, _ callbacks.CallbackInput) context.Context { + + if !h.claimOwnership(ctx) { + return ctx + } + + h.msgs = internal.NewUnboundedChan[item[*schema.Message]]() + close(h.started) + + return ctx +} + +func (h *cbHandler) onGraphStartWithStreamInput(ctx context.Context, _ *callbacks.RunInfo, + input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + input.Close() + + if !h.claimOwnership(ctx) { + return ctx + } + + h.sMsgs = internal.NewUnboundedChan[item[*schema.StreamReader[*schema.Message]]]() + close(h.started) + + return ctx +} + +func (h *cbHandler) sendMessage(msg *schema.Message) { + if h.msgs != nil { + h.msgs.Send(item[*schema.Message]{v: msg}) + } else { + sMsg := schema.StreamReaderFromArray([]*schema.Message{msg}) + h.sMsgs.Send(item[*schema.StreamReader[*schema.Message]]{v: sMsg}) + } +} + +func (h *cbHandler) sendMessageStream(sMsg *schema.StreamReader[*schema.Message]) { + if h.sMsgs != nil { + h.sMsgs.Send(item[*schema.StreamReader[*schema.Message]]{v: sMsg}) + } else { + // concat + msg, err := schema.ConcatMessageStream(sMsg) + + if err != nil { + h.msgs.Send(item[*schema.Message]{err: err}) + } else { + h.msgs.Send(item[*schema.Message]{v: msg}) + } + } +} diff --git a/flow/agent/react/option_test.go b/flow/agent/react/option_test.go new file mode 100644 index 0000000..1449089 --- /dev/null +++ b/flow/agent/react/option_test.go @@ -0,0 +1,1388 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package react + +import ( + "context" + "io" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" +) + +func TestWithMessageFuture(t *testing.T) { + ctx := context.Background() + + // Test with tool calls + t.Run("test generate with tool calls", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Mock model response with tool call + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create agent with MessageFuture + option, future := WithMessageFuture() + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + // Generate response + response, err := a.Generate(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, option) + assert.Nil(t, err) + assert.Equal(t, "final response", response.Content) + + sIter := future.GetMessageStreams() + // Should be no messages + _, hasNext, err := sIter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + + iter := future.GetMessages() + // First message should be the assistant message for tool calling + msg1, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + // Second message should be the tool response + msg2, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Tool, msg2.Role) + + // Third message should be the final response + msg3, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, "final response", msg3.Content) + + // Should be no more messages + _, hasNext, err = iter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + // Test with streaming tool calls + t.Run("test generate with streaming tool calls", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeStreamToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Mock model response with tool call + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create agent with MessageFuture + option, future := WithMessageFuture() + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + // Generate response + response, err := a.Generate(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, option) + assert.Nil(t, err) + assert.Equal(t, "final response", response.Content) + + // Get messages from future + iter := future.GetMessages() + + // First message should be the assistant message for tool calling + msg1, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + // Second message should be the tool response + msg2, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Tool, msg2.Role) + + // Third message should be the final response + msg3, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, "final response", msg3.Content) + + // Should be no more messages + _, hasNext, err = iter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + + // Test with non-streaming tool but using agent's Stream interface + t.Run("test stream with tool calls", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Mock model response with tool call + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }, + })}), nil). + Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("final response", nil)}), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create agent with MessageFuture + option, future := WithMessageFuture() + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + // Use Stream interface + stream, err := a.Stream(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, option) + assert.Nil(t, err) + + // Collect all chunks from stream + finalResponse, err := schema.ConcatMessageStream(stream) + assert.Nil(t, err) + assert.Equal(t, "final response", finalResponse.Content) + + iter := future.GetMessages() + // Should be no messages + _, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + + // Get message streams from future + sIter := future.GetMessageStreams() + + // First message should be the assistant message for tool calling + stream1, hasNext, err := sIter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.NotNil(t, stream1) + msg1, err := schema.ConcatMessageStream(stream1) + assert.Nil(t, err) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + // Second message should be the tool response + stream2, hasNext, err := sIter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.NotNil(t, stream2) + msg2, err := schema.ConcatMessageStream(stream2) + assert.Nil(t, err) + assert.Equal(t, schema.Tool, msg2.Role) + + // Third message should be the final response + stream3, hasNext, err := sIter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.NotNil(t, stream3) + msg3, err := schema.ConcatMessageStream(stream3) + assert.Nil(t, err) + assert.Equal(t, "final response", msg3.Content) + + // Should be no more messages + _, hasNext, err = sIter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + + t.Run("test stream with streaming tool calls and with concurrent goroutines", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeStreamToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + // Mock model response with tool call + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }, + })}), nil). + Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("final response", nil)}), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + // Create agent with MessageFuture + option, future := WithMessageFuture() + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + // Get message streams from future + sIter := future.GetMessageStreams() + + // First message should be the assistant message for tool calling + stream1, hasNext, err_ := sIter.Next() + assert.Nil(t, err_) + assert.True(t, hasNext) + assert.NotNil(t, stream1) + msg1, err_ := schema.ConcatMessageStream(stream1) + assert.Nil(t, err_) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + // Second message should be the tool response + stream2, hasNext, err_ := sIter.Next() + assert.Nil(t, err_) + assert.True(t, hasNext) + assert.NotNil(t, stream2) + msg2, err_ := schema.ConcatMessageStream(stream2) + assert.Nil(t, err_) + assert.Equal(t, schema.Tool, msg2.Role) + + // Third message should be the final response + stream3, hasNext, err_ := sIter.Next() + assert.Nil(t, err_) + assert.True(t, hasNext) + assert.NotNil(t, stream3) + msg3, err_ := schema.ConcatMessageStream(stream3) + assert.Nil(t, err_) + assert.Equal(t, "final response", msg3.Content) + + // Should be no more messages + _, hasNext, err_ = sIter.Next() + assert.Nil(t, err_) + assert.False(t, hasNext) + }() + + // Use Stream interface + stream, err := a.Stream(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, option) + assert.Nil(t, err) + + // Collect all chunks from stream + finalResponse, err := schema.ConcatMessageStream(stream) + assert.Nil(t, err) + assert.Equal(t, "final response", finalResponse.Content) + + wg.Wait() + }) +} + +func TestWithToolOptions(t *testing.T) { + type dummyOpt struct{ val string } + opt := tool.WrapImplSpecificOptFn(func(o *dummyOpt) { o.val = "mock" }) + agentOpt := WithToolOptions(opt) + assert.NotNil(t, agentOpt) + // The returned value should be an agent.AgentOption (function) + assert.IsType(t, agentOpt, agentOpt) +} + +func TestWithChatModelOptions(t *testing.T) { + opt := model.WithModel("mock-model") + agentOpt := WithChatModelOptions(opt) + assert.NotNil(t, agentOpt) + assert.IsType(t, agentOpt, agentOpt) +} + +// dummyBaseTool is a minimal implementation of tool.BaseTool for testing. +type dummyBaseTool struct{} + +func (d *dummyBaseTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "dummy"}, nil +} + +func (d *dummyBaseTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) { + return "dummy-response", nil +} + +type assertTool struct { + toolOptVal string + receivedToolOpt bool +} +type toolOpt struct{ val string } + +func (a *assertTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{Name: "assert_tool"}, nil +} +func (a *assertTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + opt := tool.GetImplSpecificOptions(&toolOpt{}, opts...) + if opt.val == a.toolOptVal { + a.receivedToolOpt = true + } + return "tool-response", nil +} + +func TestAgentWithAllOptions(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + + // Prepare a tool that asserts it receives the tool option + toolOptVal := "tool-opt-value" + to := tool.WrapImplSpecificOptFn(func(o *toolOpt) { o.val = toolOptVal }) + at := &assertTool{toolOptVal: toolOptVal} + + // Prepare a mock chat model that asserts it receives the model option + cm := mockModel.NewMockToolCallingChatModel(ctrl) + modelOpt := model.WithModel("test-model") + modelOptReceived := false + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ []*schema.Message, opts ...model.Option) (*schema.Message, error) { + times++ + if times == 1 { + for _, o := range opts { + opt := model.GetCommonOptions(&model.Options{}, o) + if opt.Model != nil && *opt.Model == "test-model" { + modelOptReceived = true + } + } + + info, _ := at.Info(ctx) + return schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: "", + }, + }, + }), + nil + } + + return schema.AssistantMessage("ok", nil), nil + }, + ).AnyTimes() + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + agentOpt := WithToolOptions(to) + agentOpt2 := WithChatModelOptions(modelOpt) + agentOpt3, err := WithTools(context.Background(), at) + assert.NoError(t, err) + + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&dummyBaseTool{}}, + }, + MaxStep: 20, + }) + assert.NoError(t, err) + + _, err = a.Generate(ctx, []*schema.Message{ + schema.UserMessage("call the tool"), + }, agentOpt, agentOpt2, agentOpt3[0], agentOpt3[1]) + assert.NoError(t, err) + assert.True(t, modelOptReceived, "model option should be received by chat model") + assert.True(t, at.receivedToolOpt, "tool option should be received by tool") +} + +type simpleToolForMiddlewareTest struct { + name string + result string +} + +func (s *simpleToolForMiddlewareTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: s.name, + Desc: "simple tool for middleware test", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "input": { + Desc: "input", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (s *simpleToolForMiddlewareTest) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return s.result, nil +} + +func (s *simpleToolForMiddlewareTest) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) { + return schema.StreamReaderFromArray([]string{s.result}), nil +} + +func TestMessageFuture_ToolResultMiddleware_EmitsFinalResult(t *testing.T) { + originalResult := "original_result" + modifiedResult := "modified_by_middleware" + + resultModifyingMiddleware := compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + output, err := next(ctx, input) + if err != nil { + return nil, err + } + output.Result = modifiedResult + return output, nil + } + }, + Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + output, err := next(ctx, input) + if err != nil { + return nil, err + } + output.Result = schema.StreamReaderFromArray([]string{modifiedResult}) + return output, nil + } + }, + } + + t.Run("Invoke", func(t *testing.T) { + ctx := context.Background() + testTool := &simpleToolForMiddlewareTest{name: "test_tool", result: originalResult} + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + info, err := testTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"input": "test"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + option, future := WithMessageFuture() + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware}, + }, + MaxStep: 3, + }) + assert.NoError(t, err) + + response, err := a.Generate(ctx, []*schema.Message{ + schema.UserMessage("call the tool"), + }, option) + assert.NoError(t, err) + assert.Equal(t, "final response", response.Content) + + iter := future.GetMessages() + + var allMsgs []*schema.Message + for { + msg, hasNext, err := iter.Next() + if err != nil || !hasNext { + break + } + allMsgs = append(allMsgs, msg) + } + + assert.GreaterOrEqual(t, len(allMsgs), 3, "should have at least 3 messages") + if len(allMsgs) >= 3 { + assert.Equal(t, schema.Assistant, allMsgs[0].Role) + assert.Equal(t, 1, len(allMsgs[0].ToolCalls)) + + assert.Equal(t, schema.Tool, allMsgs[1].Role) + assert.Equal(t, modifiedResult, allMsgs[1].Content, + "MessageFuture should receive the middleware-modified tool result") + assert.NotEqual(t, originalResult, allMsgs[1].Content, + "MessageFuture should NOT receive the original tool result") + + assert.Equal(t, "final response", allMsgs[2].Content) + } + }) + + t.Run("Stream", func(t *testing.T) { + ctx := context.Background() + testTool := &simpleToolForMiddlewareTest{name: "test_tool_stream", result: originalResult} + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + info, err := testTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("", []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"input": "test"}`, + }, + }, + }), + }), nil). + Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("final response", nil), + }), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + option, future := WithMessageFuture() + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{testTool}, + ToolCallMiddlewares: []compose.ToolMiddleware{resultModifyingMiddleware}, + }, + MaxStep: 3, + }) + assert.NoError(t, err) + + response, err := a.Stream(ctx, []*schema.Message{ + schema.UserMessage("call the tool"), + }, option) + assert.NoError(t, err) + + var msgs []*schema.Message + for { + msg, err := response.Recv() + if err != nil { + break + } + msgs = append(msgs, msg) + } + finalMsg, err := schema.ConcatMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, "final response", finalMsg.Content) + + iter := future.GetMessageStreams() + + var allMsgs []*schema.Message + for { + msgStream, hasNext, err := iter.Next() + if err != nil || !hasNext { + break + } + var streamMsgs []*schema.Message + for { + msg, err := msgStream.Recv() + if err != nil { + break + } + streamMsgs = append(streamMsgs, msg) + } + if len(streamMsgs) > 0 { + concated, err := schema.ConcatMessages(streamMsgs) + if err == nil { + allMsgs = append(allMsgs, concated) + } + } + } + + assert.GreaterOrEqual(t, len(allMsgs), 3, "should have at least 3 messages") + if len(allMsgs) >= 3 { + assert.Equal(t, schema.Assistant, allMsgs[0].Role) + assert.Equal(t, 1, len(allMsgs[0].ToolCalls)) + + assert.Equal(t, schema.Tool, allMsgs[1].Role) + assert.Equal(t, modifiedResult, allMsgs[1].Content, + "MessageFuture should receive the middleware-modified tool result") + assert.NotEqual(t, originalResult, allMsgs[1].Content, + "MessageFuture should NOT receive the original tool result") + + assert.Equal(t, "final response", allMsgs[2].Content) + } + }) +} + +func TestWithMessageFuture_NestedGraph(t *testing.T) { + ctx := context.Background() + + t.Run("agent in nested graph", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agentGraph, agentGraphOpts := a.ExportGraph() + + parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = parentGraph.AddGraphNode("agent", agentGraph, agentGraphOpts...) + assert.NoError(t, err) + err = parentGraph.AddEdge(compose.START, "agent") + assert.NoError(t, err) + err = parentGraph.AddEdge("agent", compose.END) + assert.NoError(t, err) + + runnable, err := parentGraph.Compile(ctx) + assert.NoError(t, err) + + option, future := WithMessageFuture() + + response, err := runnable.Invoke(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, agent.GetComposeOptions(option)...) + assert.Nil(t, err) + assert.Equal(t, "final response", response.Content) + + iter := future.GetMessages() + + msg1, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + msg2, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Tool, msg2.Role) + + msg3, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, "final response", msg3.Content) + + _, hasNext, err = iter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + + t.Run("agent in deeply nested graph", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test user"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("final response", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agentGraph, agentGraphOpts := a.ExportGraph() + + childGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = childGraph.AddGraphNode("agent", agentGraph, agentGraphOpts...) + assert.NoError(t, err) + err = childGraph.AddEdge(compose.START, "agent") + assert.NoError(t, err) + err = childGraph.AddEdge("agent", compose.END) + assert.NoError(t, err) + + parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = parentGraph.AddGraphNode("child", childGraph) + assert.NoError(t, err) + err = parentGraph.AddEdge(compose.START, "child") + assert.NoError(t, err) + err = parentGraph.AddEdge("child", compose.END) + assert.NoError(t, err) + + runnable, err := parentGraph.Compile(ctx) + assert.NoError(t, err) + + option, future := WithMessageFuture() + + response, err := runnable.Invoke(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, agent.GetComposeOptions(option)...) + assert.Nil(t, err) + assert.Equal(t, "final response", response.Content) + + iter := future.GetMessages() + + msg1, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + msg2, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Tool, msg2.Role) + + msg3, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, "final response", msg3.Content) + + _, hasNext, err = iter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + + t.Run("agent in nested graph with streaming", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "stream user"}`, + }, + }, + }), + }), nil + }). + Times(1) + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderFromArray([]*schema.Message{ + schema.AssistantMessage("streaming", nil), + schema.AssistantMessage(" final", nil), + schema.AssistantMessage(" response", nil), + }), nil + }). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agentGraph, agentGraphOpts := a.ExportGraph() + + parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = parentGraph.AddGraphNode("agent", agentGraph, agentGraphOpts...) + assert.NoError(t, err) + err = parentGraph.AddEdge(compose.START, "agent") + assert.NoError(t, err) + err = parentGraph.AddEdge("agent", compose.END) + assert.NoError(t, err) + + runnable, err := parentGraph.Compile(ctx) + assert.NoError(t, err) + + option, future := WithMessageFuture() + + streamReader, err := runnable.Stream(ctx, []*schema.Message{ + schema.UserMessage("use the greet tool"), + }, agent.GetComposeOptions(option)...) + assert.Nil(t, err) + + var finalContent string + for { + chunk, err := streamReader.Recv() + if err == io.EOF { + break + } + assert.Nil(t, err) + finalContent += chunk.Content + } + assert.Contains(t, finalContent, "final") + + siter := future.GetMessageStreams() + + stream1, hasNext, err := siter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + msg1, err := concatStreamMsg(stream1) + assert.Nil(t, err) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 1, len(msg1.ToolCalls)) + + stream2, hasNext, err := siter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + msg2, err := concatStreamMsg(stream2) + assert.Nil(t, err) + assert.Equal(t, schema.Tool, msg2.Role) + + stream3, hasNext, err := siter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + msg3, err := concatStreamMsg(stream3) + assert.Nil(t, err) + assert.Contains(t, msg3.Content, "final") + + _, hasNext, err = siter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + + t.Run("agent with multiple tool calls in nested graph", func(t *testing.T) { + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "user1"}`, + }, + }, + { + ID: "tool-call-2", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "user2"}`, + }, + }, + }), nil). + Times(1) + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("greeted both users", nil), nil). + Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agentGraph, agentGraphOpts := a.ExportGraph() + + parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = parentGraph.AddGraphNode("agent", agentGraph, agentGraphOpts...) + assert.NoError(t, err) + err = parentGraph.AddEdge(compose.START, "agent") + assert.NoError(t, err) + err = parentGraph.AddEdge("agent", compose.END) + assert.NoError(t, err) + + runnable, err := parentGraph.Compile(ctx) + assert.NoError(t, err) + + option, future := WithMessageFuture() + + response, err := runnable.Invoke(ctx, []*schema.Message{ + schema.UserMessage("greet multiple users"), + }, agent.GetComposeOptions(option)...) + assert.Nil(t, err) + assert.Equal(t, "greeted both users", response.Content) + + iter := future.GetMessages() + + msg1, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Assistant, msg1.Role) + assert.Equal(t, 2, len(msg1.ToolCalls)) + + msg2, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Tool, msg2.Role) + + msg3, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, schema.Tool, msg3.Role) + + msg4, hasNext, err := iter.Next() + assert.Nil(t, err) + assert.True(t, hasNext) + assert.Equal(t, "greeted both users", msg4.Content) + + _, hasNext, err = iter.Next() + assert.Nil(t, err) + assert.False(t, hasNext) + }) + + t.Run("direct agent invoke vs nested graph - same behavior", func(t *testing.T) { + ctrl := gomock.NewController(t) + fakeTool := &fakeToolGreetForTest{} + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil). + Times(1) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + + a1, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm1, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + option1, future1 := WithMessageFuture() + _, err = a1.Generate(ctx, []*schema.Message{ + schema.UserMessage("test direct"), + }, option1) + assert.Nil(t, err) + + directMsgs := collectAllMessages(future1.GetMessages()) + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test"}`, + }, + }, + }), nil). + Times(1) + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("done", nil), nil). + Times(1) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + + a2, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm2, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agentGraph, agentGraphOpts := a2.ExportGraph() + + parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = parentGraph.AddGraphNode("agent", agentGraph, agentGraphOpts...) + assert.NoError(t, err) + err = parentGraph.AddEdge(compose.START, "agent") + assert.NoError(t, err) + err = parentGraph.AddEdge("agent", compose.END) + assert.NoError(t, err) + + runnable, err := parentGraph.Compile(ctx) + assert.NoError(t, err) + + option2, future2 := WithMessageFuture() + _, err = runnable.Invoke(ctx, []*schema.Message{ + schema.UserMessage("test nested"), + }, agent.GetComposeOptions(option2)...) + assert.Nil(t, err) + + nestedMsgs := collectAllMessages(future2.GetMessages()) + + assert.Equal(t, len(directMsgs), len(nestedMsgs), "should have same number of messages") + for i := range directMsgs { + assert.Equal(t, directMsgs[i].Role, nestedMsgs[i].Role, "message %d role should match", i) + } + }) + + t.Run("multiple react agents in graph - agent calls tool that invokes another agent", func(t *testing.T) { + ctrl := gomock.NewController(t) + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("inner agent response", nil), nil). + Times(1) + + innerAgent, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm2, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&fakeToolGreetForTest{}}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + innerAgentTool := &agentAsTool{ + agent: innerAgent, + name: "inner_agent", + desc: "An inner agent that can greet users", + } + + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("", + []schema.ToolCall{ + { + ID: "tool-call-1", + Function: schema.FunctionCall{ + Name: "inner_agent", + Arguments: `{"query": "hello"}`, + }, + }, + }), nil). + Times(1) + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("outer agent final response", nil), nil). + Times(1) + + outerAgent, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm1, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{innerAgentTool}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + option, future := WithMessageFuture() + + response, err := outerAgent.Generate(ctx, []*schema.Message{ + schema.UserMessage("use the inner agent"), + }, option) + assert.Nil(t, err) + assert.Equal(t, "outer agent final response", response.Content) + + allMsgs := collectAllMessages(future.GetMessages()) + + assert.GreaterOrEqual(t, len(allMsgs), 3, "should have at least 3 messages") + + hasOuterToolCall := false + hasInnerResponse := false + hasOuterFinalResponse := false + for _, msg := range allMsgs { + if msg.Role == schema.Assistant && len(msg.ToolCalls) > 0 { + hasOuterToolCall = true + } + if msg.Content == "inner agent response" { + hasInnerResponse = true + } + if msg.Content == "outer agent final response" { + hasOuterFinalResponse = true + } + } + assert.True(t, hasOuterToolCall, "should have outer agent tool call") + assert.True(t, hasInnerResponse, "should have inner agent response") + assert.True(t, hasOuterFinalResponse, "should have outer agent final response") + }) + + t.Run("two sequential react agents in same graph", func(t *testing.T) { + ctrl := gomock.NewController(t) + + cm1 := mockModel.NewMockToolCallingChatModel(ctrl) + cm1.EXPECT().WithTools(gomock.Any()).Return(cm1, nil).AnyTimes() + cm1.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("agent1 response", nil), nil). + Times(1) + + cm2 := mockModel.NewMockToolCallingChatModel(ctrl) + cm2.EXPECT().WithTools(gomock.Any()).Return(cm2, nil).AnyTimes() + cm2.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(schema.AssistantMessage("agent2 response", nil), nil). + Times(1) + + agent1, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm1, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&fakeToolGreetForTest{}}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agent2, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm2, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&fakeToolGreetForTest{}}, + }, + MaxStep: 3, + }) + assert.Nil(t, err) + + agent1Graph, agent1Opts := agent1.ExportGraph() + agent2Graph, agent2Opts := agent2.ExportGraph() + + parentGraph := compose.NewGraph[[]*schema.Message, *schema.Message]() + err = parentGraph.AddGraphNode("agent1", agent1Graph, agent1Opts...) + assert.NoError(t, err) + + err = parentGraph.AddLambdaNode("transform", compose.InvokableLambda(func(ctx context.Context, msg *schema.Message) ([]*schema.Message, error) { + return []*schema.Message{schema.UserMessage("agent2 input: " + msg.Content)}, nil + })) + assert.NoError(t, err) + + err = parentGraph.AddGraphNode("agent2", agent2Graph, agent2Opts...) + assert.NoError(t, err) + + err = parentGraph.AddEdge(compose.START, "agent1") + assert.NoError(t, err) + err = parentGraph.AddEdge("agent1", "transform") + assert.NoError(t, err) + err = parentGraph.AddEdge("transform", "agent2") + assert.NoError(t, err) + err = parentGraph.AddEdge("agent2", compose.END) + assert.NoError(t, err) + + runnable, err := parentGraph.Compile(ctx) + assert.NoError(t, err) + + option, future := WithMessageFuture() + + response, err := runnable.Invoke(ctx, []*schema.Message{ + schema.UserMessage("hello"), + }, agent.GetComposeOptions(option)...) + assert.Nil(t, err) + assert.Equal(t, "agent2 response", response.Content) + + allMsgs := collectAllMessages(future.GetMessages()) + assert.Equal(t, 2, len(allMsgs)) + + responseContents := make(map[string]bool) + for _, msg := range allMsgs { + responseContents[msg.Content] = true + } + assert.True(t, responseContents["agent1 response"]) + assert.True(t, responseContents["agent2 response"]) + }) +} + +type agentAsTool struct { + agent *Agent + name string + desc string +} + +func (a *agentAsTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: a.name, + Desc: a.desc, + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "query": { + Type: "string", + Desc: "The query to send to the inner agent", + }, + }), + }, nil +} + +func (a *agentAsTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + response, err := a.agent.Generate(ctx, []*schema.Message{ + schema.UserMessage(argumentsInJSON), + }) + if err != nil { + return "", err + } + return response.Content, nil +} + +func collectAllMessages(iter *Iterator[*schema.Message]) []*schema.Message { + var msgs []*schema.Message + for { + msg, hasNext, err := iter.Next() + if err != nil || !hasNext { + break + } + msgs = append(msgs, msg) + } + return msgs +} + +func concatStreamMsg(sr *schema.StreamReader[*schema.Message]) (*schema.Message, error) { + var result *schema.Message + for { + chunk, err := sr.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if result == nil { + result = chunk + } else { + result.Content += chunk.Content + if len(chunk.ToolCalls) > 0 { + result.ToolCalls = append(result.ToolCalls, chunk.ToolCalls...) + } + } + } + return result, nil +} diff --git a/flow/agent/react/react.go b/flow/agent/react/react.go new file mode 100644 index 0000000..794a41f --- /dev/null +++ b/flow/agent/react/react.go @@ -0,0 +1,492 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package react + +import ( + "context" + "io" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/schema" +) + +type toolResultSender func(toolName, callID, result string) + +type enhancedToolResultSender func(toolName, callID string, result *schema.ToolResult) +type streamToolResultSender func(toolName, callID string, resultStream *schema.StreamReader[string]) +type enhancedStreamToolResultSender func(toolName, callID string, resultStream *schema.StreamReader[*schema.ToolResult]) +type toolResultSenders struct { + sender toolResultSender + streamSender streamToolResultSender + + enhancedResultSender enhancedToolResultSender + enhancedStreamToolResultSender enhancedStreamToolResultSender +} + +type toolResultSenderCtxKey struct{} + +func setToolResultSendersToCtx(ctx context.Context, senders *toolResultSenders) context.Context { + return context.WithValue(ctx, toolResultSenderCtxKey{}, senders) +} + +func getToolResultSendersFromCtx(ctx context.Context) *toolResultSenders { + v := ctx.Value(toolResultSenderCtxKey{}) + if v == nil { + return nil + } + return v.(*toolResultSenders) +} + +type state struct { + Messages []*schema.Message + ReturnDirectlyToolCallID string +} + +func init() { + schema.RegisterName[*state]("_eino_react_state") +} + +func newToolResultCollectorMiddleware() compose.ToolMiddleware { + return compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + senders := getToolResultSendersFromCtx(ctx) + output, err := next(ctx, input) + if err != nil { + return nil, err + } + if senders != nil && senders.sender != nil { + senders.sender(input.Name, input.CallID, output.Result) + } + return output, nil + } + }, + Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + senders := getToolResultSendersFromCtx(ctx) + output, err := next(ctx, input) + if err != nil { + return nil, err + } + if senders != nil && senders.streamSender != nil { + streams := output.Result.Copy(2) + senders.streamSender(input.Name, input.CallID, streams[0]) + output.Result = streams[1] + } + return output, nil + } + }, + EnhancedInvokable: func(next compose.EnhancedInvokableToolEndpoint) compose.EnhancedInvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) { + senders := getToolResultSendersFromCtx(ctx) + output, err := next(ctx, input) + if err != nil { + return nil, err + } + if senders != nil && senders.enhancedResultSender != nil { + senders.enhancedResultSender(input.Name, input.CallID, output.Result) + } + return output, nil + + } + }, + EnhancedStreamable: func(next compose.EnhancedStreamableToolEndpoint) compose.EnhancedStreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) { + senders := getToolResultSendersFromCtx(ctx) + output, err := next(ctx, input) + if err != nil { + return nil, err + } + if senders != nil && senders.enhancedStreamToolResultSender != nil { + streams := output.Result.Copy(2) + senders.enhancedStreamToolResultSender(input.Name, input.CallID, streams[0]) + output.Result = streams[1] + } + return output, nil + } + }, + } +} + +const ( + nodeKeyTools = "tools" + nodeKeyModel = "chat" +) + +// MessageModifier modify the input messages before the model is called. +type MessageModifier func(ctx context.Context, input []*schema.Message) []*schema.Message + +// AgentConfig is the config for ReAct agent. +type AgentConfig struct { + // ToolCallingModel is the chat model to be used for handling user messages with tool calling capability. + // This is the recommended model field to use. + ToolCallingModel model.ToolCallingChatModel + + // Deprecated: Use ToolCallingModel instead. + Model model.ChatModel + + // ToolsConfig is the config for tools node. + ToolsConfig compose.ToolsNodeConfig + + // MessageModifier. + // modify the input messages before the model is called, it's useful when you want to add some system prompt or other messages. + MessageModifier MessageModifier + + // MessageRewriter modifies message in the state, before the ChatModel is called. + // It takes the messages stored accumulated in state, modify them, and put the modified version back into state. + // Useful for compressing message history to fit the model context window, + // or if you want to make changes to messages that take effect across multiple model calls. + // NOTE: if both MessageModifier and MessageRewriter are set, MessageRewriter will be called before MessageModifier. + MessageRewriter MessageModifier + + // MaxStep. + // default 12 of steps in pregel (node num + 10). + MaxStep int `json:"max_step"` + + // Tools that will make agent return directly when the tool is called. + // When multiple tools are called and more than one tool is in the return directly list, only the first one will be returned. + ToolReturnDirectly map[string]struct{} + + // StreamToolCallChecker is a function to determine whether the model's streaming output contains tool calls. + // Different models have different ways of outputting tool calls in streaming mode: + // - Some models (like OpenAI) output tool calls directly + // - Others (like Claude) output text first, then tool calls + // This handler allows custom logic to check for tool calls in the stream. + // It should return: + // - true if the output contains tool calls and agent should continue processing + // - false if no tool calls and agent should stop + // Note: This field only needs to be configured when using streaming mode + // Note: The handler MUST close the modelOutput stream before returning + // Optional. By default, it checks if the first chunk contains tool calls. + // Note: The default implementation does not work well with Claude, which typically outputs tool calls after text content. + // Note: If your ChatModel doesn't output tool calls first, you can try adding prompts to constrain the model from generating extra text during the tool call. + StreamToolCallChecker func(ctx context.Context, modelOutput *schema.StreamReader[*schema.Message]) (bool, error) + + // GraphName is the graph name of the ReAct Agent. + // Optional. Default `ReActAgent`. + GraphName string + // ModelNodeName is the node name of the model node in the ReAct Agent graph. + // Optional. Default `ChatModel`. + ModelNodeName string + // ToolsNodeName is the node name of the tools node in the ReAct Agent graph. + // Optional. Default `Tools`. + ToolsNodeName string +} + +// NewPersonaModifier returns a MessageModifier that adds a persona message to the input. +// example: +// +// persona := "You are an expert in golang." +// config := AgentConfig{ +// Model: model, +// MessageModifier: NewPersonaModifier(persona), +// } +// agent, err := NewAgent(ctx, config) +// if err != nil {return} +// msg, err := agent.Generate(ctx, []*schema.Message{{Role: schema.User, Content: "how to build agent with eino"}}) +// if err != nil {return} +// println(msg.Content) +// +// Deprecated: Prefer directly including the persona message in the +// input when calling Generate or Stream to avoid extra copying. +func NewPersonaModifier(persona string) MessageModifier { + return func(ctx context.Context, input []*schema.Message) []*schema.Message { + res := make([]*schema.Message, 0, len(input)+1) + + res = append(res, schema.SystemMessage(persona)) + res = append(res, input...) + return res + } +} + +func firstChunkStreamToolCallChecker(_ context.Context, sr *schema.StreamReader[*schema.Message]) (bool, error) { + defer sr.Close() + + for { + msg, err := sr.Recv() + if err == io.EOF { + return false, nil + } + if err != nil { + return false, err + } + + if len(msg.ToolCalls) > 0 { + return true, nil + } + + if len(msg.Content) == 0 { // skip empty chunks at the front + continue + } + + return false, nil + } +} + +// Default graph and node names for the ReAct agent. +const ( + GraphName = "ReActAgent" + ModelNodeName = "ChatModel" + ToolsNodeName = "Tools" +) + +// SetReturnDirectly is a helper function that can be called within a tool's execution. +// It signals the ReAct agent to stop further processing and return the result of the current tool call directly. +// This is useful when the tool's output is the final answer and no more steps are needed. +// Note: If multiple tools call this function in the same step, only the last call will take effect. +// This setting has a higher priority than the AgentConfig.ToolReturnDirectly. +func SetReturnDirectly(ctx context.Context) error { + return compose.ProcessState(ctx, func(ctx context.Context, s *state) error { + s.ReturnDirectlyToolCallID = compose.GetToolCallID(ctx) + return nil + }) +} + +// Agent is the ReAct agent. +// ReAct agent is a simple agent that handles user messages with a chat model and tools. +// ReAct will call the chat model, if the message contains tool calls, it will call the tools. +// if the tool is configured to return directly, ReAct will return directly. +// otherwise, ReAct will continue to call the chat model until the message contains no tool calls. +// e.g. +// +// agent, err := ReAct.NewAgent(ctx, &react.AgentConfig{}) +// if err != nil {...} +// msg, err := agent.Generate(ctx, []*schema.Message{{Role: schema.User, Content: "how to build agent with eino"}}) +// if err != nil {...} +// println(msg.Content) +type Agent struct { + runnable compose.Runnable[[]*schema.Message, *schema.Message] + graph *compose.Graph[[]*schema.Message, *schema.Message] + graphAddNodeOpts []compose.GraphAddNodeOpt +} + +// NewAgent creates a ReAct agent that feeds tool response into next round of Chat Model generation. +// +// IMPORTANT!! For models that don't output tool calls in the first streaming chunk (e.g. Claude) +// the default StreamToolCallChecker may not work properly since it only checks the first chunk for tool calls. +// In such cases, you need to implement a custom StreamToolCallChecker that can properly detect tool calls. +func NewAgent(ctx context.Context, config *AgentConfig) (_ *Agent, err error) { + var ( + chatModel model.BaseChatModel + toolsNode *compose.ToolsNode + toolInfos []*schema.ToolInfo + toolCallChecker = config.StreamToolCallChecker + messageModifier = config.MessageModifier + ) + + graphName := GraphName + if config.GraphName != "" { + graphName = config.GraphName + } + + modelNodeName := ModelNodeName + if config.ModelNodeName != "" { + modelNodeName = config.ModelNodeName + } + + toolsNodeName := ToolsNodeName + if config.ToolsNodeName != "" { + toolsNodeName = config.ToolsNodeName + } + + if toolCallChecker == nil { + toolCallChecker = firstChunkStreamToolCallChecker + } + + if toolInfos, err = genToolInfos(ctx, config.ToolsConfig); err != nil { + return nil, err + } + + if chatModel, err = agent.ChatModelWithTools(config.Model, config.ToolCallingModel, toolInfos); err != nil { + return nil, err + } + + config.ToolsConfig.ToolCallMiddlewares = append( + []compose.ToolMiddleware{newToolResultCollectorMiddleware()}, + config.ToolsConfig.ToolCallMiddlewares..., + ) + + if toolsNode, err = compose.NewToolNode(ctx, &config.ToolsConfig); err != nil { + return nil, err + } + + graph := compose.NewGraph[[]*schema.Message, *schema.Message](compose.WithGenLocalState(func(ctx context.Context) *state { + return &state{Messages: make([]*schema.Message, 0, config.MaxStep+1)} + })) + + modelPreHandle := func(ctx context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) { + state.Messages = append(state.Messages, input...) + + if config.MessageRewriter != nil { + state.Messages = config.MessageRewriter(ctx, state.Messages) + } + + if messageModifier == nil { + return state.Messages, nil + } + + modifiedInput := make([]*schema.Message, len(state.Messages)) + copy(modifiedInput, state.Messages) + return messageModifier(ctx, modifiedInput), nil + } + + if err = graph.AddChatModelNode(nodeKeyModel, chatModel, compose.WithStatePreHandler(modelPreHandle), compose.WithNodeName(modelNodeName)); err != nil { + return nil, err + } + + if err = graph.AddEdge(compose.START, nodeKeyModel); err != nil { + return nil, err + } + + toolsNodePreHandle := func(ctx context.Context, input *schema.Message, state *state) (*schema.Message, error) { + if input == nil { + return state.Messages[len(state.Messages)-1], nil // used for rerun interrupt resume + } + state.Messages = append(state.Messages, input) + state.ReturnDirectlyToolCallID = getReturnDirectlyToolCallID(input, config.ToolReturnDirectly) + return input, nil + } + if err = graph.AddToolsNode(nodeKeyTools, toolsNode, compose.WithStatePreHandler(toolsNodePreHandle), compose.WithNodeName(toolsNodeName)); err != nil { + return nil, err + } + + modelPostBranchCondition := func(ctx context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) { + if isToolCall, err := toolCallChecker(ctx, sr); err != nil { + return "", err + } else if isToolCall { + return nodeKeyTools, nil + } + return compose.END, nil + } + + if err = graph.AddBranch(nodeKeyModel, compose.NewStreamGraphBranch(modelPostBranchCondition, map[string]bool{nodeKeyTools: true, compose.END: true})); err != nil { + return nil, err + } + + if err = buildReturnDirectly(graph); err != nil { + return nil, err + } + + compileOpts := []compose.GraphCompileOption{compose.WithMaxRunSteps(config.MaxStep), compose.WithNodeTriggerMode(compose.AnyPredecessor), compose.WithGraphName(graphName)} + runnable, err := graph.Compile(ctx, compileOpts...) + if err != nil { + return nil, err + } + + return &Agent{ + runnable: runnable, + graph: graph, + graphAddNodeOpts: []compose.GraphAddNodeOpt{compose.WithGraphCompileOptions(compileOpts...)}, + }, nil +} + +func buildReturnDirectly(graph *compose.Graph[[]*schema.Message, *schema.Message]) (err error) { + directReturn := func(ctx context.Context, msgs *schema.StreamReader[[]*schema.Message]) (*schema.StreamReader[*schema.Message], error) { + return schema.StreamReaderWithConvert(msgs, func(msgs []*schema.Message) (*schema.Message, error) { + var msg *schema.Message + err = compose.ProcessState[*state](ctx, func(_ context.Context, state *state) error { + for i := range msgs { + if msgs[i] != nil && msgs[i].ToolCallID == state.ReturnDirectlyToolCallID { + msg = msgs[i] + return nil + } + } + return nil + }) + if err != nil { + return nil, err + } + if msg == nil { + return nil, schema.ErrNoValue + } + return msg, nil + }), nil + } + + nodeKeyDirectReturn := "direct_return" + if err = graph.AddLambdaNode(nodeKeyDirectReturn, compose.TransformableLambda(directReturn)); err != nil { + return err + } + + // this branch checks if the tool called should return directly. It either leads to END or back to ChatModel + err = graph.AddBranch(nodeKeyTools, compose.NewStreamGraphBranch(func(ctx context.Context, msgsStream *schema.StreamReader[[]*schema.Message]) (endNode string, err error) { + msgsStream.Close() + + err = compose.ProcessState[*state](ctx, func(_ context.Context, state *state) error { + if len(state.ReturnDirectlyToolCallID) > 0 { + endNode = nodeKeyDirectReturn + } else { + endNode = nodeKeyModel + } + return nil + }) + if err != nil { + return "", err + } + return endNode, nil + }, map[string]bool{nodeKeyModel: true, nodeKeyDirectReturn: true})) + if err != nil { + return err + } + + return graph.AddEdge(nodeKeyDirectReturn, compose.END) +} + +func genToolInfos(ctx context.Context, config compose.ToolsNodeConfig) ([]*schema.ToolInfo, error) { + toolInfos := make([]*schema.ToolInfo, 0, len(config.Tools)) + for _, t := range config.Tools { + tl, err := t.Info(ctx) + if err != nil { + return nil, err + } + + toolInfos = append(toolInfos, tl) + } + + return toolInfos, nil +} + +func getReturnDirectlyToolCallID(input *schema.Message, toolReturnDirectly map[string]struct{}) string { + if len(toolReturnDirectly) == 0 { + return "" + } + + for _, toolCall := range input.ToolCalls { + if _, ok := toolReturnDirectly[toolCall.Function.Name]; ok { + return toolCall.ID + } + } + + return "" +} + +// Generate generates a response from the agent. +func (r *Agent) Generate(ctx context.Context, input []*schema.Message, opts ...agent.AgentOption) (*schema.Message, error) { + return r.runnable.Invoke(ctx, input, agent.GetComposeOptions(opts...)...) +} + +// Stream calls the agent and returns a stream response. +func (r *Agent) Stream(ctx context.Context, input []*schema.Message, opts ...agent.AgentOption) (output *schema.StreamReader[*schema.Message], err error) { + return r.runnable.Stream(ctx, input, agent.GetComposeOptions(opts...)...) +} + +// ExportGraph exports the underlying graph from Agent, along with the []compose.GraphAddNodeOpt to be used when adding this graph to another graph. +func (r *Agent) ExportGraph() (compose.AnyGraph, []compose.GraphAddNodeOpt) { + return r.graph, r.graphAddNodeOpts +} diff --git a/flow/agent/react/react_test.go b/flow/agent/react/react_test.go new file mode 100644 index 0000000..175cedc --- /dev/null +++ b/flow/agent/react/react_test.go @@ -0,0 +1,822 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package react + +import ( + "context" + "errors" + "fmt" + "io" + "math/rand" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + mockModel "github.com/cloudwego/eino/internal/mock/components/model" + "github.com/cloudwego/eino/schema" + template "github.com/cloudwego/eino/utils/callbacks" +) + +func TestReact(t *testing.T) { + ctx := context.Background() + + fakeTool := &fakeToolGreetForTest{ + tarCount: 3, + } + + info, err := fakeTool.Info(ctx) + assert.NoError(t, err) + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockChatModel(ctrl) + + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + times++ + if times <= 2 { + info, _ := fakeTool.Info(ctx) + + return schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStr()), + }, + }, + }), + nil + } + + return schema.AssistantMessage("bye", nil), nil + }).AnyTimes() + cm.EXPECT().BindTools(gomock.Any()).Return(nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message { + assert.Equal(t, len(input), times*2+1) + return input + }, + MaxStep: 40, + }) + assert.Nil(t, err) + + out, err := a.Generate(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "Use greet tool to continuously say hello until you get a bye response, greet names in the following order: max, bob, alice, john, marry, joe, ken, lily, please start directly! please start directly! please start directly!", + }, + }, agent.WithComposeOptions(compose.WithCallbacks(callbackForTest))) + assert.Nil(t, err) + + if out != nil { + t.Log(out.Content) + } + + // test return directly + times = 0 + a, err = NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message { + assert.Equal(t, len(input), times*2+1) + return input + }, + MaxStep: 40, + ToolReturnDirectly: map[string]struct{}{info.Name: {}}, + }) + assert.Nil(t, err) + + out, err = a.Generate(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "Use greet tool to continuously say hello until you get a bye response, greet names in the following order: max, bob, alice, john, marry, joe, ken, lily, please start directly! please start directly! please start directly!", + }, + }, agent.WithComposeOptions(compose.WithCallbacks(callbackForTest))) + assert.Nil(t, err) + + if out != nil { + t.Log(out.Content) + } +} + +func TestReactWithMessageRewriterAndModifier(t *testing.T) { + ctx := context.Background() + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + // This test simulates a single Generate call with a long history. + // The MessageRewriter should shorten the history. + // The MessageModifier should add a system prompt. + + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + // Check messages passed to the model. + // Expected: [system prompt, user: "message 2", assistant: "response 2"] + assert.Len(t, input, 3) + assert.Equal(t, schema.System, input[0].Role) + assert.Equal(t, "system prompt", input[0].Content) + assert.Equal(t, schema.User, input[1].Role) + assert.Equal(t, "message 2", input[1].Content) + assert.Equal(t, schema.Assistant, input[2].Role) + assert.Equal(t, "response 2", input[2].Content) + return schema.AssistantMessage("final response", nil), nil + }).Times(1) + cm.EXPECT().WithTools(gomock.Any()).Return(cm, nil).AnyTimes() + + ra, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + MessageRewriter: func(ctx context.Context, messages []*schema.Message) []*schema.Message { + // Keep only the last 2 messages if history is longer. + assert.Len(t, messages, 4) // user1, assistant1, user2, assistant2 + if len(messages) > 2 { + return messages[len(messages)-2:] + } + return messages + }, + MessageModifier: func(ctx context.Context, messages []*schema.Message) []*schema.Message { + // messages should be the result from rewriter + assert.Len(t, messages, 2) // user2, assistant2 + + // Add a system prompt + res := make([]*schema.Message, 0, len(messages)+1) + res = append(res, schema.SystemMessage("system prompt")) + res = append(res, messages...) + return res + }, + }) + assert.NoError(t, err) + + // Simulate a conversation history + history := []*schema.Message{ + schema.UserMessage("message 1"), + schema.AssistantMessage("response 1", nil), + schema.UserMessage("message 2"), + schema.AssistantMessage("response 2", nil), + } + + // Run the react agent + finalMsg, err := ra.Generate(ctx, history) + assert.NoError(t, err) + assert.Equal(t, "final response", finalMsg.Content) +} + +func TestReactStream(t *testing.T) { + ctx := context.Background() + + fakeTool := &fakeToolGreetForTest{ + tarCount: 20, + } + + fakeStreamTool := &fakeStreamToolGreetForTest{ + tarCount: 20, + } + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockChatModel(ctrl) + + times := 0 + cm.EXPECT().BindTools(gomock.Any()).Return(nil).AnyTimes() + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) ( + *schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + defer sw.Close() + + info, _ := fakeTool.Info(ctx) + streamInfo, _ := fakeStreamTool.Info(ctx) + + times++ + if times <= 2 { + sw.Send(schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "tool"}`, randStr()), + }, + }, + }), + nil) + return sr, nil + } else if times == 3 { + sw.Send(schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: streamInfo.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "stream tool"}`, randStr()), + }, + }, + }), + nil) + return sr, nil + } else if times == 4 { // parallel tool call + sw.Send(schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "tool"}`, randStr()), + }, + }, + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: streamInfo.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "stream tool"}`, randStr()), + }, + }, + }), + nil) + return sr, nil + } + + sw.Send(schema.AssistantMessage("bye", nil), nil) + return sr, nil + }).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool, fakeStreamTool}, + }, + + MaxStep: 40, + }) + assert.Nil(t, err) + + out, err := a.Stream(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "Use greet tool to continuously say hello until you get a bye response, greet names in the following order: max, bob, alice, john, marry, joe, ken, lily, please start directly! please start directly! please start directly!", + }, + }, agent.WithComposeOptions(compose.WithCallbacks(callbackForTest))) + if err != nil { + t.Fatal(err) + } + + defer out.Close() + + msgs := make([]*schema.Message, 0) + for { + msg, err := out.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + t.Fatal(err) + } + + msgs = append(msgs, msg) + } + + assert.Equal(t, 1, len(msgs)) + + msg, err := schema.ConcatMessages(msgs) + if err != nil { + t.Fatal(err) + } + + t.Log(msg.Content) + + info, err := fakeStreamTool.Info(ctx) + assert.NoError(t, err) + + // test return directly + a, err = NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool, fakeStreamTool}, + }, + + MaxStep: 40, + ToolReturnDirectly: map[string]struct{}{info.Name: {}}, // one of the two tools is return directly + }) + assert.Nil(t, err) + + times = 0 + out, err = a.Stream(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "Use greet tool to continuously say hello until you get a bye response, greet names in the following order: max, bob, alice, john, marry, joe, ken, lily, please start directly! please start directly! please start directly!", + }, + }, agent.WithComposeOptions(compose.WithCallbacks(callbackForTest))) + if err != nil { + t.Fatal(err) + } + + defer out.Close() + + msgs = make([]*schema.Message, 0) + for { + msg, err := out.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + t.Fatal(err) + } + + msgs = append(msgs, msg) + } + + assert.Equal(t, 1, len(msgs)) + + msg, err = schema.ConcatMessages(msgs) + if err != nil { + t.Fatal(err) + } + + t.Log(msg.Content) + + // return directly tool call within parallel tool calls + out, err = a.Stream(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "Use greet tool to continuously say hello until you get a bye response, greet names in the following order: max, bob, alice, john, marry, joe, ken, lily, please start directly! please start directly! please start directly!", + }, + }, agent.WithComposeOptions(compose.WithCallbacks(callbackForTest))) + assert.NoError(t, err) + + defer out.Close() + + msgs = make([]*schema.Message, 0) + for { + msg, err := out.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + assert.NoError(t, err) + } + + msgs = append(msgs, msg) + } + + assert.Equal(t, 1, len(msgs)) + + msg, err = schema.ConcatMessages(msgs) + assert.NoError(t, err) + + t.Log("parallel tool call with return directly: ", msg.Content) +} + +func TestReactWithModifier(t *testing.T) { + ctx := context.Background() + + fakeTool := &fakeToolGreetForTest{} + ctrl := gomock.NewController(t) + cm := mockModel.NewMockChatModel(ctrl) + + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + times++ + if times <= 2 { + info, _ := fakeTool.Info(ctx) + + return schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStr()), + }, + }, + }), + nil + } + + return schema.AssistantMessage("bye", nil), nil + }).AnyTimes() + cm.EXPECT().BindTools(gomock.Any()).Return(nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool}, + }, + MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message { + res := make([]*schema.Message, 0, len(input)+1) + + res = append(res, schema.SystemMessage("you are a helpful assistant")) + res = append(res, input...) + return res + }, + + MaxStep: 40, + }) + + assert.Nil(t, err) + + out, err := a.Generate(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "hello", + }, + }, agent.WithComposeOptions(compose.WithCallbacks(callbackForTest))) + if err != nil { + t.Fatal(err) + } + + if out != nil { + t.Log(out.Content) + } +} + +func TestAgentInGraph(t *testing.T) { + t.Run("agent generate in chain", func(t *testing.T) { + ctx := context.Background() + + fakeTool := &fakeToolGreetForTest{} + ctrl := gomock.NewController(t) + cm := mockModel.NewMockChatModel(ctrl) + + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + + times += 1 + if times <= 2 { + info, _ := fakeTool.Info(ctx) + + return schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStr()), + }, + }, + }), + nil + } + + return schema.AssistantMessage("bye", nil), nil + + }).Times(3) + cm.EXPECT().BindTools(gomock.Any()).Return(nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{fakeTool, &fakeStreamToolGreetForTest{}}, + }, + + MaxStep: 40, + }) + assert.Nil(t, err) + + chain := compose.NewChain[[]*schema.Message, string]() + agentLambda, err := compose.AnyLambda(a.Generate, a.Stream, nil, nil) + assert.Nil(t, err) + + chain. + AppendLambda(agentLambda). + AppendLambda(compose.InvokableLambda(func(ctx context.Context, input *schema.Message) (string, error) { + t.Log("got agent response: ", input.Content) + return input.Content, nil + })) + r, err := chain.Compile(ctx) + assert.Nil(t, err) + + res, err := r.Invoke(ctx, []*schema.Message{{Role: schema.User, Content: "hello"}}, + compose.WithCallbacks(callbackForTest)) + assert.Nil(t, err) + + t.Log(res) + }) + + t.Run("agent stream in chain", func(t *testing.T) { + + fakeStreamTool := &fakeStreamToolGreetForTest{} + ctx := context.Background() + ctrl := gomock.NewController(t) + cm := mockModel.NewMockChatModel(ctrl) + + times := 0 + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) ( + *schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + defer sw.Close() + + times += 1 + if times <= 2 { + info, _ := fakeStreamTool.Info(ctx) + sw.Send(schema.AssistantMessage("hello max", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: fmt.Sprintf(`{"name": "%s", "hh": "123"}`, randStr()), + }, + }, + }), + nil) + return sr, nil + } + + sw.Send(schema.AssistantMessage("bye", nil), nil) + return sr, nil + }).Times(3) + cm.EXPECT().BindTools(gomock.Any()).Return(nil).AnyTimes() + + a, err := NewAgent(ctx, &AgentConfig{ + Model: cm, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{&fakeToolGreetForTest{}, fakeStreamTool}, + }, + + MaxStep: 40, + }) + assert.Nil(t, err) + + chain := compose.NewChain[[]*schema.Message, string]() + agentGraph, opts := a.ExportGraph() + assert.Nil(t, err) + + chain. + AppendGraph(agentGraph, opts...). + AppendLambda(compose.InvokableLambda(func(ctx context.Context, input *schema.Message) (string, error) { + t.Log("got agent response: ", input.Content) + return input.Content, nil + })) + r, err := chain.Compile(ctx) + assert.Nil(t, err) + + outStream, err := r.Stream(ctx, []*schema.Message{{Role: schema.User, Content: "hello"}}, + compose.WithCallbacks(callbackForTest)) + if err != nil { + t.Fatal(err) + } + + defer outStream.Close() + + msg := "" + for { + msgItem, err := outStream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + t.Fatal(err) + } + + msg += msgItem + } + + t.Log(msg) + }) + +} + +func TestWithTools(t *testing.T) { + ctx := context.Background() + + fakeTool := &fakeToolGreetForTest{ + tarCount: 2, + } + fakeStreamTool := &fakeStreamToolGreetForTest{ + tarCount: 2, + } + + ctrl := gomock.NewController(t) + cm := mockModel.NewMockToolCallingChatModel(ctrl) + + times := 0 + cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + times++ + if times <= 1 { + info, _ := fakeTool.Info(ctx) + return schema.AssistantMessage("calling tool", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test"}`, + }, + }, + }), + nil + } + return schema.AssistantMessage("done", nil), nil + }).AnyTimes() + + cm.EXPECT().Stream(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, input []*schema.Message, opts ...model.Option) ( + *schema.StreamReader[*schema.Message], error) { + sr, sw := schema.Pipe[*schema.Message](1) + defer sw.Close() + + times++ + if times <= 2 { + info, _ := fakeStreamTool.Info(ctx) + sw.Send(schema.AssistantMessage("calling stream tool", + []schema.ToolCall{ + { + ID: randStr(), + Function: schema.FunctionCall{ + Name: info.Name, + Arguments: `{"name": "test"}`, + }, + }, + }), + nil) + return sr, nil + } + + sw.Send(schema.AssistantMessage("stream done", nil), nil) + return sr, nil + }).AnyTimes() + + // Test WithTools function + toolOptions, err := WithTools(ctx, fakeTool, fakeStreamTool) + assert.NoError(t, err) + assert.Len(t, toolOptions, 2, "WithTools should return exactly 2 options") + + // Create agent without tools in config + a, err := NewAgent(ctx, &AgentConfig{ + ToolCallingModel: cm, + MaxStep: 10, + }) + assert.NoError(t, err) + + // Test Generate with WithTools options + times = 0 + msg, err := a.Generate(ctx, []*schema.Message{ + schema.UserMessage("test generate with tools"), + }, toolOptions...) + assert.NoError(t, err) + assert.Equal(t, "done", msg.Content) + + // Test Stream with WithTools options + times = 0 + stream, err := a.Stream(ctx, []*schema.Message{ + schema.UserMessage("test stream with tools"), + }, toolOptions...) + assert.NoError(t, err) + + defer stream.Close() + msgs := make([]*schema.Message, 0) + for { + msg, err := stream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + assert.NoError(t, err) + } + msgs = append(msgs, msg) + } + + assert.Len(t, msgs, 1) + concatMsg, err := schema.ConcatMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, "stream done", concatMsg.Content) + + // Test error case - tool Info() returns error + errorTool := &errorToolForTest{} + _, err = WithTools(ctx, errorTool) + assert.Error(t, err) + assert.Contains(t, err.Error(), "info error") +} + +// Helper tool for testing error cases +type errorToolForTest struct{} + +func (t *errorToolForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return nil, errors.New("info error") +} + +func (t *errorToolForTest) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) { + return "", nil +} + +type fakeStreamToolGreetForTest struct { + tarCount int + curCount int +} + +func (t *fakeStreamToolGreetForTest) StreamableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) ( + *schema.StreamReader[string], error) { + p := &fakeToolInput{} + err := sonic.UnmarshalString(argumentsInJSON, p) + if err != nil { + return nil, err + } + + if t.curCount >= t.tarCount { + s := schema.StreamReaderFromArray([]string{`{"say": "bye"}`}) + return s, nil + } + t.curCount++ + s := schema.StreamReaderFromArray([]string{fmt.Sprintf(`{"say": "hello %v"}`, p.Name)}) + return s, nil +} + +type fakeToolGreetForTest struct { + tarCount int + curCount int +} + +func (t *fakeToolGreetForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "greet", + Desc: "greet with name", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Desc: "user name who to greet", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (t *fakeStreamToolGreetForTest) Info(_ context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "greet in stream", + Desc: "greet with name in stream", + ParamsOneOf: schema.NewParamsOneOfByParams( + map[string]*schema.ParameterInfo{ + "name": { + Desc: "user name who to greet", + Required: true, + Type: schema.String, + }, + }), + }, nil +} + +func (t *fakeToolGreetForTest) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) { + p := &fakeToolInput{} + err := sonic.UnmarshalString(argumentsInJSON, p) + if err != nil { + return "", err + } + + if t.curCount >= t.tarCount { + return `{"say": "bye"}`, nil + } + + t.curCount++ + return fmt.Sprintf(`{"say": "hello %v"}`, p.Name), nil +} + +type fakeToolInput struct { + Name string `json:"name"` +} + +func randStr() string { + seeds := []rune("this is a seed") + b := make([]rune, 8) + for i := range b { + b[i] = seeds[rand.Intn(len(seeds))] + } + return string(b) +} + +var callbackForTest = BuildAgentCallback(&template.ModelCallbackHandler{}, &template.ToolCallbackHandler{}) diff --git a/flow/agent/utils.go b/flow/agent/utils.go new file mode 100644 index 0000000..d8100bb --- /dev/null +++ b/flow/agent/utils.go @@ -0,0 +1,52 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agent + +import ( + "errors" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" +) + +// ChatModelWithTools returns a chat model configured with tool schemas. +// If a ToolCallingChatModel is provided, it is used directly (and optionally +// configured with tools). Otherwise, a plain ChatModel is bound with tools. +func ChatModelWithTools(cm model.ChatModel, toolCallingModel model.ToolCallingChatModel, toolInfos []*schema.ToolInfo) ( + model.BaseChatModel, error) { + + if toolCallingModel != nil { + if len(toolInfos) == 0 { + return toolCallingModel, nil + } + return toolCallingModel.WithTools(toolInfos) + } + + if cm != nil { + if len(toolInfos) == 0 { + return cm, nil + } + err := cm.BindTools(toolInfos) + if err != nil { + return nil, err + } + + return cm, nil + } + + return nil, errors.New("no chat model provided") +} diff --git a/flow/indexer/parent/parent.go b/flow/indexer/parent/parent.go new file mode 100644 index 0000000..aff63b7 --- /dev/null +++ b/flow/indexer/parent/parent.go @@ -0,0 +1,162 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package parent provides an indexer that assigns stable IDs to sub-documents +// and preserves relationships to their original parent document. +package parent + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/schema" +) + +// Config configures the parent indexer that assigns IDs to sub-documents. +type Config struct { + // Indexer is the underlying indexer implementation that handles the actual document indexing. + // For example: a vector database indexer like Milvus, or a full-text search indexer like Elasticsearch. + Indexer indexer.Indexer + + // Transformer processes documents before indexing, typically splitting them into smaller chunks. + // Each sub-document generated by the transformer must retain its parent document's ID. + // For example: if a document with ID "doc_1" is split into 3 chunks, all chunks will initially + // have ID "doc_1". These IDs will later be modified by the SubIDGenerator. + // + // Example transformations: + // - A text splitter that breaks down large documents into paragraphs + // - A code splitter that separates code files into functions + Transformer document.Transformer + + // ParentIDKey specifies the metadata key used to store the original document's ID in each sub-document. + // For example: if ParentIDKey is "parent_id", each sub-document will have metadata like: + // {"parent_id": "original_doc_123"} + ParentIDKey string + + // SubIDGenerator generates unique IDs for sub-documents based on their parent document ID. + // For example: if parent ID is "doc_1" and we need 3 sub-document IDs, it might generate: + // ["doc_1_chunk_1", "doc_1_chunk_2", "doc_1_chunk_3"] + // + // Parameters: + // - ctx: context for the operation + // - parentID: the ID of the parent document + // - num: number of sub-document IDs needed + // Returns: + // - []string: slice of generated sub-document IDs + // - error: any error encountered during ID generation + SubIDGenerator func(ctx context.Context, parentID string, num int) ([]string, error) +} + +// NewIndexer creates a new parent indexer that handles document splitting and sub-document management. +// +// Parameters: +// - ctx: context for the operation +// - config: configuration for the parent indexer +// +// Example usage: +// +// indexer, err := NewIndexer(ctx, &Config{ +// Indexer: milvusIndexer, +// Transformer: textSplitter, +// ParentIDKey: "source_doc_id", +// SubIDGenerator: func(ctx context.Context, parentID string, num int) ([]string, error) { +// ids := make([]string, num) +// for i := 0; i < num; i++ { +// ids[i] = fmt.Sprintf("%s_chunk_%d", parentID, i+1) +// } +// return ids, nil +// }, +// }) +// +// Returns: +// - indexer.Indexer: the created parent indexer +// - error: any error encountered during creation +func NewIndexer(ctx context.Context, config *Config) (indexer.Indexer, error) { + if config.Indexer == nil { + return nil, fmt.Errorf("indexer is empty") + } + if config.Transformer == nil { + return nil, fmt.Errorf("transformer is empty") + } + if config.SubIDGenerator == nil { + return nil, fmt.Errorf("sub id generator is empty") + } + + return &parentIndexer{ + indexer: config.Indexer, + transformer: config.Transformer, + parentIDKey: config.ParentIDKey, + subIDGenerator: config.SubIDGenerator, + }, nil +} + +type parentIndexer struct { + indexer indexer.Indexer + transformer document.Transformer + parentIDKey string + subIDGenerator func(ctx context.Context, parentID string, num int) ([]string, error) +} + +func (p *parentIndexer) Store(ctx context.Context, docs []*schema.Document, opts ...indexer.Option) ([]string, error) { + subDocs, err := p.transformer.Transform(ctx, docs) + if err != nil { + return nil, fmt.Errorf("transform docs fail: %w", err) + } + if len(subDocs) == 0 { + return nil, fmt.Errorf("doc transformer returned no documents") + } + currentID := subDocs[0].ID + startIdx := 0 + for i, subDoc := range subDocs { + if subDoc.MetaData == nil { + subDoc.MetaData = make(map[string]any) + } + subDoc.MetaData[p.parentIDKey] = subDoc.ID + + if subDoc.ID == currentID { + continue + } + + // generate new doc id + subIDs, err_ := p.subIDGenerator(ctx, subDocs[startIdx].ID, i-startIdx) + if err_ != nil { + return nil, err_ + } + if len(subIDs) != i-startIdx { + return nil, fmt.Errorf("generated sub IDs' num is unexpected") + } + for j := startIdx; j < i; j++ { + subDocs[j].ID = subIDs[j-startIdx] + } + startIdx = i + currentID = subDoc.ID + } + // generate new doc id + subIDs, err := p.subIDGenerator(ctx, subDocs[startIdx].ID, len(subDocs)-startIdx) + if err != nil { + return nil, err + } + if len(subIDs) != len(subDocs)-startIdx { + return nil, fmt.Errorf("generated sub IDs' num is unexpected") + } + for j := startIdx; j < len(subDocs); j++ { + subDocs[j].ID = subIDs[j-startIdx] + } + + return p.indexer.Store(ctx, subDocs, opts...) +} diff --git a/flow/indexer/parent/parent_test.go b/flow/indexer/parent/parent_test.go new file mode 100644 index 0000000..82b3621 --- /dev/null +++ b/flow/indexer/parent/parent_test.go @@ -0,0 +1,121 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parent + +import ( + "context" + "fmt" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/schema" +) + +type testIndexer struct{} + +func (t *testIndexer) Store(ctx context.Context, docs []*schema.Document, opts ...indexer.Option) (ids []string, err error) { + ret := make([]string, len(docs)) + for i, d := range docs { + ret[i] = d.ID + if !strings.HasPrefix(d.ID, d.MetaData["parent"].(string)) { + return nil, fmt.Errorf("invalid parent key") + } + } + return ret, nil +} + +type testTransformer struct { +} + +func (t *testTransformer) Transform(ctx context.Context, src []*schema.Document, opts ...document.TransformerOption) ([]*schema.Document, error) { + var ret []*schema.Document + for _, d := range src { + ret = append(ret, &schema.Document{ + ID: d.ID, + Content: d.Content[:len(d.Content)/2], + MetaData: deepCopyMap(d.MetaData), + }, &schema.Document{ + ID: d.ID, + Content: d.Content[len(d.Content)/2:], + MetaData: deepCopyMap(d.MetaData), + }) + } + return ret, nil +} + +func TestParentIndexer(t *testing.T) { + tests := []struct { + name string + config *Config + input []*schema.Document + want []string + }{ + { + name: "success", + config: &Config{ + Indexer: &testIndexer{}, + Transformer: &testTransformer{}, + ParentIDKey: "parent", + SubIDGenerator: func(ctx context.Context, parentID string, num int) ([]string, error) { + ret := make([]string, num) + for i := range ret { + ret[i] = parentID + strconv.Itoa(i) + } + return ret, nil + }, + }, + input: []*schema.Document{{ + ID: "id", + Content: "1234567890", + MetaData: map[string]interface{}{}, + }, { + ID: "ID", + Content: "0987654321", + MetaData: map[string]interface{}{}, + }}, + want: []string{"id0", "id1", "ID0", "ID1"}, + }, + } + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + index, err := NewIndexer(ctx, tt.config) + if err != nil { + t.Fatal(err) + } + ret, err := index.Store(ctx, tt.input) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(ret, tt.want) { + t.Errorf("NewHeaderSplitter() got = %v, want %v", ret, tt.want) + } + }) + } +} + +func deepCopyMap(in map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}) + for k, v := range in { + out[k] = v + } + return out +} diff --git a/flow/retriever/multiquery/multi_query.go b/flow/retriever/multiquery/multi_query.go new file mode 100644 index 0000000..1d064cd --- /dev/null +++ b/flow/retriever/multiquery/multi_query.go @@ -0,0 +1,211 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package multiquery implements a query-rewriting retriever that expands +// user queries into multiple variants to improve recall. +package multiquery + +import ( + "context" + "fmt" + "strings" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/retriever/utils" + "github.com/cloudwego/eino/schema" +) + +const ( + defaultRewritePrompt = `You are an helpful assistant. + Your role is to create three different versions of the user query to retrieve relevant documents from store. + Your goal is to improve the performance of similarity search by generating text from different perspectives based on the user query. + Only provide the generated queries and separate them by newlines. + user query: {{query}}` + defaultQueryVariable = "query" + defaultMaxQueriesNum = 5 +) + +var deduplicateFusion = func(ctx context.Context, docs [][]*schema.Document) ([]*schema.Document, error) { + m := map[string]bool{} + var ret []*schema.Document + for i := range docs { + for j := range docs[i] { + if _, ok := m[docs[i][j].ID]; !ok { + m[docs[i][j].ID] = true + ret = append(ret, docs[i][j]) + } + } + } + return ret, nil +} + +// NewRetriever creates a multi-query retriever. +// multi-query retriever is useful when you want to retrieve documents from multiple retrievers with different queries. +// e.g. +// +// multiRetriever := multiquery.NewRetriever(ctx, &multiquery.Config{}) +// docs, err := multiRetriever.Retrieve(ctx, "how to build agent with eino") +// if err != nil { +// ... +// } +// println(docs) +func NewRetriever(ctx context.Context, config *Config) (retriever.Retriever, error) { + var err error + + // config validate + if config.OrigRetriever == nil { + return nil, fmt.Errorf("OrigRetriever is required") + } + if config.RewriteHandler == nil && config.RewriteLLM == nil { + return nil, fmt.Errorf("at least one of RewriteHandler and RewriteLLM must not be empty") + } + + // construct rewrite chain + rewriteChain := compose.NewChain[string, []string]() + if config.RewriteHandler != nil { + rewriteChain.AppendLambda(compose.InvokableLambda(config.RewriteHandler), compose.WithNodeName("CustomQueryRewriter")) + } else { + tpl := config.RewriteTemplate + variable := config.QueryVar + parser := config.LLMOutputParser + if tpl == nil { + tpl = prompt.FromMessages(schema.Jinja2, schema.UserMessage(defaultRewritePrompt)) + variable = defaultQueryVariable + } + if parser == nil { + parser = func(ctx context.Context, message *schema.Message) ([]string, error) { + return strings.Split(message.Content, "\n"), nil + } + } + + rewriteChain. + AppendLambda(compose.InvokableLambda(func(ctx context.Context, input string) (output map[string]any, err error) { + return map[string]any{variable: input}, nil + }), compose.WithNodeName("Converter")). + AppendChatTemplate(tpl). + AppendChatModel(config.RewriteLLM). + AppendLambda(compose.InvokableLambda(parser), compose.WithNodeName("OutputParser")) + } + rewriteRunner, err := rewriteChain.Compile(ctx, compose.WithGraphName("QueryRewrite")) + if err != nil { + return nil, err + } + + maxQueriesNum := config.MaxQueriesNum + if maxQueriesNum == 0 { + maxQueriesNum = defaultMaxQueriesNum + } + + fusionFunc := config.FusionFunc + if fusionFunc == nil { + fusionFunc = deduplicateFusion + } + + return &multiQueryRetriever{ + queryRunner: rewriteRunner, + maxQueriesNum: maxQueriesNum, + origRetriever: config.OrigRetriever, + fusionFunc: fusionFunc, + }, nil +} + +// Config is the config for multi-query retriever. +type Config struct { + // Rewrite + // 1. set the following fields to use llm to generate multi queries + // a. chat model, required + RewriteLLM model.ChatModel + // b. prompt llm to generate multi queries, we provide default template so you can leave this field blank + RewriteTemplate prompt.ChatTemplate + // c. origin query variable of your custom template, it can be empty if you use default template + QueryVar string + // d. parser llm output to queries, split content using "\n" by default + LLMOutputParser func(context.Context, *schema.Message) ([]string, error) + // 2. set RewriteHandler to provide custom query generation logic, possibly without a ChatModel. If this field is set, it takes precedence over other configurations above + RewriteHandler func(ctx context.Context, query string) ([]string, error) + // limit max queries num that Rewrite generates, and excess queries will be truncated, 5 by default + MaxQueriesNum int + + // Origin Retriever + OrigRetriever retriever.Retriever + + // fusion docs recalled from multi retrievers, remove dup based on document id by default + FusionFunc func(ctx context.Context, docs [][]*schema.Document) ([]*schema.Document, error) +} + +type multiQueryRetriever struct { + queryRunner compose.Runnable[string, []string] + maxQueriesNum int + origRetriever retriever.Retriever + fusionFunc func(ctx context.Context, docs [][]*schema.Document) ([]*schema.Document, error) +} + +// Retrieve retrieves documents from the multi-query retriever. +func (m *multiQueryRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + // generate queries + queries, err := m.queryRunner.Invoke(ctx, query) + if err != nil { + return nil, err + } + if len(queries) > m.maxQueriesNum { + queries = queries[:m.maxQueriesNum] + } + + // retrieve + tasks := make([]*utils.RetrieveTask, len(queries)) + for i := range queries { + tasks[i] = &utils.RetrieveTask{Retriever: m.origRetriever, Query: queries[i]} + } + utils.ConcurrentRetrieveWithCallback(ctx, tasks) + result := make([][]*schema.Document, len(queries)) + for i, task := range tasks { + if task.Err != nil { + return nil, task.Err + } + result[i] = task.Result + } + + // fusion + ctx = ctxWithFusionRunInfo(ctx) + ctx = callbacks.OnStart(ctx, result) + fusionDocs, err := m.fusionFunc(ctx, result) + if err != nil { + callbacks.OnError(ctx, err) + return nil, err + } + callbacks.OnEnd(ctx, fusionDocs) + return fusionDocs, nil +} + +// GetType returns the type of the retriever (MultiQuery). +func (m *multiQueryRetriever) GetType() string { + return "MultiQuery" +} + +func ctxWithFusionRunInfo(ctx context.Context) context.Context { + runInfo := &callbacks.RunInfo{ + Component: compose.ComponentOfLambda, + Type: "FusionFunc", + } + + runInfo.Name = runInfo.Type + string(runInfo.Component) + + return callbacks.ReuseHandlers(ctx, runInfo) +} diff --git a/flow/retriever/multiquery/multi_query_test.go b/flow/retriever/multiquery/multi_query_test.go new file mode 100644 index 0000000..84baf06 --- /dev/null +++ b/flow/retriever/multiquery/multi_query_test.go @@ -0,0 +1,118 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package multiquery + +import ( + "context" + "strings" + "testing" + + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +type mockRetriever struct { +} + +func (m *mockRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + var ret []*schema.Document + if strings.Contains(query, "1") { + ret = append(ret, &schema.Document{ID: "1"}) + } + if strings.Contains(query, "2") { + ret = append(ret, &schema.Document{ID: "2"}) + } + if strings.Contains(query, "3") { + ret = append(ret, &schema.Document{ID: "3"}) + } + if strings.Contains(query, "4") { + ret = append(ret, &schema.Document{ID: "4"}) + } + if strings.Contains(query, "5") { + ret = append(ret, &schema.Document{ID: "5"}) + } + return ret, nil +} + +type mockModel struct { +} + +func (m *mockModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + return &schema.Message{ + Content: "12\n23\n34\n14\n23\n45", + }, nil +} + +func (m *mockModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + panic("implement me") +} + +func (m *mockModel) BindTools(tools []*schema.ToolInfo) error { + panic("implement me") +} + +func TestMultiQueryRetriever(t *testing.T) { + ctx := context.Background() + + // use default llm + mqr, err := NewRetriever(ctx, &Config{ + RewriteLLM: &mockModel{}, + OrigRetriever: &mockRetriever{}, + }) + if err != nil { + t.Fatal(err) + } + c := compose.NewChain[string, []*schema.Document]() + cr, err := c.AppendRetriever(mqr).Compile(ctx) + if err != nil { + t.Fatal(err) + } + + result, err := cr.Invoke(ctx, "query") + if err != nil { + t.Fatal(err) + } + if len(result) != 4 { + t.Fatal("default llm retrieve result is unexpected") + } + + // use custom + mqr, err = NewRetriever(ctx, &Config{ + RewriteHandler: func(ctx context.Context, query string) ([]string, error) { + return []string{"1", "3", "5"}, nil + }, + OrigRetriever: &mockRetriever{}, + }) + if err != nil { + t.Fatal(err) + } + c = compose.NewChain[string, []*schema.Document]() + cr, err = c.AppendRetriever(mqr).Compile(ctx) + if err != nil { + t.Fatal(err) + } + + result, err = cr.Invoke(ctx, "query") + if err != nil { + t.Fatal(err) + } + if len(result) != 3 { + t.Fatal("default llm retrieve result is unexpected") + } +} diff --git a/flow/retriever/parent/doc.go b/flow/retriever/parent/doc.go new file mode 100644 index 0000000..1b5937c --- /dev/null +++ b/flow/retriever/parent/doc.go @@ -0,0 +1,19 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package parent provides a retriever that maps sub-document results +// back to their original parent documents. +package parent diff --git a/flow/retriever/parent/parent.go b/flow/retriever/parent/parent.go new file mode 100644 index 0000000..979afeb --- /dev/null +++ b/flow/retriever/parent/parent.go @@ -0,0 +1,113 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parent + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/schema" +) + +// Config configures the parent retriever. +type Config struct { + // Retriever specifies the original retriever used to retrieve documents. + // For example: a vector database retriever like Milvus, or a full-text search retriever like Elasticsearch. + Retriever retriever.Retriever + // ParentIDKey specifies the key used in the sub-document metadata to store the parent document ID. + // Documents without this key will be removed from the recall results. + // For example: if ParentIDKey is "parent_id", it will look for metadata like: + // {"parent_id": "original_doc_123"} + ParentIDKey string + // OrigDocGetter specifies the method for getting original documents by ids from the sub-document metadata. + // Parameters: + // - ctx: context for the operation + // - ids: slice of parent document IDs to retrieve + // Returns: + // - []*schema.Document: slice of retrieved parent documents + // - error: any error encountered during retrieval + // + // For example: if sub-documents with parent IDs ["doc_1", "doc_2"] are retrieved, + // OrigDocGetter will be called to fetch the original documents with these IDs. + OrigDocGetter func(ctx context.Context, ids []string) ([]*schema.Document, error) +} + +// NewRetriever creates a new parent retriever that handles retrieving original documents +// based on sub-document search results. +// +// Parameters: +// - ctx: context for the operation +// - config: configuration for the parent retriever +// +// Example usage: +// +// retriever, err := NewRetriever(ctx, &Config{ +// Retriever: milvusRetriever, +// ParentIDKey: "source_doc_id", +// OrigDocGetter: func(ctx context.Context, ids []string) ([]*schema.Document, error) { +// return documentStore.GetByIDs(ctx, ids) +// }, +// }) +// +// Returns: +// - retriever.Retriever: the created parent retriever +// - error: any error encountered during creation +func NewRetriever(ctx context.Context, config *Config) (retriever.Retriever, error) { + if config.Retriever == nil { + return nil, fmt.Errorf("retriever is required") + } + if config.OrigDocGetter == nil { + return nil, fmt.Errorf("orig doc getter is required") + } + return &parentRetriever{ + retriever: config.Retriever, + parentIDKey: config.ParentIDKey, + origDocGetter: config.OrigDocGetter, + }, nil +} + +type parentRetriever struct { + retriever retriever.Retriever + parentIDKey string + origDocGetter func(ctx context.Context, ids []string) ([]*schema.Document, error) +} + +func (p *parentRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + subDocs, err := p.retriever.Retrieve(ctx, query, opts...) + if err != nil { + return nil, err + } + ids := make([]string, 0, len(subDocs)) + for _, subDoc := range subDocs { + if k, ok := subDoc.MetaData[p.parentIDKey]; ok { + if s, okk := k.(string); okk && !inList(s, ids) { + ids = append(ids, s) + } + } + } + return p.origDocGetter(ctx, ids) +} + +func inList(elem string, list []string) bool { + for _, v := range list { + if v == elem { + return true + } + } + return false +} diff --git a/flow/retriever/parent/parent_test.go b/flow/retriever/parent/parent_test.go new file mode 100644 index 0000000..6487e25 --- /dev/null +++ b/flow/retriever/parent/parent_test.go @@ -0,0 +1,88 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parent + +import ( + "context" + "reflect" + "testing" + + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/schema" +) + +type testRetriever struct{} + +func (t *testRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + ret := make([]*schema.Document, 0) + for i := range query { + ret = append(ret, &schema.Document{ + ID: "", + Content: "", + MetaData: map[string]interface{}{ + "parent": query[i : i+1], + }, + }) + } + return ret, nil +} + +func TestParentRetriever(t *testing.T) { + tests := []struct { + name string + config *Config + input string + want []*schema.Document + }{ + { + name: "success", + config: &Config{ + Retriever: &testRetriever{}, + ParentIDKey: "parent", + OrigDocGetter: func(ctx context.Context, ids []string) ([]*schema.Document, error) { + var ret []*schema.Document + for i := range ids { + ret = append(ret, &schema.Document{ID: ids[i]}) + } + return ret, nil + }, + }, + input: "123233", + want: []*schema.Document{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + }, + }, + } + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := NewRetriever(ctx, tt.config) + if err != nil { + t.Fatal(err) + } + ret, err := r.Retrieve(ctx, tt.input) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(ret, tt.want) { + t.Errorf("got %v, want %v", ret, tt.want) + } + }) + } +} diff --git a/flow/retriever/router/router.go b/flow/retriever/router/router.go new file mode 100644 index 0000000..f036338 --- /dev/null +++ b/flow/retriever/router/router.go @@ -0,0 +1,195 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package router provides retrieval routing helpers that merge results +// from multiple retrievers and apply ranking strategies. +package router + +import ( + "context" + "fmt" + "sort" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/retriever/utils" + "github.com/cloudwego/eino/schema" +) + +var rrf = func(ctx context.Context, result map[string][]*schema.Document) ([]*schema.Document, error) { + if len(result) < 1 { + return nil, fmt.Errorf("no documents") + } + if len(result) == 1 { + for _, docs := range result { + return docs, nil + } + } + + docRankMap := make(map[string]float64) + docMap := make(map[string]*schema.Document) + for _, v := range result { + for i := range v { + docMap[v[i].ID] = v[i] + if _, ok := docRankMap[v[i].ID]; !ok { + docRankMap[v[i].ID] = 1.0 / float64(i+60) + } else { + docRankMap[v[i].ID] += 1.0 / float64(i+60) + } + } + } + docList := make([]*schema.Document, 0, len(docMap)) + for id := range docMap { + docList = append(docList, docMap[id]) + } + + sort.Slice(docList, func(i, j int) bool { + return docRankMap[docList[i].ID] > docRankMap[docList[j].ID] + }) + + return docList, nil +} + +// NewRetriever creates a router retriever. +// router retriever is useful when you want to retrieve documents from multiple retrievers with different queries. +// eg. +// +// routerRetriever := router.NewRetriever(ctx, &router.Config{}) +// docs, err := routerRetriever.Retrieve(ctx, "how to build agent with eino") +// if err != nil { +// ... +// } +// println(docs) +func NewRetriever(ctx context.Context, config *Config) (retriever.Retriever, error) { + if len(config.Retrievers) == 0 { + return nil, fmt.Errorf("retrievers is empty") + } + + router := config.Router + if router == nil { + var retrieverSet []string + for k := range config.Retrievers { + retrieverSet = append(retrieverSet, k) + } + router = func(ctx context.Context, query string) ([]string, error) { + return retrieverSet, nil + } + } + + fusion := config.FusionFunc + if fusion == nil { + fusion = rrf + } + + return &routerRetriever{ + retrievers: config.Retrievers, + router: config.Router, + fusionFunc: fusion, + }, nil +} + +// Config is the config for router retriever. +type Config struct { + // Retrievers is the retrievers to be used. + Retrievers map[string]retriever.Retriever + // Router is the function to route the query to the retrievers. + Router func(ctx context.Context, query string) ([]string, error) + // FusionFunc is the function to fuse the documents from the retrievers. + FusionFunc func(ctx context.Context, result map[string][]*schema.Document) ([]*schema.Document, error) +} + +type routerRetriever struct { + retrievers map[string]retriever.Retriever + router func(ctx context.Context, query string) ([]string, error) + fusionFunc func(ctx context.Context, result map[string][]*schema.Document) ([]*schema.Document, error) +} + +// Retrieve retrieves documents from the router retriever. +func (e *routerRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + routeCtx := ctxWithRouterRunInfo(ctx) + routeCtx = callbacks.OnStart(routeCtx, query) + retrieverNames, err := e.router(routeCtx, query) + if err != nil { + callbacks.OnError(routeCtx, err) + return nil, err + } + if len(retrieverNames) == 0 { + err = fmt.Errorf("no retriever has been selected") + callbacks.OnError(routeCtx, err) + return nil, err + } + callbacks.OnEnd(routeCtx, retrieverNames) + + // retrieve + tasks := make([]*utils.RetrieveTask, len(retrieverNames)) + for i := range retrieverNames { + r, ok := e.retrievers[retrieverNames[i]] + if !ok { + return nil, fmt.Errorf("router output[%s] has not registered", retrieverNames[i]) + } + tasks[i] = &utils.RetrieveTask{ + Name: retrieverNames[i], + Retriever: r, + Query: query, + RetrieveOptions: opts, + } + } + utils.ConcurrentRetrieveWithCallback(ctx, tasks) + result := make(map[string][]*schema.Document) + for i := range tasks { + if tasks[i].Err != nil { + return nil, tasks[i].Err + } + result[tasks[i].Name] = tasks[i].Result + } + + // fusion + fusionCtx := ctxWithFusionRunInfo(ctx) + fusionCtx = callbacks.OnStart(fusionCtx, result) + fusionDocs, err := e.fusionFunc(fusionCtx, result) + if err != nil { + callbacks.OnError(fusionCtx, err) + return nil, err + } + callbacks.OnEnd(fusionCtx, fusionDocs) + return fusionDocs, nil +} + +// GetType returns the type of the retriever (Router). +func (e *routerRetriever) GetType() string { return "Router" } + +func ctxWithRouterRunInfo(ctx context.Context) context.Context { + runInfo := &callbacks.RunInfo{ + Component: compose.ComponentOfLambda, + Type: "Router", + } + + runInfo.Name = runInfo.Type + string(runInfo.Component) + + return callbacks.ReuseHandlers(ctx, runInfo) +} + +func ctxWithFusionRunInfo(ctx context.Context) context.Context { + runInfo := &callbacks.RunInfo{ + Component: compose.ComponentOfLambda, + Type: "FusionFunc", + } + + runInfo.Name = runInfo.Type + string(runInfo.Component) + + return callbacks.ReuseHandlers(ctx, runInfo) +} diff --git a/flow/retriever/router/router_test.go b/flow/retriever/router/router_test.go new file mode 100644 index 0000000..edf1af1 --- /dev/null +++ b/flow/retriever/router/router_test.go @@ -0,0 +1,134 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package router + +import ( + "context" + "reflect" + "strings" + "testing" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/schema" +) + +type mockRetriever struct { +} + +func (m *mockRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + var ret []*schema.Document + if strings.Contains(query, "1") { + ret = append(ret, &schema.Document{ID: "1"}) + } + if strings.Contains(query, "2") { + ret = append(ret, &schema.Document{ID: "2"}) + } + if strings.Contains(query, "3") { + ret = append(ret, &schema.Document{ID: "3"}) + } + if strings.Contains(query, "4") { + ret = append(ret, &schema.Document{ID: "4"}) + } + if strings.Contains(query, "5") { + ret = append(ret, &schema.Document{ID: "5"}) + } + return ret, nil +} + +func (m *mockRetriever) GetType() string { + return "Mock" +} + +func TestRouterRetriever(t *testing.T) { + ctx := context.Background() + r, err := NewRetriever(ctx, &Config{ + Retrievers: map[string]retriever.Retriever{ + "1": &mockRetriever{}, + "2": &mockRetriever{}, + "3": &mockRetriever{}, + }, + Router: func(ctx context.Context, query string) ([]string, error) { + return []string{"2", "3"}, nil + }, + FusionFunc: func(ctx context.Context, result map[string][]*schema.Document) ([]*schema.Document, error) { + var ret []*schema.Document + for _, v := range result { + ret = append(ret, v...) + } + return ret, nil + }, + }) + if err != nil { + t.Fatal(err) + } + + handler := callbacks.NewHandlerBuilder(). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + switch info.Name { + case "FusionFuncLambda": + if _, ok := output.([]*schema.Document); !ok { + t.Fatal("FusionFuncLambda output is not a []*schema.Document") + } + case "RouterLambda": + if _, ok := output.([]string); !ok { + t.Fatal("RouterLambda output is not a []string") + } + case "MockRetriever": + if _, ok := output.([]*schema.Document); !ok { + t.Fatal("MockRetriever output is not a []string") + } + default: + t.Fatalf("unknown name: %s", info.Name) + } + return ctx + }). + OnErrorFn(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + t.Fatal(err) + return ctx + }).Build() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{}, handler) + result, err := r.Retrieve(ctx, "3") + if err != nil { + t.Fatal(err) + } + if len(result) != 2 { + t.Fatal("expected 2 results") + } +} + +func TestRRF(t *testing.T) { + doc1 := &schema.Document{ID: "1"} + doc2 := &schema.Document{ID: "2"} + doc3 := &schema.Document{ID: "3"} + doc4 := &schema.Document{ID: "4"} + doc5 := &schema.Document{ID: "5"} + + input := map[string][]*schema.Document{ + "1": {doc1, doc2, doc3, doc4, doc5}, + "2": {doc2, doc3, doc4, doc5, doc1}, + "3": {doc3, doc4, doc5, doc1, doc2}, + } + + result, err := rrf(context.Background(), input) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(result, []*schema.Document{doc3, doc2, doc4, doc1, doc5}) { + t.Fatal("rrf fail") + } +} diff --git a/flow/retriever/utils/utils.go b/flow/retriever/utils/utils.go new file mode 100644 index 0000000..4fab29c --- /dev/null +++ b/flow/retriever/utils/utils.go @@ -0,0 +1,86 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package utils provides helper utilities for retriever flows, including +// concurrent retrieval with callback instrumentation. +package utils + +import ( + "context" + "fmt" + "sync" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/schema" +) + +// RetrieveTask is a task for retrieving documents. +// RetrieveTask represents a single retrieval job with its result or error. +type RetrieveTask struct { + Name string + Retriever retriever.Retriever + Query string + RetrieveOptions []retriever.Option + Result []*schema.Document + Err error +} + +// ConcurrentRetrieveWithCallback concurrently retrieves documents with callback. +func ConcurrentRetrieveWithCallback(ctx context.Context, tasks []*RetrieveTask) { + wg := sync.WaitGroup{} + for i := range tasks { + wg.Add(1) + go func(ctx context.Context, t *RetrieveTask) { + ctx = ctxWithRetrieverRunInfo(ctx, t.Retriever) + + defer func() { + if e := recover(); e != nil { + t.Err = fmt.Errorf("retrieve panic, query: %s, error: %v", t.Query, e) + ctx = callbacks.OnError(ctx, t.Err) + } + wg.Done() + }() + + ctx = callbacks.OnStart(ctx, t.Query) + docs, err := t.Retriever.Retrieve(ctx, t.Query, t.RetrieveOptions...) + if err != nil { + callbacks.OnError(ctx, err) + t.Err = err + return + } + + callbacks.OnEnd(ctx, docs) + t.Result = docs + }(ctx, tasks[i]) + } + wg.Wait() +} + +func ctxWithRetrieverRunInfo(ctx context.Context, r retriever.Retriever) context.Context { + runInfo := &callbacks.RunInfo{ + Component: components.ComponentOfRetriever, + } + + if typ, okk := components.GetType(r); okk { + runInfo.Type = typ + } + + runInfo.Name = runInfo.Type + string(runInfo.Component) + + return callbacks.ReuseHandlers(ctx, runInfo) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0b87a6c --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module github.com/cloudwego/eino + +go 1.18 + +require ( + github.com/bmatcuk/doublestar/v4 v4.10.0 + github.com/bytedance/sonic v1.15.0 + github.com/eino-contrib/jsonschema v1.0.3 + github.com/google/uuid v1.6.0 + github.com/nikolalohinski/gonja v1.5.3 + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f + github.com/smartystreets/goconvey v1.8.1 + github.com/stretchr/testify v1.10.0 + github.com/wk8/go-ordered-map/v2 v2.1.8 + go.uber.org/mock v0.4.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smarty/assertions v1.15.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5813766 --- /dev/null +++ b/go.sum @@ -0,0 +1,133 @@ +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= +github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/callbacks/inject.go b/internal/callbacks/inject.go new file mode 100644 index 0000000..0ee1579 --- /dev/null +++ b/internal/callbacks/inject.go @@ -0,0 +1,203 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/schema" +) + +func InitCallbacks(ctx context.Context, info *RunInfo, handlers ...Handler) context.Context { + mgr, ok := newManager(info, handlers...) + if ok { + return ctxWithManager(ctx, mgr) + } + + return ctxWithManager(ctx, nil) +} + +func EnsureRunInfo(ctx context.Context, typ string, comp components.Component) context.Context { + cbm, ok := managerFromCtx(ctx) + if !ok { + return InitCallbacks(ctx, &RunInfo{ + Type: typ, + Component: comp, + }) + } + if cbm.runInfo == nil { + return ReuseHandlers(ctx, &RunInfo{ + Type: typ, + Component: comp, + }) + } + return ctx +} + +func ReuseHandlers(ctx context.Context, info *RunInfo) context.Context { + cbm, ok := managerFromCtx(ctx) + if !ok { + return InitCallbacks(ctx, info) + } + return ctxWithManager(ctx, cbm.withRunInfo(info)) +} + +func AppendHandlers(ctx context.Context, info *RunInfo, handlers ...Handler) context.Context { + cbm, ok := managerFromCtx(ctx) + if !ok { + return InitCallbacks(ctx, info, handlers...) + } + nh := make([]Handler, len(cbm.handlers)+len(handlers)) + copy(nh[:len(cbm.handlers)], cbm.handlers) + copy(nh[len(cbm.handlers):], handlers) + return InitCallbacks(ctx, info, nh...) +} + +type Handle[T any] func(context.Context, T, *RunInfo, []Handler) (context.Context, T) + +func On[T any](ctx context.Context, inOut T, handle Handle[T], timing CallbackTiming, start bool) (context.Context, T) { + mgr, ok := managerFromCtx(ctx) + if !ok { + return ctx, inOut + } + nMgr := *mgr + + var info *RunInfo + if start { + info = nMgr.runInfo + nMgr.runInfo = nil + ctx = context.WithValue(ctx, CtxRunInfoKey{}, info) + } else { + if nMgr.runInfo != nil { + info = nMgr.runInfo + } else { + info, _ = ctx.Value(CtxRunInfoKey{}).(*RunInfo) + } + } + + hs := make([]Handler, 0, len(nMgr.handlers)+len(nMgr.globalHandlers)) + for _, handler := range append(nMgr.handlers, nMgr.globalHandlers...) { + timingChecker, ok_ := handler.(TimingChecker) + if !ok_ || timingChecker.Needed(ctx, info, timing) { + hs = append(hs, handler) + } + } + + var out T + ctx, out = handle(ctx, inOut, info, hs) + return ctxWithManager(ctx, &nMgr), out +} + +func OnStartHandle[T any](ctx context.Context, input T, + runInfo *RunInfo, handlers []Handler) (context.Context, T) { + + for i := len(handlers) - 1; i >= 0; i-- { + ctx = handlers[i].OnStart(ctx, runInfo, input) + } + + return ctx, input +} + +func OnEndHandle[T any](ctx context.Context, output T, + runInfo *RunInfo, handlers []Handler) (context.Context, T) { + + for _, handler := range handlers { + ctx = handler.OnEnd(ctx, runInfo, output) + } + + return ctx, output +} + +func BuildOnEndHandleWithCopy[T any](copyFn func(T, int) []T) Handle[T] { + return func(ctx context.Context, output T, runInfo *RunInfo, handlers []Handler) (context.Context, T) { + if len(handlers) == 0 { + return ctx, output + } + + copies := copyFn(output, len(handlers)) + + for i, handler := range handlers { + ctx = handler.OnEnd(ctx, runInfo, copies[i]) + } + + return ctx, output + } +} + +func OnWithStreamHandle[S any]( + ctx context.Context, + inOut S, + handlers []Handler, + cpy func(int) []S, + handle func(context.Context, Handler, S) context.Context) (context.Context, S) { + + if len(handlers) == 0 { + return ctx, inOut + } + + inOuts := cpy(len(handlers) + 1) + + for i, handler := range handlers { + ctx = handle(ctx, handler, inOuts[i]) + } + + return ctx, inOuts[len(inOuts)-1] +} + +func OnStartWithStreamInputHandle[T any](ctx context.Context, input *schema.StreamReader[T], + runInfo *RunInfo, handlers []Handler) (context.Context, *schema.StreamReader[T]) { + + handlers = generic.Reverse(handlers) + + cpy := input.Copy + + handle := func(ctx context.Context, handler Handler, in *schema.StreamReader[T]) context.Context { + in_ := schema.StreamReaderWithConvert(in, func(i T) (CallbackInput, error) { + return i, nil + }) + return handler.OnStartWithStreamInput(ctx, runInfo, in_) + } + + return OnWithStreamHandle(ctx, input, handlers, cpy, handle) +} + +func OnEndWithStreamOutputHandle[T any](ctx context.Context, output *schema.StreamReader[T], + runInfo *RunInfo, handlers []Handler) (context.Context, *schema.StreamReader[T]) { + + cpy := output.Copy + + handle := func(ctx context.Context, handler Handler, out *schema.StreamReader[T]) context.Context { + out_ := schema.StreamReaderWithConvert(out, func(i T) (CallbackOutput, error) { + return i, nil + }) + return handler.OnEndWithStreamOutput(ctx, runInfo, out_) + } + + return OnWithStreamHandle(ctx, output, handlers, cpy, handle) +} + +func OnErrorHandle(ctx context.Context, err error, + runInfo *RunInfo, handlers []Handler) (context.Context, error) { + + for _, handler := range handlers { + ctx = handler.OnError(ctx, runInfo, err) + } + + return ctx, err +} diff --git a/internal/callbacks/interface.go b/internal/callbacks/interface.go new file mode 100644 index 0000000..9e7665c --- /dev/null +++ b/internal/callbacks/interface.go @@ -0,0 +1,54 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/schema" +) + +type RunInfo struct { + // Name is the graph node name for display purposes, not unique. + // Passed from compose.WithNodeName(). + Name string + Type string + Component components.Component +} + +type CallbackInput any + +type CallbackOutput any + +type Handler interface { + OnStart(ctx context.Context, info *RunInfo, input CallbackInput) context.Context + OnEnd(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context + + OnError(ctx context.Context, info *RunInfo, err error) context.Context + + OnStartWithStreamInput(ctx context.Context, info *RunInfo, + input *schema.StreamReader[CallbackInput]) context.Context + OnEndWithStreamOutput(ctx context.Context, info *RunInfo, + output *schema.StreamReader[CallbackOutput]) context.Context +} + +type CallbackTiming uint8 + +type TimingChecker interface { + Needed(ctx context.Context, info *RunInfo, timing CallbackTiming) bool +} diff --git a/internal/callbacks/manager.go b/internal/callbacks/manager.go new file mode 100644 index 0000000..6daf82e --- /dev/null +++ b/internal/callbacks/manager.go @@ -0,0 +1,70 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import "context" + +type CtxManagerKey struct{} +type CtxRunInfoKey struct{} + +type manager struct { + globalHandlers []Handler + handlers []Handler + runInfo *RunInfo +} + +var GlobalHandlers []Handler + +func newManager(runInfo *RunInfo, handlers ...Handler) (*manager, bool) { + if len(handlers)+len(GlobalHandlers) == 0 { + return nil, false + } + + hs := make([]Handler, len(GlobalHandlers)) + copy(hs, GlobalHandlers) + + return &manager{ + globalHandlers: hs, + handlers: handlers, + runInfo: runInfo, + }, true +} + +func ctxWithManager(ctx context.Context, manager *manager) context.Context { + return context.WithValue(ctx, CtxManagerKey{}, manager) +} + +func (m *manager) withRunInfo(runInfo *RunInfo) *manager { + if m == nil { + return nil + } + + n := *m + n.runInfo = runInfo + return &n +} + +func managerFromCtx(ctx context.Context) (*manager, bool) { + v := ctx.Value(CtxManagerKey{}) + m, ok := v.(*manager) + if ok && m != nil { + n := *m + return &n, true + } + + return nil, false +} diff --git a/internal/channel.go b/internal/channel.go new file mode 100644 index 0000000..fa42153 --- /dev/null +++ b/internal/channel.go @@ -0,0 +1,94 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import "sync" + +// UnboundedChan represents a channel with unlimited capacity +type UnboundedChan[T any] struct { + buffer []T // Internal buffer to store data + mutex sync.Mutex // Mutex to protect buffer access + notEmpty *sync.Cond // Condition variable to wait for data + closed bool // Indicates if the channel has been closed +} + +// NewUnboundedChan initializes and returns an UnboundedChan +func NewUnboundedChan[T any]() *UnboundedChan[T] { + ch := &UnboundedChan[T]{} + ch.notEmpty = sync.NewCond(&ch.mutex) + return ch +} + +// Send puts an item into the channel +func (ch *UnboundedChan[T]) Send(value T) { + ch.mutex.Lock() + defer ch.mutex.Unlock() + + if ch.closed { + panic("send on closed channel") + } + + ch.buffer = append(ch.buffer, value) + ch.notEmpty.Signal() // Wake up one goroutine waiting to receive +} + +// TrySend attempts to put an item into the channel. +// Returns false if the channel is closed, true otherwise. +func (ch *UnboundedChan[T]) TrySend(value T) bool { + ch.mutex.Lock() + defer ch.mutex.Unlock() + + if ch.closed { + return false + } + + ch.buffer = append(ch.buffer, value) + ch.notEmpty.Signal() + return true +} + +// Receive gets an item from the channel (blocks if empty). +// Returns (value, true) if an item was received. +// Returns (zero, false) if the channel was closed with no data remaining. +func (ch *UnboundedChan[T]) Receive() (T, bool) { + ch.mutex.Lock() + defer ch.mutex.Unlock() + + for len(ch.buffer) == 0 && !ch.closed { + ch.notEmpty.Wait() + } + + if len(ch.buffer) == 0 { + var zero T + return zero, false + } + + val := ch.buffer[0] + ch.buffer = ch.buffer[1:] + return val, true +} + +// Close marks the channel as closed +func (ch *UnboundedChan[T]) Close() { + ch.mutex.Lock() + defer ch.mutex.Unlock() + + if !ch.closed { + ch.closed = true + ch.notEmpty.Broadcast() + } +} diff --git a/internal/channel_test.go b/internal/channel_test.go new file mode 100644 index 0000000..736a274 --- /dev/null +++ b/internal/channel_test.go @@ -0,0 +1,221 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "sync" + "testing" + "time" +) + +func TestUnboundedChan_Send(t *testing.T) { + ch := NewUnboundedChan[string]() + + // Test sending a value + ch.Send("test") + if len(ch.buffer) != 1 { + t.Errorf("buffer length should be 1, got %d", len(ch.buffer)) + } + if ch.buffer[0] != "test" { + t.Errorf("expected 'test', got '%s'", ch.buffer[0]) + } + + // Test sending multiple values + ch.Send("test2") + ch.Send("test3") + if len(ch.buffer) != 3 { + t.Errorf("buffer length should be 3, got %d", len(ch.buffer)) + } +} + +func TestUnboundedChan_SendPanic(t *testing.T) { + ch := NewUnboundedChan[int]() + ch.Close() + + // Test sending to closed channel should panic + defer func() { + if r := recover(); r == nil { + t.Error("sending to closed channel should panic") + } + }() + + ch.Send(1) +} + +func TestUnboundedChan_Receive(t *testing.T) { + ch := NewUnboundedChan[int]() + + // Send values + ch.Send(1) + ch.Send(2) + + // Test receiving values + val, ok := ch.Receive() + if !ok { + t.Error("receive should succeed") + } + if val != 1 { + t.Errorf("expected 1, got %d", val) + } + + val, ok = ch.Receive() + if !ok { + t.Error("receive should succeed") + } + if val != 2 { + t.Errorf("expected 2, got %d", val) + } +} + +func TestUnboundedChan_ReceiveFromClosed(t *testing.T) { + ch := NewUnboundedChan[int]() + ch.Close() + + // Test receiving from closed, empty channel + val, ok := ch.Receive() + if ok { + t.Error("receive from closed, empty channel should return ok=false") + } + if val != 0 { + t.Errorf("expected zero value, got %d", val) + } + + // Test receiving from closed channel with values + ch = NewUnboundedChan[int]() + ch.Send(42) + ch.Close() + + val, ok = ch.Receive() + if !ok { + t.Error("receive should succeed") + } + if val != 42 { + t.Errorf("expected 42, got %d", val) + } + + // After consuming all values + val, ok = ch.Receive() + if ok { + t.Error("receive from closed, empty channel should return ok=false") + } +} + +func TestUnboundedChan_Close(t *testing.T) { + ch := NewUnboundedChan[int]() + + // Test closing + ch.Close() + if !ch.closed { + t.Error("channel should be marked as closed") + } + + // Test double closing (should not panic) + ch.Close() +} + +func TestUnboundedChan_Concurrency(t *testing.T) { + ch := NewUnboundedChan[int]() + const numSenders = 5 + const numReceivers = 3 + const messagesPerSender = 100 + + var rwg, swg sync.WaitGroup + rwg.Add(numReceivers) + swg.Add(numSenders) + + // Start senders + for i := 0; i < numSenders; i++ { + go func(id int) { + defer swg.Done() + for j := 0; j < messagesPerSender; j++ { + ch.Send(id*messagesPerSender + j) + time.Sleep(time.Microsecond) // Small delay to increase concurrency chance + } + }(i) + } + + // Start receivers + received := make([]int, 0, numSenders*messagesPerSender) + var mu sync.Mutex + + for i := 0; i < numReceivers; i++ { + go func() { + defer rwg.Done() + for { + val, ok := ch.Receive() + if !ok { + return + } + mu.Lock() + received = append(received, val) + mu.Unlock() + } + }() + } + + // Wait for senders to finish + swg.Wait() + ch.Close() + + // Wait for all goroutines to finish + rwg.Wait() + + // Verify we received all messages + if len(received) != numSenders*messagesPerSender { + t.Errorf("expected %d messages, got %d", numSenders*messagesPerSender, len(received)) + } + + // Create a map to check for duplicates and missing values + receivedMap := make(map[int]bool) + for _, val := range received { + receivedMap[val] = true + } + + if len(receivedMap) != numSenders*messagesPerSender { + t.Error("duplicate or missing messages detected") + } +} + +func TestUnboundedChan_BlockingReceive(t *testing.T) { + ch := NewUnboundedChan[int]() + + // Test that Receive blocks when channel is empty + receiveDone := make(chan bool) + go func() { + ch.Receive() + receiveDone <- true + }() + + // Check that receive is blocked + select { + case <-receiveDone: + t.Error("Receive should block on empty channel") + case <-time.After(50 * time.Millisecond): + // This is expected + } + + // Send a value to unblock + ch.Send(1) + + // Now receive should complete + select { + case <-receiveDone: + // This is expected + case <-time.After(50 * time.Millisecond): + t.Error("Receive should have unblocked") + } +} diff --git a/internal/concat.go b/internal/concat.go new file mode 100644 index 0000000..fd9b8ab --- /dev/null +++ b/internal/concat.go @@ -0,0 +1,224 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "fmt" + "reflect" + "strings" + "time" + + "github.com/cloudwego/eino/internal/generic" +) + +var ( + concatFuncs = map[reflect.Type]any{ + generic.TypeOf[string](): concatStrings, + generic.TypeOf[int8](): useLast[int8], + generic.TypeOf[int16](): useLast[int16], + generic.TypeOf[int32](): useLast[int32], + generic.TypeOf[int64](): useLast[int64], + generic.TypeOf[int](): useLast[int], + generic.TypeOf[uint8](): useLast[uint8], + generic.TypeOf[uint16](): useLast[uint16], + generic.TypeOf[uint32](): useLast[uint32], + generic.TypeOf[uint64](): useLast[uint64], + generic.TypeOf[uint](): useLast[uint], + generic.TypeOf[bool](): useLast[bool], + generic.TypeOf[float32](): useLast[float32], + generic.TypeOf[float64](): useLast[float64], + generic.TypeOf[time.Time](): useLast[time.Time], + generic.TypeOf[time.Duration](): useLast[time.Duration], + } +) + +func useLast[T any](s []T) (T, error) { + return s[len(s)-1], nil +} + +func concatStrings(ss []string) (string, error) { + var n int + for _, s := range ss { + n += len(s) + } + + var b strings.Builder + b.Grow(n) + for _, s := range ss { + _, err := b.WriteString(s) + if err != nil { + return "", err + } + } + + return b.String(), nil +} + +func RegisterStreamChunkConcatFunc[T any](fn func([]T) (T, error)) { + concatFuncs[generic.TypeOf[T]()] = fn +} + +func GetConcatFunc(typ reflect.Type) func(reflect.Value) (reflect.Value, error) { + if fn, ok := concatFuncs[typ]; ok { + return func(a reflect.Value) (reflect.Value, error) { + rvs := reflect.ValueOf(fn).Call([]reflect.Value{a}) + var err error + if !rvs[1].IsNil() { + err = rvs[1].Interface().(error) + } + return rvs[0], err + } + } + + return nil +} + +// ConcatItems the caller should ensure len(items) > 1 +func ConcatItems[T any](items []T) (T, error) { + typ := generic.TypeOf[T]() + v := reflect.ValueOf(items) + + var cv reflect.Value + var err error + + // handle map kind + if typ.Kind() == reflect.Map { + cv, err = concatMaps(v) + } else { + cv, err = ConcatSliceValue(v) + } + + if err != nil { + var t T + return t, err + } + + return cv.Interface().(T), nil +} + +func concatMaps(ms reflect.Value) (reflect.Value, error) { + typ := ms.Type().Elem() + + rms := reflect.MakeMap(reflect.MapOf(typ.Key(), generic.TypeOf[[]any]())) + ret := reflect.MakeMap(typ) + + n := ms.Len() + for i := 0; i < n; i++ { + m := ms.Index(i) + + for _, key := range m.MapKeys() { + vals := rms.MapIndex(key) + if !vals.IsValid() { + var s []any + vals = reflect.ValueOf(s) + } + + val := m.MapIndex(key) + vals = reflect.Append(vals, val) + rms.SetMapIndex(key, vals) + } + } + + for _, key := range rms.MapKeys() { + vals := rms.MapIndex(key) + + anyVals := vals.Interface().([]any) + if len(anyVals) == 1 { + ele := anyVals[0] + if ele == nil { // we cannot SetMapIndex with nil because it will delete the key + ret.SetMapIndex(key, reflect.Zero(typ.Elem())) + continue + } + + ret.SetMapIndex(key, reflect.ValueOf(ele)) + continue + } + + v, err := toSliceValue(anyVals) + if err != nil { + return reflect.Value{}, err + } + + var cv reflect.Value + + if v.Type().Elem().Kind() == reflect.Map { + cv, err = concatMaps(v) + } else { + cv, err = ConcatSliceValue(v) + } + + if err != nil { + return reflect.Value{}, err + } + + ret.SetMapIndex(key, cv) + } + + return ret, nil +} + +func ConcatSliceValue(val reflect.Value) (reflect.Value, error) { + elmType := val.Type().Elem() + + if val.Len() == 1 { + return val.Index(0), nil + } + + f := GetConcatFunc(elmType) + if f != nil { + return f(val) + } + + // if all elements in the slice are empty, return an empty value + // if there is exactly one non-empty element in the slice, return that non-empty element + // otherwise, throw an error. + var filtered reflect.Value + for i := 0; i < val.Len(); i++ { + oneVal := val.Index(i) + if !oneVal.IsZero() { + if filtered.IsValid() { + return reflect.Value{}, fmt.Errorf("cannot concat multiple non-zero value of type %s", elmType) + } + + filtered = oneVal + } + } + if !filtered.IsValid() { + filtered = reflect.New(elmType).Elem() + } + + return filtered, nil +} + +func toSliceValue(vs []any) (reflect.Value, error) { + typ := reflect.TypeOf(vs[0]) + + ret := reflect.MakeSlice(reflect.SliceOf(typ), len(vs), len(vs)) + ret.Index(0).Set(reflect.ValueOf(vs[0])) + + for i := 1; i < len(vs); i++ { + v := vs[i] + vt := reflect.TypeOf(v) + if typ != vt { + return reflect.Value{}, fmt.Errorf("unexpected slice element type. Got %v, expected %v", typ, vt) + } + + ret.Index(i).Set(reflect.ValueOf(v)) + } + + return ret, nil +} diff --git a/internal/concat_test.go b/internal/concat_test.go new file mode 100644 index 0000000..12b4837 --- /dev/null +++ b/internal/concat_test.go @@ -0,0 +1,52 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConcat(t *testing.T) { + t.Run("concat map chunks with nil value", func(t *testing.T) { + c1 := map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c1": nil, + }, + }, + } + c2 := map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c2": "c2", + }, + }, + } + m, err := ConcatItems([]map[string]any{c1, c2}) + assert.Nil(t, err) + assert.Equal(t, map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c1": nil, + "c2": "c2", + }, + }, + }, m) + }) +} diff --git a/internal/core/address.go b/internal/core/address.go new file mode 100644 index 0000000..bb2400a --- /dev/null +++ b/internal/core/address.go @@ -0,0 +1,338 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package core + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/cloudwego/eino/internal/generic" +) + +// AddressSegmentType defines the type of a segment in an execution address. +type AddressSegmentType string + +// Address represents a full, hierarchical address to a point in the execution structure. +type Address []AddressSegment + +// String converts an Address into its unique string representation. +func (p Address) String() string { + if p == nil { + return "" + } + var sb strings.Builder + for i, s := range p { + sb.WriteString(string(s.Type)) + sb.WriteString(":") + sb.WriteString(s.ID) + if s.SubID != "" { + sb.WriteString(":") + sb.WriteString(s.SubID) + } + if i != len(p)-1 { + sb.WriteString(";") + } + } + return sb.String() +} + +func (p Address) Equals(other Address) bool { + if len(p) != len(other) { + return false + } + for i := range p { + if p[i].Type != other[i].Type || p[i].ID != other[i].ID || p[i].SubID != other[i].SubID { + return false + } + } + return true +} + +// AddressSegment represents a single segment in the hierarchical address of an execution point. +// A sequence of AddressSegments uniquely identifies a location within a potentially nested structure. +type AddressSegment struct { + // ID is the unique identifier for this segment, e.g., the node's key or the tool's name. + ID string + // Type indicates whether this address segment is a graph node, a tool call, an agent, etc. + Type AddressSegmentType + // In some cases, ID alone are not unique enough, we need this SubID to guarantee uniqueness. + // e.g. parallel tool calls with the same name but different tool call IDs. + SubID string +} + +type addrCtxKey struct{} + +type addrCtx struct { + addr Address + interruptState *InterruptState + isResumeTarget bool + resumeData any +} + +type globalResumeInfoKey struct{} + +type globalResumeInfo struct { + mu sync.RWMutex + id2ResumeData map[string]any + id2ResumeDataUsed map[string]bool + id2State map[string]InterruptState + id2StateUsed map[string]bool + id2Addr map[string]Address +} + +// GetCurrentAddress returns the hierarchical address of the currently executing component. +// The address is a sequence of segments, each identifying a structural part of the execution +// like an agent, a graph node, or a tool call. This can be useful for logging or debugging. +func GetCurrentAddress(ctx context.Context) Address { + if p, ok := ctx.Value(addrCtxKey{}).(*addrCtx); ok { + return p.addr + } + + return nil +} + +// AppendAddressSegment creates a new execution context for a sub-component (e.g., a graph node or a tool call). +// +// It extends the current context's address with a new segment and populates the new context with the +// appropriate interrupt state and resume data for that specific sub-address. +// +// - ctx: The parent context, typically the one passed into the component's Invoke/Stream method. +// - segType: The type of the new address segment (e.g., "node", "tool"). +// - segID: The unique ID for the new address segment. +func AppendAddressSegment(ctx context.Context, segType AddressSegmentType, segID string, + subID string) context.Context { + // get current address + currentAddress := GetCurrentAddress(ctx) + if len(currentAddress) == 0 { + currentAddress = []AddressSegment{ + { + Type: segType, + ID: segID, + SubID: subID, + }, + } + } else { + newAddress := make([]AddressSegment, len(currentAddress)+1) + copy(newAddress, currentAddress) + newAddress[len(newAddress)-1] = AddressSegment{ + Type: segType, + ID: segID, + SubID: subID, + } + currentAddress = newAddress + } + + runCtx := &addrCtx{ + addr: currentAddress, + } + + rInfo, hasRInfo := getResumeInfo(ctx) + if !hasRInfo { + return context.WithValue(ctx, addrCtxKey{}, runCtx) + } + + rInfo.mu.Lock() + defer rInfo.mu.Unlock() + + var id string + for id_, addr := range rInfo.id2Addr { + if addr.Equals(currentAddress) { + if used, ok := rInfo.id2StateUsed[id_]; !ok || !used { + runCtx.interruptState = generic.PtrOf(rInfo.id2State[id_]) + rInfo.id2StateUsed[id_] = true + id = id_ + break + } + } + } + + used := rInfo.id2ResumeDataUsed[id] + if !used { + rData, existed := rInfo.id2ResumeData[id] + if existed { + rInfo.id2ResumeDataUsed[id] = true + runCtx.resumeData = rData + runCtx.isResumeTarget = true + } + } + + if !runCtx.isResumeTarget { + for id_, addr := range rInfo.id2Addr { + if len(addr) > len(currentAddress) && addr[:len(currentAddress)].Equals(currentAddress) { + if !rInfo.id2ResumeDataUsed[id_] { + runCtx.isResumeTarget = true + break + } + } + } + } + + return context.WithValue(ctx, addrCtxKey{}, runCtx) +} + +// GetNextResumptionPoints finds the immediate child resumption points for a given parent address. +func GetNextResumptionPoints(ctx context.Context) (map[string]bool, error) { + parentAddr := GetCurrentAddress(ctx) + + rInfo, exists := getResumeInfo(ctx) + if !exists { + return nil, fmt.Errorf("GetNextResumptionPoints: failed to get resume info from context") + } + + rInfo.mu.RLock() + defer rInfo.mu.RUnlock() + + nextPoints := make(map[string]bool) + parentAddrLen := len(parentAddr) + + for _, addr := range rInfo.id2Addr { + // Check if addr is a potential child (must be longer than parent) + if len(addr) <= parentAddrLen { + continue + } + + // Check if it has the parent address as a prefix + var isPrefix bool + if parentAddrLen == 0 { + isPrefix = true + } else { + isPrefix = addr[:parentAddrLen].Equals(parentAddr) + } + + if !isPrefix { + continue + } + + // We are looking for immediate children. + // The address of an immediate child should be one segment longer. + childAddr := addr[parentAddrLen : parentAddrLen+1] + childID := childAddr[0].ID + + // Avoid adding duplicates. + if _, ok := nextPoints[childID]; !ok { + nextPoints[childID] = true + } + } + + return nextPoints, nil +} + +// BatchResumeWithData is the core function for preparing a resume context. It injects a map +// of resume targets and their corresponding data into the context. +// +// The `resumeData` map should contain the interrupt IDs (which are the string form of addresses) of the +// components to be resumed as keys. The value can be the resume data for that component, or `nil` +// if no data is needed (equivalent to using `Resume`). +// +// This function is the foundation for the "Explicit Targeted Resume" strategy. Components whose interrupt IDs +// are present as keys in the map will receive `isResumeFlow = true` when they call `GetResumeContext`. +func BatchResumeWithData(ctx context.Context, resumeData map[string]any) context.Context { + rInfo, ok := ctx.Value(globalResumeInfoKey{}).(*globalResumeInfo) + if !ok { + // Create a new globalResumeInfo and copy the map to prevent external mutation. + newMap := make(map[string]any, len(resumeData)) + for k, v := range resumeData { + newMap[k] = v + } + return context.WithValue(ctx, globalResumeInfoKey{}, &globalResumeInfo{ + id2ResumeData: newMap, + id2ResumeDataUsed: make(map[string]bool), + id2StateUsed: make(map[string]bool), + }) + } + + rInfo.mu.Lock() + defer rInfo.mu.Unlock() + if rInfo.id2ResumeData == nil { + rInfo.id2ResumeData = make(map[string]any) + } + for id, data := range resumeData { + rInfo.id2ResumeData[id] = data + } + return ctx +} + +func PopulateInterruptState(ctx context.Context, id2Addr map[string]Address, + id2State map[string]InterruptState) context.Context { + rInfo, ok := ctx.Value(globalResumeInfoKey{}).(*globalResumeInfo) + if ok { + rInfo.mu.Lock() + defer rInfo.mu.Unlock() + + if rInfo.id2Addr == nil { + rInfo.id2Addr = make(map[string]Address) + } + for id, addr := range id2Addr { + rInfo.id2Addr[id] = addr + } + if rInfo.id2State == nil { + rInfo.id2State = make(map[string]InterruptState) + } + for id, state := range id2State { + rInfo.id2State[id] = state + } + } else { + rInfo = &globalResumeInfo{ + id2Addr: id2Addr, + id2State: id2State, + id2StateUsed: make(map[string]bool), + id2ResumeDataUsed: make(map[string]bool), + } + ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo) + } + + runCtx, ok := getRunCtx(ctx) + if ok { + for id_, addr := range id2Addr { + if addr.Equals(runCtx.addr) { + if used, ok := rInfo.id2StateUsed[id_]; !ok || !used { + runCtx.interruptState = generic.PtrOf(rInfo.id2State[id_]) + rInfo.id2StateUsed[id_] = true + } + + if used, ok := rInfo.id2ResumeDataUsed[id_]; !ok || !used { + runCtx.isResumeTarget = true + runCtx.resumeData = rInfo.id2ResumeData[id_] + rInfo.id2ResumeDataUsed[id_] = true + } + + break + } + } + } + + return ctx +} + +func getResumeInfo(ctx context.Context) (*globalResumeInfo, bool) { + info, ok := ctx.Value(globalResumeInfoKey{}).(*globalResumeInfo) + return info, ok +} + +type InterruptInfo struct { + Info any + IsRootCause bool +} + +func (i *InterruptInfo) String() string { + if i == nil { + return "" + } + return fmt.Sprintf("interrupt info: Info=%v, IsRootCause=%v", i.Info, i.IsRootCause) +} diff --git a/internal/core/interrupt.go b/internal/core/interrupt.go new file mode 100644 index 0000000..38ddbda --- /dev/null +++ b/internal/core/interrupt.go @@ -0,0 +1,349 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package core + +import ( + "context" + "fmt" + "reflect" + + "github.com/google/uuid" +) + +type CheckPointStore interface { + Get(ctx context.Context, checkPointID string) ([]byte, bool, error) + Set(ctx context.Context, checkPointID string, checkPoint []byte) error +} + +// CheckPointDeleter is an optional interface that CheckPointStore implementations +// can implement to support explicit checkpoint deletion. +// +// If the Store does not implement this interface, stale checkpoints will NOT be +// automatically cleaned up. The store owner is responsible for managing checkpoint +// lifecycle in that case (e.g., via TTL, external cleanup, or implementing this +// interface). +type CheckPointDeleter interface { + Delete(ctx context.Context, checkPointID string) error +} + +type InterruptSignal struct { + ID string + Address + InterruptInfo + InterruptState + Subs []*InterruptSignal +} + +func (is *InterruptSignal) Error() string { + return fmt.Sprintf("interrupt signal: ID=%s, Addr=%s, Info=%s, State=%s, SubsLen=%d", + is.ID, is.Address.String(), is.InterruptInfo.String(), is.InterruptState.String(), len(is.Subs)) +} + +type InterruptState struct { + State any + LayerSpecificPayload any +} + +func (is *InterruptState) String() string { + if is == nil { + return "" + } + return fmt.Sprintf("interrupt state: State=%v, LayerSpecificPayload=%v", is.State, is.LayerSpecificPayload) +} + +// InterruptConfig holds optional parameters for creating an interrupt. +type InterruptConfig struct { + LayerPayload any +} + +// InterruptOption is a function that configures an InterruptConfig. +type InterruptOption func(*InterruptConfig) + +// WithLayerPayload creates an option to attach layer-specific metadata +// to the interrupt's state. +func WithLayerPayload(payload any) InterruptOption { + return func(c *InterruptConfig) { + c.LayerPayload = payload + } +} + +func Interrupt(ctx context.Context, info any, state any, subContexts []*InterruptSignal, opts ...InterruptOption) ( + *InterruptSignal, error) { + addr := GetCurrentAddress(ctx) + + // Apply options to get config + config := &InterruptConfig{} + for _, opt := range opts { + opt(config) + } + + myPoint := InterruptInfo{ + Info: info, + } + + if len(subContexts) == 0 { + myPoint.IsRootCause = true + return &InterruptSignal{ + ID: uuid.NewString(), + Address: addr, + InterruptInfo: myPoint, + InterruptState: InterruptState{ + State: state, + LayerSpecificPayload: config.LayerPayload, + }, + }, nil + } + + return &InterruptSignal{ + ID: uuid.NewString(), + Address: addr, + InterruptInfo: myPoint, + InterruptState: InterruptState{ + State: state, + LayerSpecificPayload: config.LayerPayload, + }, + Subs: subContexts, + }, nil +} + +// InterruptCtx provides a complete, user-facing context for a single, resumable interrupt point. +type InterruptCtx struct { + // ID is the unique, fully-qualified address of the interrupt point. + // It is constructed by joining the individual Address segments, e.g., "agent:A;node:graph_a;tool:tool_call_123". + // This ID should be used when providing resume data via ResumeWithData. + ID string + // Address is the structured sequence of AddressSegment segments that leads to the interrupt point. + Address Address + // Info is the user-facing information associated with the interrupt, provided by the component that triggered it. + Info any + // IsRootCause indicates whether the interrupt point is the exact root cause for an interruption. + IsRootCause bool + // Parent points to the context of the parent component in the interrupt chain. + // It is nil for the top-level interrupt. + Parent *InterruptCtx +} + +func (ic *InterruptCtx) EqualsWithoutID(other *InterruptCtx) bool { + if ic == nil && other == nil { + return true + } + + if ic == nil || other == nil { + return false + } + + if !ic.Address.Equals(other.Address) { + return false + } + + if ic.IsRootCause != other.IsRootCause { + return false + } + + if ic.Info != nil || other.Info != nil { + if ic.Info == nil || other.Info == nil { + return false + } + + if !reflect.DeepEqual(ic.Info, other.Info) { + return false + } + } + + if ic.Parent != nil || other.Parent != nil { + if ic.Parent == nil || other.Parent == nil { + return false + } + + if !ic.Parent.EqualsWithoutID(other.Parent) { + return false + } + } + + return true +} + +// InterruptContextsProvider is an interface for errors that contain interrupt contexts. +// This allows different packages to check for and extract interrupt contexts from errors +// without needing to know the concrete error type. +type InterruptContextsProvider interface { + GetInterruptContexts() []*InterruptCtx +} + +// FromInterruptContexts converts a list of user-facing InterruptCtx objects into an +// internal InterruptSignal tree. It correctly handles common ancestors and ensures +// that the resulting tree is consistent with the original interrupt chain. +// +// This method is primarily used by components that bridge different execution environments. +// For example, an `adk.AgentTool` might catch an `adk.InterruptInfo`, extract the +// `adk.InterruptCtx` objects from it, and then call this method on each one. The resulting +// error signals are then typically aggregated into a single error using `compose.CompositeInterrupt` +// to be returned from the tool's `InvokableRun` method. +// FromInterruptContexts reconstructs a single InterruptSignal tree from a list of +// user-facing InterruptCtx objects. It correctly merges common ancestors. +func FromInterruptContexts(contexts []*InterruptCtx) *InterruptSignal { + if len(contexts) == 0 { + return nil + } + + signalMap := make(map[string]*InterruptSignal) + var rootSignal *InterruptSignal + + // getOrCreateSignal is a recursive helper that builds the tree bottom-up. + var getOrCreateSignal func(*InterruptCtx) *InterruptSignal + getOrCreateSignal = func(ctx *InterruptCtx) *InterruptSignal { + if ctx == nil { + return nil + } + // If we've already created a signal for this context, return it. + if signal, exists := signalMap[ctx.ID]; exists { + return signal + } + + // Create the signal for the current context. + newSignal := &InterruptSignal{ + ID: ctx.ID, + Address: ctx.Address, + InterruptInfo: InterruptInfo{ + Info: ctx.Info, + IsRootCause: ctx.IsRootCause, + }, + } + signalMap[ctx.ID] = newSignal // Cache it immediately. + + // Recursively ensure the parent exists. If it doesn't, this is the root. + if parentSignal := getOrCreateSignal(ctx.Parent); parentSignal != nil { + parentSignal.Subs = append(parentSignal.Subs, newSignal) + } else { + rootSignal = newSignal + } + return newSignal + } + + // Process all contexts to ensure all branches of the tree are built. + for _, ctx := range contexts { + _ = getOrCreateSignal(ctx) + } + + return rootSignal +} + +// ToInterruptContexts converts the internal InterruptSignal tree into a list of +// user-facing InterruptCtx objects for the root causes of the interruption. +// Each returned context has its Parent field populated (if it has a parent), +// allowing traversal up the interrupt chain. +// +// If allowedSegmentTypes is nil, all segment types are kept and addresses are unchanged. +// If allowedSegmentTypes is provided, it: +// 1. Filters the parent chain to only keep contexts whose leaf segment type is allowed +// 2. Strips non-allowed segment types from all addresses +func ToInterruptContexts(is *InterruptSignal, allowedSegmentTypes []AddressSegmentType) []*InterruptCtx { + if is == nil { + return nil + } + var rootCauseContexts []*InterruptCtx + + var buildContexts func(*InterruptSignal, *InterruptCtx) + buildContexts = func(signal *InterruptSignal, parentCtx *InterruptCtx) { + currentCtx := &InterruptCtx{ + ID: signal.ID, + Address: signal.Address, + Info: signal.InterruptInfo.Info, + IsRootCause: signal.InterruptInfo.IsRootCause, + Parent: parentCtx, + } + + if currentCtx.IsRootCause { + rootCauseContexts = append(rootCauseContexts, currentCtx) + } + + for _, subSignal := range signal.Subs { + buildContexts(subSignal, currentCtx) + } + } + + buildContexts(is, nil) + + if len(allowedSegmentTypes) > 0 { + allowedSet := make(map[AddressSegmentType]bool, len(allowedSegmentTypes)) + for _, t := range allowedSegmentTypes { + allowedSet[t] = true + } + + for _, ctx := range rootCauseContexts { + filterParentChain(ctx, allowedSet) + encapsulateContextAddresses(ctx, allowedSet) + } + } + + return rootCauseContexts +} + +func filterParentChain(ctx *InterruptCtx, allowedSet map[AddressSegmentType]bool) { + if ctx == nil { + return + } + + parent := ctx.Parent + for parent != nil { + if len(parent.Address) > 0 && allowedSet[parent.Address[len(parent.Address)-1].Type] { + break + } + parent = parent.Parent + } + + ctx.Parent = parent + + filterParentChain(parent, allowedSet) +} + +func encapsulateContextAddresses(ctx *InterruptCtx, allowedSet map[AddressSegmentType]bool) { + for c := ctx; c != nil; c = c.Parent { + newAddr := make(Address, 0, len(c.Address)) + for _, seg := range c.Address { + if allowedSet[seg.Type] { + newAddr = append(newAddr, seg) + } + } + c.Address = newAddr + } +} + +// SignalToPersistenceMaps flattens an InterruptSignal tree into two maps suitable for persistence in a checkpoint. +func SignalToPersistenceMaps(is *InterruptSignal) (map[string]Address, map[string]InterruptState) { + id2addr := make(map[string]Address) + id2state := make(map[string]InterruptState) + + if is == nil { + return id2addr, id2state + } + + var traverse func(*InterruptSignal) + traverse = func(signal *InterruptSignal) { + // Add current signal's data to the maps. + id2addr[signal.ID] = signal.Address + id2state[signal.ID] = signal.InterruptState // The embedded struct + + // Recurse into children. + for _, sub := range signal.Subs { + traverse(sub) + } + } + + traverse(is) + return id2addr, id2state +} diff --git a/internal/core/interrupt_test.go b/internal/core/interrupt_test.go new file mode 100644 index 0000000..86d1149 --- /dev/null +++ b/internal/core/interrupt_test.go @@ -0,0 +1,1128 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package core + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Define AddressSegmentType constants locally to avoid dependency cycles +const ( + AddressSegmentAgent AddressSegmentType = "agent" + AddressSegmentTool AddressSegmentType = "tool" + AddressSegmentNode AddressSegmentType = "node" +) + +func TestInterruptConversion(t *testing.T) { + // Test Case 1: Simple Chain (A -> B -> C) + t.Run("SimpleChain", func(t *testing.T) { + // Manually construct the user-facing contexts with parent pointers + ctxA := &InterruptCtx{ID: "A", IsRootCause: false} + ctxB := &InterruptCtx{ID: "B", Parent: ctxA, IsRootCause: false} + ctxC := &InterruptCtx{ID: "C", Parent: ctxB, IsRootCause: true} + + // The input to FromInterruptContexts is just the root cause leaf node + contexts := []*InterruptCtx{ctxC} + + // Convert from user-facing contexts to internal signal tree + signal := FromInterruptContexts(contexts) + + // Assertions for the signal tree structure + assert.NotNil(t, signal) + assert.Equal(t, "A", signal.ID) + assert.Len(t, signal.Subs, 1) + assert.Equal(t, "B", signal.Subs[0].ID) + assert.Len(t, signal.Subs[0].Subs, 1) + assert.Equal(t, "C", signal.Subs[0].Subs[0].ID) + assert.True(t, signal.Subs[0].Subs[0].IsRootCause) + + // Convert back from the signal tree to user-facing contexts + finalContexts := ToInterruptContexts(signal, nil) + + // Assertions for the final list of contexts + assert.Len(t, finalContexts, 1) + finalC := finalContexts[0] + assert.Equal(t, "C", finalC.ID) + assert.True(t, finalC.IsRootCause) + assert.NotNil(t, finalC.Parent) + assert.Equal(t, "B", finalC.Parent.ID) + assert.NotNil(t, finalC.Parent.Parent) + assert.Equal(t, "A", finalC.Parent.Parent.ID) + assert.Nil(t, finalC.Parent.Parent.Parent) + }) + + // Test Case 2: Multiple Root Causes with Shared Parent (B -> D, C -> D) + t.Run("MultipleRootsSharedParent", func(t *testing.T) { + // Manually construct the contexts + ctxD := &InterruptCtx{ID: "D", IsRootCause: false} + ctxB := &InterruptCtx{ID: "B", Parent: ctxD, IsRootCause: true} + ctxC := &InterruptCtx{ID: "C", Parent: ctxD, IsRootCause: true} + + // The input contains both root cause leaves + contexts := []*InterruptCtx{ctxB, ctxC} + + // Convert to signal tree + signal := FromInterruptContexts(contexts) + + // Assertions for the signal tree structure (should merge at D) + assert.NotNil(t, signal) + assert.Equal(t, "D", signal.ID) + assert.Len(t, signal.Subs, 2) + // Order of subs is not guaranteed, so we check for presence + subIDs := []string{signal.Subs[0].ID, signal.Subs[1].ID} + assert.Contains(t, subIDs, "B") + assert.Contains(t, subIDs, "C") + + // Convert back to user-facing contexts + finalContexts := ToInterruptContexts(signal, nil) + + // Assertions for the final list of contexts + assert.Len(t, finalContexts, 2) + finalIDs := []string{finalContexts[0].ID, finalContexts[1].ID} + assert.Contains(t, finalIDs, "B") + assert.Contains(t, finalIDs, "C") + + // Check parent linking for one of the branches + var finalB *InterruptCtx + if finalContexts[0].ID == "B" { + finalB = finalContexts[0] + } else { + finalB = finalContexts[1] + } + assert.NotNil(t, finalB.Parent) + assert.Equal(t, "D", finalB.Parent.ID) + assert.Nil(t, finalB.Parent.Parent) + }) + + // Test Case 3: Nil and Empty Inputs + t.Run("NilAndEmpty", func(t *testing.T) { + assert.Nil(t, FromInterruptContexts(nil)) + assert.Nil(t, FromInterruptContexts([]*InterruptCtx{})) + assert.Nil(t, ToInterruptContexts(nil, nil)) + }) +} + +func TestSignalToPersistenceMaps(t *testing.T) { + // Test Case 1: Nil Signal + t.Run("NilSignal", func(t *testing.T) { + id2addr, id2state := SignalToPersistenceMaps(nil) + assert.NotNil(t, id2addr) + assert.NotNil(t, id2state) + assert.Empty(t, id2addr) + assert.Empty(t, id2state) + }) + + // Test Case 2: Single Node Signal + t.Run("SingleNode", func(t *testing.T) { + signal := &InterruptSignal{ + ID: "node1", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + }, + InterruptState: InterruptState{ + State: "test state", + LayerSpecificPayload: "test payload", + }, + } + + id2addr, id2state := SignalToPersistenceMaps(signal) + + assert.Len(t, id2addr, 1) + assert.Len(t, id2state, 1) + + assert.Equal(t, signal.Address, id2addr["node1"]) + assert.Equal(t, signal.InterruptState, id2state["node1"]) + }) + + // Test Case 3: Simple Tree Structure + t.Run("SimpleTree", func(t *testing.T) { + child1 := &InterruptSignal{ + ID: "child1", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + }, + InterruptState: InterruptState{ + State: "child1 state", + }, + } + + child2 := &InterruptSignal{ + ID: "child2", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool2"}, + }, + InterruptState: InterruptState{ + State: "child2 state", + }, + } + + parent := &InterruptSignal{ + ID: "parent", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + }, + InterruptState: InterruptState{ + State: "parent state", + }, + Subs: []*InterruptSignal{child1, child2}, + } + + id2addr, id2state := SignalToPersistenceMaps(parent) + + // Should contain all 3 nodes + assert.Len(t, id2addr, 3) + assert.Len(t, id2state, 3) + + // Check parent node + assert.Equal(t, parent.Address, id2addr["parent"]) + assert.Equal(t, parent.InterruptState, id2state["parent"]) + + // Check child nodes + assert.Equal(t, child1.Address, id2addr["child1"]) + assert.Equal(t, child1.InterruptState, id2state["child1"]) + assert.Equal(t, child2.Address, id2addr["child2"]) + assert.Equal(t, child2.InterruptState, id2state["child2"]) + }) + + // Test Case 4: Deeply Nested Tree + t.Run("DeeplyNestedTree", func(t *testing.T) { + leaf1 := &InterruptSignal{ + ID: "leaf1", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + {Type: AddressSegmentNode, ID: "node1"}, + }, + InterruptState: InterruptState{ + State: "leaf1 state", + }, + } + + leaf2 := &InterruptSignal{ + ID: "leaf2", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + {Type: AddressSegmentNode, ID: "node2"}, + }, + InterruptState: InterruptState{ + State: "leaf2 state", + }, + } + + middle := &InterruptSignal{ + ID: "middle", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + }, + InterruptState: InterruptState{ + State: "middle state", + }, + Subs: []*InterruptSignal{leaf1, leaf2}, + } + + root := &InterruptSignal{ + ID: "root", + Address: Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + }, + InterruptState: InterruptState{ + State: "root state", + }, + Subs: []*InterruptSignal{middle}, + } + + id2addr, id2state := SignalToPersistenceMaps(root) + + // Should contain all 4 nodes + assert.Len(t, id2addr, 4) + assert.Len(t, id2state, 4) + + // Verify all nodes are present + assert.Equal(t, root.Address, id2addr["root"]) + assert.Equal(t, root.InterruptState, id2state["root"]) + assert.Equal(t, middle.Address, id2addr["middle"]) + assert.Equal(t, middle.InterruptState, id2state["middle"]) + assert.Equal(t, leaf1.Address, id2addr["leaf1"]) + assert.Equal(t, leaf1.InterruptState, id2state["leaf1"]) + assert.Equal(t, leaf2.Address, id2addr["leaf2"]) + assert.Equal(t, leaf2.InterruptState, id2state["leaf2"]) + }) + + // Test Case 5: Complex Tree with Multiple Branches + t.Run("ComplexTree", func(t *testing.T) { + // Create a complex tree structure with multiple branches + branch1Leaf1 := &InterruptSignal{ID: "b1l1", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b1l1"}} + branch1Leaf2 := &InterruptSignal{ID: "b1l2", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b1l2"}} + branch1 := &InterruptSignal{ID: "b1", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b1"}, Subs: []*InterruptSignal{branch1Leaf1, branch1Leaf2}} + + branch2Leaf1 := &InterruptSignal{ID: "b2l1", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b2l1"}} + branch2 := &InterruptSignal{ID: "b2", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b2"}, Subs: []*InterruptSignal{branch2Leaf1}} + + root := &InterruptSignal{ID: "root", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "root"}, Subs: []*InterruptSignal{branch1, branch2}} + + id2addr, id2state := SignalToPersistenceMaps(root) + + // Should contain all 6 nodes + assert.Len(t, id2addr, 6) + assert.Len(t, id2state, 6) + + // Verify all nodes are present + expectedNodes := []string{"root", "b1", "b2", "b1l1", "b1l2", "b2l1"} + for _, nodeID := range expectedNodes { + assert.Contains(t, id2addr, nodeID) + assert.Contains(t, id2state, nodeID) + } + }) + + // Test Case 6: Empty InterruptState Values + t.Run("EmptyInterruptState", func(t *testing.T) { + signal := &InterruptSignal{ + ID: "node1", + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + InterruptState: InterruptState{ + // Empty state values + }, + } + + id2addr, id2state := SignalToPersistenceMaps(signal) + + assert.Len(t, id2addr, 1) + assert.Len(t, id2state, 1) + assert.Equal(t, signal.Address, id2addr["node1"]) + assert.Equal(t, signal.InterruptState, id2state["node1"]) + }) +} + +func TestGetCurrentAddress(t *testing.T) { + // Test Case 1: No Address in Context + t.Run("NoAddressInContext", func(t *testing.T) { + ctx := context.Background() + addr := GetCurrentAddress(ctx) + assert.Nil(t, addr) + }) + + // Test Case 2: Address in Context + t.Run("AddressInContext", func(t *testing.T) { + ctx := context.Background() + expectedAddr := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + } + + // Create a context with address using internal addrCtx + runCtx := &addrCtx{ + addr: expectedAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + addr := GetCurrentAddress(ctx) + assert.Equal(t, expectedAddr, addr) + }) +} + +func TestGetNextResumptionPoints(t *testing.T) { + // Test Case 1: No Resume Info in Context + t.Run("NoResumeInfo", func(t *testing.T) { + ctx := context.Background() + _, err := GetNextResumptionPoints(ctx) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get resume info") + }) + + // Test Case 2: Empty Resume Info + t.Run("EmptyResumeInfo", func(t *testing.T) { + ctx := context.Background() + rInfo := &globalResumeInfo{ + id2Addr: make(map[string]Address), + } + ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo) + + points, err := GetNextResumptionPoints(ctx) + assert.NoError(t, err) + assert.Empty(t, points) + }) + + // Test Case 3: Valid Resume Points + t.Run("ValidResumePoints", func(t *testing.T) { + ctx := context.Background() + + // Set up current address + currentAddr := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + } + runCtx := &addrCtx{ + addr: currentAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + // Set up resume info with child addresses + rInfo := &globalResumeInfo{ + id2Addr: map[string]Address{ + "child1": { + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + }, + "child2": { + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool2"}, + }, + "unrelated": { + {Type: AddressSegmentAgent, ID: "agent2"}, + }, + }, + } + ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo) + + points, err := GetNextResumptionPoints(ctx) + assert.NoError(t, err) + assert.Len(t, points, 2) + assert.True(t, points["tool1"]) + assert.True(t, points["tool2"]) + }) + + // Test Case 4: Root Address (Empty Parent) + t.Run("RootAddress", func(t *testing.T) { + ctx := context.Background() + + // Empty current address (root) + runCtx := &addrCtx{ + addr: Address{}, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + // Set up resume info with various addresses + rInfo := &globalResumeInfo{ + id2Addr: map[string]Address{ + "agent1": { + {Type: AddressSegmentAgent, ID: "agent1"}, + }, + "agent2": { + {Type: AddressSegmentAgent, ID: "agent2"}, + }, + }, + } + ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo) + + points, err := GetNextResumptionPoints(ctx) + assert.NoError(t, err) + assert.Len(t, points, 2) + assert.True(t, points["agent1"]) + assert.True(t, points["agent2"]) + }) +} + +func TestBatchResumeWithData(t *testing.T) { + // Test Case 1: New Resume Data + t.Run("NewResumeData", func(t *testing.T) { + ctx := context.Background() + resumeData := map[string]any{ + "id1": "data1", + "id2": "data2", + } + + newCtx := BatchResumeWithData(ctx, resumeData) + + // Verify the data was set correctly + rInfo, ok := newCtx.Value(globalResumeInfoKey{}).(*globalResumeInfo) + assert.True(t, ok) + assert.NotNil(t, rInfo) + assert.Equal(t, "data1", rInfo.id2ResumeData["id1"]) + assert.Equal(t, "data2", rInfo.id2ResumeData["id2"]) + }) + + // Test Case 2: Merge with Existing Resume Data + t.Run("MergeWithExisting", func(t *testing.T) { + ctx := context.Background() + + // First call with initial data + initialData := map[string]any{ + "id1": "initial", + } + ctx = BatchResumeWithData(ctx, initialData) + + // Second call with additional data + additionalData := map[string]any{ + "id2": "additional", + } + newCtx := BatchResumeWithData(ctx, additionalData) + + // Verify both data sets are present + rInfo, ok := newCtx.Value(globalResumeInfoKey{}).(*globalResumeInfo) + assert.True(t, ok) + assert.NotNil(t, rInfo) + assert.Equal(t, "initial", rInfo.id2ResumeData["id1"]) + assert.Equal(t, "additional", rInfo.id2ResumeData["id2"]) + }) + + // Test Case 3: Empty Resume Data + t.Run("EmptyResumeData", func(t *testing.T) { + ctx := context.Background() + newCtx := BatchResumeWithData(ctx, map[string]any{}) + + rInfo, ok := newCtx.Value(globalResumeInfoKey{}).(*globalResumeInfo) + assert.True(t, ok) + assert.NotNil(t, rInfo) + assert.Empty(t, rInfo.id2ResumeData) + }) +} + +func TestGetInterruptState(t *testing.T) { + // Test Case 1: No Interrupt State + t.Run("NoInterruptState", func(t *testing.T) { + ctx := context.Background() + wasInterrupted, hasState, state := GetInterruptState[string](ctx) + assert.False(t, wasInterrupted) + assert.False(t, hasState) + assert.Equal(t, "", state) + }) + + // Test Case 2: With Interrupt State + t.Run("WithInterruptState", func(t *testing.T) { + ctx := context.Background() + + // Create a context with interrupt state + runCtx := &addrCtx{ + interruptState: &InterruptState{ + State: "test state", + }, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + wasInterrupted, hasState, state := GetInterruptState[string](ctx) + assert.True(t, wasInterrupted) + assert.True(t, hasState) + assert.Equal(t, "test state", state) + }) + + // Test Case 3: Wrong Type for Interrupt State + t.Run("WrongType", func(t *testing.T) { + ctx := context.Background() + + // Create a context with interrupt state of wrong type + runCtx := &addrCtx{ + interruptState: &InterruptState{ + State: 123, // int instead of string + }, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + wasInterrupted, hasState, state := GetInterruptState[string](ctx) + assert.True(t, wasInterrupted) + assert.False(t, hasState) // Should be false due to type mismatch + assert.Equal(t, "", state) + }) + + // Test Case 4: Nil Interrupt State + t.Run("NilInterruptState", func(t *testing.T) { + ctx := context.Background() + + // Create a context with nil interrupt state + runCtx := &addrCtx{ + interruptState: nil, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + wasInterrupted, hasState, state := GetInterruptState[string](ctx) + assert.False(t, wasInterrupted) // Should be false because interruptState is nil + assert.False(t, hasState) // Should be false because state is nil + assert.Equal(t, "", state) + }) +} + +func TestGetResumeContext(t *testing.T) { + // Test Case 1: Not Resume Target + t.Run("NotResumeTarget", func(t *testing.T) { + ctx := context.Background() + isResumeTarget, hasData, data := GetResumeContext[string](ctx) + assert.False(t, isResumeTarget) + assert.False(t, hasData) + assert.Equal(t, "", data) + }) + + // Test Case 2: Resume Target with Data + t.Run("ResumeTargetWithData", func(t *testing.T) { + ctx := context.Background() + + // Create a context as resume target with data + runCtx := &addrCtx{ + isResumeTarget: true, + resumeData: "resume data", + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + isResumeTarget, hasData, data := GetResumeContext[string](ctx) + assert.True(t, isResumeTarget) + assert.True(t, hasData) + assert.Equal(t, "resume data", data) + }) + + // Test Case 3: Resume Target without Data + t.Run("ResumeTargetWithoutData", func(t *testing.T) { + ctx := context.Background() + + // Create a context as resume target without data + runCtx := &addrCtx{ + isResumeTarget: true, + resumeData: nil, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + isResumeTarget, hasData, data := GetResumeContext[string](ctx) + assert.True(t, isResumeTarget) + assert.False(t, hasData) + assert.Equal(t, "", data) + }) + + // Test Case 4: Wrong Type for Resume Data + t.Run("WrongType", func(t *testing.T) { + ctx := context.Background() + + // Create a context with resume data of wrong type + runCtx := &addrCtx{ + isResumeTarget: true, + resumeData: 123, // int instead of string + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + isResumeTarget, hasData, data := GetResumeContext[string](ctx) + assert.True(t, isResumeTarget) + assert.False(t, hasData) // Should be false due to type mismatch + assert.Equal(t, "", data) + }) +} + +func TestWithLayerPayload(t *testing.T) { + // Test Case 1: Basic Usage + t.Run("BasicUsage", func(t *testing.T) { + config := &InterruptConfig{} + opt := WithLayerPayload("test payload") + opt(config) + assert.Equal(t, "test payload", config.LayerPayload) + }) + + // Test Case 2: Nil Payload + t.Run("NilPayload", func(t *testing.T) { + config := &InterruptConfig{LayerPayload: "existing"} + opt := WithLayerPayload(nil) + opt(config) + assert.Nil(t, config.LayerPayload) + }) + + // Test Case 3: Complex Payload + t.Run("ComplexPayload", func(t *testing.T) { + config := &InterruptConfig{} + payload := map[string]any{ + "key1": "value1", + "key2": 123, + } + opt := WithLayerPayload(payload) + opt(config) + assert.Equal(t, payload, config.LayerPayload) + }) +} + +func TestInterruptFunction(t *testing.T) { + // Test Case 1: Simple Interrupt without SubContexts + t.Run("SimpleInterrupt", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a mock address + expectedAddr := Address{{Type: AddressSegmentAgent, ID: "test-agent"}} + runCtx := &addrCtx{ + addr: expectedAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + info := "test info" + state := "test state" + + signal, err := Interrupt(ctx, info, state, nil) + assert.NoError(t, err) + assert.NotNil(t, signal) + assert.NotEmpty(t, signal.ID) + assert.Equal(t, info, signal.Info) + assert.Equal(t, state, signal.State) + assert.True(t, signal.IsRootCause) + assert.Equal(t, expectedAddr, signal.Address) + }) + + // Test Case 2: Interrupt with SubContexts + t.Run("InterruptWithSubContexts", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a mock address + expectedAddr := Address{{Type: AddressSegmentAgent, ID: "parent-agent"}} + runCtx := &addrCtx{ + addr: expectedAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + // Create sub contexts + subContexts := []*InterruptSignal{ + { + ID: "child1", + Address: Address{{Type: AddressSegmentAgent, ID: "child1"}}, + }, + { + ID: "child2", + Address: Address{{Type: AddressSegmentAgent, ID: "child2"}}, + }, + } + + info := "parent info" + state := "parent state" + + signal, err := Interrupt(ctx, info, state, subContexts) + assert.NoError(t, err) + assert.NotNil(t, signal) + assert.NotEmpty(t, signal.ID) + assert.Equal(t, info, signal.Info) + assert.Equal(t, state, signal.State) + assert.False(t, signal.IsRootCause) // Should be false when there are sub contexts + assert.Len(t, signal.Subs, 2) + assert.Equal(t, "child1", signal.Subs[0].ID) + assert.Equal(t, "child2", signal.Subs[1].ID) + }) + + // Test Case 3: Interrupt with Options + t.Run("InterruptWithOptions", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a mock address + expectedAddr := Address{{Type: AddressSegmentAgent, ID: "test-agent"}} + runCtx := &addrCtx{ + addr: expectedAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + info := "test info" + state := "test state" + layerPayload := "layer payload" + + signal, err := Interrupt(ctx, info, state, nil, WithLayerPayload(layerPayload)) + assert.NoError(t, err) + assert.NotNil(t, signal) + assert.Equal(t, layerPayload, signal.LayerSpecificPayload) + }) + + // Test Case 4: Empty SubContexts + t.Run("EmptySubContexts", func(t *testing.T) { + ctx := context.Background() + + // Create a context with a mock address + expectedAddr := Address{{Type: AddressSegmentAgent, ID: "test-agent"}} + runCtx := &addrCtx{ + addr: expectedAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + info := "test info" + state := "test state" + + signal, err := Interrupt(ctx, info, state, []*InterruptSignal{}) + assert.NoError(t, err) + assert.NotNil(t, signal) + assert.True(t, signal.IsRootCause) // Should be true when sub contexts is empty + assert.Empty(t, signal.Subs) + }) +} + +func TestAddressMethods(t *testing.T) { + // Test Case 1: Address.String() + t.Run("AddressString", func(t *testing.T) { + addr := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + {Type: AddressSegmentNode, ID: "node1", SubID: "sub1"}, + } + + result := addr.String() + expected := "agent:agent1;tool:tool1;node:node1:sub1" + assert.Equal(t, expected, result) + }) + + // Test Case 2: Address.String() with empty address + t.Run("EmptyAddressString", func(t *testing.T) { + var addr Address + result := addr.String() + assert.Equal(t, "", result) + }) + + // Test Case 3: Address.Equals() with equal addresses + t.Run("AddressEquals", func(t *testing.T) { + addr1 := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + } + addr2 := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + } + + assert.True(t, addr1.Equals(addr2)) + }) + + // Test Case 4: Address.Equals() with different addresses + t.Run("AddressNotEquals", func(t *testing.T) { + addr1 := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + } + addr2 := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool2"}, + } + + assert.False(t, addr1.Equals(addr2)) + }) + + // Test Case 5: Address.Equals() with different lengths + t.Run("AddressDifferentLengths", func(t *testing.T) { + addr1 := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + {Type: AddressSegmentTool, ID: "tool1"}, + } + addr2 := Address{ + {Type: AddressSegmentAgent, ID: "agent1"}, + } + + assert.False(t, addr1.Equals(addr2)) + }) + + // Test Case 6: Address.Equals() with SubID differences + t.Run("AddressSubIDDifference", func(t *testing.T) { + addr1 := Address{ + {Type: AddressSegmentAgent, ID: "agent1", SubID: "sub1"}, + } + addr2 := Address{ + {Type: AddressSegmentAgent, ID: "agent1", SubID: "sub2"}, + } + + assert.False(t, addr1.Equals(addr2)) + }) +} + +func TestAppendAddressSegment(t *testing.T) { + // Test Case 1: Append to empty address + t.Run("AppendToEmpty", func(t *testing.T) { + ctx := context.Background() + + newCtx := AppendAddressSegment(ctx, AddressSegmentAgent, "agent1", "") + + addr := GetCurrentAddress(newCtx) + assert.Len(t, addr, 1) + assert.Equal(t, AddressSegmentAgent, addr[0].Type) + assert.Equal(t, "agent1", addr[0].ID) + assert.Equal(t, "", addr[0].SubID) + }) + + // Test Case 2: Append to existing address + t.Run("AppendToExisting", func(t *testing.T) { + ctx := context.Background() + + // First append + ctx = AppendAddressSegment(ctx, AddressSegmentAgent, "agent1", "") + + // Second append + newCtx := AppendAddressSegment(ctx, AddressSegmentTool, "tool1", "call1") + + addr := GetCurrentAddress(newCtx) + assert.Len(t, addr, 2) + assert.Equal(t, AddressSegmentAgent, addr[0].Type) + assert.Equal(t, "agent1", addr[0].ID) + assert.Equal(t, AddressSegmentTool, addr[1].Type) + assert.Equal(t, "tool1", addr[1].ID) + assert.Equal(t, "call1", addr[1].SubID) + }) + + // Test Case 3: Append with SubID + t.Run("AppendWithSubID", func(t *testing.T) { + ctx := context.Background() + + newCtx := AppendAddressSegment(ctx, AddressSegmentTool, "tool1", "call123") + + addr := GetCurrentAddress(newCtx) + assert.Len(t, addr, 1) + assert.Equal(t, AddressSegmentTool, addr[0].Type) + assert.Equal(t, "tool1", addr[0].ID) + assert.Equal(t, "call123", addr[0].SubID) + }) +} + +func TestPopulateInterruptState(t *testing.T) { + // Test Case 1: Populate with matching address + t.Run("PopulateMatchingAddress", func(t *testing.T) { + ctx := context.Background() + + // Set up current address + currentAddr := Address{{Type: AddressSegmentAgent, ID: "agent1"}} + runCtx := &addrCtx{ + addr: currentAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + // Set up interrupt state data + id2Addr := map[string]Address{ + "interrupt1": currentAddr, + } + id2State := map[string]InterruptState{ + "interrupt1": {State: "test state"}, + } + + newCtx := PopulateInterruptState(ctx, id2Addr, id2State) + + // Verify the state was populated + wasInterrupted, hasState, state := GetInterruptState[string](newCtx) + assert.True(t, wasInterrupted) + assert.True(t, hasState) + assert.Equal(t, "test state", state) + }) + + // Test Case 2: Populate with non-matching address + t.Run("PopulateNonMatchingAddress", func(t *testing.T) { + ctx := context.Background() + + // Set up current address + currentAddr := Address{{Type: AddressSegmentAgent, ID: "agent1"}} + runCtx := &addrCtx{ + addr: currentAddr, + } + ctx = context.WithValue(ctx, addrCtxKey{}, runCtx) + + // Set up interrupt state data with different address + id2Addr := map[string]Address{ + "interrupt1": {{Type: AddressSegmentAgent, ID: "agent2"}}, + } + id2State := map[string]InterruptState{ + "interrupt1": {State: "test state"}, + } + + newCtx := PopulateInterruptState(ctx, id2Addr, id2State) + + // Verify the state was NOT populated (no matching address) + wasInterrupted, hasState, state := GetInterruptState[string](newCtx) + assert.False(t, wasInterrupted) + assert.False(t, hasState) + assert.Equal(t, "", state) + }) + + // Test Case 3: Populate with empty data + t.Run("PopulateEmptyData", func(t *testing.T) { + ctx := context.Background() + + newCtx := PopulateInterruptState(ctx, map[string]Address{}, map[string]InterruptState{}) + + // Verify no state was populated + wasInterrupted, hasState, state := GetInterruptState[string](newCtx) + assert.False(t, wasInterrupted) + assert.False(t, hasState) + assert.Equal(t, "", state) + }) +} + +func TestStringMethods(t *testing.T) { + // Test Case 1: InterruptSignal.Error() + t.Run("InterruptSignalError", func(t *testing.T) { + signal := &InterruptSignal{ + ID: "test-id", + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + InterruptInfo: InterruptInfo{ + Info: "test info", + }, + InterruptState: InterruptState{ + State: "test state", + LayerSpecificPayload: "test payload", + }, + Subs: []*InterruptSignal{ + {ID: "sub1"}, + }, + } + + errorStr := signal.Error() + expectedContains := []string{ + "interrupt signal:", + "ID=test-id", + "Addr=agent:agent1", + "Info=interrupt info: Info=test info, IsRootCause=false", + "State=interrupt state: State=test state, LayerSpecificPayload=test payload", + "SubsLen=1", + } + + for _, expected := range expectedContains { + assert.Contains(t, errorStr, expected) + } + }) + + // Test Case 2: InterruptState.String() + t.Run("InterruptStateString", func(t *testing.T) { + state := &InterruptState{ + State: "test state", + LayerSpecificPayload: "test payload", + } + + result := state.String() + expected := "interrupt state: State=test state, LayerSpecificPayload=test payload" + assert.Equal(t, expected, result) + }) + + // Test Case 3: InterruptState.String() with nil + t.Run("InterruptStateStringNil", func(t *testing.T) { + var state *InterruptState + result := state.String() + assert.Equal(t, "", result) + }) + + // Test Case 4: InterruptInfo.String() + t.Run("InterruptInfoString", func(t *testing.T) { + info := &InterruptInfo{ + Info: "test info", + IsRootCause: true, + } + + result := info.String() + expected := "interrupt info: Info=test info, IsRootCause=true" + assert.Equal(t, expected, result) + }) + + // Test Case 5: InterruptInfo.String() with nil + t.Run("InterruptInfoStringNil", func(t *testing.T) { + var info *InterruptInfo + result := info.String() + assert.Equal(t, "", result) + }) +} + +func TestInterruptCtxEqualsWithoutID(t *testing.T) { + // Test Case 1: Equal contexts + t.Run("EqualContexts", func(t *testing.T) { + ctx1 := &InterruptCtx{ + ID: "id1", + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Info: "info1", + IsRootCause: true, + } + ctx2 := &InterruptCtx{ + ID: "id2", // Different ID should be ignored + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Info: "info1", + IsRootCause: true, + } + + assert.True(t, ctx1.EqualsWithoutID(ctx2)) + }) + + // Test Case 2: Different addresses + t.Run("DifferentAddresses", func(t *testing.T) { + ctx1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + } + ctx2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent2"}}, + } + + assert.False(t, ctx1.EqualsWithoutID(ctx2)) + }) + + // Test Case 3: Different root cause flags + t.Run("DifferentRootCause", func(t *testing.T) { + ctx1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + IsRootCause: true, + } + ctx2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + IsRootCause: false, + } + + assert.False(t, ctx1.EqualsWithoutID(ctx2)) + }) + + // Test Case 4: Different info + t.Run("DifferentInfo", func(t *testing.T) { + ctx1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Info: "info1", + } + ctx2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Info: "info2", + } + + assert.False(t, ctx1.EqualsWithoutID(ctx2)) + }) + + // Test Case 5: Nil contexts + t.Run("NilContexts", func(t *testing.T) { + var ctx1 *InterruptCtx + var ctx2 *InterruptCtx + + assert.True(t, ctx1.EqualsWithoutID(ctx2)) + + ctx3 := &InterruptCtx{} + assert.False(t, ctx1.EqualsWithoutID(ctx3)) + assert.False(t, ctx3.EqualsWithoutID(ctx1)) + }) + + // Test Case 6: With parent contexts + t.Run("WithParentContexts", func(t *testing.T) { + parent1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "parent"}}, + } + parent2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "parent"}}, + } + + ctx1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Parent: parent1, + } + ctx2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Parent: parent2, + } + + assert.True(t, ctx1.EqualsWithoutID(ctx2)) + }) + + // Test Case 7: Different parent contexts + t.Run("DifferentParentContexts", func(t *testing.T) { + parent1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "parent1"}}, + } + parent2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "parent2"}}, + } + + ctx1 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Parent: parent1, + } + ctx2 := &InterruptCtx{ + Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}}, + Parent: parent2, + } + + assert.False(t, ctx1.EqualsWithoutID(ctx2)) + }) +} diff --git a/internal/core/resume.go b/internal/core/resume.go new file mode 100644 index 0000000..c638bf2 --- /dev/null +++ b/internal/core/resume.go @@ -0,0 +1,109 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package core + +import "context" + +// GetInterruptState provides a type-safe way to check for and retrieve the persisted state from a previous interruption. +// It is the primary function a component should use to understand its past state. +// +// It returns three values: +// - wasInterrupted (bool): True if the node was part of a previous interruption, regardless of whether state was provided. +// - state (T): The typed state object, if it was provided and matches type `T`. +// - hasState (bool): True if state was provided during the original interrupt and successfully cast to type `T`. +func GetInterruptState[T any](ctx context.Context) (wasInterrupted bool, hasState bool, state T) { + rCtx, ok := getRunCtx(ctx) + if !ok || rCtx.interruptState == nil { + return + } + + wasInterrupted = true + if rCtx.interruptState.State == nil { + return + } + + state, hasState = rCtx.interruptState.State.(T) + return +} + +// GetResumeContext checks if the current component is the target of a resume operation +// and retrieves any data provided by the user for that resumption. +// +// This function is typically called *after* a component has already determined it is in a +// resumed state by calling GetInterruptState. +// +// It returns three values: +// - isResumeTarget: A boolean that is true if the current component's address OR any of its +// descendant addresses was explicitly targeted by a call to Resume() or ResumeWithData(). +// This allows composite components (like tools containing nested graphs) to know they should +// execute their children to reach the actual resume target. +// - hasData: A boolean that is true if data was provided for this specific component (i.e., not nil). +// - data: The typed data provided by the user. +// +// ### How to Use This Function: A Decision Framework +// +// The correct usage pattern depends on the application's desired resume strategy. +// +// #### Strategy 1: Implicit "Resume All" +// In some use cases, any resume operation implies that *all* interrupted points should proceed. +// For example, if an application's UI only provides a single "Continue" button for a set of +// interruptions. In this model, a component can often just use `GetInterruptState` to see if +// `wasInterrupted` is true and then proceed with its logic, as it can assume it is an intended target. +// It may still call `GetResumeContext` to check for optional data, but the `isResumeFlow` flag is less critical. +// +// #### Strategy 2: Explicit "Targeted Resume" (Most Common) +// For applications with multiple, distinct interrupt points that must be resumed independently, it is +// crucial to differentiate which point is being resumed. This is the primary use case for the `isResumeTarget` flag. +// - If `isResumeTarget` is `true`: Your component (or one of its descendants) is the target. +// If `hasData` is true, you are the direct target and should consume the data. +// If `hasData` is false, a descendant is the target—execute your children to reach it. +// - If `isResumeTarget` is `false`: Neither you nor your descendants are the target. You MUST +// re-interrupt (e.g., by returning `StatefulInterrupt(...)`) to preserve your state. +// +// ### Guidance for Composite Components +// +// Composite components (like `Graph` or other `Runnable`s that contain sub-processes) have a dual role: +// 1. Check for Self-Targeting: A composite component can itself be the target of a resume +// operation, for instance, to modify its internal state. It may call `GetResumeContext` +// to check for data targeted at its own address. +// 2. Act as a Conduit: After checking for itself, its primary role is to re-execute its children, +// allowing the resume context to flow down to them. It must not consume a resume signal +// intended for one of its descendants. +func GetResumeContext[T any](ctx context.Context) (isResumeTarget bool, hasData bool, data T) { + rCtx, ok := getRunCtx(ctx) + if !ok { + return + } + + isResumeTarget = rCtx.isResumeTarget + if !isResumeTarget { + return + } + + // It is a resume flow, now check for data + if rCtx.resumeData == nil { + return // hasData is false + } + + data, hasData = rCtx.resumeData.(T) + return +} + +func getRunCtx(ctx context.Context) (*addrCtx, bool) { + rCtx, ok := ctx.Value(addrCtxKey{}).(*addrCtx) + return rCtx, ok +} diff --git a/internal/generic/generic.go b/internal/generic/generic.go new file mode 100644 index 0000000..2f3293c --- /dev/null +++ b/internal/generic/generic.go @@ -0,0 +1,90 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generic + +import ( + "reflect" +) + +// NewInstance create an instance of the given type T. +// the main purpose of this function is to create an instance of a type, can handle the type of T is a pointer or not. +// eg. NewInstance[int] returns 0. +// eg. NewInstance[*int] returns *0 (will be ptr of 0, not nil!). +func NewInstance[T any]() T { + typ := TypeOf[T]() + + switch typ.Kind() { + case reflect.Map: + return reflect.MakeMap(typ).Interface().(T) + case reflect.Slice, reflect.Array: + return reflect.MakeSlice(typ, 0, 0).Interface().(T) + case reflect.Ptr: + typ = typ.Elem() + origin := reflect.New(typ) + inst := origin + + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + inst = inst.Elem() + inst.Set(reflect.New(typ)) + } + + return origin.Interface().(T) + default: + var t T + return t + } +} + +// TypeOf returns the type of T. +// eg. TypeOf[int] returns reflect.TypeOf(int). +// eg. TypeOf[*int] returns reflect.TypeOf(*int). +func TypeOf[T any]() reflect.Type { + return reflect.TypeOf((*T)(nil)).Elem() +} + +// PtrOf returns a pointer of T. +// useful when you want to get a pointer of a value, in some config, for example. +// eg. PtrOf[int] returns *int. +// eg. PtrOf[*int] returns **int. +func PtrOf[T any](v T) *T { + return &v +} + +type Pair[F, S any] struct { + First F + Second S +} + +// Reverse returns a new slice with elements in reversed order. +func Reverse[S ~[]E, E any](s S) S { + d := make(S, len(s)) + for i := 0; i < len(s); i++ { + d[i] = s[len(s)-i-1] + } + + return d +} + +// CopyMap copies a map to a new map. +func CopyMap[K comparable, V any](src map[K]V) map[K]V { + dst := make(map[K]V, len(src)) + for k, v := range src { + dst[k] = v + } + return dst +} diff --git a/internal/generic/generic_test.go b/internal/generic/generic_test.go new file mode 100644 index 0000000..a44ee53 --- /dev/null +++ b/internal/generic/generic_test.go @@ -0,0 +1,104 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewInstance(t *testing.T) { + t.Run("struct", func(t *testing.T) { + type Test struct{} + + inst := NewInstance[Test]() + + assert.IsType(t, Test{}, inst) + }) + + t.Run("pointer", func(t *testing.T) { + type Test struct{} + + inst := NewInstance[*Test]() + + assert.IsType(t, &Test{}, inst) + }) + + t.Run("interface", func(t *testing.T) { + type Test interface{} + + inst := NewInstance[Test]() + assert.IsType(t, Test(nil), inst) + }) + + t.Run("pointer of pointer of pointer", func(t *testing.T) { + type Test struct { + Value int + } + + inst := NewInstance[***Test]() + + ptr := &Test{} + ptrOfPtr := &ptr + assert.NotNil(t, inst) + assert.NotNil(t, *inst) + assert.IsType(t, ptrOfPtr, *inst) + assert.NotNil(t, **inst) + assert.Equal(t, Test{Value: 0}, ***inst) + }) + + t.Run("primitive_map", func(t *testing.T) { + inst := NewInstance[map[string]any]() + assert.NotNil(t, inst) + inst["a"] = 1 + assert.Equal(t, map[string]any{"a": 1}, inst) + }) + + t.Run("primitive_slice", func(t *testing.T) { + inst := NewInstance[[]int]() + assert.NotNil(t, inst) + inst = append(inst, 1) + assert.Equal(t, []int{1}, inst) + }) + + t.Run("primitive_string", func(t *testing.T) { + inst := NewInstance[string]() + assert.Equal(t, "", inst) + }) + + t.Run("primitive_int64", func(t *testing.T) { + inst := NewInstance[int64]() + assert.Equal(t, int64(0), inst) + }) +} + +func TestReverse(t *testing.T) { + t.Run("reverse int slice", func(t *testing.T) { + input := []int{1, 2, 3, 4, 5} + expected := []int{5, 4, 3, 2, 1} + result := Reverse(input) + assert.Equal(t, expected, result) + }) + + t.Run("reverse string slice", func(t *testing.T) { + input := []string{"a", "b", "c"} + expected := []string{"c", "b", "a"} + result := Reverse(input) + assert.Equal(t, expected, result) + }) +} diff --git a/internal/generic/type_name.go b/internal/generic/type_name.go new file mode 100644 index 0000000..4829e74 --- /dev/null +++ b/internal/generic/type_name.go @@ -0,0 +1,71 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generic + +import ( + "reflect" + "regexp" + "runtime" + "strings" +) + +var ( + regOfAnonymousFunc = regexp.MustCompile(`^func[0-9]+`) + regOfNumber = regexp.MustCompile(`^\d+$`) +) + +// ParseTypeName returns the name of the type of the given value. +// It takes a reflect.Value as input and processes it to determine the underlying type. If the type is a pointer, it dereferences it to get the actual type. (the optimization of this function) +// eg: ParseTypeName(reflect.ValueOf(&&myStruct{})) returns "myStruct" (not "**myStruct") +// +// If the type is a function, it retrieves the function's name, handling both named and anonymous functions. +// examples of function paths: [package_path].[receiver_type].[func_name] +// named function: xxx/utils.ParseTypeName +// method: xxx/utils.(*MyStruct).Method +// anonymous function: xxx/utils.TestParseTypeName.func6.1 +func ParseTypeName(val reflect.Value) string { + typ := val.Type() + + for typ.Kind() == reflect.Pointer { + typ = typ.Elem() + } + + if typ.Kind() == reflect.Func { + funcName := runtime.FuncForPC(val.Pointer()).Name() + idx := strings.LastIndex(funcName, ".") + if idx < 0 { + if funcName != "" { + return funcName + } + return "" + } + + name := funcName[idx+1:] + + if regOfAnonymousFunc.MatchString(name) { + return "" + } + + if regOfNumber.MatchString(name) { + return "" + } + + return name + } + + return typ.Name() +} diff --git a/internal/generic/type_name_test.go b/internal/generic/type_name_test.go new file mode 100644 index 0000000..4230174 --- /dev/null +++ b/internal/generic/type_name_test.go @@ -0,0 +1,86 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generic + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTypeName(t *testing.T) { + t.Run("named_struct", func(t *testing.T) { + type OpenAI struct{} + model := &OpenAI{} + name := ParseTypeName(reflect.Indirect(reflect.ValueOf(model))) + assert.Equal(t, "OpenAI", name) + }) + + t.Run("anonymous_struct", func(t *testing.T) { + model := &struct{}{} + name := ParseTypeName(reflect.ValueOf(model)) + assert.Equal(t, "", name) + }) + + t.Run("anonymous_struct_from_func", func(t *testing.T) { + model := genStruct() + name := ParseTypeName(reflect.ValueOf(model)) + assert.Equal(t, "", name) + }) + + t.Run("named_interface", func(t *testing.T) { + type OpenAI interface{} + model := OpenAI(&struct{}{}) + name := ParseTypeName(reflect.ValueOf(model)) + assert.Equal(t, "", name) + + name = ParseTypeName(reflect.ValueOf((*OpenAI)(nil))) + assert.Equal(t, "OpenAI", name) + }) + + t.Run("named_function", func(t *testing.T) { + f := genOpenAI + name := ParseTypeName(reflect.ValueOf(f)) + assert.Equal(t, "genOpenAI", name) + }) + + t.Run("anonymous_function", func(t *testing.T) { + f := genAnonymousFunc() + name := ParseTypeName(reflect.ValueOf(f)) + assert.Equal(t, "", name) + + ff := func(n string) { + _ = n + } + + name = ParseTypeName(reflect.ValueOf(ff)) + assert.Equal(t, "", name) + }) +} + +func genStruct() *struct{} { + return &struct{}{} +} + +func genOpenAI() {} + +func genAnonymousFunc() func(n string) { + return func(n string) { + _ = n + } +} diff --git a/internal/gmap/gmap.go b/internal/gmap/gmap.go new file mode 100644 index 0000000..7eae8b5 --- /dev/null +++ b/internal/gmap/gmap.go @@ -0,0 +1,121 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gmap + +// Concat returns the unions of maps as a new map. +// +// 💡 NOTE: +// +// - Once the key conflicts, the newer value always replace the older one ([DiscardOld]), +// - If the result is an empty set, always return an empty map instead of nil +// +// 🚀 EXAMPLE: +// +// m := map[int]int{1: 1, 2: 2} +// Concat(m, nil) ⏩ map[int]int{1: 1, 2: 2} +// Concat(m, map[int]{3: 3}) ⏩ map[int]int{1: 1, 2: 2, 3: 3} +// Concat(m, map[int]{2: -1}) ⏩ map[int]int{1: 1, 2: -1} // "2:2" is replaced by the newer "2:-1" +// +// 💡 AKA: Merge, Union, Combine +func Concat[K comparable, V any](ms ...map[K]V) map[K]V { + // FastPath: no map or only one map given. + if len(ms) == 0 { + return make(map[K]V) + } + if len(ms) == 1 { + return cloneWithoutNilCheck(ms[0]) + } + + var maxLen int + for _, m := range ms { + if len(m) > maxLen { + maxLen = len(m) + } + } + ret := make(map[K]V, maxLen) + // FastPath: all maps are empty. + if maxLen == 0 { + return ret + } + + // Concat all maps. + for _, m := range ms { + for k, v := range m { + ret[k] = v + } + } + return ret +} + +// Map applies function f to each key and value of map m. +// Results of f are returned as a new map. +// +// 🚀 EXAMPLE: +// +// f := func(k, v int) (string, string) { return strconv.Itoa(k), strconv.Itoa(v) } +// Map(map[int]int{1: 1}, f) ⏩ map[string]string{"1": "1"} +// Map(map[int]int{}, f) ⏩ map[string]string{} +func Map[K1, K2 comparable, V1, V2 any](m map[K1]V1, f func(K1, V1) (K2, V2)) map[K2]V2 { + r := make(map[K2]V2, len(m)) + for k, v := range m { + k2, v2 := f(k, v) + r[k2] = v2 + } + return r +} + +// Values returns the values of the map m. +// +// 🚀 EXAMPLE: +// +// m := map[int]string{1: "1", 2: "2", 3: "3", 4: "4"} +// Values(m) ⏩ []string{"1", "4", "2", "3"} //⚠️INDETERMINATE ORDER⚠️ +// +// ⚠️ WARNING: The keys values be in an indeterminate order, +func Values[K comparable, V any](m map[K]V) []V { + r := make([]V, 0, len(m)) + for _, v := range m { + r = append(r, v) + } + return r +} + +// Clone returns a shallow copy of map. +// If the given map is nil, nil is returned. +// +// 🚀 EXAMPLE: +// +// Clone(map[int]int{1: 1, 2: 2}) ⏩ map[int]int{1: 1, 2: 2} +// Clone(map[int]int{}) ⏩ map[int]int{} +// Clone[int, int](nil) ⏩ nil +// +// 💡 HINT: Both keys and values are copied using assignment (=), so this is a shallow clone. +// 💡 AKA: Copy +func Clone[K comparable, V any, M ~map[K]V](m M) M { + if m == nil { + return nil + } + return cloneWithoutNilCheck(m) +} + +func cloneWithoutNilCheck[K comparable, V any, M ~map[K]V](m M) M { + r := make(M, len(m)) + for k, v := range m { + r[k] = v + } + return r +} diff --git a/internal/gmap/gmap_test.go b/internal/gmap/gmap_test.go new file mode 100644 index 0000000..1c0f1ef --- /dev/null +++ b/internal/gmap/gmap_test.go @@ -0,0 +1,89 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gmap + +import ( + "fmt" + "sort" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMerge(t *testing.T) { + assert.Equal(t, map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, + Concat(map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, nil)) + assert.Equal(t, map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, + Concat[int, int](nil, map[int]int{1: 1, 2: 2, 3: 3, 4: 4})) + assert.Equal(t, map[int]int{}, Concat[int, int](nil, nil)) + assert.Equal(t, map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, + Concat(map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, map[int]int{1: 1, 2: 2, 3: 3, 4: 4})) + assert.Equal(t, map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, + Concat(map[int]int{1: 0, 2: 0}, map[int]int{1: 1, 2: 2, 3: 3, 4: 4})) + assert.Equal(t, map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, + Concat(map[int]int{1: 1, 2: 1}, map[int]int{2: 2, 3: 3, 4: 4})) +} + +func TestMap(t *testing.T) { + assert.Equal(t, + map[string]string{"1": "1", "2": "2"}, + Map(map[int]int{1: 1, 2: 2}, func(k, v int) (string, string) { + return strconv.Itoa(k), strconv.Itoa(v) + })) + assert.Equal(t, + map[string]string{}, + Map(map[int]int{}, func(k, v int) (string, string) { + return strconv.Itoa(k), strconv.Itoa(v) + })) +} + +func TestValues(t *testing.T) { + { + keys := Values(map[int]string{1: "1", 2: "2", 3: "3", 4: "4"}) + sort.Strings(keys) + assert.Equal(t, []string{"1", "2", "3", "4"}, keys) + } + assert.Equal(t, []string{}, Values(map[int]string{})) + assert.Equal(t, []string{}, Values[int, string](nil)) +} + +func TestClone(t *testing.T) { + assert.Equal(t, map[int]int{1: 1, 2: 2}, Clone(map[int]int{1: 1, 2: 2})) + var nilMap map[int]int + assert.Equal(t, map[int]int{}, Clone(map[int]int{})) + assert.NotEqual(t, (map[int]int)(nil), Clone(map[int]int{})) + assert.Equal(t, (map[int]int)(nil), Clone(nilMap)) + assert.NotEqual(t, map[int]int{}, Clone(nilMap)) + + // Test new type. + type I2I map[int]int + assert.Equal(t, I2I{1: 1, 2: 2}, Clone(I2I{1: 1, 2: 2})) + assert.Equal(t, "gmap.I2I", fmt.Sprintf("%T", Clone(I2I{}))) + + // Test shallow clone. + src := map[int]*int{1: ptr(1), 2: ptr(2)} + dst := Clone(src) + assert.Equal(t, src, dst) + assert.True(t, src[1] == dst[1]) + assert.True(t, src[2] == dst[2]) +} + +// Ptr returns a pointer to the given value. +func ptr[T any](v T) *T { + return &v +} diff --git a/internal/gslice/gslice.go b/internal/gslice/gslice.go new file mode 100644 index 0000000..a9c05fe --- /dev/null +++ b/internal/gslice/gslice.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gslice + +// ToMap collects elements of slice to map, both map keys and values are produced +// by mapping function f. +// +// 🚀 EXAMPLE: +// +// type Foo struct { +// ID int +// Name string +// } +// mapper := func(f Foo) (int, string) { return f.ID, f.Name } +// ToMap([]Foo{}, mapper) ⏩ map[int]string{} +// s := []Foo{{1, "one"}, {2, "two"}, {3, "three"}} +// ToMap(s, mapper) ⏩ map[int]string{1: "one", 2: "two", 3: "three"} +func ToMap[T, V any, K comparable](s []T, f func(T) (K, V)) map[K]V { + m := make(map[K]V, len(s)) + for _, e := range s { + k, v := f(e) + m[k] = v + } + return m +} diff --git a/internal/gslice/gslice_test.go b/internal/gslice/gslice_test.go new file mode 100644 index 0000000..b55c854 --- /dev/null +++ b/internal/gslice/gslice_test.go @@ -0,0 +1,36 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gslice + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToMap(t *testing.T) { + type Foo struct { + ID int + Name string + } + mapper := func(f Foo) (int, string) { return f.ID, f.Name } + assert.Equal(t, map[int]string{}, ToMap([]Foo{}, mapper)) + assert.Equal(t, map[int]string{}, ToMap(nil, mapper)) + assert.Equal(t, + map[int]string{1: "one", 2: "two", 3: "three"}, + ToMap([]Foo{{1, "one"}, {2, "two"}, {3, "three"}}, mapper)) +} diff --git a/internal/merge.go b/internal/merge.go new file mode 100644 index 0000000..6d0f94d --- /dev/null +++ b/internal/merge.go @@ -0,0 +1,81 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package internal + +import ( + "fmt" + "reflect" + + "github.com/cloudwego/eino/internal/generic" +) + +var mergeFuncs = map[reflect.Type]any{} + +func RegisterValuesMergeFunc[T any](fn func([]T) (T, error)) { + mergeFuncs[generic.TypeOf[T]()] = fn +} + +func GetMergeFunc(typ reflect.Type) func([]any) (any, error) { + if fn, ok := mergeFuncs[typ]; ok { + return func(vs []any) (any, error) { + rvs := reflect.MakeSlice(reflect.SliceOf(typ), 0, len(vs)) + for _, v := range vs { + if t := reflect.TypeOf(v); t != typ { + return nil, fmt.Errorf( + "(values merge) field type mismatch. expected: '%v', got: '%v'", typ, t) + } + rvs = reflect.Append(rvs, reflect.ValueOf(v)) + } + + rets := reflect.ValueOf(fn).Call([]reflect.Value{rvs}) + var err error + if !rets[1].IsNil() { + err = rets[1].Interface().(error) + } + return rets[0].Interface(), err + } + } + + if typ.Kind() == reflect.Map { + return func(vs []any) (any, error) { + return mergeMap(typ, vs) + } + } + + return nil +} + +func mergeMap(typ reflect.Type, vs []any) (any, error) { + merged := reflect.MakeMap(typ) + for _, v := range vs { + if t := reflect.TypeOf(v); t != typ { + return nil, fmt.Errorf( + "(values merge map) field type mismatch. expected: '%v', got: '%v'", typ, t) + } + + iter := reflect.ValueOf(v).MapRange() + for iter.Next() { + key, val := iter.Key(), iter.Value() + if merged.MapIndex(key).IsValid() { + return nil, fmt.Errorf("(values merge map) duplicated key ('%v') found", key.Interface()) + } + merged.SetMapIndex(key, val) + } + } + + return merged.Interface(), nil +} diff --git a/internal/mock/adk/Agent_mock.go b/internal/mock/adk/Agent_mock.go new file mode 100644 index 0000000..9b98c2d --- /dev/null +++ b/internal/mock/adk/Agent_mock.go @@ -0,0 +1,171 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -destination ../internal/mock/adk/Agent_mock.go --package adk -source interface.go +// + +// Package adk is a generated GoMock package. +package adk + +import ( + context "context" + reflect "reflect" + + adk "github.com/cloudwego/eino/adk" + gomock "go.uber.org/mock/gomock" +) + +// MockAgent is a mock of Agent interface. +type MockAgent struct { + ctrl *gomock.Controller + recorder *MockAgentMockRecorder + isgomock struct{} +} + +// MockAgentMockRecorder is the mock recorder for MockAgent. +type MockAgentMockRecorder struct { + mock *MockAgent +} + +// NewMockAgent creates a new mock instance. +func NewMockAgent(ctrl *gomock.Controller) *MockAgent { + mock := &MockAgent{ctrl: ctrl} + mock.recorder = &MockAgentMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAgent) EXPECT() *MockAgentMockRecorder { + return m.recorder +} + +// Description mocks base method. +func (m *MockAgent) Description(ctx context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Description", ctx) + ret0, _ := ret[0].(string) + return ret0 +} + +// Description indicates an expected call of Description. +func (mr *MockAgentMockRecorder) Description(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Description", reflect.TypeOf((*MockAgent)(nil).Description), ctx) +} + +// Name mocks base method. +func (m *MockAgent) Name(ctx context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name", ctx) + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockAgentMockRecorder) Name(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockAgent)(nil).Name), ctx) +} + +// Run mocks base method. +func (m *MockAgent) Run(ctx context.Context, input *adk.AgentInput, options ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range options { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Run", varargs...) + ret0, _ := ret[0].(*adk.AsyncIterator[*adk.AgentEvent]) + return ret0 +} + +// Run indicates an expected call of Run. +func (mr *MockAgentMockRecorder) Run(ctx, input any, options ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, options...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockAgent)(nil).Run), varargs...) +} + +// MockOnSubAgents is a mock of OnSubAgents interface. +type MockOnSubAgents struct { + ctrl *gomock.Controller + recorder *MockOnSubAgentsMockRecorder + isgomock struct{} +} + +// MockOnSubAgentsMockRecorder is the mock recorder for MockOnSubAgents. +type MockOnSubAgentsMockRecorder struct { + mock *MockOnSubAgents +} + +// NewMockOnSubAgents creates a new mock instance. +func NewMockOnSubAgents(ctrl *gomock.Controller) *MockOnSubAgents { + mock := &MockOnSubAgents{ctrl: ctrl} + mock.recorder = &MockOnSubAgentsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOnSubAgents) EXPECT() *MockOnSubAgentsMockRecorder { + return m.recorder +} + +// OnDisallowTransferToParent mocks base method. +func (m *MockOnSubAgents) OnDisallowTransferToParent(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnDisallowTransferToParent", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnDisallowTransferToParent indicates an expected call of OnDisallowTransferToParent. +func (mr *MockOnSubAgentsMockRecorder) OnDisallowTransferToParent(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnDisallowTransferToParent", reflect.TypeOf((*MockOnSubAgents)(nil).OnDisallowTransferToParent), ctx) +} + +// OnSetAsSubAgent mocks base method. +func (m *MockOnSubAgents) OnSetAsSubAgent(ctx context.Context, parent adk.Agent) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnSetAsSubAgent", ctx, parent) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnSetAsSubAgent indicates an expected call of OnSetAsSubAgent. +func (mr *MockOnSubAgentsMockRecorder) OnSetAsSubAgent(ctx, parent any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnSetAsSubAgent", reflect.TypeOf((*MockOnSubAgents)(nil).OnSetAsSubAgent), ctx, parent) +} + +// OnSetSubAgents mocks base method. +func (m *MockOnSubAgents) OnSetSubAgents(ctx context.Context, subAgents []adk.Agent) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnSetSubAgents", ctx, subAgents) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnSetSubAgents indicates an expected call of OnSetSubAgents. +func (mr *MockOnSubAgentsMockRecorder) OnSetSubAgents(ctx, subAgents any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnSetSubAgents", reflect.TypeOf((*MockOnSubAgents)(nil).OnSetSubAgents), ctx, subAgents) +} diff --git a/internal/mock/components/document/document_mock.go b/internal/mock/components/document/document_mock.go new file mode 100644 index 0000000..e941bae --- /dev/null +++ b/internal/mock/components/document/document_mock.go @@ -0,0 +1,116 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package document is a generated GoMock package. +package document + +import ( + context "context" + reflect "reflect" + + document "github.com/cloudwego/eino/components/document" + schema "github.com/cloudwego/eino/schema" + gomock "go.uber.org/mock/gomock" +) + +// MockLoader is a mock of Loader interface. +type MockLoader struct { + ctrl *gomock.Controller + recorder *MockLoaderMockRecorder +} + +// MockLoaderMockRecorder is the mock recorder for MockLoader. +type MockLoaderMockRecorder struct { + mock *MockLoader +} + +// NewMockLoader creates a new mock instance. +func NewMockLoader(ctrl *gomock.Controller) *MockLoader { + mock := &MockLoader{ctrl: ctrl} + mock.recorder = &MockLoaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoader) EXPECT() *MockLoaderMockRecorder { + return m.recorder +} + +// Load mocks base method. +func (m *MockLoader) Load(ctx context.Context, src document.Source, opts ...document.LoaderOption) ([]*schema.Document, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, src} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Load", varargs...) + ret0, _ := ret[0].([]*schema.Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Load indicates an expected call of Load. +func (mr *MockLoaderMockRecorder) Load(ctx, src interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, src}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockLoader)(nil).Load), varargs...) +} + +// MockTransformer is a mock of Transformer interface. +type MockTransformer struct { + ctrl *gomock.Controller + recorder *MockTransformerMockRecorder +} + +// MockTransformerMockRecorder is the mock recorder for MockTransformer. +type MockTransformerMockRecorder struct { + mock *MockTransformer +} + +// NewMockTransformer creates a new mock instance. +func NewMockTransformer(ctrl *gomock.Controller) *MockTransformer { + mock := &MockTransformer{ctrl: ctrl} + mock.recorder = &MockTransformerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTransformer) EXPECT() *MockTransformerMockRecorder { + return m.recorder +} + +// Transform mocks base method. +func (m *MockTransformer) Transform(ctx context.Context, src []*schema.Document, opts ...document.TransformerOption) ([]*schema.Document, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, src} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Transform", varargs...) + ret0, _ := ret[0].([]*schema.Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Transform indicates an expected call of Transform. +func (mr *MockTransformerMockRecorder) Transform(ctx, src interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, src}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transform", reflect.TypeOf((*MockTransformer)(nil).Transform), varargs...) +} diff --git a/internal/mock/components/embedding/Embedding_mock.go b/internal/mock/components/embedding/Embedding_mock.go new file mode 100644 index 0000000..f0c1063 --- /dev/null +++ b/internal/mock/components/embedding/Embedding_mock.go @@ -0,0 +1,77 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -destination ../../internal/mock/components/embedding/Embedding_mock.go --package embedding -source interface.go +// + +// Package embedding is a generated GoMock package. +package embedding + +import ( + context "context" + reflect "reflect" + + embedding "github.com/cloudwego/eino/components/embedding" + gomock "go.uber.org/mock/gomock" +) + +// MockEmbedder is a mock of Embedder interface. +type MockEmbedder struct { + ctrl *gomock.Controller + recorder *MockEmbedderMockRecorder +} + +// MockEmbedderMockRecorder is the mock recorder for MockEmbedder. +type MockEmbedderMockRecorder struct { + mock *MockEmbedder +} + +// NewMockEmbedder creates a new mock instance. +func NewMockEmbedder(ctrl *gomock.Controller) *MockEmbedder { + mock := &MockEmbedder{ctrl: ctrl} + mock.recorder = &MockEmbedderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEmbedder) EXPECT() *MockEmbedderMockRecorder { + return m.recorder +} + +// EmbedStrings mocks base method. +func (m *MockEmbedder) EmbedStrings(ctx context.Context, texts []string, opts ...embedding.Option) ([][]float64, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, texts} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "EmbedStrings", varargs...) + ret0, _ := ret[0].([][]float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EmbedStrings indicates an expected call of EmbedStrings. +func (mr *MockEmbedderMockRecorder) EmbedStrings(ctx, texts any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, texts}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmbedStrings", reflect.TypeOf((*MockEmbedder)(nil).EmbedStrings), varargs...) +} diff --git a/internal/mock/components/indexer/indexer_mock.go b/internal/mock/components/indexer/indexer_mock.go new file mode 100644 index 0000000..829af22 --- /dev/null +++ b/internal/mock/components/indexer/indexer_mock.go @@ -0,0 +1,78 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -destination ../../internal/mock/components/indexer/indexer_mock.go --package indexer -source interface.go +// + +// Package indexer is a generated GoMock package. +package indexer + +import ( + context "context" + reflect "reflect" + + indexer "github.com/cloudwego/eino/components/indexer" + schema "github.com/cloudwego/eino/schema" + gomock "go.uber.org/mock/gomock" +) + +// MockIndexer is a mock of Indexer interface. +type MockIndexer struct { + ctrl *gomock.Controller + recorder *MockIndexerMockRecorder +} + +// MockIndexerMockRecorder is the mock recorder for MockIndexer. +type MockIndexerMockRecorder struct { + mock *MockIndexer +} + +// NewMockIndexer creates a new mock instance. +func NewMockIndexer(ctrl *gomock.Controller) *MockIndexer { + mock := &MockIndexer{ctrl: ctrl} + mock.recorder = &MockIndexerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIndexer) EXPECT() *MockIndexerMockRecorder { + return m.recorder +} + +// Store mocks base method. +func (m *MockIndexer) Store(ctx context.Context, docs []*schema.Document, opts ...indexer.Option) ([]string, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, docs} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Store", varargs...) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Store indicates an expected call of Store. +func (mr *MockIndexerMockRecorder) Store(ctx, docs any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, docs}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockIndexer)(nil).Store), varargs...) +} diff --git a/internal/mock/components/model/ChatModel_mock.go b/internal/mock/components/model/ChatModel_mock.go new file mode 100644 index 0000000..c597d7b --- /dev/null +++ b/internal/mock/components/model/ChatModel_mock.go @@ -0,0 +1,256 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -destination ../../internal/mock/components/model/ChatModel_mock.go --package model -source interface.go +// + +// Package model is a generated GoMock package. +package model + +import ( + context "context" + reflect "reflect" + + model "github.com/cloudwego/eino/components/model" + schema "github.com/cloudwego/eino/schema" + gomock "go.uber.org/mock/gomock" +) + +// MockBaseChatModel is a mock of BaseChatModel interface. +type MockBaseChatModel struct { + ctrl *gomock.Controller + recorder *MockBaseChatModelMockRecorder + isgomock struct{} +} + +// MockBaseChatModelMockRecorder is the mock recorder for MockBaseChatModel. +type MockBaseChatModelMockRecorder struct { + mock *MockBaseChatModel +} + +// NewMockBaseChatModel creates a new mock instance. +func NewMockBaseChatModel(ctrl *gomock.Controller) *MockBaseChatModel { + mock := &MockBaseChatModel{ctrl: ctrl} + mock.recorder = &MockBaseChatModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBaseChatModel) EXPECT() *MockBaseChatModelMockRecorder { + return m.recorder +} + +// Generate mocks base method. +func (m *MockBaseChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Generate", varargs...) + ret0, _ := ret[0].(*schema.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Generate indicates an expected call of Generate. +func (mr *MockBaseChatModelMockRecorder) Generate(ctx, input any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockBaseChatModel)(nil).Generate), varargs...) +} + +// Stream mocks base method. +func (m *MockBaseChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Stream", varargs...) + ret0, _ := ret[0].(*schema.StreamReader[*schema.Message]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Stream indicates an expected call of Stream. +func (mr *MockBaseChatModelMockRecorder) Stream(ctx, input any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stream", reflect.TypeOf((*MockBaseChatModel)(nil).Stream), varargs...) +} + +// MockChatModel is a mock of ChatModel interface. +type MockChatModel struct { + ctrl *gomock.Controller + recorder *MockChatModelMockRecorder + isgomock struct{} +} + +// MockChatModelMockRecorder is the mock recorder for MockChatModel. +type MockChatModelMockRecorder struct { + mock *MockChatModel +} + +// NewMockChatModel creates a new mock instance. +func NewMockChatModel(ctrl *gomock.Controller) *MockChatModel { + mock := &MockChatModel{ctrl: ctrl} + mock.recorder = &MockChatModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockChatModel) EXPECT() *MockChatModelMockRecorder { + return m.recorder +} + +// BindTools mocks base method. +func (m *MockChatModel) BindTools(tools []*schema.ToolInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BindTools", tools) + ret0, _ := ret[0].(error) + return ret0 +} + +// BindTools indicates an expected call of BindTools. +func (mr *MockChatModelMockRecorder) BindTools(tools any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindTools", reflect.TypeOf((*MockChatModel)(nil).BindTools), tools) +} + +// Generate mocks base method. +func (m *MockChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Generate", varargs...) + ret0, _ := ret[0].(*schema.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Generate indicates an expected call of Generate. +func (mr *MockChatModelMockRecorder) Generate(ctx, input any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockChatModel)(nil).Generate), varargs...) +} + +// Stream mocks base method. +func (m *MockChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Stream", varargs...) + ret0, _ := ret[0].(*schema.StreamReader[*schema.Message]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Stream indicates an expected call of Stream. +func (mr *MockChatModelMockRecorder) Stream(ctx, input any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stream", reflect.TypeOf((*MockChatModel)(nil).Stream), varargs...) +} + +// MockToolCallingChatModel is a mock of ToolCallingChatModel interface. +type MockToolCallingChatModel struct { + ctrl *gomock.Controller + recorder *MockToolCallingChatModelMockRecorder + isgomock struct{} +} + +// MockToolCallingChatModelMockRecorder is the mock recorder for MockToolCallingChatModel. +type MockToolCallingChatModelMockRecorder struct { + mock *MockToolCallingChatModel +} + +// NewMockToolCallingChatModel creates a new mock instance. +func NewMockToolCallingChatModel(ctrl *gomock.Controller) *MockToolCallingChatModel { + mock := &MockToolCallingChatModel{ctrl: ctrl} + mock.recorder = &MockToolCallingChatModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockToolCallingChatModel) EXPECT() *MockToolCallingChatModelMockRecorder { + return m.recorder +} + +// Generate mocks base method. +func (m *MockToolCallingChatModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Generate", varargs...) + ret0, _ := ret[0].(*schema.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Generate indicates an expected call of Generate. +func (mr *MockToolCallingChatModelMockRecorder) Generate(ctx, input any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockToolCallingChatModel)(nil).Generate), varargs...) +} + +// Stream mocks base method. +func (m *MockToolCallingChatModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) { + m.ctrl.T.Helper() + varargs := []any{ctx, input} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Stream", varargs...) + ret0, _ := ret[0].(*schema.StreamReader[*schema.Message]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Stream indicates an expected call of Stream. +func (mr *MockToolCallingChatModelMockRecorder) Stream(ctx, input any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, input}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stream", reflect.TypeOf((*MockToolCallingChatModel)(nil).Stream), varargs...) +} + +// WithTools mocks base method. +func (m *MockToolCallingChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithTools", tools) + ret0, _ := ret[0].(model.ToolCallingChatModel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WithTools indicates an expected call of WithTools. +func (mr *MockToolCallingChatModelMockRecorder) WithTools(tools any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithTools", reflect.TypeOf((*MockToolCallingChatModel)(nil).WithTools), tools) +} diff --git a/internal/mock/components/retriever/retriever_mock.go b/internal/mock/components/retriever/retriever_mock.go new file mode 100644 index 0000000..386371a --- /dev/null +++ b/internal/mock/components/retriever/retriever_mock.go @@ -0,0 +1,78 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -destination ../../internal/mock/components/retriever/retriever_mock.go --package retriever -source interface.go +// + +// Package retriever is a generated GoMock package. +package retriever + +import ( + context "context" + reflect "reflect" + + retriever "github.com/cloudwego/eino/components/retriever" + schema "github.com/cloudwego/eino/schema" + gomock "go.uber.org/mock/gomock" +) + +// MockRetriever is a mock of Retriever interface. +type MockRetriever struct { + ctrl *gomock.Controller + recorder *MockRetrieverMockRecorder +} + +// MockRetrieverMockRecorder is the mock recorder for MockRetriever. +type MockRetrieverMockRecorder struct { + mock *MockRetriever +} + +// NewMockRetriever creates a new mock instance. +func NewMockRetriever(ctrl *gomock.Controller) *MockRetriever { + mock := &MockRetriever{ctrl: ctrl} + mock.recorder = &MockRetrieverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRetriever) EXPECT() *MockRetrieverMockRecorder { + return m.recorder +} + +// Retrieve mocks base method. +func (m *MockRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, query} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Retrieve", varargs...) + ret0, _ := ret[0].([]*schema.Document) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Retrieve indicates an expected call of Retrieve. +func (mr *MockRetrieverMockRecorder) Retrieve(ctx, query any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, query}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRetriever)(nil).Retrieve), varargs...) +} diff --git a/internal/mock/doc.go b/internal/mock/doc.go new file mode 100644 index 0000000..020235d --- /dev/null +++ b/internal/mock/doc.go @@ -0,0 +1,51 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package mock provides mock implementations for testing purposes. +// +// This package aims to provide mock implementations for interfaces in the components package, +// making it easier to use in testing environments. It includes mock implementations for +// various core components such as retrievers, tools, message handlers, and graph runners. +// +// Directory Structure: +// - components/: Contains mock implementations for various components +// - retriever/: Provides mock implementation for the Retriever interface +// - retriever_mock.go: Mock implementation for document retrieval +// - tool/: Mock implementations for tool-related interfaces +// - message/: Mock implementations for message handling components +// - graph/: Mock implementations for graph execution components +// - stream/: Mock implementations for streaming components +// +// Usage: +// These mock implementations are primarily used in unit tests and integration tests, +// allowing developers to conduct tests without depending on actual external services. +// Each mock component strictly follows the contract of its corresponding interface +// while providing controllable behaviors and results. +// +// Examples: +// +// - Using mock retriever: +// retriever := mock.NewMockRetriever() +// // Configure retriever behavior +// +// - Using mock tool: +// tool := mock.NewMockTool() +// // Configure tool behavior +// +// - Using mock graph runner: +// runner := mock.NewMockGraphRunner() +// // Configure runner behavior +package mock diff --git a/internal/safe/panic.go b/internal/safe/panic.go new file mode 100644 index 0000000..16bc1dc --- /dev/null +++ b/internal/safe/panic.go @@ -0,0 +1,40 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package safe + +import ( + "fmt" +) + +type panicErr struct { + info any + stack []byte +} + +func (p *panicErr) Error() string { + return fmt.Sprintf("panic error: %v, \nstack: %s", p.info, string(p.stack)) +} + +// NewPanicErr creates a new panic error. +// panicErr is a wrapper of panic info and stack trace. +// it implements the error interface, can print error message of info and stack trace. +func NewPanicErr(info any, stack []byte) error { + return &panicErr{ + info: info, + stack: stack, + } +} diff --git a/internal/safe/panic_test.go b/internal/safe/panic_test.go new file mode 100644 index 0000000..bbe0728 --- /dev/null +++ b/internal/safe/panic_test.go @@ -0,0 +1,28 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package safe + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPanicErr(t *testing.T) { + err := NewPanicErr("info", []byte("stack")) + assert.Equal(t, "panic error: info, \nstack: stack", err.Error()) +} diff --git a/internal/serialization/serialization.go b/internal/serialization/serialization.go new file mode 100644 index 0000000..e59ed90 --- /dev/null +++ b/internal/serialization/serialization.go @@ -0,0 +1,697 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package serialization + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/bytedance/sonic" +) + +var m = map[string]reflect.Type{} +var rm = map[reflect.Type]string{} + +func init() { + _ = GenericRegister[int]("_eino_int") + _ = GenericRegister[int8]("_eino_int8") + _ = GenericRegister[int16]("_eino_int16") + _ = GenericRegister[int32]("_eino_int32") + _ = GenericRegister[int64]("_eino_int64") + _ = GenericRegister[uint]("_eino_uint") + _ = GenericRegister[uint8]("_eino_uint8") + _ = GenericRegister[uint16]("_eino_uint16") + _ = GenericRegister[uint32]("_eino_uint32") + _ = GenericRegister[uint64]("_eino_uint64") + _ = GenericRegister[float32]("_eino_float32") + _ = GenericRegister[float64]("_eino_float64") + _ = GenericRegister[complex64]("_eino_complex64") + _ = GenericRegister[complex128]("_eino_complex128") + _ = GenericRegister[uintptr]("_eino_uintptr") + _ = GenericRegister[bool]("_eino_bool") + _ = GenericRegister[string]("_eino_string") + _ = GenericRegister[any]("_eino_any") +} + +func GenericRegister[T any](key string) error { + t := reflect.TypeOf((*T)(nil)).Elem() + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if nt, ok := m[key]; ok { + return fmt.Errorf("key[%s] already registered to %s", key, nt.String()) + } + if nk, ok := rm[t]; ok { + return fmt.Errorf("type[%s] already registered to %s", t.String(), nk) + } + m[key] = t + rm[t] = key + return nil +} + +type InternalSerializer struct{} + +func (i *InternalSerializer) Marshal(v any) ([]byte, error) { + is, err := internalMarshal(v, nil) + if err != nil { + return nil, err + } + + return sonic.Marshal(is) +} + +func (i *InternalSerializer) Unmarshal(data []byte, v any) error { + val, err := unmarshal(data, reflect.TypeOf(v)) + if err != nil { + return fmt.Errorf("failed to unmarshal: %w", err) + } + + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return fmt.Errorf("failed to unmarshal: value must be a non-nil pointer") + } + + target := rv.Elem() + if !target.CanSet() { + return fmt.Errorf("failed to unmarshal: output value must be settable") + } + + if val == nil { + target.Set(reflect.Zero(target.Type())) + return nil + } + + source := reflect.ValueOf(val) + + var set func(target, source reflect.Value) bool + set = func(target, source reflect.Value) bool { + if !source.IsValid() { + target.Set(reflect.Zero(target.Type())) + return true + } + if source.Type().AssignableTo(target.Type()) { + target.Set(source) + return true + } + + if target.Kind() == reflect.Ptr { + if target.IsNil() { + if !target.CanSet() { + return false + } + target.Set(reflect.New(target.Type().Elem())) + } + return set(target.Elem(), source) + } + + if source.Kind() == reflect.Ptr { + if source.IsNil() { + target.Set(reflect.Zero(target.Type())) + return true + } + return set(target, source.Elem()) + } + + if source.Type().ConvertibleTo(target.Type()) { + target.Set(source.Convert(target.Type())) + return true + } + + return false + } + + if set(target, source) { + return nil + } + + return fmt.Errorf("failed to unmarshal: cannot assign %s to %s", reflect.TypeOf(val), target.Type()) +} + +func unmarshal(data []byte, t reflect.Type) (any, error) { + is := &internalStruct{} + err := sonic.Unmarshal(data, is) + if err != nil { + return nil, err + } + return internalUnmarshal(is, t) +} + +type internalStruct struct { + Type *valueType `json:",omitempty"` + + JSONValue json.RawMessage `json:",omitempty"` + + // map or struct + // in map, the key is the serialized map key anyway todo: if key is string, don't serialize + // in struct, the key is the original field name + MapValues map[string]*internalStruct `json:",omitempty"` + + // slice + SliceValues []*internalStruct `json:",omitempty"` +} + +type valueType struct { + PointerNum uint32 `json:",omitempty"` + + SimpleType string `json:",omitempty"` + + StructType string `json:",omitempty"` + + MapKeyType *valueType `json:",omitempty"` + MapValueType *valueType `json:",omitempty"` + + SliceValueType *valueType `json:",omitempty"` +} + +func extractType(t reflect.Type) (*valueType, error) { + ret := &valueType{} + for t.Kind() == reflect.Ptr { + ret.PointerNum += 1 + t = t.Elem() + } + var err error + if t.Kind() == reflect.Map { + ret.MapKeyType, err = extractType(t.Key()) + if err != nil { + return nil, err + } + ret.MapValueType, err = extractType(t.Elem()) + if err != nil { + return nil, err + } + } else if t.Kind() == reflect.Slice || t.Kind() == reflect.Array { + ret.SliceValueType, err = extractType(t.Elem()) + if err != nil { + return nil, err + } + } else { + key, ok := rm[t] + if !ok { + return ret, fmt.Errorf("unknown type: %s", t.String()) + } + ret.SimpleType = key + } + return ret, nil +} + +func restoreType(vt *valueType) (reflect.Type, error) { + if vt.SimpleType != "" { + rt, ok := m[vt.SimpleType] + if !ok { + return nil, fmt.Errorf("unknown type: %s", vt.SimpleType) + } + return resolvePointerNum(vt.PointerNum, rt), nil + } + if vt.StructType != "" { + rt, ok := m[vt.StructType] + if !ok { + return nil, fmt.Errorf("unknown type: %s", vt.StructType) + } + return resolvePointerNum(vt.PointerNum, rt), nil + } + if vt.MapKeyType != nil { + rkt, err := restoreType(vt.MapKeyType) + if err != nil { + return nil, err + } + rvt, err := restoreType(vt.MapValueType) + if err != nil { + return nil, err + } + return resolvePointerNum(vt.PointerNum, reflect.MapOf(rkt, rvt)), nil + } + if vt.SliceValueType != nil { + rt, err := restoreType(vt.SliceValueType) + if err != nil { + return nil, err + } + return resolvePointerNum(vt.PointerNum, reflect.SliceOf(rt)), nil + } + return nil, fmt.Errorf("empty value") +} + +func internalMarshal(v any, fieldType reflect.Type) (*internalStruct, error) { + if v == nil || + (reflect.ValueOf(v).IsZero() && fieldType != nil && fieldType.Kind() != reflect.Interface) { + return nil, nil + } + + ret := &internalStruct{} + rv := reflect.ValueOf(v) + rt := rv.Type() + typeUnspecific := fieldType == nil || fieldType.Kind() == reflect.Interface + + var pointerNum uint32 + for rt.Kind() == reflect.Ptr { + pointerNum++ + if !rv.IsNil() { + rv = rv.Elem() + rt = rt.Elem() + continue + } + for rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + if typeUnspecific { + // need type registered + key, ok := rm[rt] + if !ok { + return nil, fmt.Errorf("unknown type: %v", rt) + } + ret.Type = &valueType{ + PointerNum: pointerNum, + SimpleType: key, + } + } + ret.JSONValue = json.RawMessage("null") + return ret, nil + } + + switch rt.Kind() { + case reflect.Struct: + if typeUnspecific { + // need type registered + key, ok := rm[rt] + if !ok { + return nil, fmt.Errorf("unknown type: %v", rt) + } + + if checkMarshaler(rt) { + ret.Type = &valueType{ + PointerNum: pointerNum, + SimpleType: key, + } + } else { + ret.Type = &valueType{ + PointerNum: pointerNum, + StructType: key, + } + } + } + + if checkMarshaler(rt) { + // Use rv.Addr() when possible so that pointer-receiver MarshalJSON methods + // are callable. rv is addressable when obtained from pointer dereference. + // When not addressable, copy into an addressable temporary. + var marshalTarget any + if rv.CanAddr() { + marshalTarget = rv.Addr().Interface() + } else { + tmp := reflect.New(rt) + tmp.Elem().Set(rv) + marshalTarget = tmp.Interface() + } + jsonBytes, err := json.Marshal(marshalTarget) + if err != nil { + return nil, err + } + ret.JSONValue = jsonBytes + return ret, nil + } + + ret.MapValues = make(map[string]*internalStruct) + + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + // only handle exported fields + if field.PkgPath == "" { + k := field.Name + v := rv.Field(i) + + internalValue, err := internalMarshal(v.Interface(), field.Type) + if err != nil { + return nil, err + } + + ret.MapValues[k] = internalValue + } + } + + return ret, nil + case reflect.Map: + if typeUnspecific { + var err error + ret.Type = &valueType{ + PointerNum: pointerNum, + } + // map key type + ret.Type.MapKeyType, err = extractType(rt.Key()) + if err != nil { + return nil, err + } + + // map value type + ret.Type.MapValueType, err = extractType(rt.Elem()) + if err != nil { + return nil, err + } + } + + ret.MapValues = make(map[string]*internalStruct) + + iter := rv.MapRange() + for iter.Next() { + k := iter.Key() + v := iter.Value() + + internalValue, err := internalMarshal(v.Interface(), rt.Elem()) + if err != nil { + return nil, err + } + + keyStr, err := sonic.MarshalString(k.Interface()) + if err != nil { + return nil, fmt.Errorf("marshaling map key[%v] fail: %v", k.Interface(), err) + } + ret.MapValues[keyStr] = internalValue + } + + return ret, nil + case reflect.Slice, reflect.Array: + if typeUnspecific { + var err error + ret.Type = &valueType{PointerNum: pointerNum} + ret.Type.SliceValueType, err = extractType(rt.Elem()) + if err != nil { + return nil, err + } + } + + length := rv.Len() + ret.SliceValues = make([]*internalStruct, length) + + for i := 0; i < length; i++ { + internalValue, err := internalMarshal(rv.Index(i).Interface(), rt.Elem()) + if err != nil { + return nil, err + } + ret.SliceValues[i] = internalValue + } + + return ret, nil + + default: + if typeUnspecific { + key, ok := rm[rv.Type()] + if !ok { + return nil, fmt.Errorf("unknown type: %v", rt) + } + ret.Type = &valueType{ + PointerNum: pointerNum, + SimpleType: key, + } + } + + jsonBytes, err := json.Marshal(rv.Interface()) + if err != nil { + return nil, err + } + ret.JSONValue = jsonBytes + return ret, nil + } +} + +func internalUnmarshal(v *internalStruct, typ reflect.Type) (any, error) { + if v == nil { + return nil, nil + } + + if v.Type == nil { + // specific type + if checkMarshaler(typ) { + pv := reflect.New(typ) + err := json.Unmarshal(v.JSONValue, pv.Interface()) + if err != nil { + return nil, err + } + return pv.Elem().Interface(), nil + } + return internalSpecificTypeUnmarshal(v, typ) + } + + if len(v.Type.SimpleType) != 0 { + // based type + t, ok := m[v.Type.SimpleType] + if !ok { + return nil, fmt.Errorf("unknown type key: %v", v.Type) + } + pResult := reflect.New(resolvePointerNum(v.Type.PointerNum, t)) + err := sonic.Unmarshal(v.JSONValue, pResult.Interface()) + if err != nil { + return nil, fmt.Errorf("unmarshal type[%s] fail: %v, data: %s", t.String(), err, string(v.JSONValue)) + } + return pResult.Elem().Interface(), nil + } + + if len(v.Type.StructType) > 0 { + // struct + rt, ok := m[v.Type.StructType] + if !ok { + return nil, fmt.Errorf("unknown type key: %v", v.Type.StructType) + } + result, dResult := createValueFromType(resolvePointerNum(v.Type.PointerNum, rt)) + + err := setStructFields(dResult, v.MapValues) + if err != nil { + return nil, err + } + + return result.Interface(), nil + } + + if v.Type.MapKeyType != nil { + // map + rkt, err := restoreType(v.Type.MapKeyType) + if err != nil { + return nil, err + } + rvt, err := restoreType(v.Type.MapValueType) + if err != nil { + return nil, err + } + + result, dResult := createValueFromType(reflect.MapOf(rkt, rvt)) + err = setMapKVs(dResult, v.MapValues) + if err != nil { + return nil, err + } + return result.Interface(), nil + } + + // slice + rvt, err := restoreType(v.Type.SliceValueType) + if err != nil { + return nil, err + } + + result, dResult := createValueFromType(reflect.SliceOf(rvt)) + err = setSliceElems(dResult, v.SliceValues) + if err != nil { + return nil, err + } + return result.Interface(), nil +} + +func internalSpecificTypeUnmarshal(is *internalStruct, typ reflect.Type) (any, error) { + _, dtyp := derefPointerNum(typ) + result, dResult := createValueFromType(typ) + + if dtyp.Kind() == reflect.Struct { + err := setStructFields(dResult, is.MapValues) + if err != nil { + return nil, err + } + return result.Interface(), nil + } else if dtyp.Kind() == reflect.Map { + err := setMapKVs(dResult, is.MapValues) + if err != nil { + return nil, err + } + return result.Interface(), nil + } else if dtyp.Kind() == reflect.Array || dtyp.Kind() == reflect.Slice { + err := setSliceElems(dResult, is.SliceValues) + if err != nil { + return nil, err + } + return result.Interface(), nil + } + // simple type + v := reflect.New(typ) + err := sonic.Unmarshal(is.JSONValue, v.Interface()) + if err != nil { + return nil, fmt.Errorf("unmarshal type[%s] fail: %v", typ.String(), err) + } + return v.Elem().Interface(), nil +} + +func setSliceElems(dResult reflect.Value, values []*internalStruct) error { + t := dResult.Type() + + // Handle arrays differently from slices + // Arrays have fixed size and cannot use reflect.Append + if dResult.Kind() == reflect.Array { + for i, internalValue := range values { + if i >= dResult.Len() { + return fmt.Errorf("array index out of bounds: trying to set index %d in array of length %d", i, dResult.Len()) + } + value, err := internalUnmarshal(internalValue, t.Elem()) + if err != nil { + return fmt.Errorf("unmarshal array[%s] element %d fail: %v", t.Elem(), i, err) + } + if value == nil { + dResult.Index(i).Set(reflect.Zero(t.Elem())) + } else { + dResult.Index(i).Set(reflect.ValueOf(value)) + } + } + return nil + } + + // For slices, use Append as before + for _, internalValue := range values { + value, err := internalUnmarshal(internalValue, t.Elem()) + if err != nil { + return fmt.Errorf("unmarshal slice[%s] fail: %v", t.Elem(), err) + } + if value == nil { + // empty value + dResult.Set(reflect.Append(dResult, reflect.New(t.Elem()).Elem())) + } else { + dResult.Set(reflect.Append(dResult, reflect.ValueOf(value))) + } + } + return nil +} + +func setMapKVs(dResult reflect.Value, values map[string]*internalStruct) error { + t := dResult.Type() + for marshaledMapKey, internalValue := range values { + prkv := reflect.New(t.Key()) + err := sonic.UnmarshalString(marshaledMapKey, prkv.Interface()) + if err != nil { + return fmt.Errorf("unmarshal map key[%v] to type[%s] fail: %v", marshaledMapKey, t.Key(), err) + } + + value, err := internalUnmarshal(internalValue, t.Elem()) + if err != nil { + return fmt.Errorf("unmarshal map value fail: %v", err) + } + if value == nil { + dResult.SetMapIndex(prkv.Elem(), reflect.New(t.Elem()).Elem()) + } else { + dResult.SetMapIndex(prkv.Elem(), reflect.ValueOf(value)) + } + } + return nil +} + +func setStructFields(dResult reflect.Value, values map[string]*internalStruct) error { + t := dResult.Type() + for k, internalValue := range values { + sf, ok := t.FieldByName(k) + if !ok { + continue + } + value, err := internalUnmarshal(internalValue, sf.Type) + if err != nil { + return fmt.Errorf("unmarshal map field[%v] fail: %v", k, err) + } + err = setStructField(t, dResult, k, value) + if err != nil { + return err + } + } + return nil +} + +func setStructField(t reflect.Type, s reflect.Value, fieldName string, val any) error { + field := s.FieldByName(fieldName) + if !field.CanSet() { + return fmt.Errorf("unmarshal map fail, can not set field %v", fieldName) + } + if val == nil { + rft, ok := t.FieldByName(fieldName) + if !ok { + return fmt.Errorf("unmarshal map fail, cannot find field: %v", fieldName) + } + field.Set(reflect.New(rft.Type).Elem()) + } else { + field.Set(reflect.ValueOf(val)) + } + return nil +} + +func resolvePointerNum(pointerNum uint32, t reflect.Type) reflect.Type { + for i := uint32(0); i < pointerNum; i++ { + t = reflect.PointerTo(t) + } + return t +} + +func derefPointerNum(t reflect.Type) (uint32, reflect.Type) { + var ptrCount uint32 = 0 + + for t != nil && t.Kind() == reflect.Ptr { + t = t.Elem() + ptrCount++ + } + + return ptrCount, t +} + +func createValueFromType(t reflect.Type) (value reflect.Value, derefValue reflect.Value) { + value = reflect.New(t).Elem() + + derefValue = value + for derefValue.Kind() == reflect.Ptr { + if derefValue.IsNil() { + derefValue.Set(reflect.New(derefValue.Type().Elem())) + } + derefValue = derefValue.Elem() + } + + if derefValue.Kind() == reflect.Map && derefValue.IsNil() { + derefValue.Set(reflect.MakeMap(derefValue.Type())) + } + + // Use Len() == 0 instead of IsNil() for slices to avoid panic + // IsNil() can panic on uninitialized slice values created via reflect.New().Elem() + if derefValue.Kind() == reflect.Slice { + if derefValue.Len() == 0 && derefValue.Cap() == 0 { + derefValue.Set(reflect.MakeSlice(derefValue.Type(), 0, 0)) + } + } + // Arrays cannot be nil and don't need initialization + + return value, derefValue +} + +var marshalerType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() +var unmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() + +func checkMarshaler(t reflect.Type) bool { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if (t.Implements(marshalerType) || reflect.PointerTo(t).Implements(marshalerType)) && + (t.Implements(unmarshalerType) || reflect.PointerTo(t).Implements(unmarshalerType)) { + return true + } + return false +} diff --git a/internal/serialization/serialization_test.go b/internal/serialization/serialization_test.go new file mode 100644 index 0000000..014c7fd --- /dev/null +++ b/internal/serialization/serialization_test.go @@ -0,0 +1,351 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package serialization + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type myInterface interface { + Method() +} +type myStruct struct { + A string +} + +func (m *myStruct) Method() {} + +type myStruct2 struct { + A any + B myInterface + C map[string]**myStruct + D map[myStruct]any + E []any + f string + G myStruct3 + H *myStruct4 + I []*myStruct3 + J map[string]myStruct3 + K myStruct4 + L []*myStruct4 + M map[string]myStruct4 +} + +type myStruct3 struct { + FieldA string +} + +type myStruct4 struct { + FieldA string +} + +func (m *myStruct4) UnmarshalJSON(bytes []byte) error { + m.FieldA = string(bytes) + return nil +} + +func (m myStruct4) MarshalJSON() ([]byte, error) { + return []byte(m.FieldA), nil +} + +func TestSerialization(t *testing.T) { + _ = GenericRegister[myStruct]("myStruct") + _ = GenericRegister[myStruct2]("myStruct2") + _ = GenericRegister[myInterface]("myInterface") + ms := myStruct{A: "test"} + pms := &ms + pointerOfPointerOfMyStruct := &pms + + ms1 := myStruct{A: "1"} + ms2 := myStruct{A: "2"} + ms3 := myStruct{A: "3"} + ms4 := myStruct{A: "4"} + values := []any{ + 10, + "test", + ms, + pms, + pointerOfPointerOfMyStruct, + myInterface(pms), + []int{1, 2, 3}, + []any{1, "test"}, + []myInterface{nil, &myStruct{A: "1"}, &myStruct{A: "2"}}, + map[string]string{"123": "123", "abc": "abc"}, + map[string]myInterface{"1": nil, "2": pms}, + map[string]any{"123": 1, "abc": &myStruct{A: "1"}, "bcd": nil}, + map[myStruct]any{ + ms1: 1, + ms2: &myStruct{ + A: "2", + }, + ms3: nil, + ms4: []any{ + 1, + pointerOfPointerOfMyStruct, + "123", &myStruct{ + A: "1", + }, + nil, + map[myStruct]any{ + ms1: 1, + ms2: nil, + }, + }, + }, + myStruct2{ + A: "123", + B: &myStruct{ + A: "test", + }, + C: map[string]**myStruct{ + "a": pointerOfPointerOfMyStruct, + }, + D: map[myStruct]any{{"a"}: 1}, + E: []any{1, "2", 3}, + f: "", + G: myStruct3{ + FieldA: "1", + }, + H: nil, + I: []*myStruct3{ + {FieldA: "2"}, {FieldA: "3"}, + }, + J: map[string]myStruct3{ + "1": {FieldA: "4"}, + "2": {FieldA: "5"}, + }, + K: myStruct4{ + FieldA: "1", + }, + L: []*myStruct4{ + {FieldA: "2"}, {FieldA: "3"}, + }, + M: map[string]myStruct4{ + "1": {FieldA: "4"}, + "2": {FieldA: "5"}, + }, + }, + map[string]map[string][]map[string][][]string{ + "1": { + "a": []map[string][][]string{ + {"b": { + {"c"}, + {"d"}, + }}, + }, + }, + }, + []*myStruct{}, + &myStruct{}, + } + + for _, value := range values { + data, err := (&InternalSerializer{}).Marshal(value) + assert.NoError(t, err) + v := reflect.New(reflect.TypeOf(value)).Interface() + err = (&InternalSerializer{}).Unmarshal(data, v) + assert.NoError(t, err) + assert.Equal(t, value, reflect.ValueOf(v).Elem().Interface()) + } +} + +type myStruct5 struct { + FieldA string +} + +func (m *myStruct5) UnmarshalJSON(bytes []byte) error { + m.FieldA = "FieldA" + return nil +} + +func (m myStruct5) MarshalJSON() ([]byte, error) { + return []byte("1"), nil +} + +func TestMarshalStruct(t *testing.T) { + assert.NoError(t, GenericRegister[myStruct5]("myStruct5")) + s := myStruct5{FieldA: "1"} + data, err := (&InternalSerializer{}).Marshal(s) + assert.NoError(t, err) + result := &myStruct5{} + err = (&InternalSerializer{}).Unmarshal(data, result) + assert.NoError(t, err) + assert.Equal(t, myStruct5{FieldA: "FieldA"}, *result) + + ma := map[string]any{ + "1": s, + } + data, err = (&InternalSerializer{}).Marshal(ma) + assert.NoError(t, err) + result2 := map[string]any{} + err = (&InternalSerializer{}).Unmarshal(data, &result2) + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "1": myStruct5{FieldA: "FieldA"}, + }, result2) +} + +type unmarshalTestStruct struct { + Foo string + Bar int +} + +func init() { + // Register types for the serializer to work. + // This is necessary for the serializer to know how to handle custom struct types. + err := GenericRegister[unmarshalTestStruct]("unmarshalTestStruct") + if err != nil { + panic(err) + } +} + +func TestInternalSerializer_Unmarshal(t *testing.T) { + s := InternalSerializer{} + + t.Run("success cases", func(t *testing.T) { + // Helper to create a pointer to a value, needed for the expected value in one test case. + ptr := func(i int) *int { return &i } + + testCases := []struct { + name string + inputValue any + outputPtr any + expectedVal any + }{ + { + name: "simple type", + inputValue: 123, + outputPtr: new(int), + expectedVal: 123, + }, + { + name: "struct type", + inputValue: unmarshalTestStruct{Foo: "hello", Bar: 42}, + outputPtr: new(unmarshalTestStruct), + expectedVal: unmarshalTestStruct{Foo: "hello", Bar: 42}, + }, + { + name: "pointer to struct", + inputValue: &unmarshalTestStruct{Foo: "world", Bar: 99}, + outputPtr: new(*unmarshalTestStruct), + expectedVal: &unmarshalTestStruct{Foo: "world", Bar: 99}, + }, + { + name: "unmarshal pointer to value", + inputValue: &unmarshalTestStruct{Foo: "p2v", Bar: 1}, + outputPtr: new(unmarshalTestStruct), + expectedVal: unmarshalTestStruct{Foo: "p2v", Bar: 1}, + }, + { + name: "unmarshal value to pointer", + inputValue: unmarshalTestStruct{Foo: "v2p", Bar: 2}, + outputPtr: new(*unmarshalTestStruct), + expectedVal: &unmarshalTestStruct{Foo: "v2p", Bar: 2}, + }, + { + name: "unmarshal nil pointer", + inputValue: (*unmarshalTestStruct)(nil), + outputPtr: &struct{ v *unmarshalTestStruct }{v: &unmarshalTestStruct{}}, // placeholder to be replaced + expectedVal: (*unmarshalTestStruct)(nil), + }, + { + name: "convertible types", + inputValue: int32(42), + outputPtr: new(int64), + expectedVal: int64(42), + }, + { + name: "pointer to pointer destination", + inputValue: 12345, + outputPtr: new(*int), + expectedVal: ptr(12345), + }, + { + name: "unmarshal to any", + inputValue: unmarshalTestStruct{Foo: "any", Bar: 101}, + outputPtr: new(any), + expectedVal: unmarshalTestStruct{Foo: "any", Bar: 101}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data, err := s.Marshal(tc.inputValue) + require.NoError(t, err) + + // Special handling for the nil test case to correctly pass the pointer. + if tc.name == "unmarshal nil pointer" { + target := tc.outputPtr.(*struct{ v *unmarshalTestStruct }) + err = s.Unmarshal(data, &target.v) + require.NoError(t, err) + assert.Nil(t, target.v) + return + } + + err = s.Unmarshal(data, tc.outputPtr) + require.NoError(t, err) + + // Dereference the pointer to get the actual value for comparison. + actualVal := reflect.ValueOf(tc.outputPtr).Elem().Interface() + assert.Equal(t, tc.expectedVal, actualVal) + }) + } + }) + + t.Run("error cases", func(t *testing.T) { + data, err := s.Marshal(123) + require.NoError(t, err) + + t.Run("destination not a pointer", func(t *testing.T) { + var output int + err := s.Unmarshal(data, output) + require.Error(t, err) + assert.Contains(t, err.Error(), "value must be a non-nil pointer") + }) + + t.Run("destination is a nil pointer", func(t *testing.T) { + var output *int // nil + err := s.Unmarshal(data, output) + require.Error(t, err) + assert.Contains(t, err.Error(), "value must be a non-nil pointer") + }) + + t.Run("type mismatch", func(t *testing.T) { + strData, mErr := s.Marshal("i am a string") + require.NoError(t, mErr) + + var output int + err := s.Unmarshal(strData, &output) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot assign") + }) + + t.Run("unconvertible types", func(t *testing.T) { + intData, mErr := s.Marshal(123) + require.NoError(t, mErr) + + var output bool + err := s.Unmarshal(intData, &output) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot assign") + }) + }) +} diff --git a/schema/agentic_message.go b/schema/agentic_message.go new file mode 100644 index 0000000..2474e5d --- /dev/null +++ b/schema/agentic_message.go @@ -0,0 +1,2284 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "bytes" + "context" + "encoding/gob" + "encoding/json" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/bytedance/sonic" + "github.com/eino-contrib/jsonschema" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/schema/claude" + "github.com/cloudwego/eino/schema/gemini" + "github.com/cloudwego/eino/schema/openai" +) + +type ContentBlockType string + +const ( + ContentBlockTypeReasoning ContentBlockType = "reasoning" + ContentBlockTypeUserInputText ContentBlockType = "user_input_text" + ContentBlockTypeUserInputImage ContentBlockType = "user_input_image" + ContentBlockTypeUserInputAudio ContentBlockType = "user_input_audio" + ContentBlockTypeUserInputVideo ContentBlockType = "user_input_video" + ContentBlockTypeUserInputFile ContentBlockType = "user_input_file" + ContentBlockTypeToolSearchResult ContentBlockType = "tool_search_result" + ContentBlockTypeAssistantGenText ContentBlockType = "assistant_gen_text" + ContentBlockTypeAssistantGenImage ContentBlockType = "assistant_gen_image" + ContentBlockTypeAssistantGenAudio ContentBlockType = "assistant_gen_audio" + ContentBlockTypeAssistantGenVideo ContentBlockType = "assistant_gen_video" + ContentBlockTypeFunctionToolCall ContentBlockType = "function_tool_call" + ContentBlockTypeFunctionToolResult ContentBlockType = "function_tool_result" + ContentBlockTypeServerToolCall ContentBlockType = "server_tool_call" + ContentBlockTypeServerToolResult ContentBlockType = "server_tool_result" + ContentBlockTypeMCPToolCall ContentBlockType = "mcp_tool_call" + ContentBlockTypeMCPToolResult ContentBlockType = "mcp_tool_result" + ContentBlockTypeMCPListToolsResult ContentBlockType = "mcp_list_tools_result" + ContentBlockTypeMCPToolApprovalRequest ContentBlockType = "mcp_tool_approval_request" + ContentBlockTypeMCPToolApprovalResponse ContentBlockType = "mcp_tool_approval_response" +) + +type AgenticRoleType string + +const ( + AgenticRoleTypeSystem AgenticRoleType = "system" + AgenticRoleTypeUser AgenticRoleType = "user" + AgenticRoleTypeAssistant AgenticRoleType = "assistant" +) + +type AgenticMessage struct { + // Role is the message role. + Role AgenticRoleType `json:"role"` + + // ContentBlocks is the list of content blocks. + ContentBlocks []*ContentBlock `json:"content_blocks,omitempty"` + + // ResponseMeta is the response metadata. + ResponseMeta *AgenticResponseMeta `json:"response_meta,omitempty"` + + // Extra is the additional information. + Extra map[string]any `json:"extra,omitempty"` +} + +type AgenticResponseMeta struct { + // TokenUsage is the token usage. + TokenUsage *TokenUsage `json:"token_usage,omitempty"` + + // OpenAIExtension is the extension for OpenAI. + OpenAIExtension *openai.ResponseMetaExtension `json:"openai_extension,omitempty"` + + // GeminiExtension is the extension for Gemini. + GeminiExtension *gemini.ResponseMetaExtension `json:"gemini_extension,omitempty"` + + // ClaudeExtension is the extension for Claude. + ClaudeExtension *claude.ResponseMetaExtension `json:"claude_extension,omitempty"` + + // Extension is the extension for other models, supplied by the component implementer. + Extension any `json:"extension,omitempty"` +} + +type ContentBlock struct { + Type ContentBlockType `json:"type"` + + // Reasoning contains the reasoning content generated by the model. + Reasoning *Reasoning `json:"reasoning,omitempty"` + + // UserInputText contains the text content provided by the user. + UserInputText *UserInputText `json:"user_input_text,omitempty"` + + // UserInputImage contains the image content provided by the user. + UserInputImage *UserInputImage `json:"user_input_image,omitempty"` + + // UserInputAudio contains the audio content provided by the user. + UserInputAudio *UserInputAudio `json:"user_input_audio,omitempty"` + + // UserInputVideo contains the video content provided by the user. + UserInputVideo *UserInputVideo `json:"user_input_video,omitempty"` + + // UserInputFile contains the file content provided by the user. + UserInputFile *UserInputFile `json:"user_input_file,omitempty"` + + // AssistantGenText contains the text content generated by the model. + AssistantGenText *AssistantGenText `json:"assistant_gen_text,omitempty"` + + // AssistantGenImage contains the image content generated by the model. + AssistantGenImage *AssistantGenImage `json:"assistant_gen_image,omitempty"` + + // AssistantGenAudio contains the audio content generated by the model. + AssistantGenAudio *AssistantGenAudio `json:"assistant_gen_audio,omitempty"` + + // AssistantGenVideo contains the video content generated by the model. + AssistantGenVideo *AssistantGenVideo `json:"assistant_gen_video,omitempty"` + + // FunctionToolCall contains the invocation details for a user-defined tool. + FunctionToolCall *FunctionToolCall `json:"function_tool_call,omitempty"` + + // FunctionToolResult contains the result returned from a user-defined tool call. + FunctionToolResult *FunctionToolResult `json:"function_tool_result,omitempty"` + + // ToolSearchFunctionToolResult contains the result of a client-side custom tool search tool call. + // It carries the full definitions of newly discovered tools so that the model can + // recognize which tools have been added and are now available for invocation. + ToolSearchFunctionToolResult *ToolSearchFunctionToolResult `json:"tool_search_function_tool_result,omitempty"` + + // ServerToolCall contains the invocation details for a provider built-in tool executed on the model server. + ServerToolCall *ServerToolCall `json:"server_tool_call,omitempty"` + + // ServerToolResult contains the result returned from a provider built-in tool executed on the model server. + ServerToolResult *ServerToolResult `json:"server_tool_result,omitempty"` + + // MCPToolCall contains the invocation details for an MCP tool managed by the model server. + MCPToolCall *MCPToolCall `json:"mcp_tool_call,omitempty"` + + // MCPToolResult contains the result returned from an MCP tool managed by the model server. + MCPToolResult *MCPToolResult `json:"mcp_tool_result,omitempty"` + + // MCPListToolsResult contains the list of available MCP tools reported by the model server. + MCPListToolsResult *MCPListToolsResult `json:"mcp_list_tools_result,omitempty"` + + // MCPToolApprovalRequest contains the user approval request for an MCP tool call when required. + MCPToolApprovalRequest *MCPToolApprovalRequest `json:"mcp_tool_approval_request,omitempty"` + + // MCPToolApprovalResponse contains the user's approval decision for an MCP tool call. + MCPToolApprovalResponse *MCPToolApprovalResponse `json:"mcp_tool_approval_response,omitempty"` + + // StreamingMeta contains metadata for streaming responses. + // Only set for streaming responses. + StreamingMeta *StreamingMeta `json:"streaming_meta,omitempty"` + + // Extra contains additional information for the content block. + Extra map[string]any `json:"extra,omitempty"` +} + +type StreamingMeta struct { + // Index specifies the index position of this block in the final response. + Index int `json:"index"` +} + +type UserInputText struct { + // Text is the text content. + Text string `json:"text,omitempty"` +} + +type UserInputImage struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "image/png". + MIMEType string `json:"mime_type,omitempty"` + + // Detail is the quality of the image url. + Detail ImageURLDetail `json:"detail,omitempty"` +} + +type UserInputAudio struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "audio/wav". + MIMEType string `json:"mime_type,omitempty"` +} + +type UserInputVideo struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "video/mp4". + MIMEType string `json:"mime_type,omitempty"` +} + +type UserInputFile struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Name is the filename. + Name string `json:"name,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "application/pdf". + MIMEType string `json:"mime_type,omitempty"` +} + +type AssistantGenText struct { + // Text is the generated text. + Text string `json:"text,omitempty"` + + // OpenAIExtension is the extension for OpenAI. + OpenAIExtension *openai.AssistantGenTextExtension `json:"openai_extension,omitempty"` + + // ClaudeExtension is the extension for Claude. + ClaudeExtension *claude.AssistantGenTextExtension `json:"claude_extension,omitempty"` + + // Extension is the extension for other models, supplied by the component implementer. + Extension any `json:"extension,omitempty"` +} + +type AssistantGenImage struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "image/png". + MIMEType string `json:"mime_type,omitempty"` +} + +type AssistantGenAudio struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "audio/wav". + MIMEType string `json:"mime_type,omitempty"` +} + +type AssistantGenVideo struct { + // URL is the HTTP/HTTPS link. + URL string `json:"url,omitempty"` + + // Base64Data is the binary data in Base64 encoded string format. + Base64Data string `json:"base64_data,omitempty"` + + // MIMEType is the mime type, e.g. "video/mp4". + MIMEType string `json:"mime_type,omitempty"` +} + +type Reasoning struct { + // Text is either the thought summary or the raw reasoning text itself. + Text string `json:"text,omitempty"` + + // Signature contains encrypted reasoning tokens. + // Required by some models when passing reasoning text back. + Signature string `json:"signature,omitempty"` + + // OpenAIExtension is the extension for OpenAI. + OpenAIExtension *openai.ReasoningExtension `json:"openai_extension,omitempty"` +} + +type FunctionToolCall struct { + // CallID is the unique identifier for the tool call. + CallID string `json:"call_id,omitempty"` + + // Name specifies the function tool invoked. + Name string `json:"name"` + + // Arguments is the JSON string arguments for the function tool call. + Arguments string `json:"arguments,omitempty"` +} + +// FunctionToolResultContentBlockType identifies which media field of a +// FunctionToolResultContentBlock is populated. +type FunctionToolResultContentBlockType string + +const ( + FunctionToolResultContentBlockTypeText FunctionToolResultContentBlockType = "text" + FunctionToolResultContentBlockTypeImage FunctionToolResultContentBlockType = "image" + FunctionToolResultContentBlockTypeAudio FunctionToolResultContentBlockType = "audio" + FunctionToolResultContentBlockTypeVideo FunctionToolResultContentBlockType = "video" + FunctionToolResultContentBlockTypeFile FunctionToolResultContentBlockType = "file" +) + +// FunctionToolResultContentBlock represents a single content block within a multimodal +// function tool result. Type identifies which of the media fields is populated; +// exactly one of the media fields should be set to match Type. +type FunctionToolResultContentBlock struct { + // Type identifies which media field below is populated. + Type FunctionToolResultContentBlockType `json:"type"` + // Text contains the text content of the block. + Text *UserInputText `json:"text,omitempty"` + // Image contains the image content of the block. + Image *UserInputImage `json:"image,omitempty"` + // Audio contains the audio content of the block. + Audio *UserInputAudio `json:"audio,omitempty"` + // Video contains the video content of the block. + Video *UserInputVideo `json:"video,omitempty"` + // File contains the file content of the block. + File *UserInputFile `json:"file,omitempty"` + + // Extra holds additional metadata for model-specific or custom extensions. + Extra map[string]any `json:"extra,omitempty"` +} + +func (b *FunctionToolResultContentBlock) String() string { + switch b.Type { + case FunctionToolResultContentBlockTypeText: + if b.Text != nil { + return b.Text.String() + } + return "empty text block\n" + case FunctionToolResultContentBlockTypeImage: + if b.Image != nil { + return b.Image.String() + } + return "empty image block\n" + case FunctionToolResultContentBlockTypeAudio: + if b.Audio != nil { + return b.Audio.String() + } + return "empty audio block\n" + case FunctionToolResultContentBlockTypeVideo: + if b.Video != nil { + return b.Video.String() + } + return "empty video block\n" + case FunctionToolResultContentBlockTypeFile: + if b.File != nil { + return b.File.String() + } + return "empty file block\n" + case "": + return "unknown block type: \n" + default: + return fmt.Sprintf("unknown block type: %s\n", b.Type) + } +} + +type FunctionToolResult struct { + // CallID is the unique identifier for the tool call. + CallID string `json:"call_id,omitempty"` + + // Name specifies the function tool invoked. + Name string `json:"name"` + + // Content holds the tool execution output as an ordered list of content blocks. + // Each block carries its own type (text, image, audio, video, file), allowing + // text-only and multimodal results to share a uniform representation. + Content []*FunctionToolResultContentBlock `json:"content,omitempty"` +} + +// ToolSearchFunctionToolResult represents the result of a client-side custom tool search +// function tool call. Unlike a regular FunctionToolResult, this carries a ToolSearchResult +// containing the full definitions of newly discovered tools, so the model can recognize +// which tools have been added and are now available for invocation. +type ToolSearchFunctionToolResult struct { + // CallID is the unique identifier for the tool call. + CallID string `json:"call_id,omitempty"` + + // Name specifies the function tool invoked. + Name string `json:"name"` + + // Result is the function tool result returned by the user + Result *ToolSearchResult `json:"result,omitempty"` +} + +func (t *ToolSearchFunctionToolResult) String() string { + if t.Result != nil { + return t.Result.String() + } + return "" +} + +type ServerToolCall struct { + // Name specifies the server-side tool invoked. + // Supplied by the model server (e.g., `web_search` for OpenAI, `googleSearch` for Gemini). + Name string `json:"name"` + + // CallID is the unique identifier for the tool call. + // Empty if not provided by the model server. + CallID string `json:"call_id,omitempty"` + + // Arguments are the raw inputs to the server-side tool, + // supplied by the component implementer. + Arguments any `json:"arguments,omitempty"` +} + +type ServerToolResult struct { + // Name specifies the server-side tool invoked. + // Supplied by the model server (e.g., `web_search` for OpenAI, `googleSearch` for Gemini). + Name string `json:"name"` + + // CallID is the unique identifier for the tool call. + // Empty if not provided by the model server. + CallID string `json:"call_id,omitempty"` + + // Content refers to the raw output generated by the server-side tool, + // supplied by the component implementer. + Content any `json:"content,omitempty"` +} + +type MCPToolCall struct { + // ServerLabel is the MCP server label used to identify it in tool calls + ServerLabel string `json:"server_label,omitempty"` + + // ApprovalRequestID is the approval request ID. + ApprovalRequestID string `json:"approval_request_id,omitempty"` + + // CallID is the unique ID of the tool call. + CallID string `json:"call_id,omitempty"` + + // Name is the name of the tool to run. + Name string `json:"name"` + + // Arguments is the JSON string arguments for the tool call. + Arguments string `json:"arguments,omitempty"` +} + +type MCPToolResult struct { + // ServerLabel is the MCP server label used to identify it in tool calls + ServerLabel string `json:"server_label,omitempty"` + + // CallID is the unique ID of the tool call. + CallID string `json:"call_id,omitempty"` + + // Name is the name of the tool to run. + Name string `json:"name"` + + // Content is the JSON string with the tool result. + Content string `json:"content,omitempty"` + + // Error returned when the server fails to run the tool. + Error *MCPToolCallError `json:"error,omitempty"` +} + +type MCPToolCallError struct { + // Code is the error code. + Code *int64 `json:"code,omitempty"` + + // Message is the error message. + Message string `json:"message,omitempty"` +} + +type MCPListToolsResult struct { + // ServerLabel is the MCP server label used to identify it in tool calls. + ServerLabel string `json:"server_label,omitempty"` + + // Tools is the list of tools available on the server. + Tools []*MCPListToolsItem `json:"tools,omitempty"` + + // Error returned when the server fails to list tools. + Error string `json:"error,omitempty"` +} + +type MCPListToolsItem struct { + // Name is the name of the tool. + Name string `json:"name"` + + // Description is the description of the tool. + Description string `json:"description"` + + // InputSchema is the JSON schema that describes the tool input parameters. + InputSchema *jsonschema.Schema `json:"input_schema,omitempty"` +} + +type mcpListToolsItemGob struct { + Name string + Description string + InputSchemaJSON []byte +} + +func (m *MCPListToolsItem) GobEncode() ([]byte, error) { + g := mcpListToolsItemGob{ + Name: m.Name, + Description: m.Description, + } + if m.InputSchema != nil { + b, err := json.Marshal(m.InputSchema) + if err != nil { + return nil, fmt.Errorf("failed to marshal MCPListToolsItem.InputSchema: %w", err) + } + g.InputSchemaJSON = b + } + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(&g); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (m *MCPListToolsItem) GobDecode(data []byte) error { + var g mcpListToolsItemGob + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&g); err != nil { + return err + } + m.Name = g.Name + m.Description = g.Description + if len(g.InputSchemaJSON) > 0 { + m.InputSchema = &jsonschema.Schema{} + if err := sonic.Unmarshal(g.InputSchemaJSON, m.InputSchema); err != nil { + return fmt.Errorf("failed to unmarshal MCPListToolsItem.InputSchema: %w", err) + } + } + return nil +} + +type MCPToolApprovalRequest struct { + // ID is the approval request ID. + ID string `json:"id,omitempty"` + + // Name is the name of the tool to run. + Name string `json:"name"` + + // Arguments is the JSON string arguments for the tool call. + Arguments string `json:"arguments,omitempty"` + + // ServerLabel is the MCP server label used to identify it in tool calls. + ServerLabel string `json:"server_label,omitempty"` +} + +type MCPToolApprovalResponse struct { + // ApprovalRequestID is the approval request ID being responded to. + ApprovalRequestID string `json:"approval_request_id,omitempty"` + + // Approve indicates whether the request is approved. + Approve bool `json:"approve"` + + // Reason is the rationale for the decision. + // Optional. + Reason string `json:"reason,omitempty"` +} + +// SystemAgenticMessage represents a message with AgenticRoleType "system". +func SystemAgenticMessage(text string) *AgenticMessage { + return &AgenticMessage{ + Role: AgenticRoleTypeSystem, + ContentBlocks: []*ContentBlock{NewContentBlock(&UserInputText{Text: text})}, + } +} + +// UserAgenticMessage represents a message with AgenticRoleType "user". +func UserAgenticMessage(text string) *AgenticMessage { + return &AgenticMessage{ + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{NewContentBlock(&UserInputText{Text: text})}, + } +} + +type contentBlockVariant interface { + Reasoning | userInputVariant | assistantGenVariant | functionToolCallVariant | serverToolCallVariant | mcpToolCallVariant +} + +type userInputVariant interface { + UserInputText | UserInputImage | UserInputAudio | UserInputVideo | UserInputFile +} + +type assistantGenVariant interface { + AssistantGenText | AssistantGenImage | AssistantGenAudio | AssistantGenVideo +} + +type functionToolCallVariant interface { + FunctionToolCall | FunctionToolResult | ToolSearchFunctionToolResult +} + +type serverToolCallVariant interface { + ServerToolCall | ServerToolResult +} + +type mcpToolCallVariant interface { + MCPToolCall | MCPToolResult | MCPListToolsResult | MCPToolApprovalRequest | MCPToolApprovalResponse +} + +// NewContentBlock creates a new ContentBlock with the given content. +func NewContentBlock[T contentBlockVariant](content *T) *ContentBlock { + switch b := any(content).(type) { + case *Reasoning: + return &ContentBlock{Type: ContentBlockTypeReasoning, Reasoning: b} + case *UserInputText: + return &ContentBlock{Type: ContentBlockTypeUserInputText, UserInputText: b} + case *UserInputImage: + return &ContentBlock{Type: ContentBlockTypeUserInputImage, UserInputImage: b} + case *UserInputAudio: + return &ContentBlock{Type: ContentBlockTypeUserInputAudio, UserInputAudio: b} + case *UserInputVideo: + return &ContentBlock{Type: ContentBlockTypeUserInputVideo, UserInputVideo: b} + case *UserInputFile: + return &ContentBlock{Type: ContentBlockTypeUserInputFile, UserInputFile: b} + case *ToolSearchFunctionToolResult: + return &ContentBlock{Type: ContentBlockTypeToolSearchResult, ToolSearchFunctionToolResult: b} + case *AssistantGenText: + return &ContentBlock{Type: ContentBlockTypeAssistantGenText, AssistantGenText: b} + case *AssistantGenImage: + return &ContentBlock{Type: ContentBlockTypeAssistantGenImage, AssistantGenImage: b} + case *AssistantGenAudio: + return &ContentBlock{Type: ContentBlockTypeAssistantGenAudio, AssistantGenAudio: b} + case *AssistantGenVideo: + return &ContentBlock{Type: ContentBlockTypeAssistantGenVideo, AssistantGenVideo: b} + case *FunctionToolCall: + return &ContentBlock{Type: ContentBlockTypeFunctionToolCall, FunctionToolCall: b} + case *FunctionToolResult: + return &ContentBlock{Type: ContentBlockTypeFunctionToolResult, FunctionToolResult: b} + case *ServerToolCall: + return &ContentBlock{Type: ContentBlockTypeServerToolCall, ServerToolCall: b} + case *ServerToolResult: + return &ContentBlock{Type: ContentBlockTypeServerToolResult, ServerToolResult: b} + case *MCPToolCall: + return &ContentBlock{Type: ContentBlockTypeMCPToolCall, MCPToolCall: b} + case *MCPToolResult: + return &ContentBlock{Type: ContentBlockTypeMCPToolResult, MCPToolResult: b} + case *MCPListToolsResult: + return &ContentBlock{Type: ContentBlockTypeMCPListToolsResult, MCPListToolsResult: b} + case *MCPToolApprovalRequest: + return &ContentBlock{Type: ContentBlockTypeMCPToolApprovalRequest, MCPToolApprovalRequest: b} + case *MCPToolApprovalResponse: + return &ContentBlock{Type: ContentBlockTypeMCPToolApprovalResponse, MCPToolApprovalResponse: b} + default: + return nil + } +} + +// NewContentBlockChunk creates a new ContentBlock with the given content and streaming metadata. +func NewContentBlockChunk[T contentBlockVariant](content *T, meta *StreamingMeta) *ContentBlock { + block := NewContentBlock(content) + block.StreamingMeta = meta + return block +} + +// AgenticMessagesTemplate is the interface for agentic messages template. +// It's used to render a template to a list of agentic messages. +// e.g. +// +// chatTemplate := prompt.FromAgenticMessages( +// &schema.AgenticMessage{ +// Role: schema.AgenticRoleTypeSystem, +// ContentBlocks: []*schema.ContentBlock{ +// {Type: schema.ContentBlockTypeUserInputText, UserInputText: &schema.UserInputText{Text: "you are an eino helper"}}, +// }, +// }, +// schema.AgenticMessagesPlaceholder("history", false), // <= this will use the value of "history" in params +// ) +// msgs, err := chatTemplate.Format(ctx, params) +type AgenticMessagesTemplate interface { + Format(ctx context.Context, vs map[string]any, formatType FormatType) ([]*AgenticMessage, error) +} + +var _ AgenticMessagesTemplate = &AgenticMessage{} +var _ AgenticMessagesTemplate = AgenticMessagesPlaceholder("", false) + +type agenticMessagesPlaceholder struct { + key string + optional bool +} + +// AgenticMessagesPlaceholder can render a placeholder to a list of agentic messages in params. +// e.g. +// +// placeholder := AgenticMessagesPlaceholder("history", false) +// params := map[string]any{ +// "history": []*schema.AgenticMessage{ +// &schema.AgenticMessage{ +// Role: schema.AgenticRoleTypeSystem, +// ContentBlocks: []*schema.ContentBlock{ +// {Type: schema.ContentBlockTypeUserInputText, UserInputText: &schema.UserInputText{Text: "you are an eino helper"}}, +// }, +// }, +// }, +// } +// chatTemplate := chatTpl := prompt.FromMessages( +// schema.AgenticMessagesPlaceholder("history", false), // <= this will use the value of "history" in params +// ) +// msgs, err := chatTemplate.Format(ctx, params) +func AgenticMessagesPlaceholder(key string, optional bool) AgenticMessagesTemplate { + return &agenticMessagesPlaceholder{ + key: key, + optional: optional, + } +} + +func (p *agenticMessagesPlaceholder) Format(_ context.Context, vs map[string]any, _ FormatType) ([]*AgenticMessage, error) { + v, ok := vs[p.key] + if !ok { + if p.optional { + return []*AgenticMessage{}, nil + } + + return nil, fmt.Errorf("message placeholder format: %s not found", p.key) + } + + msgs, ok := v.([]*AgenticMessage) + if !ok { + return nil, fmt.Errorf("only agentic messages can be used to format message placeholder, key: %v, actual type: %v", p.key, reflect.TypeOf(v)) + } + + return msgs, nil +} + +// Format returns the agentic messages after rendering by the given formatType. +// It formats only the user input fields (UserInputText, UserInputImage, UserInputAudio, UserInputVideo, UserInputFile). +// e.g. +// +// msg := &schema.AgenticMessage{ +// Role: schema.AgenticRoleTypeUser, +// ContentBlocks: []*schema.ContentBlock{ +// {Type: schema.ContentBlockTypeUserInputText, UserInputText: &schema.UserInputText{Text: "hello {name}"}}, +// }, +// } +// msgs, err := msg.Format(ctx, map[string]any{"name": "eino"}, schema.FString) +// // msgs[0].ContentBlocks[0].UserInputText.Text will be "hello eino" +func (m *AgenticMessage) Format(_ context.Context, vs map[string]any, formatType FormatType) ([]*AgenticMessage, error) { + copied := *m + + if len(m.ContentBlocks) > 0 { + copiedBlocks := make([]*ContentBlock, len(m.ContentBlocks)) + for i, block := range m.ContentBlocks { + if block == nil { + copiedBlocks[i] = nil + continue + } + + copiedBlock := *block + var err error + + switch block.Type { + case ContentBlockTypeUserInputText: + if block.UserInputText != nil { + copiedBlock.UserInputText, err = formatUserInputText(block.UserInputText, vs, formatType) + if err != nil { + return nil, err + } + } + case ContentBlockTypeUserInputImage: + if block.UserInputImage != nil { + copiedBlock.UserInputImage, err = formatUserInputImage(block.UserInputImage, vs, formatType) + if err != nil { + return nil, err + } + } + case ContentBlockTypeUserInputAudio: + if block.UserInputAudio != nil { + copiedBlock.UserInputAudio, err = formatUserInputAudio(block.UserInputAudio, vs, formatType) + if err != nil { + return nil, err + } + } + case ContentBlockTypeUserInputVideo: + if block.UserInputVideo != nil { + copiedBlock.UserInputVideo, err = formatUserInputVideo(block.UserInputVideo, vs, formatType) + if err != nil { + return nil, err + } + } + case ContentBlockTypeUserInputFile: + if block.UserInputFile != nil { + copiedBlock.UserInputFile, err = formatUserInputFile(block.UserInputFile, vs, formatType) + if err != nil { + return nil, err + } + } + } + + copiedBlocks[i] = &copiedBlock + } + copied.ContentBlocks = copiedBlocks + } + + return []*AgenticMessage{&copied}, nil +} + +func formatUserInputText(uit *UserInputText, vs map[string]any, formatType FormatType) (*UserInputText, error) { + text, err := formatContent(uit.Text, vs, formatType) + if err != nil { + return nil, err + } + copied := *uit + copied.Text = text + return &copied, nil +} + +func formatUserInputImage(uii *UserInputImage, vs map[string]any, formatType FormatType) (*UserInputImage, error) { + copied := *uii + if uii.URL != "" { + url, err := formatContent(uii.URL, vs, formatType) + if err != nil { + return nil, err + } + copied.URL = url + } + if uii.Base64Data != "" { + base64data, err := formatContent(uii.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copied.Base64Data = base64data + } + return &copied, nil +} + +func formatUserInputAudio(uia *UserInputAudio, vs map[string]any, formatType FormatType) (*UserInputAudio, error) { + copied := *uia + if uia.URL != "" { + url, err := formatContent(uia.URL, vs, formatType) + if err != nil { + return nil, err + } + copied.URL = url + } + if uia.Base64Data != "" { + base64data, err := formatContent(uia.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copied.Base64Data = base64data + } + return &copied, nil +} + +func formatUserInputVideo(uiv *UserInputVideo, vs map[string]any, formatType FormatType) (*UserInputVideo, error) { + copied := *uiv + if uiv.URL != "" { + url, err := formatContent(uiv.URL, vs, formatType) + if err != nil { + return nil, err + } + copied.URL = url + } + if uiv.Base64Data != "" { + base64data, err := formatContent(uiv.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copied.Base64Data = base64data + } + return &copied, nil +} + +func formatUserInputFile(uif *UserInputFile, vs map[string]any, formatType FormatType) (*UserInputFile, error) { + copied := *uif + if uif.URL != "" { + url, err := formatContent(uif.URL, vs, formatType) + if err != nil { + return nil, err + } + copied.URL = url + } + if uif.Name != "" { + name, err := formatContent(uif.Name, vs, formatType) + if err != nil { + return nil, err + } + copied.Name = name + } + if uif.Base64Data != "" { + base64data, err := formatContent(uif.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copied.Base64Data = base64data + } + return &copied, nil +} + +// ConcatAgenticMessagesArray concatenates multiple streams of AgenticMessage into a single slice of AgenticMessage. +func ConcatAgenticMessagesArray(mas [][]*AgenticMessage) ([]*AgenticMessage, error) { + return buildConcatGenericArray[AgenticMessage](ConcatAgenticMessages)(mas) +} + +// ConcatAgenticMessages concatenates a list of AgenticMessage chunks into a single AgenticMessage. +func ConcatAgenticMessages(msgs []*AgenticMessage) (*AgenticMessage, error) { + var ( + role AgenticRoleType + blocks []*ContentBlock + metas []*AgenticResponseMeta + extra map[string]any + blockIndices []int + indexToBlocks = map[int][]*ContentBlock{} + extraList = make([]map[string]any, 0, len(msgs)) + ) + + if len(msgs) == 1 { + return msgs[0], nil + } + + for idx, msg := range msgs { + if msg == nil { + return nil, fmt.Errorf("message at index %d is nil", idx) + } + + if msg.Role != "" { + if role == "" { + role = msg.Role + } else if role != msg.Role { + return nil, fmt.Errorf("cannot concat messages with different roles: got '%s' and '%s'", role, msg.Role) + } + } + + for _, block := range msg.ContentBlocks { + if block == nil { + continue + } + if block.StreamingMeta == nil { + // Non-streaming block + if len(blockIndices) > 0 { + // Cannot mix streaming and non-streaming blocks + return nil, fmt.Errorf("found non-streaming block after streaming blocks") + } + // Collect non-streaming block + blocks = append(blocks, block) + } else { + // Streaming block + if len(blocks) > 0 { + // Cannot mix non-streaming and streaming blocks + return nil, fmt.Errorf("found streaming block after non-streaming blocks") + } + // Collect streaming block by index + if blocks_, ok := indexToBlocks[block.StreamingMeta.Index]; ok { + indexToBlocks[block.StreamingMeta.Index] = append(blocks_, block) + } else { + blockIndices = append(blockIndices, block.StreamingMeta.Index) + indexToBlocks[block.StreamingMeta.Index] = []*ContentBlock{block} + } + } + } + + if msg.ResponseMeta != nil { + metas = append(metas, msg.ResponseMeta) + } + + if msg.Extra != nil { + extraList = append(extraList, msg.Extra) + } + } + + meta, err := concatAgenticResponseMeta(metas) + if err != nil { + return nil, fmt.Errorf("failed to concat agentic response meta: %w", err) + } + + if len(blockIndices) > 0 { + // All blocks are streaming, concat each group by index + indexToBlock := map[int]*ContentBlock{} + for idx, bs := range indexToBlocks { + var b *ContentBlock + b, err = concatChunksOfSameContentBlock(bs) + if err != nil { + return nil, err + } + indexToBlock[idx] = b + } + blocks = make([]*ContentBlock, 0, len(blockIndices)) + sort.Slice(blockIndices, func(i, j int) bool { + return blockIndices[i] < blockIndices[j] + }) + for _, idx := range blockIndices { + blocks = append(blocks, indexToBlock[idx]) + } + } + + if len(extraList) > 0 { + extra, err = concatExtra(extraList) + if err != nil { + return nil, err + } + } + + return &AgenticMessage{ + Role: role, + ResponseMeta: meta, + ContentBlocks: blocks, + Extra: extra, + }, nil +} + +func concatAgenticResponseMeta(metas []*AgenticResponseMeta) (ret *AgenticResponseMeta, err error) { + if len(metas) == 0 { + return nil, nil + } + + openaiExtensions := make([]*openai.ResponseMetaExtension, 0, len(metas)) + claudeExtensions := make([]*claude.ResponseMetaExtension, 0, len(metas)) + geminiExtensions := make([]*gemini.ResponseMetaExtension, 0, len(metas)) + tokenUsages := make([]*TokenUsage, 0, len(metas)) + + var ( + extType reflect.Type + extensions reflect.Value + ) + + for _, meta := range metas { + if meta.TokenUsage != nil { + tokenUsages = append(tokenUsages, meta.TokenUsage) + } + + var isConsistent bool + + if meta.Extension != nil { + extType, isConsistent = validateExtensionType(extType, meta.Extension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'", + extType, reflect.TypeOf(meta.Extension)) + } + if !extensions.IsValid() { + extensions = reflect.MakeSlice(reflect.SliceOf(extType), 0, len(metas)) + } + extensions = reflect.Append(extensions, reflect.ValueOf(meta.Extension)) + } + + if meta.OpenAIExtension != nil { + extType, isConsistent = validateExtensionType(extType, meta.OpenAIExtension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'", + extType, reflect.TypeOf(meta.OpenAIExtension)) + } + openaiExtensions = append(openaiExtensions, meta.OpenAIExtension) + } + + if meta.ClaudeExtension != nil { + extType, isConsistent = validateExtensionType(extType, meta.ClaudeExtension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'", + extType, reflect.TypeOf(meta.ClaudeExtension)) + } + claudeExtensions = append(claudeExtensions, meta.ClaudeExtension) + } + + if meta.GeminiExtension != nil { + extType, isConsistent = validateExtensionType(extType, meta.GeminiExtension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'", + extType, reflect.TypeOf(meta.GeminiExtension)) + } + geminiExtensions = append(geminiExtensions, meta.GeminiExtension) + } + } + + ret = &AgenticResponseMeta{ + TokenUsage: concatTokenUsage(tokenUsages), + } + + if extensions.IsValid() && !extensions.IsZero() { + var extension reflect.Value + extension, err = internal.ConcatSliceValue(extensions) + if err != nil { + return nil, fmt.Errorf("failed to concat extensions: %w", err) + } + ret.Extension = extension.Interface() + } + + if len(openaiExtensions) > 0 { + ret.OpenAIExtension, err = openai.ConcatResponseMetaExtensions(openaiExtensions) + if err != nil { + return nil, fmt.Errorf("failed to concat openai extensions: %w", err) + } + } + + if len(claudeExtensions) > 0 { + ret.ClaudeExtension, err = claude.ConcatResponseMetaExtensions(claudeExtensions) + if err != nil { + return nil, fmt.Errorf("failed to concat claude extensions: %w", err) + } + } + + if len(geminiExtensions) > 0 { + ret.GeminiExtension, err = gemini.ConcatResponseMetaExtensions(geminiExtensions) + if err != nil { + return nil, fmt.Errorf("failed to concat gemini extensions: %w", err) + } + } + + return ret, nil +} + +func concatTokenUsage(usages []*TokenUsage) *TokenUsage { + if len(usages) == 0 { + return nil + } + + ret := &TokenUsage{} + + for _, usage := range usages { + if usage == nil { + continue + } + + if usage.PromptTokens > ret.PromptTokens { + ret.PromptTokens = usage.PromptTokens + } + if usage.CompletionTokens > ret.CompletionTokens { + ret.CompletionTokens = usage.CompletionTokens + } + if usage.TotalTokens > ret.TotalTokens { + ret.TotalTokens = usage.TotalTokens + } + if usage.PromptTokenDetails.CachedTokens > ret.PromptTokenDetails.CachedTokens { + ret.PromptTokenDetails.CachedTokens = usage.PromptTokenDetails.CachedTokens + } + if usage.CompletionTokensDetails.ReasoningTokens > ret.CompletionTokensDetails.ReasoningTokens { + ret.CompletionTokensDetails.ReasoningTokens = usage.CompletionTokensDetails.ReasoningTokens + } + } + + return ret +} + +func concatChunksOfSameContentBlock(blocks []*ContentBlock) (*ContentBlock, error) { + if len(blocks) == 0 { + return nil, fmt.Errorf("no content blocks to concat") + } + + blockType := blocks[0].Type + + switch blockType { + case ContentBlockTypeReasoning: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *Reasoning { return b.Reasoning }, + concatReasoning) + + case ContentBlockTypeUserInputText: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *UserInputText { return b.UserInputText }, + concatUserInputTexts) + + case ContentBlockTypeUserInputImage: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *UserInputImage { return b.UserInputImage }, + concatUserInputImages) + + case ContentBlockTypeUserInputAudio: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *UserInputAudio { return b.UserInputAudio }, + concatUserInputAudios) + + case ContentBlockTypeUserInputVideo: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *UserInputVideo { return b.UserInputVideo }, + concatUserInputVideos) + + case ContentBlockTypeUserInputFile: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *UserInputFile { return b.UserInputFile }, + concatUserInputFiles) + + case ContentBlockTypeToolSearchResult: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *ToolSearchFunctionToolResult { return b.ToolSearchFunctionToolResult }, + concatToolSearchFunctionToolResult) + + case ContentBlockTypeAssistantGenText: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *AssistantGenText { return b.AssistantGenText }, + concatAssistantGenTexts) + + case ContentBlockTypeAssistantGenImage: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *AssistantGenImage { return b.AssistantGenImage }, + concatAssistantGenImages) + + case ContentBlockTypeAssistantGenAudio: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *AssistantGenAudio { return b.AssistantGenAudio }, + concatAssistantGenAudios) + + case ContentBlockTypeAssistantGenVideo: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *AssistantGenVideo { return b.AssistantGenVideo }, + concatAssistantGenVideos) + + case ContentBlockTypeFunctionToolCall: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *FunctionToolCall { return b.FunctionToolCall }, + concatFunctionToolCalls) + + case ContentBlockTypeFunctionToolResult: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *FunctionToolResult { return b.FunctionToolResult }, + concatFunctionToolResults) + + case ContentBlockTypeServerToolCall: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *ServerToolCall { return b.ServerToolCall }, + concatServerToolCalls) + + case ContentBlockTypeServerToolResult: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *ServerToolResult { return b.ServerToolResult }, + concatServerToolResults) + + case ContentBlockTypeMCPToolCall: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *MCPToolCall { return b.MCPToolCall }, + concatMCPToolCalls) + + case ContentBlockTypeMCPToolResult: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *MCPToolResult { return b.MCPToolResult }, + concatMCPToolResults) + + case ContentBlockTypeMCPListToolsResult: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *MCPListToolsResult { return b.MCPListToolsResult }, + concatMCPListToolsResults) + + case ContentBlockTypeMCPToolApprovalRequest: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *MCPToolApprovalRequest { return b.MCPToolApprovalRequest }, + concatMCPToolApprovalRequests) + + case ContentBlockTypeMCPToolApprovalResponse: + return concatContentBlockHelper(blocks, blockType, + func(b *ContentBlock) *MCPToolApprovalResponse { return b.MCPToolApprovalResponse }, + concatMCPToolApprovalResponses) + + default: + return nil, fmt.Errorf("unknown content block type: %s", blockType) + } +} + +// concatContentBlockHelper is a generic helper function that reduces code duplication +// for concatenating content blocks of a specific type. +func concatContentBlockHelper[T contentBlockVariant]( + blocks []*ContentBlock, + expectedType ContentBlockType, + getter func(*ContentBlock) *T, + concatFunc func([]*T) (*T, error), +) (*ContentBlock, error) { + items, err := genericGetTFromContentBlocks(blocks, func(block *ContentBlock) (*T, error) { + if block.Type != expectedType { + return nil, fmt.Errorf("content block type mismatch: expected '%s', but got '%s'", expectedType, block.Type) + } + item := getter(block) + if item == nil { + return nil, fmt.Errorf("'%s' content is nil", expectedType) + } + return item, nil + }) + if err != nil { + return nil, err + } + + concatenated, err := concatFunc(items) + if err != nil { + return nil, fmt.Errorf("failed to concat '%s' content blocks: %w", expectedType, err) + } + + extras := make([]map[string]any, 0, len(blocks)) + for _, block := range blocks { + if len(block.Extra) > 0 { + extras = append(extras, block.Extra) + } + } + + var extra map[string]any + if len(extras) > 0 { + extra, err = internal.ConcatItems(extras) + if err != nil { + return nil, fmt.Errorf("failed to concat content block extras: %w", err) + } + } + + block := NewContentBlock(concatenated) + block.Extra = extra + + return block, nil +} + +func genericGetTFromContentBlocks[T any](blocks []*ContentBlock, checkAndGetter func(block *ContentBlock) (T, error)) ([]T, error) { + ret := make([]T, 0, len(blocks)) + for _, block := range blocks { + t, err := checkAndGetter(block) + if err != nil { + return nil, err + } + ret = append(ret, t) + } + return ret, nil +} + +func concatReasoning(reasons []*Reasoning) (ret *Reasoning, err error) { + if len(reasons) == 0 { + return nil, fmt.Errorf("no reasoning found") + } + + ret = &Reasoning{} + + openaiExtensions := make([]*openai.ReasoningExtension, 0, len(reasons)) + + for _, r := range reasons { + if r == nil { + continue + } + if r.Text != "" { + ret.Text += r.Text + } + if r.Signature != "" { + ret.Signature += r.Signature + } + if r.OpenAIExtension != nil { + openaiExtensions = append(openaiExtensions, r.OpenAIExtension) + } + } + + if len(openaiExtensions) > 0 { + ret.OpenAIExtension, err = openai.ConcatReasoningExtensions(openaiExtensions) + if err != nil { + return nil, fmt.Errorf("failed to concat openai reasoning extensions: %w", err) + } + } + + return ret, nil +} + +func concatUserInputTexts(texts []*UserInputText) (*UserInputText, error) { + if len(texts) == 0 { + return nil, fmt.Errorf("no user input text found") + } + if len(texts) == 1 { + return texts[0], nil + } + return nil, fmt.Errorf("cannot concat multiple user input texts") +} + +func concatUserInputImages(images []*UserInputImage) (*UserInputImage, error) { + if len(images) == 0 { + return nil, fmt.Errorf("no user input image found") + } + if len(images) == 1 { + return images[0], nil + } + return nil, fmt.Errorf("cannot concat multiple user input images") +} + +func concatUserInputAudios(audios []*UserInputAudio) (*UserInputAudio, error) { + if len(audios) == 0 { + return nil, fmt.Errorf("no user input audio found") + } + if len(audios) == 1 { + return audios[0], nil + } + return nil, fmt.Errorf("cannot concat multiple user input audios") +} + +func concatUserInputVideos(videos []*UserInputVideo) (*UserInputVideo, error) { + if len(videos) == 0 { + return nil, fmt.Errorf("no user input video found") + } + if len(videos) == 1 { + return videos[0], nil + } + return nil, fmt.Errorf("cannot concat multiple user input videos") +} + +func concatUserInputFiles(files []*UserInputFile) (*UserInputFile, error) { + if len(files) == 0 { + return nil, fmt.Errorf("no user input file found") + } + if len(files) == 1 { + return files[0], nil + } + return nil, fmt.Errorf("cannot concat multiple user input files") +} + +func concatToolSearchFunctionToolResult(results []*ToolSearchFunctionToolResult) (*ToolSearchFunctionToolResult, error) { + if len(results) == 0 { + return nil, fmt.Errorf("no tool search results found") + } + if len(results) == 1 { + return results[0], nil + } + return nil, fmt.Errorf("cannot concat multiple tool search results") +} + +func concatAssistantGenTexts(texts []*AssistantGenText) (ret *AssistantGenText, err error) { + if len(texts) == 0 { + return nil, fmt.Errorf("no assistant generated text found") + } + if len(texts) == 1 { + return texts[0], nil + } + + ret = &AssistantGenText{} + + openaiExtensions := make([]*openai.AssistantGenTextExtension, 0, len(texts)) + claudeExtensions := make([]*claude.AssistantGenTextExtension, 0, len(texts)) + + var ( + extType reflect.Type + extensions reflect.Value + ) + + for _, t := range texts { + if t == nil { + continue + } + + ret.Text += t.Text + + var isConsistent bool + + if t.Extension != nil { + extType, isConsistent = validateExtensionType(extType, t.Extension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in assistant generated text chunks: '%s' vs '%s'", + extType, reflect.TypeOf(t.Extension)) + } + if !extensions.IsValid() { + extensions = reflect.MakeSlice(reflect.SliceOf(extType), 0, len(texts)) + } + extensions = reflect.Append(extensions, reflect.ValueOf(t.Extension)) + } + + if t.OpenAIExtension != nil { + extType, isConsistent = validateExtensionType(extType, t.OpenAIExtension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in assistant generated text chunks: '%s' vs '%s'", + extType, reflect.TypeOf(t.OpenAIExtension)) + } + openaiExtensions = append(openaiExtensions, t.OpenAIExtension) + } + + if t.ClaudeExtension != nil { + extType, isConsistent = validateExtensionType(extType, t.ClaudeExtension) + if !isConsistent { + return nil, fmt.Errorf("inconsistent extension types in assistant generated text chunks: '%s' vs '%s'", + extType, reflect.TypeOf(t.ClaudeExtension)) + } + claudeExtensions = append(claudeExtensions, t.ClaudeExtension) + } + } + + if extensions.IsValid() && !extensions.IsZero() { + ret.Extension, err = internal.ConcatSliceValue(extensions) + if err != nil { + return nil, err + } + } + + if len(openaiExtensions) > 0 { + ret.OpenAIExtension, err = openai.ConcatAssistantGenTextExtensions(openaiExtensions) + if err != nil { + return nil, err + } + } + + if len(claudeExtensions) > 0 { + ret.ClaudeExtension, err = claude.ConcatAssistantGenTextExtensions(claudeExtensions) + if err != nil { + return nil, err + } + } + + return ret, nil +} + +func concatAssistantGenImages(images []*AssistantGenImage) (*AssistantGenImage, error) { + if len(images) == 0 { + return nil, fmt.Errorf("no assistant gen image found") + } + if len(images) == 1 { + return images[0], nil + } + + ret := &AssistantGenImage{} + + for _, img := range images { + if img == nil { + continue + } + + ret.Base64Data += img.Base64Data + + if ret.URL == "" { + ret.URL = img.URL + } else if img.URL != "" && ret.URL != img.URL { + return nil, fmt.Errorf("inconsistent URLs in assistant generated image chunks: '%s' vs '%s'", ret.URL, img.URL) + } + + if ret.MIMEType == "" { + ret.MIMEType = img.MIMEType + } else if img.MIMEType != "" && ret.MIMEType != img.MIMEType { + return nil, fmt.Errorf("inconsistent MIME types in assistant generated image chunks: '%s' vs '%s'", ret.MIMEType, img.MIMEType) + } + } + + return ret, nil +} + +func concatAssistantGenAudios(audios []*AssistantGenAudio) (*AssistantGenAudio, error) { + if len(audios) == 0 { + return nil, fmt.Errorf("no assistant gen audio found") + } + if len(audios) == 1 { + return audios[0], nil + } + + ret := &AssistantGenAudio{} + + for _, audio := range audios { + if audio == nil { + continue + } + + ret.Base64Data += audio.Base64Data + + if ret.URL == "" { + ret.URL = audio.URL + } else if audio.URL != "" && ret.URL != audio.URL { + return nil, fmt.Errorf("inconsistent URLs in assistant generated audio chunks: '%s' vs '%s'", ret.URL, audio.URL) + } + + if ret.MIMEType == "" { + ret.MIMEType = audio.MIMEType + } else if audio.MIMEType != "" && ret.MIMEType != audio.MIMEType { + return nil, fmt.Errorf("inconsistent MIME types in assistant generated audio chunks: '%s' vs '%s'", ret.MIMEType, audio.MIMEType) + } + } + + return ret, nil +} + +func concatAssistantGenVideos(videos []*AssistantGenVideo) (*AssistantGenVideo, error) { + if len(videos) == 0 { + return nil, fmt.Errorf("no assistant gen video found") + } + if len(videos) == 1 { + return videos[0], nil + } + + ret := &AssistantGenVideo{} + + for _, video := range videos { + if video == nil { + continue + } + + ret.Base64Data += video.Base64Data + + if ret.URL == "" { + ret.URL = video.URL + } else if video.URL != "" && ret.URL != video.URL { + return nil, fmt.Errorf("inconsistent URLs in assistant generated video chunks: '%s' vs '%s'", ret.URL, video.URL) + } + + if ret.MIMEType == "" { + ret.MIMEType = video.MIMEType + } else if video.MIMEType != "" && ret.MIMEType != video.MIMEType { + return nil, fmt.Errorf("inconsistent MIME types in assistant generated video chunks: '%s' vs '%s'", ret.MIMEType, video.MIMEType) + } + } + + return ret, nil +} + +func concatFunctionToolCalls(calls []*FunctionToolCall) (*FunctionToolCall, error) { + if len(calls) == 0 { + return nil, fmt.Errorf("no function tool call found") + } + if len(calls) == 1 { + return calls[0], nil + } + + ret := &FunctionToolCall{} + + for _, c := range calls { + if c == nil { + continue + } + + if ret.CallID == "" { + ret.CallID = c.CallID + } else if c.CallID != "" && c.CallID != ret.CallID { + return nil, fmt.Errorf("expected call ID '%s' for function tool call, but got '%s'", ret.CallID, c.CallID) + } + + if ret.Name == "" { + ret.Name = c.Name + } else if c.Name != "" && c.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for function tool call, but got '%s'", ret.Name, c.Name) + } + + ret.Arguments += c.Arguments + } + + return ret, nil +} + +func concatFunctionToolResults(results []*FunctionToolResult) (*FunctionToolResult, error) { + if len(results) == 0 { + return nil, fmt.Errorf("no function tool result found") + } + if len(results) == 1 { + return results[0], nil + } + + ret := &FunctionToolResult{} + + for _, r := range results { + if r == nil { + continue + } + + if ret.CallID == "" { + ret.CallID = r.CallID + } else if r.CallID != "" && r.CallID != ret.CallID { + return nil, fmt.Errorf("expected call ID '%s' for function tool result, but got '%s'", ret.CallID, r.CallID) + } + + if ret.Name == "" { + ret.Name = r.Name + } else if r.Name != "" && r.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for function tool result, but got '%s'", ret.Name, r.Name) + } + + for _, b := range r.Content { + if b == nil { + continue + } + ret.Content = append(ret.Content, b) + } + } + + return ret, nil +} + +func concatServerToolCalls(calls []*ServerToolCall) (ret *ServerToolCall, err error) { + if len(calls) == 0 { + return nil, fmt.Errorf("no server tool call found") + } + if len(calls) == 1 { + return calls[0], nil + } + + ret = &ServerToolCall{} + + var ( + argsType reflect.Type + argsChunks reflect.Value + ) + + for _, c := range calls { + if c == nil { + continue + } + + if ret.CallID == "" { + ret.CallID = c.CallID + } else if c.CallID != "" && c.CallID != ret.CallID { + return nil, fmt.Errorf("expected call ID '%s' for server tool call, but got '%s'", ret.CallID, c.CallID) + } + + if ret.Name == "" { + ret.Name = c.Name + } else if c.Name != "" && c.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for server tool call, but got '%s'", ret.Name, c.Name) + } + + if c.Arguments != nil { + argsType_ := reflect.TypeOf(c.Arguments) + if argsType == nil { + argsType = argsType_ + argsChunks = reflect.MakeSlice(reflect.SliceOf(argsType), 0, len(calls)) + } else if argsType != argsType_ { + return nil, fmt.Errorf("expected type '%s' for server tool call arguments, but got '%s'", argsType, argsType_) + } + argsChunks = reflect.Append(argsChunks, reflect.ValueOf(c.Arguments)) + } + } + + if argsChunks.IsValid() && !argsChunks.IsZero() { + arguments, err := internal.ConcatSliceValue(argsChunks) + if err != nil { + return nil, err + } + ret.Arguments = arguments.Interface() + } + + return ret, nil +} + +func concatServerToolResults(results []*ServerToolResult) (ret *ServerToolResult, err error) { + if len(results) == 0 { + return nil, fmt.Errorf("no server tool result found") + } + if len(results) == 1 { + return results[0], nil + } + + ret = &ServerToolResult{} + + var ( + resType reflect.Type + resChunks reflect.Value + ) + + for _, r := range results { + if r == nil { + continue + } + + if ret.CallID == "" { + ret.CallID = r.CallID + } else if r.CallID != "" && r.CallID != ret.CallID { + return nil, fmt.Errorf("expected call ID '%s' for server tool result, but got '%s'", ret.CallID, r.CallID) + } + + if ret.Name == "" { + ret.Name = r.Name + } else if r.Name != "" && r.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for server tool result, but got '%s'", ret.Name, r.Name) + } + + if r.Content != nil { + resType_ := reflect.TypeOf(r.Content) + if resType == nil { + resType = resType_ + resChunks = reflect.MakeSlice(reflect.SliceOf(resType), 0, len(results)) + } else if resType != resType_ { + return nil, fmt.Errorf("expected type '%s' for server tool result, but got '%s'", resType, resType_) + } + resChunks = reflect.Append(resChunks, reflect.ValueOf(r.Content)) + } + } + + if resChunks.IsValid() && !resChunks.IsZero() { + result, err := internal.ConcatSliceValue(resChunks) + if err != nil { + return nil, fmt.Errorf("failed to concat server tool result: %v", err) + } + ret.Content = result.Interface() + } + + return ret, nil +} + +func concatMCPToolCalls(calls []*MCPToolCall) (*MCPToolCall, error) { + if len(calls) == 0 { + return nil, fmt.Errorf("no mcp tool call found") + } + if len(calls) == 1 { + return calls[0], nil + } + + ret := &MCPToolCall{} + + for _, c := range calls { + if c == nil { + continue + } + + ret.Arguments += c.Arguments + + if ret.ServerLabel == "" { + ret.ServerLabel = c.ServerLabel + } else if c.ServerLabel != "" && c.ServerLabel != ret.ServerLabel { + return nil, fmt.Errorf("expected server label '%s' for mcp tool call, but got '%s'", ret.ServerLabel, c.ServerLabel) + } + + if ret.CallID == "" { + ret.CallID = c.CallID + } else if c.CallID != "" && c.CallID != ret.CallID { + return nil, fmt.Errorf("expected call ID '%s' for mcp tool call, but got '%s'", ret.CallID, c.CallID) + } + + if ret.Name == "" { + ret.Name = c.Name + } else if c.Name != "" && c.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for mcp tool call, but got '%s'", ret.Name, c.Name) + } + } + + return ret, nil +} + +func concatMCPToolResults(results []*MCPToolResult) (*MCPToolResult, error) { + if len(results) == 0 { + return nil, fmt.Errorf("no mcp tool result found") + } + if len(results) == 1 { + return results[0], nil + } + + ret := &MCPToolResult{} + + for _, r := range results { + if r == nil { + continue + } + + if r.Content != "" { + ret.Content = r.Content + } + + if ret.ServerLabel == "" { + ret.ServerLabel = r.ServerLabel + } else if r.ServerLabel != "" && r.ServerLabel != ret.ServerLabel { + return nil, fmt.Errorf("expected server label '%s' for mcp tool result, but got '%s'", ret.ServerLabel, r.ServerLabel) + } + + if ret.CallID == "" { + ret.CallID = r.CallID + } else if r.CallID != "" && r.CallID != ret.CallID { + return nil, fmt.Errorf("expected call ID '%s' for mcp tool result, but got '%s'", ret.CallID, r.CallID) + } + + if ret.Name == "" { + ret.Name = r.Name + } else if r.Name != "" && r.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for mcp tool result, but got '%s'", ret.Name, r.Name) + } + + if r.Error != nil { + ret.Error = r.Error + } + } + + return ret, nil +} + +func concatMCPListToolsResults(results []*MCPListToolsResult) (*MCPListToolsResult, error) { + if len(results) == 0 { + return nil, fmt.Errorf("no mcp list tools result found") + } + if len(results) == 1 { + return results[0], nil + } + + ret := &MCPListToolsResult{} + + for _, r := range results { + if r == nil { + continue + } + + ret.Tools = append(ret.Tools, r.Tools...) + + if r.Error != "" { + ret.Error = r.Error + } + + if ret.ServerLabel == "" { + ret.ServerLabel = r.ServerLabel + } else if r.ServerLabel != "" && r.ServerLabel != ret.ServerLabel { + return nil, fmt.Errorf("expected server label '%s' for mcp list tools result, but got '%s'", ret.ServerLabel, r.ServerLabel) + } + } + + return ret, nil +} + +func concatMCPToolApprovalRequests(requests []*MCPToolApprovalRequest) (*MCPToolApprovalRequest, error) { + if len(requests) == 0 { + return nil, fmt.Errorf("no mcp tool approval request found") + } + if len(requests) == 1 { + return requests[0], nil + } + + ret := &MCPToolApprovalRequest{} + + for _, r := range requests { + if r == nil { + continue + } + + ret.Arguments += r.Arguments + + if ret.ID == "" { + ret.ID = r.ID + } else if r.ID != "" && r.ID != ret.ID { + return nil, fmt.Errorf("expected request ID '%s' for mcp tool approval request, but got '%s'", ret.ID, r.ID) + } + + if ret.Name == "" { + ret.Name = r.Name + } else if r.Name != "" && r.Name != ret.Name { + return nil, fmt.Errorf("expected tool name '%s' for mcp tool approval request, but got '%s'", ret.Name, r.Name) + } + + if ret.ServerLabel == "" { + ret.ServerLabel = r.ServerLabel + } else if r.ServerLabel != "" && r.ServerLabel != ret.ServerLabel { + return nil, fmt.Errorf("expected server label '%s' for mcp tool approval request, but got '%s'", ret.ServerLabel, r.ServerLabel) + } + } + + return ret, nil +} + +func concatMCPToolApprovalResponses(responses []*MCPToolApprovalResponse) (*MCPToolApprovalResponse, error) { + if len(responses) == 0 { + return nil, fmt.Errorf("no mcp tool approval response found") + } + if len(responses) == 1 { + return responses[0], nil + } + return nil, fmt.Errorf("cannot concat multiple mcp tool approval responses") +} + +// String returns the string representation of AgenticMessage. +func (m *AgenticMessage) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf("role: %s\n", m.Role)) + + if len(m.ContentBlocks) > 0 { + sb.WriteString("content_blocks:\n") + for i, block := range m.ContentBlocks { + if block == nil { + continue + } + sb.WriteString(fmt.Sprintf(" [%d] %s", i, block.String())) + } + } + + if m.ResponseMeta != nil { + sb.WriteString(m.ResponseMeta.String()) + } + + return sb.String() +} + +// String returns the string representation of ContentBlock. +// nolint +func (b *ContentBlock) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf("type: %s\n", b.Type)) + + switch b.Type { + case ContentBlockTypeReasoning: + if b.Reasoning != nil { + sb.WriteString(b.Reasoning.String()) + } + case ContentBlockTypeUserInputText: + if b.UserInputText != nil { + sb.WriteString(b.UserInputText.String()) + } + case ContentBlockTypeUserInputImage: + if b.UserInputImage != nil { + sb.WriteString(b.UserInputImage.String()) + } + case ContentBlockTypeUserInputAudio: + if b.UserInputAudio != nil { + sb.WriteString(b.UserInputAudio.String()) + } + case ContentBlockTypeUserInputVideo: + if b.UserInputVideo != nil { + sb.WriteString(b.UserInputVideo.String()) + } + case ContentBlockTypeUserInputFile: + if b.UserInputFile != nil { + sb.WriteString(b.UserInputFile.String()) + } + case ContentBlockTypeToolSearchResult: + if b.ToolSearchFunctionToolResult != nil { + sb.WriteString(b.ToolSearchFunctionToolResult.String()) + } + case ContentBlockTypeAssistantGenText: + if b.AssistantGenText != nil { + sb.WriteString(b.AssistantGenText.String()) + } + case ContentBlockTypeAssistantGenImage: + if b.AssistantGenImage != nil { + sb.WriteString(b.AssistantGenImage.String()) + } + case ContentBlockTypeAssistantGenAudio: + if b.AssistantGenAudio != nil { + sb.WriteString(b.AssistantGenAudio.String()) + } + case ContentBlockTypeAssistantGenVideo: + if b.AssistantGenVideo != nil { + sb.WriteString(b.AssistantGenVideo.String()) + } + case ContentBlockTypeFunctionToolCall: + if b.FunctionToolCall != nil { + sb.WriteString(b.FunctionToolCall.String()) + } + case ContentBlockTypeFunctionToolResult: + if b.FunctionToolResult != nil { + sb.WriteString(b.FunctionToolResult.String()) + } + case ContentBlockTypeServerToolCall: + if b.ServerToolCall != nil { + sb.WriteString(b.ServerToolCall.String()) + } + case ContentBlockTypeServerToolResult: + if b.ServerToolResult != nil { + sb.WriteString(b.ServerToolResult.String()) + } + case ContentBlockTypeMCPToolCall: + if b.MCPToolCall != nil { + sb.WriteString(b.MCPToolCall.String()) + } + case ContentBlockTypeMCPToolResult: + if b.MCPToolResult != nil { + sb.WriteString(b.MCPToolResult.String()) + } + case ContentBlockTypeMCPListToolsResult: + if b.MCPListToolsResult != nil { + sb.WriteString(b.MCPListToolsResult.String()) + } + case ContentBlockTypeMCPToolApprovalRequest: + if b.MCPToolApprovalRequest != nil { + sb.WriteString(b.MCPToolApprovalRequest.String()) + } + case ContentBlockTypeMCPToolApprovalResponse: + if b.MCPToolApprovalResponse != nil { + sb.WriteString(b.MCPToolApprovalResponse.String()) + } + } + + if b.StreamingMeta != nil { + sb.WriteString(fmt.Sprintf(" stream_index: %d\n", b.StreamingMeta.Index)) + } + + return sb.String() +} + +// String returns the string representation of Reasoning. +func (r *Reasoning) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" text: %s\n", r.Text)) + if r.Signature != "" { + sb.WriteString(fmt.Sprintf(" signature: %s\n", truncateString(r.Signature, 50))) + } + return sb.String() +} + +// String returns the string representation of UserInputText. +func (u *UserInputText) String() string { + return fmt.Sprintf(" text: %s\n", u.Text) +} + +// String returns the string representation of UserInputImage. +func (u *UserInputImage) String() string { + return formatMediaString(u.URL, u.Base64Data, u.MIMEType, string(u.Detail)) +} + +// String returns the string representation of UserInputAudio. +func (u *UserInputAudio) String() string { + return formatMediaString(u.URL, u.Base64Data, u.MIMEType, "") +} + +// String returns the string representation of UserInputVideo. +func (u *UserInputVideo) String() string { + return formatMediaString(u.URL, u.Base64Data, u.MIMEType, "") +} + +// String returns the string representation of UserInputFile. +func (u *UserInputFile) String() string { + sb := &strings.Builder{} + if u.Name != "" { + sb.WriteString(fmt.Sprintf(" name: %s\n", u.Name)) + } + sb.WriteString(formatMediaString(u.URL, u.Base64Data, u.MIMEType, "")) + return sb.String() +} + +// String returns the string representation of AssistantGenText. +func (a *AssistantGenText) String() string { + return fmt.Sprintf(" text: %s\n", a.Text) +} + +// String returns the string representation of AssistantGenImage. +func (a *AssistantGenImage) String() string { + return formatMediaString(a.URL, a.Base64Data, a.MIMEType, "") +} + +// String returns the string representation of AssistantGenAudio. +func (a *AssistantGenAudio) String() string { + return formatMediaString(a.URL, a.Base64Data, a.MIMEType, "") +} + +// String returns the string representation of AssistantGenVideo. +func (a *AssistantGenVideo) String() string { + return formatMediaString(a.URL, a.Base64Data, a.MIMEType, "") +} + +// String returns the string representation of FunctionToolCall. +func (f *FunctionToolCall) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" call_id: %s\n", f.CallID)) + sb.WriteString(fmt.Sprintf(" name: %s\n", f.Name)) + sb.WriteString(fmt.Sprintf(" arguments: %s\n", f.Arguments)) + return sb.String() +} + +// String returns the string representation of FunctionToolResult. +func (f *FunctionToolResult) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" call_id: %s\n", f.CallID)) + sb.WriteString(fmt.Sprintf(" name: %s\n", f.Name)) + if len(f.Content) > 0 { + sb.WriteString(fmt.Sprintf(" content: (%d blocks)\n", len(f.Content))) + for i, block := range f.Content { + if block == nil { + continue + } + sb.WriteString(fmt.Sprintf(" [%d] %s", i, block.String())) + } + } + return sb.String() +} + +// String returns the string representation of ServerToolCall. +func (s *ServerToolCall) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" name: %s\n", s.Name)) + if s.CallID != "" { + sb.WriteString(fmt.Sprintf(" call_id: %s\n", s.CallID)) + } + sb.WriteString(fmt.Sprintf(" arguments: %s\n", printAny(s.Arguments))) + return sb.String() +} + +// String returns the string representation of ServerToolResult. +func (s *ServerToolResult) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" name: %s\n", s.Name)) + if s.CallID != "" { + sb.WriteString(fmt.Sprintf(" call_id: %s\n", s.CallID)) + } + sb.WriteString(fmt.Sprintf(" content: %s\n", printAny(s.Content))) + return sb.String() +} + +// String returns the string representation of MCPToolCall. +func (m *MCPToolCall) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" server_label: %s\n", m.ServerLabel)) + sb.WriteString(fmt.Sprintf(" call_id: %s\n", m.CallID)) + sb.WriteString(fmt.Sprintf(" name: %s\n", m.Name)) + sb.WriteString(fmt.Sprintf(" arguments: %s\n", m.Arguments)) + return sb.String() +} + +// String returns the string representation of MCPToolResult. +func (m *MCPToolResult) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" call_id: %s\n", m.CallID)) + sb.WriteString(fmt.Sprintf(" name: %s\n", m.Name)) + sb.WriteString(fmt.Sprintf(" content: %s\n", m.Content)) + if m.Error != nil { + if m.Error.Code != nil { + sb.WriteString(fmt.Sprintf(" error: [%d] %s\n", *m.Error.Code, m.Error.Message)) + } else { + sb.WriteString(fmt.Sprintf(" error: %s\n", m.Error.Message)) + } + } + return sb.String() +} + +// String returns the string representation of MCPListToolsResult. +func (m *MCPListToolsResult) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" server_label: %s\n", m.ServerLabel)) + sb.WriteString(fmt.Sprintf(" tools: %d items\n", len(m.Tools))) + for _, tool := range m.Tools { + sb.WriteString(fmt.Sprintf(" - %s: %s\n", tool.Name, tool.Description)) + } + if m.Error != "" { + sb.WriteString(fmt.Sprintf(" error: %s\n", m.Error)) + } + return sb.String() +} + +// String returns the string representation of MCPToolApprovalRequest. +func (m *MCPToolApprovalRequest) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" server_label: %s\n", m.ServerLabel)) + sb.WriteString(fmt.Sprintf(" id: %s\n", m.ID)) + sb.WriteString(fmt.Sprintf(" name: %s\n", m.Name)) + sb.WriteString(fmt.Sprintf(" arguments: %s\n", m.Arguments)) + return sb.String() +} + +// String returns the string representation of MCPToolApprovalResponse. +func (m *MCPToolApprovalResponse) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf(" approval_request_id: %s\n", m.ApprovalRequestID)) + sb.WriteString(fmt.Sprintf(" approve: %v\n", m.Approve)) + if m.Reason != "" { + sb.WriteString(fmt.Sprintf(" reason: %s\n", m.Reason)) + } + return sb.String() +} + +// String returns the string representation of AgenticResponseMeta. +func (a *AgenticResponseMeta) String() string { + sb := &strings.Builder{} + sb.WriteString("response_meta:\n") + if a.TokenUsage != nil { + sb.WriteString(fmt.Sprintf(" token_usage: prompt=%d, completion=%d, total=%d\n", + a.TokenUsage.PromptTokens, + a.TokenUsage.CompletionTokens, + a.TokenUsage.TotalTokens)) + } + return sb.String() +} + +// truncateString truncates a string to maxLen characters, adding "..." if truncated +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} + +// formatMediaString formats URL, Base64Data, MIMEType and Detail for media content +func formatMediaString(url, base64Data string, mimeType string, detail string) string { + sb := &strings.Builder{} + if url != "" { + sb.WriteString(fmt.Sprintf(" url: %s\n", truncateString(url, 100))) + } + if base64Data != "" { + // Only show first few characters of base64 data + sb.WriteString(fmt.Sprintf(" base64_data: %s... (%d bytes)\n", truncateString(base64Data, 20), len(base64Data))) + } + if mimeType != "" { + sb.WriteString(fmt.Sprintf(" mime_type: %s\n", mimeType)) + } + if detail != "" { + sb.WriteString(fmt.Sprintf(" detail: %s\n", detail)) + } + return sb.String() +} + +func validateExtensionType(expected reflect.Type, actual any) (reflect.Type, bool) { + if actual == nil { + return expected, true + } + actualType := reflect.TypeOf(actual) + if expected == nil { + return actualType, true + } + if expected != actualType { + return expected, false + } + return expected, true +} + +func printAny(a any) string { + switch v := a.(type) { + case string: + return v + case fmt.Stringer: + return v.String() + default: + b, err := json.MarshalIndent(a, "", " ") + if err != nil { + return fmt.Sprintf("%v", a) + } + return string(b) + } +} diff --git a/schema/agentic_message_test.go b/schema/agentic_message_test.go new file mode 100644 index 0000000..32dc96c --- /dev/null +++ b/schema/agentic_message_test.go @@ -0,0 +1,1729 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConcatAgenticMessages(t *testing.T) { + t.Run("single message", func(t *testing.T) { + msg := &AgenticMessage{ + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Hello", + }, + }, + }, + } + + result, err := ConcatAgenticMessages([]*AgenticMessage{msg}) + assert.NoError(t, err) + assert.Equal(t, msg, result) + }) + + t.Run("nil message in stream", func(t *testing.T) { + msgs := []*AgenticMessage{ + {Role: AgenticRoleTypeAssistant}, + nil, + {Role: AgenticRoleTypeAssistant}, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "message at index 1 is nil") + }) + + t.Run("different roles", func(t *testing.T) { + msgs := []*AgenticMessage{ + {Role: AgenticRoleTypeUser}, + {Role: AgenticRoleTypeAssistant}, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot concat messages with different roles") + }) + + t.Run("concat text blocks", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Hello ", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "World!", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, AgenticRoleTypeAssistant, result.Role) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "Hello World!", result.ContentBlocks[0].AssistantGenText.Text) + }) + + t.Run("concat reasoning with nil index", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeReasoning, + Reasoning: &Reasoning{ + Text: "First ", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeReasoning, + Reasoning: &Reasoning{ + Text: "Second", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "First Second", result.ContentBlocks[0].Reasoning.Text) + }) + + t.Run("concat reasoning with index", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeReasoning, + Reasoning: &Reasoning{ + Text: "Part1-", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeReasoning, + Reasoning: &Reasoning{ + Text: "Part3", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "Part1-Part3", result.ContentBlocks[0].Reasoning.Text) + }) + + t.Run("concat user input text", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Hello ", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "World!", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "Hello World!", result.ContentBlocks[0].AssistantGenText.Text) + }) + + t.Run("concat assistant gen image", func(t *testing.T) { + base1 := "1" + base2 := "2" + + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenImage, + AssistantGenImage: &AssistantGenImage{ + Base64Data: base1, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenImage, + AssistantGenImage: &AssistantGenImage{ + Base64Data: base2, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "12", result.ContentBlocks[0].AssistantGenImage.Base64Data) + }) + + t.Run("concat user input audio - should error", func(t *testing.T) { + url1 := "https://example.com/audio1.mp3" + url2 := "https://example.com/audio2.mp3" + + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputAudio, + UserInputAudio: &UserInputAudio{ + URL: url1, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputAudio, + UserInputAudio: &UserInputAudio{ + URL: url2, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot concat multiple user input audios") + }) + + t.Run("concat user input video - should error", func(t *testing.T) { + url1 := "https://example.com/video1.mp4" + url2 := "https://example.com/video2.mp4" + + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputVideo, + UserInputVideo: &UserInputVideo{ + URL: url1, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputVideo, + UserInputVideo: &UserInputVideo{ + URL: url2, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot concat multiple user input videos") + }) + + t.Run("concat assistant gen text", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Generated ", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Text", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "Generated Text", result.ContentBlocks[0].AssistantGenText.Text) + }) + + t.Run("concat assistant gen image", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenImage, + AssistantGenImage: &AssistantGenImage{ + Base64Data: "part1", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenImage, + AssistantGenImage: &AssistantGenImage{ + Base64Data: "part2", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "part1part2", result.ContentBlocks[0].AssistantGenImage.Base64Data) + }) + + t.Run("concat assistant gen audio", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenAudio, + AssistantGenAudio: &AssistantGenAudio{ + Base64Data: "audio1", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenAudio, + AssistantGenAudio: &AssistantGenAudio{ + Base64Data: "audio2", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "audio1audio2", result.ContentBlocks[0].AssistantGenAudio.Base64Data) + }) + + t.Run("concat assistant gen video", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenVideo, + AssistantGenVideo: &AssistantGenVideo{ + Base64Data: "video1", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenVideo, + AssistantGenVideo: &AssistantGenVideo{ + Base64Data: "video2", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "video1video2", result.ContentBlocks[0].AssistantGenVideo.Base64Data) + }) + + t.Run("concat function tool call", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeFunctionToolCall, + FunctionToolCall: &FunctionToolCall{ + CallID: "call_123", + Name: "get_weather", + Arguments: `{"location`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeFunctionToolCall, + FunctionToolCall: &FunctionToolCall{ + Arguments: `":"NYC"}`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "call_123", result.ContentBlocks[0].FunctionToolCall.CallID) + assert.Equal(t, "get_weather", result.ContentBlocks[0].FunctionToolCall.Name) + assert.Equal(t, `{"location":"NYC"}`, result.ContentBlocks[0].FunctionToolCall.Arguments) + }) + + t.Run("concat function tool result", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeFunctionToolResult, + FunctionToolResult: &FunctionToolResult{ + CallID: "call_123", + Name: "get_weather", + Content: []*FunctionToolResultContentBlock{ + {Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: `{"temp`}}, + }, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeFunctionToolResult, + FunctionToolResult: &FunctionToolResult{ + Content: []*FunctionToolResultContentBlock{ + {Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: `":72}`}}, + }, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "call_123", result.ContentBlocks[0].FunctionToolResult.CallID) + assert.Equal(t, "get_weather", result.ContentBlocks[0].FunctionToolResult.Name) + assert.Equal(t, 2, len(result.ContentBlocks[0].FunctionToolResult.Content)) + assert.Equal(t, `{"temp`, result.ContentBlocks[0].FunctionToolResult.Content[0].Text.Text) + assert.Equal(t, `":72}`, result.ContentBlocks[0].FunctionToolResult.Content[1].Text.Text) + }) + + t.Run("concat server tool call", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeServerToolCall, + ServerToolCall: &ServerToolCall{ + CallID: "server_call_1", + Name: "server_func", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeServerToolCall, + ServerToolCall: &ServerToolCall{ + Arguments: map[string]any{"key": "value"}, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "server_call_1", result.ContentBlocks[0].ServerToolCall.CallID) + assert.Equal(t, "server_func", result.ContentBlocks[0].ServerToolCall.Name) + assert.NotNil(t, result.ContentBlocks[0].ServerToolCall.Arguments) + }) + + t.Run("concat server tool result", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeServerToolResult, + ServerToolResult: &ServerToolResult{ + CallID: "server_call_1", + Name: "server_func", + Content: "result1", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeServerToolResult, + ServerToolResult: &ServerToolResult{}, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "server_call_1", result.ContentBlocks[0].ServerToolResult.CallID) + assert.Equal(t, "server_func", result.ContentBlocks[0].ServerToolResult.Name) + assert.Equal(t, "result1", result.ContentBlocks[0].ServerToolResult.Content) + }) + + t.Run("concat mcp tool call", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolCall, + MCPToolCall: &MCPToolCall{ + ServerLabel: "mcp-server", + CallID: "mcp_call_1", + Name: "mcp_func", + Arguments: `{"arg`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolCall, + MCPToolCall: &MCPToolCall{ + Arguments: `":123}`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolCall.ServerLabel) + assert.Equal(t, "mcp_call_1", result.ContentBlocks[0].MCPToolCall.CallID) + assert.Equal(t, "mcp_func", result.ContentBlocks[0].MCPToolCall.Name) + assert.Equal(t, `{"arg":123}`, result.ContentBlocks[0].MCPToolCall.Arguments) + }) + + t.Run("concat mcp tool result", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolResult, + MCPToolResult: &MCPToolResult{ + ServerLabel: "mcp-server", + CallID: "mcp_call_1", + Name: "mcp_func", + Content: `First`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolResult, + MCPToolResult: &MCPToolResult{ + Content: `Second`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolResult.ServerLabel) + assert.Equal(t, "mcp_call_1", result.ContentBlocks[0].MCPToolResult.CallID) + assert.Equal(t, "mcp_func", result.ContentBlocks[0].MCPToolResult.Name) + assert.Equal(t, `Second`, result.ContentBlocks[0].MCPToolResult.Content) + }) + + t.Run("concat mcp list tools", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPListToolsResult, + MCPListToolsResult: &MCPListToolsResult{ + ServerLabel: "mcp-server", + Tools: []*MCPListToolsItem{ + {Name: "tool1"}, + }, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPListToolsResult, + MCPListToolsResult: &MCPListToolsResult{ + Tools: []*MCPListToolsItem{ + {Name: "tool2"}, + }, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPListToolsResult.ServerLabel) + assert.Len(t, result.ContentBlocks[0].MCPListToolsResult.Tools, 2) + }) + + t.Run("concat mcp tool approval request", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolApprovalRequest, + MCPToolApprovalRequest: &MCPToolApprovalRequest{ + ID: "approval_1", + Name: "approval_func", + ServerLabel: "mcp-server", + Arguments: `{"request`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolApprovalRequest, + MCPToolApprovalRequest: &MCPToolApprovalRequest{ + Arguments: `":1}`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "approval_1", result.ContentBlocks[0].MCPToolApprovalRequest.ID) + assert.Equal(t, "approval_func", result.ContentBlocks[0].MCPToolApprovalRequest.Name) + assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolApprovalRequest.ServerLabel) + assert.Equal(t, `{"request":1}`, result.ContentBlocks[0].MCPToolApprovalRequest.Arguments) + }) + + t.Run("concat mcp tool approval response - should error", func(t *testing.T) { + response1 := &MCPToolApprovalResponse{ + ApprovalRequestID: "approval_1", + Approve: false, + } + response2 := &MCPToolApprovalResponse{ + ApprovalRequestID: "approval_1", + Approve: true, + } + + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolApprovalResponse, + MCPToolApprovalResponse: response1, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolApprovalResponse, + MCPToolApprovalResponse: response2, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot concat multiple mcp tool approval responses") + }) + + t.Run("concat response meta", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ResponseMeta: &AgenticResponseMeta{ + TokenUsage: &TokenUsage{ + PromptTokens: 10, + CompletionTokens: 5, + PromptTokenDetails: PromptTokenDetails{ + CachedTokens: 3, + }, + CompletionTokensDetails: CompletionTokensDetails{ + ReasoningTokens: 2, + }, + TotalTokens: 15, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ResponseMeta: &AgenticResponseMeta{ + TokenUsage: &TokenUsage{ + PromptTokens: 8, + CompletionTokens: 15, + PromptTokenDetails: PromptTokenDetails{ + CachedTokens: 5, + }, + CompletionTokensDetails: CompletionTokensDetails{ + ReasoningTokens: 4, + }, + TotalTokens: 23, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.NotNil(t, result.ResponseMeta) + assert.Equal(t, 15, result.ResponseMeta.TokenUsage.CompletionTokens) + assert.Equal(t, 10, result.ResponseMeta.TokenUsage.PromptTokens) + assert.Equal(t, 23, result.ResponseMeta.TokenUsage.TotalTokens) + assert.Equal(t, 5, result.ResponseMeta.TokenUsage.PromptTokenDetails.CachedTokens) + assert.Equal(t, 4, result.ResponseMeta.TokenUsage.CompletionTokensDetails.ReasoningTokens) + }) + + t.Run("mixed streaming and non-streaming blocks error", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Hello", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "World", + }, + // No StreamingMeta - non-streaming + }, + }, + }, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "found non-streaming block after streaming blocks") + }) + + t.Run("concat MCP tool call", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolCall, + MCPToolCall: &MCPToolCall{ + ServerLabel: "mcp-server", + CallID: "call_456", + Name: "list_files", + Arguments: `{"path`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeMCPToolCall, + MCPToolCall: &MCPToolCall{ + Arguments: `":"/tmp"}`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 1) + assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolCall.ServerLabel) + assert.Equal(t, "call_456", result.ContentBlocks[0].MCPToolCall.CallID) + assert.Equal(t, `{"path":"/tmp"}`, result.ContentBlocks[0].MCPToolCall.Arguments) + }) + + t.Run("concat user input text - should error", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputText, + UserInputText: &UserInputText{ + Text: "What is ", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + { + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputText, + UserInputText: &UserInputText{ + Text: "the weather?", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + }, + }, + } + + _, err := ConcatAgenticMessages(msgs) + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot concat multiple user input texts") + }) + + t.Run("multiple stream indexes - sparse indexes", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Index0-", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Index2-", + }, + StreamingMeta: &StreamingMeta{Index: 2}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Part2", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Part2", + }, + StreamingMeta: &StreamingMeta{Index: 2}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 2) + assert.Equal(t, "Index0-Part2", result.ContentBlocks[0].AssistantGenText.Text) + assert.Equal(t, "Index2-Part2", result.ContentBlocks[1].AssistantGenText.Text) + }) + + t.Run("multiple stream indexes - mixed content types", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Text ", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeFunctionToolCall, + FunctionToolCall: &FunctionToolCall{ + CallID: "call_1", + Name: "func1", + Arguments: `{"a`, + }, + StreamingMeta: &StreamingMeta{Index: 1}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "Content", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeFunctionToolCall, + FunctionToolCall: &FunctionToolCall{ + Arguments: `":1}`, + }, + StreamingMeta: &StreamingMeta{Index: 1}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 2) + assert.Equal(t, "Text Content", result.ContentBlocks[0].AssistantGenText.Text) + assert.Equal(t, "call_1", result.ContentBlocks[1].FunctionToolCall.CallID) + assert.Equal(t, "func1", result.ContentBlocks[1].FunctionToolCall.Name) + assert.Equal(t, `{"a":1}`, result.ContentBlocks[1].FunctionToolCall.Arguments) + }) + + t.Run("multiple stream indexes - three indexes", func(t *testing.T) { + msgs := []*AgenticMessage{ + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "A", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "B", + }, + StreamingMeta: &StreamingMeta{Index: 1}, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "C", + }, + StreamingMeta: &StreamingMeta{Index: 2}, + }, + }, + }, + { + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "1", + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "2", + }, + StreamingMeta: &StreamingMeta{Index: 1}, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "3", + }, + StreamingMeta: &StreamingMeta{Index: 2}, + }, + }, + }, + } + + result, err := ConcatAgenticMessages(msgs) + assert.NoError(t, err) + assert.Len(t, result.ContentBlocks, 3) + assert.Equal(t, "A1", result.ContentBlocks[0].AssistantGenText.Text) + assert.Equal(t, "B2", result.ContentBlocks[1].AssistantGenText.Text) + assert.Equal(t, "C3", result.ContentBlocks[2].AssistantGenText.Text) + }) +} + +func TestAgenticMessageFormat(t *testing.T) { + m := &AgenticMessage{ + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputText, + UserInputText: &UserInputText{Text: "{a}"}, + }, + { + Type: ContentBlockTypeUserInputImage, + UserInputImage: &UserInputImage{ + URL: "{b}", + Base64Data: "{c}", + }, + }, + { + Type: ContentBlockTypeUserInputAudio, + UserInputAudio: &UserInputAudio{ + URL: "{d}", + Base64Data: "{e}", + }, + }, + { + Type: ContentBlockTypeUserInputVideo, + UserInputVideo: &UserInputVideo{ + URL: "{f}", + Base64Data: "{g}", + }, + }, + { + Type: ContentBlockTypeUserInputFile, + UserInputFile: &UserInputFile{ + URL: "{h}", + Base64Data: "{i}", + }, + }, + }, + } + + result, err := m.Format(context.Background(), map[string]any{ + "a": "1", "b": "2", "c": "3", "d": "4", "e": "5", "f": "6", "g": "7", "h": "8", "i": "9", + }, FString) + assert.NoError(t, err) + assert.Equal(t, []*AgenticMessage{{ + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputText, + UserInputText: &UserInputText{Text: "1"}, + }, + { + Type: ContentBlockTypeUserInputImage, + UserInputImage: &UserInputImage{ + URL: "2", + Base64Data: "3", + }, + }, + { + Type: ContentBlockTypeUserInputAudio, + UserInputAudio: &UserInputAudio{ + URL: "4", + Base64Data: "5", + }, + }, + { + Type: ContentBlockTypeUserInputVideo, + UserInputVideo: &UserInputVideo{ + URL: "6", + Base64Data: "7", + }, + }, + { + Type: ContentBlockTypeUserInputFile, + UserInputFile: &UserInputFile{ + URL: "8", + Base64Data: "9", + }, + }, + }, + }}, result) +} + +func TestAgenticPlaceholderFormat(t *testing.T) { + ctx := context.Background() + ph := AgenticMessagesPlaceholder("a", false) + + result, err := ph.Format(ctx, map[string]any{ + "a": []*AgenticMessage{{Role: AgenticRoleTypeUser}, {Role: AgenticRoleTypeUser}}, + }, FString) + assert.NoError(t, err) + assert.Equal(t, 2, len(result)) + + ph = AgenticMessagesPlaceholder("a", true) + + result, err = ph.Format(ctx, map[string]any{}, FString) + assert.NoError(t, err) + assert.Equal(t, 0, len(result)) +} + +func ptrOf[T any](v T) *T { + return &v +} + +func TestAgenticMessageString(t *testing.T) { + longBase64 := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + + msg := &AgenticMessage{ + Role: AgenticRoleTypeAssistant, + ContentBlocks: []*ContentBlock{ + { + Type: ContentBlockTypeUserInputText, + UserInputText: &UserInputText{ + Text: "What's the weather like in New York City today?", + }, + }, + { + Type: ContentBlockTypeUserInputImage, + UserInputImage: &UserInputImage{ + URL: "https://example.com/weather-map.jpg", + Base64Data: longBase64, + MIMEType: "image/jpeg", + Detail: ImageURLDetailHigh, + }, + }, + { + Type: ContentBlockTypeUserInputAudio, + UserInputAudio: &UserInputAudio{ + URL: "http://audio.com", + Base64Data: "audio_data", + MIMEType: "audio/mp3", + }, + }, + { + Type: ContentBlockTypeUserInputVideo, + UserInputVideo: &UserInputVideo{ + URL: "http://video.com", + Base64Data: "video_data", + MIMEType: "video/mp4", + }, + }, + { + Type: ContentBlockTypeUserInputFile, + UserInputFile: &UserInputFile{ + URL: "http://file.com", + Name: "file.txt", + Base64Data: "file_data", + MIMEType: "text/plain", + }, + }, + { + Type: ContentBlockTypeAssistantGenText, + AssistantGenText: &AssistantGenText{ + Text: "I'll check the current weather in New York City for you.", + }, + }, + { + Type: ContentBlockTypeAssistantGenImage, + AssistantGenImage: &AssistantGenImage{ + URL: "http://gen_image.com", + Base64Data: "gen_image_data", + MIMEType: "image/png", + }, + }, + { + Type: ContentBlockTypeAssistantGenAudio, + AssistantGenAudio: &AssistantGenAudio{ + URL: "http://gen_audio.com", + Base64Data: "gen_audio_data", + MIMEType: "audio/wav", + }, + }, + { + Type: ContentBlockTypeAssistantGenVideo, + AssistantGenVideo: &AssistantGenVideo{ + URL: "http://gen_video.com", + Base64Data: "gen_video_data", + MIMEType: "video/mp4", + }, + }, + { + Type: ContentBlockTypeReasoning, + Reasoning: &Reasoning{ + Text: "First, I need to identify the location (New York City) from the user's query.\n" + + "Then, I should call the weather API to get current conditions.\n" + + "Finally, I'll format the response in a user-friendly way with temperature and conditions.", + Signature: "encrypted_reasoning_content_that_is_very_long_and_will_be_truncated_for_display", + }, + }, + { + Type: ContentBlockTypeFunctionToolCall, + FunctionToolCall: &FunctionToolCall{ + CallID: "call_weather_123", + Name: "get_current_weather", + Arguments: `{"location":"New York City","unit":"fahrenheit"}`, + }, + StreamingMeta: &StreamingMeta{Index: 0}, + }, + { + Type: ContentBlockTypeFunctionToolResult, + FunctionToolResult: &FunctionToolResult{ + CallID: "call_weather_123", + Name: "get_current_weather", + Content: []*FunctionToolResultContentBlock{ + {Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: `{"temperature":72,"condition":"sunny","humidity":45,"wind_speed":8}`}}, + }, + }, + }, + { + Type: ContentBlockTypeServerToolCall, + ServerToolCall: &ServerToolCall{ + Name: "server_tool", + CallID: "call_1", + Arguments: map[string]any{"a": 1}, + }, + }, + { + Type: ContentBlockTypeServerToolResult, + ServerToolResult: &ServerToolResult{ + Name: "server_tool", + CallID: "call_1", + Content: map[string]any{"success": true}, + }, + }, + { + Type: ContentBlockTypeMCPToolApprovalRequest, + MCPToolApprovalRequest: &MCPToolApprovalRequest{ + ID: "req_1", + Name: "mcp_tool", + ServerLabel: "mcp_server", + Arguments: "{}", + }, + }, + { + Type: ContentBlockTypeMCPToolApprovalResponse, + MCPToolApprovalResponse: &MCPToolApprovalResponse{ + ApprovalRequestID: "req_1", + Approve: true, + Reason: "looks good", + }, + }, + { + Type: ContentBlockTypeMCPToolCall, + MCPToolCall: &MCPToolCall{ + ServerLabel: "weather-mcp-server", + CallID: "mcp_forecast_456", + Name: "get_7day_forecast", + Arguments: `{"city":"New York","days":7}`, + }, + }, + { + Type: ContentBlockTypeMCPToolResult, + MCPToolResult: &MCPToolResult{ + CallID: "mcp_forecast_456", + Name: "get_7day_forecast", + Content: `{"status":"partial","days_available":3}`, + Error: &MCPToolCallError{ + Code: ptrOf[int64](503), + Message: "Service temporarily unavailable for full 7-day forecast", + }, + }, + }, + { + Type: ContentBlockTypeMCPListToolsResult, + MCPListToolsResult: &MCPListToolsResult{ + ServerLabel: "weather-mcp-server", + Tools: []*MCPListToolsItem{ + {Name: "get_current_weather", Description: "Get current weather conditions for a location"}, + {Name: "get_7day_forecast", Description: "Get 7-day weather forecast"}, + {Name: "get_weather_alerts", Description: "Get active weather alerts and warnings"}, + }, + }, + }, + }, + ResponseMeta: &AgenticResponseMeta{ + TokenUsage: &TokenUsage{ + PromptTokens: 250, + CompletionTokens: 180, + TotalTokens: 430, + }, + }, + } + + // Print the formatted output + output := msg.String() + + assert.Equal(t, `role: assistant +content_blocks: + [0] type: user_input_text + text: What's the weather like in New York City today? + [1] type: user_input_image + url: https://example.com/weather-map.jpg + base64_data: iVBORw0KGgoAAAANSUhE...... (96 bytes) + mime_type: image/jpeg + detail: high + [2] type: user_input_audio + url: http://audio.com + base64_data: audio_data... (10 bytes) + mime_type: audio/mp3 + [3] type: user_input_video + url: http://video.com + base64_data: video_data... (10 bytes) + mime_type: video/mp4 + [4] type: user_input_file + name: file.txt + url: http://file.com + base64_data: file_data... (9 bytes) + mime_type: text/plain + [5] type: assistant_gen_text + text: I'll check the current weather in New York City for you. + [6] type: assistant_gen_image + url: http://gen_image.com + base64_data: gen_image_data... (14 bytes) + mime_type: image/png + [7] type: assistant_gen_audio + url: http://gen_audio.com + base64_data: gen_audio_data... (14 bytes) + mime_type: audio/wav + [8] type: assistant_gen_video + url: http://gen_video.com + base64_data: gen_video_data... (14 bytes) + mime_type: video/mp4 + [9] type: reasoning + text: First, I need to identify the location (New York City) from the user's query. +Then, I should call the weather API to get current conditions. +Finally, I'll format the response in a user-friendly way with temperature and conditions. + signature: encrypted_reasoning_content_that_is_very_long_and_... + [10] type: function_tool_call + call_id: call_weather_123 + name: get_current_weather + arguments: {"location":"New York City","unit":"fahrenheit"} + stream_index: 0 + [11] type: function_tool_result + call_id: call_weather_123 + name: get_current_weather + content: (1 blocks) + [0] text: {"temperature":72,"condition":"sunny","humidity":45,"wind_speed":8} + [12] type: server_tool_call + name: server_tool + call_id: call_1 + arguments: { + "a": 1 +} + [13] type: server_tool_result + name: server_tool + call_id: call_1 + content: { + "success": true +} + [14] type: mcp_tool_approval_request + server_label: mcp_server + id: req_1 + name: mcp_tool + arguments: {} + [15] type: mcp_tool_approval_response + approval_request_id: req_1 + approve: true + reason: looks good + [16] type: mcp_tool_call + server_label: weather-mcp-server + call_id: mcp_forecast_456 + name: get_7day_forecast + arguments: {"city":"New York","days":7} + [17] type: mcp_tool_result + call_id: mcp_forecast_456 + name: get_7day_forecast + content: {"status":"partial","days_available":3} + error: [503] Service temporarily unavailable for full 7-day forecast + [18] type: mcp_list_tools_result + server_label: weather-mcp-server + tools: 3 items + - get_current_weather: Get current weather conditions for a location + - get_7day_forecast: Get 7-day weather forecast + - get_weather_alerts: Get active weather alerts and warnings +response_meta: + token_usage: prompt=250, completion=180, total=430 +`, output) + + t.Run("nil/empty fields", func(t *testing.T) { + msg := &AgenticMessage{ + Role: AgenticRoleTypeUser, + ContentBlocks: []*ContentBlock{ + {Type: ContentBlockTypeUserInputAudio, UserInputAudio: &UserInputAudio{}}, // empty + {Type: ContentBlockTypeUserInputVideo, UserInputVideo: &UserInputVideo{}}, + {Type: ContentBlockTypeUserInputFile, UserInputFile: &UserInputFile{}}, + {Type: ContentBlockTypeAssistantGenImage, AssistantGenImage: &AssistantGenImage{}}, + {Type: ContentBlockTypeAssistantGenAudio, AssistantGenAudio: &AssistantGenAudio{}}, + {Type: ContentBlockTypeAssistantGenVideo, AssistantGenVideo: &AssistantGenVideo{}}, + {Type: ContentBlockTypeServerToolCall, ServerToolCall: &ServerToolCall{Name: "t"}}, // No CallID + {Type: ContentBlockTypeServerToolResult, ServerToolResult: &ServerToolResult{Name: "t"}}, // No CallID + {Type: ContentBlockTypeMCPToolResult, MCPToolResult: &MCPToolResult{Name: "t"}}, // No Error + {Type: ContentBlockTypeMCPListToolsResult, MCPListToolsResult: &MCPListToolsResult{}}, // No Error + {Type: ContentBlockTypeMCPToolApprovalResponse, MCPToolApprovalResponse: &MCPToolApprovalResponse{Approve: false}}, // No Reason + nil, // Nil block in slice + }, + } + + s := msg.String() + assert.Contains(t, s, "type: user_input_audio") + assert.NotContains(t, s, "mime_type:") + assert.Contains(t, s, "type: server_tool_call") + }) + + t.Run("nil content struct in block", func(t *testing.T) { + // Test cases where the specific content struct is nil but type is set + // This shouldn't crash and should just print type + msg := &AgenticMessage{ + ContentBlocks: []*ContentBlock{ + {Type: ContentBlockTypeReasoning, Reasoning: nil}, + {Type: ContentBlockTypeUserInputText, UserInputText: nil}, + {Type: ContentBlockTypeUserInputImage, UserInputImage: nil}, + {Type: ContentBlockTypeUserInputAudio, UserInputAudio: nil}, + {Type: ContentBlockTypeUserInputVideo, UserInputVideo: nil}, + {Type: ContentBlockTypeUserInputFile, UserInputFile: nil}, + {Type: ContentBlockTypeAssistantGenText, AssistantGenText: nil}, + {Type: ContentBlockTypeAssistantGenImage, AssistantGenImage: nil}, + {Type: ContentBlockTypeAssistantGenAudio, AssistantGenAudio: nil}, + {Type: ContentBlockTypeAssistantGenVideo, AssistantGenVideo: nil}, + {Type: ContentBlockTypeFunctionToolCall, FunctionToolCall: nil}, + {Type: ContentBlockTypeFunctionToolResult, FunctionToolResult: nil}, + {Type: ContentBlockTypeServerToolCall, ServerToolCall: nil}, + {Type: ContentBlockTypeServerToolResult, ServerToolResult: nil}, + {Type: ContentBlockTypeMCPToolCall, MCPToolCall: nil}, + {Type: ContentBlockTypeMCPToolResult, MCPToolResult: nil}, + {Type: ContentBlockTypeMCPListToolsResult, MCPListToolsResult: nil}, + {Type: ContentBlockTypeMCPToolApprovalRequest, MCPToolApprovalRequest: nil}, + {Type: ContentBlockTypeMCPToolApprovalResponse, MCPToolApprovalResponse: nil}, + }, + } + s := msg.String() + assert.Contains(t, s, "type: reasoning") + // ensure no panic and basic output present + }) +} + +func TestSystemAgenticMessage(t *testing.T) { + t.Run("basic", func(t *testing.T) { + msg := SystemAgenticMessage("system") + assert.Equal(t, AgenticRoleTypeSystem, msg.Role) + assert.Len(t, msg.ContentBlocks, 1) + assert.Equal(t, "system", msg.ContentBlocks[0].UserInputText.Text) + }) +} + +func TestUserAgenticMessage(t *testing.T) { + t.Run("basic", func(t *testing.T) { + msg := UserAgenticMessage("user") + assert.Equal(t, AgenticRoleTypeUser, msg.Role) + assert.Len(t, msg.ContentBlocks, 1) + assert.Equal(t, "user", msg.ContentBlocks[0].UserInputText.Text) + }) +} + +func TestNewContentBlock(t *testing.T) { + cbType := reflect.TypeOf(ContentBlock{}) + for i := 0; i < cbType.NumField(); i++ { + field := cbType.Field(i) + + // Skip non-content fields + if field.Name == "Type" || field.Name == "Extra" || field.Name == "StreamingMeta" { + continue + } + + t.Run(field.Name, func(t *testing.T) { + // Ensure field is a pointer + assert.Equal(t, reflect.Ptr, field.Type.Kind(), "Field %s should be a pointer", field.Name) + + // Create a new instance of the field's type + // field.Type is *T, so Elem() is T. reflect.New(T) returns *T. + elemType := field.Type.Elem() + inputVal := reflect.New(elemType) + input := inputVal.Interface() + + // Call NewContentBlock (generic) via type switch + var block *ContentBlock + switch v := input.(type) { + case *Reasoning: + block = NewContentBlock(v) + case *UserInputText: + block = NewContentBlock(v) + case *UserInputImage: + block = NewContentBlock(v) + case *UserInputAudio: + block = NewContentBlock(v) + case *UserInputVideo: + block = NewContentBlock(v) + case *UserInputFile: + block = NewContentBlock(v) + case *ToolSearchFunctionToolResult: + block = NewContentBlock(v) + case *AssistantGenText: + block = NewContentBlock(v) + case *AssistantGenImage: + block = NewContentBlock(v) + case *AssistantGenAudio: + block = NewContentBlock(v) + case *AssistantGenVideo: + block = NewContentBlock(v) + case *FunctionToolCall: + block = NewContentBlock(v) + case *FunctionToolResult: + block = NewContentBlock(v) + case *ServerToolCall: + block = NewContentBlock(v) + case *ServerToolResult: + block = NewContentBlock(v) + case *MCPToolCall: + block = NewContentBlock(v) + case *MCPToolResult: + block = NewContentBlock(v) + case *MCPListToolsResult: + block = NewContentBlock(v) + case *MCPToolApprovalRequest: + block = NewContentBlock(v) + case *MCPToolApprovalResponse: + block = NewContentBlock(v) + default: + t.Fatalf("unsupported ContentBlock field type: %T", input) + } + + // Assertions + assert.NotNil(t, block, "NewContentBlock should return non-nil for type %T", input) + + // Check if the corresponding field in block is set equals to input + blockVal := reflect.ValueOf(block).Elem() + fieldVal := blockVal.FieldByName(field.Name) + assert.True(t, fieldVal.IsValid(), "Field %s not found in result", field.Name) + assert.Equal(t, input, fieldVal.Interface(), "Field %s should match input", field.Name) + + // Check Type is set + typeVal := blockVal.FieldByName("Type") + assert.NotEmpty(t, typeVal.String(), "Type should be set for %s", field.Name) + }) + } +} + +func TestNewContentBlockChunk_NilMeta(t *testing.T) { + require.NotPanics(t, func() { + block := NewContentBlockChunk(&AssistantGenText{Text: "test"}, nil) + require.NotNil(t, block) + assert.Nil(t, block.StreamingMeta) + }, "NewContentBlockChunk should handle nil meta without panic") +} + +func TestConcatAssistantGenTexts_ExtensionOverwrite(t *testing.T) { + type testExtension struct { + Value string + } + + texts := []*AssistantGenText{ + {Text: "Hello ", Extension: &testExtension{Value: "ext1"}}, + {Text: "world", Extension: &testExtension{Value: "ext2"}}, + } + + result, err := concatAssistantGenTexts(texts) + if err != nil { + t.Logf("Concat error (may be expected if ConcatSliceValue doesn't handle this type): %v", err) + t.Skip("Skipping: ConcatSliceValue doesn't support test type") + } + require.NotNil(t, result) + + assert.Equal(t, "Hello world", result.Text) + + if result.Extension != nil { + t.Logf("Extension type: %T, value: %v", result.Extension, result.Extension) + _, isSlice := result.Extension.([]*testExtension) + if isSlice { + t.Log("WARNING: Extension is a raw slice instead of a concatenated value. " + + "Line 1381 in agentic_message.go overwrites the ConcatSliceValue result " + + "with extensions.Interface(), discarding the concatenation.") + } + } +} + +func TestFunctionToolResultBlockString(t *testing.T) { + t.Run("empty type", func(t *testing.T) { + b := &FunctionToolResultContentBlock{Text: &UserInputText{Text: "x"}} + assert.Equal(t, "unknown block type: \n", b.String()) + }) + + t.Run("known type but empty payload", func(t *testing.T) { + b := &FunctionToolResultContentBlock{Type: FunctionToolResultContentBlockTypeText} + assert.Equal(t, "empty text block\n", b.String()) + }) + + t.Run("unknown type value", func(t *testing.T) { + b := &FunctionToolResultContentBlock{Type: FunctionToolResultContentBlockType("weird")} + assert.Equal(t, "unknown block type: weird\n", b.String()) + }) +} + +func TestConcatFunctionToolResults(t *testing.T) { + t.Run("direct append", func(t *testing.T) { + results := []*FunctionToolResult{ + {CallID: "c1", Name: "tool1", Content: []*FunctionToolResultContentBlock{ + {Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: "hello"}}, + }}, + {CallID: "c1", Name: "tool1", Content: []*FunctionToolResultContentBlock{ + {Type: FunctionToolResultContentBlockTypeImage, Image: &UserInputImage{URL: "http://img.png"}}, + }}, + } + got, err := concatFunctionToolResults(results) + require.NoError(t, err) + assert.Len(t, got.Content, 2) + assert.Equal(t, "hello", got.Content[0].Text.Text) + assert.Equal(t, "http://img.png", got.Content[1].Image.URL) + }) +} diff --git a/schema/claude/consts.go b/schema/claude/consts.go new file mode 100644 index 0000000..714b036 --- /dev/null +++ b/schema/claude/consts.go @@ -0,0 +1,27 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package claude defines constants for claude. +package claude + +type TextCitationType string + +const ( + TextCitationTypeCharLocation TextCitationType = "char_location" + TextCitationTypePageLocation TextCitationType = "page_location" + TextCitationTypeContentBlockLocation TextCitationType = "content_block_location" + TextCitationTypeWebSearchResultLocation TextCitationType = "web_search_result_location" +) diff --git a/schema/claude/extension.go b/schema/claude/extension.go new file mode 100644 index 0000000..239941b --- /dev/null +++ b/schema/claude/extension.go @@ -0,0 +1,134 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package claude + +import ( + "fmt" +) + +type ResponseMetaExtension struct { + ID string `json:"id,omitempty"` + StopReason string `json:"stop_reason,omitempty"` + StopSequence string `json:"stop_sequence,omitempty"` + StopDetails *StopDetails `json:"stop_details,omitempty"` +} + +type StopDetails struct { + Category string `json:"category,omitempty"` + Explanation string `json:"explanation,omitempty"` +} + +type AssistantGenTextExtension struct { + Citations []*TextCitation `json:"citations,omitempty"` +} + +type TextCitation struct { + Type TextCitationType `json:"type,omitempty"` + + CharLocation *CitationCharLocation `json:"char_location,omitempty"` + PageLocation *CitationPageLocation `json:"page_location,omitempty"` + ContentBlockLocation *CitationContentBlockLocation `json:"content_block_location,omitempty"` + WebSearchResultLocation *CitationWebSearchResultLocation `json:"web_search_result_location,omitempty"` +} + +type CitationCharLocation struct { + CitedText string `json:"cited_text,omitempty"` + + DocumentTitle string `json:"document_title,omitempty"` + DocumentIndex int `json:"document_index,omitempty"` + + StartCharIndex int `json:"start_char_index,omitempty"` + EndCharIndex int `json:"end_char_index,omitempty"` +} + +type CitationPageLocation struct { + CitedText string `json:"cited_text,omitempty"` + + DocumentTitle string `json:"document_title,omitempty"` + DocumentIndex int `json:"document_index,omitempty"` + + StartPageNumber int `json:"start_page_number,omitempty"` + EndPageNumber int `json:"end_page_number,omitempty"` +} + +type CitationContentBlockLocation struct { + CitedText string `json:"cited_text,omitempty"` + + DocumentTitle string `json:"document_title,omitempty"` + DocumentIndex int `json:"document_index,omitempty"` + + StartBlockIndex int `json:"start_block_index,omitempty"` + EndBlockIndex int `json:"end_block_index,omitempty"` +} + +type CitationWebSearchResultLocation struct { + CitedText string `json:"cited_text,omitempty"` + + Title string `json:"title,omitempty"` + URL string `json:"url,omitempty"` + + EncryptedIndex string `json:"encrypted_index,omitempty"` +} + +// ConcatAssistantGenTextExtensions merges multiple AssistantGenTextExtension chunks into one. +func ConcatAssistantGenTextExtensions(chunks []*AssistantGenTextExtension) (*AssistantGenTextExtension, error) { + if len(chunks) == 0 { + return nil, fmt.Errorf("no assistant generated text extension found") + } + if len(chunks) == 1 { + return chunks[0], nil + } + + ret := &AssistantGenTextExtension{ + Citations: make([]*TextCitation, 0, len(chunks)), + } + + for _, ext := range chunks { + ret.Citations = append(ret.Citations, ext.Citations...) + } + + return ret, nil +} + +// ConcatResponseMetaExtensions merges multiple ResponseMetaExtension chunks into one. +func ConcatResponseMetaExtensions(chunks []*ResponseMetaExtension) (*ResponseMetaExtension, error) { + if len(chunks) == 0 { + return nil, fmt.Errorf("no response meta extension found") + } + if len(chunks) == 1 { + return chunks[0], nil + } + + ret := &ResponseMetaExtension{} + + for _, ext := range chunks { + if ext.ID != "" { + ret.ID = ext.ID + } + if ext.StopReason != "" { + ret.StopReason = ext.StopReason + } + if ext.StopSequence != "" { + ret.StopSequence = ext.StopSequence + } + if ext.StopDetails != nil { + ret.StopDetails = ext.StopDetails + } + } + + return ret, nil +} diff --git a/schema/claude/extension_test.go b/schema/claude/extension_test.go new file mode 100644 index 0000000..474fe74 --- /dev/null +++ b/schema/claude/extension_test.go @@ -0,0 +1,190 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package claude + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConcatAssistantGenTextExtensions(t *testing.T) { + t.Run("multiple extensions - concatenates all citations", func(t *testing.T) { + exts := []*AssistantGenTextExtension{ + { + Citations: []*TextCitation{ + { + Type: "char_location", + CharLocation: &CitationCharLocation{ + CitedText: "citation 1", + DocumentIndex: 0, + }, + }, + }, + }, + { + Citations: []*TextCitation{ + { + Type: "page_location", + PageLocation: &CitationPageLocation{ + CitedText: "citation 2", + StartPageNumber: 1, + EndPageNumber: 2, + }, + }, + { + Type: "web_search_result_location", + WebSearchResultLocation: &CitationWebSearchResultLocation{ + CitedText: "citation 3", + URL: "https://example.com", + }, + }, + }, + }, + { + Citations: []*TextCitation{ + { + Type: "content_block_location", + ContentBlockLocation: &CitationContentBlockLocation{ + CitedText: "citation 4", + StartBlockIndex: 0, + EndBlockIndex: 5, + }, + }, + }, + }, + } + + result, err := ConcatAssistantGenTextExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Citations, 4) + assert.Equal(t, "citation 1", result.Citations[0].CharLocation.CitedText) + assert.Equal(t, "citation 2", result.Citations[1].PageLocation.CitedText) + assert.Equal(t, "citation 3", result.Citations[2].WebSearchResultLocation.CitedText) + assert.Equal(t, "citation 4", result.Citations[3].ContentBlockLocation.CitedText) + }) + + t.Run("mixed empty and non-empty citations", func(t *testing.T) { + exts := []*AssistantGenTextExtension{ + {Citations: nil}, + { + Citations: []*TextCitation{ + { + Type: "char_location", + CharLocation: &CitationCharLocation{ + CitedText: "text1", + }, + }, + }, + }, + {Citations: []*TextCitation{}}, + { + Citations: []*TextCitation{ + { + Type: "page_location", + PageLocation: &CitationPageLocation{ + CitedText: "text2", + }, + }, + }, + }, + } + + result, err := ConcatAssistantGenTextExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Citations, 2) + assert.Equal(t, "text1", result.Citations[0].CharLocation.CitedText) + assert.Equal(t, "text2", result.Citations[1].PageLocation.CitedText) + }) + + t.Run("streaming scenario - citations arrive in chunks", func(t *testing.T) { + // Simulates streaming where citations arrive progressively + exts := []*AssistantGenTextExtension{ + { + Citations: []*TextCitation{ + {Type: "char_location", CharLocation: &CitationCharLocation{CitedText: "chunk1"}}, + }, + }, + { + Citations: []*TextCitation{ + {Type: "char_location", CharLocation: &CitationCharLocation{CitedText: "chunk2"}}, + }, + }, + { + Citations: []*TextCitation{ + {Type: "char_location", CharLocation: &CitationCharLocation{CitedText: "chunk3"}}, + }, + }, + } + + result, err := ConcatAssistantGenTextExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Citations, 3) + assert.Equal(t, "chunk1", result.Citations[0].CharLocation.CitedText) + assert.Equal(t, "chunk2", result.Citations[1].CharLocation.CitedText) + assert.Equal(t, "chunk3", result.Citations[2].CharLocation.CitedText) + }) +} + +func TestConcatResponseMetaExtensions(t *testing.T) { + t.Run("multiple extensions - takes last non-empty values", func(t *testing.T) { + exts := []*ResponseMetaExtension{ + { + ID: "msg_1", + StopReason: "stop_1", + }, + { + ID: "msg_2", + StopReason: "", + }, + { + ID: "", + StopReason: "stop_3", + }, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "msg_2", result.ID) // Last non-empty ID + assert.Equal(t, "stop_3", result.StopReason) // Last non-empty StopReason + }) + + t.Run("all empty fields", func(t *testing.T) { + exts := []*ResponseMetaExtension{ + {ID: "", StopReason: ""}, + {ID: "", StopReason: ""}, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "", result.ID) + assert.Equal(t, "", result.StopReason) + }) + + t.Run("streaming scenario - ID in first chunk, StopReason in last", func(t *testing.T) { + exts := []*ResponseMetaExtension{ + {ID: "msg_stream_123", StopReason: ""}, + {ID: "", StopReason: ""}, + {ID: "", StopReason: "end_turn"}, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "msg_stream_123", result.ID) + assert.Equal(t, "end_turn", result.StopReason) + }) +} diff --git a/schema/doc.go b/schema/doc.go new file mode 100644 index 0000000..c8ba7a3 --- /dev/null +++ b/schema/doc.go @@ -0,0 +1,86 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package schema defines the core data structures and utilities shared across +// all Eino components. +// +// # Key Types +// +// [Message] is the universal unit of communication between users, models, and +// tools. It carries role, text content, multimodal media, tool calls, and +// response metadata. Helper constructors — [UserMessage], [SystemMessage], +// [AssistantMessage], [ToolMessage] — cover the most common cases. +// +// [Document] represents a piece of text with a metadata map. Typed accessors +// (Score, SubIndexes, DenseVector, SparseVector, DSLInfo, ExtraInfo) read and +// write well-known metadata keys so pipeline stages can pass structured data +// without coupling to specific struct types. +// +// [ToolInfo] describes a tool's name, description, and parameter schema. +// Parameters can be declared either as a [ParameterInfo] map (simple, struct- +// like) or as a raw [jsonschema.Schema] (full JSON Schema 2020-12 expressiveness). +// [ToolChoice] controls whether the model must, may, or must not call tools. +// +// # Streaming +// +// [StreamReader] and [StreamWriter] are the building blocks for streaming data +// through Eino pipelines. Create a linked pair with [Pipe]: +// +// sr, sw := schema.Pipe[*schema.Message](10) +// go func() { +// defer sw.Close() +// sw.Send(chunk, nil) +// }() +// defer sr.Close() +// for { +// chunk, err := sr.Recv() +// if errors.Is(err, io.EOF) { break } +// } +// +// Important constraints: +// - A StreamReader is read-once: only one goroutine may call Recv. +// - Always call Close, even when the loop ends on io.EOF, to release resources. +// - To give the same stream to multiple consumers, call [StreamReader.Copy]. +// +// # Four Streaming Paradigms +// +// Eino components and Lambda functions are classified by their input/output +// streaming shape. The framework automatically bridges mismatches: +// +// - Invoke: non-streaming in, non-streaming out (ping-pong). +// - Stream: non-streaming in, StreamReader out (server-streaming). ChatModel +// and Tool support this. +// - Collect: StreamReader in, non-streaming out (client-streaming). Useful +// for branch conditions that decide after the first chunk. +// - Transform: StreamReader in, StreamReader out (bidirectional). +// +// When an upstream node outputs T but a downstream node only accepts +// StreamReader[T], the framework wraps T in a single-chunk StreamReader — +// this is called a "fake stream". It satisfies the interface but does NOT +// reduce time-to-first-chunk. Conversely, when a downstream node only accepts +// T but the upstream outputs StreamReader[T], the framework automatically +// concatenates the stream into a complete T. +// +// Utility functions: +// - [StreamReaderFromArray] wraps a slice as a stream (useful in tests). +// - [MergeStreamReaders] fans-in multiple streams into one. +// - [MergeNamedStreamReaders] like MergeStreamReaders but emits [SourceEOF] +// when each named source ends, useful for tracking per-source completion. +// - [StreamReaderWithConvert] transforms element types; return [ErrNoValue] +// from the convert function to skip an element. +// +// See https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/stream_programming_essentials/ +package schema diff --git a/schema/document.go b/schema/document.go new file mode 100644 index 0000000..09c19d5 --- /dev/null +++ b/schema/document.go @@ -0,0 +1,220 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +const ( + docMetaDataKeySubIndexes = "_sub_indexes" + docMetaDataKeyScore = "_score" + docMetaDataKeyExtraInfo = "_extra_info" + docMetaDataKeyDSL = "_dsl" + docMetaDataKeyDenseVector = "_dense_vector" + docMetaDataKeySparseVector = "_sparse_vector" +) + +// Document is a piece of text with a metadata map. It is the shared currency +// between Loader, Transformer, Indexer, and Retriever components. +// +// Metadata is an open map[string]any that lets pipeline stages attach typed +// values to a document without creating a new struct. Well-known keys are +// managed through typed accessor methods — Score, SubIndexes, DenseVector, +// SparseVector, DSLInfo, ExtraInfo — so callers never need to reference the +// raw key strings. +// +// Transformer implementations should preserve existing metadata and merge new +// keys rather than replacing the map outright, so provenance information +// accumulated by earlier stages is not lost. +type Document struct { + // ID is the unique identifier of the document. + ID string `json:"id"` + // Content is the content of the document. + Content string `json:"content"` + // MetaData is the metadata of the document, can be used to store extra information. + MetaData map[string]any `json:"meta_data"` +} + +// String returns the content of the document. +func (d *Document) String() string { + return d.Content +} + +// WithSubIndexes sets the sub-indexes on the document metadata and returns the +// document for chaining. Sub-indexes let an Indexer route a document into +// multiple logical partitions of a vector store simultaneously. +// Use [Document.SubIndexes] to retrieve them. +func (d *Document) WithSubIndexes(indexes []string) *Document { + if d.MetaData == nil { + d.MetaData = make(map[string]any) + } + + d.MetaData[docMetaDataKeySubIndexes] = indexes + + return d +} + +// SubIndexes returns the sub indexes of the document. +// can use doc.WithSubIndexes() to set the sub indexes. +func (d *Document) SubIndexes() []string { + if d.MetaData == nil { + return nil + } + + indexes, ok := d.MetaData[docMetaDataKeySubIndexes].([]string) + if ok { + return indexes + } + + return nil +} + +// WithScore sets the relevance score on the document, typically written by a +// Retriever after ranking results. A higher score means higher relevance. +// Note: [retriever.WithScoreThreshold] filters by this value, not sort order. +// Use [Document.Score] to retrieve it. +func (d *Document) WithScore(score float64) *Document { + if d.MetaData == nil { + d.MetaData = make(map[string]any) + } + + d.MetaData[docMetaDataKeyScore] = score + + return d +} + +// Score returns the score of the document. +// can use doc.WithScore() to set the score. +func (d *Document) Score() float64 { + if d.MetaData == nil { + return 0 + } + + score, ok := d.MetaData[docMetaDataKeyScore].(float64) + if ok { + return score + } + + return 0 +} + +// WithExtraInfo sets the extra info of the document. +// can use doc.ExtraInfo() to get the extra info. +func (d *Document) WithExtraInfo(extraInfo string) *Document { + if d.MetaData == nil { + d.MetaData = make(map[string]any) + } + + d.MetaData[docMetaDataKeyExtraInfo] = extraInfo + + return d +} + +// ExtraInfo returns the extra info of the document. +// can use doc.WithExtraInfo() to set the extra info. +func (d *Document) ExtraInfo() string { + if d.MetaData == nil { + return "" + } + + extraInfo, ok := d.MetaData[docMetaDataKeyExtraInfo].(string) + if ok { + return extraInfo + } + + return "" +} + +// WithDSLInfo attaches a domain-specific-language query description to the +// document. This is consumed by Retriever implementations that support +// structured queries (e.g., filter expressions) alongside vector search. +// Use [Document.DSLInfo] to retrieve it. +func (d *Document) WithDSLInfo(dslInfo map[string]any) *Document { + if d.MetaData == nil { + d.MetaData = make(map[string]any) + } + + d.MetaData[docMetaDataKeyDSL] = dslInfo + + return d +} + +// DSLInfo returns the dsl info of the document. +// can use doc.WithDSLInfo() to set the dsl info. +func (d *Document) DSLInfo() map[string]any { + if d.MetaData == nil { + return nil + } + + dslInfo, ok := d.MetaData[docMetaDataKeyDSL].(map[string]any) + if ok { + return dslInfo + } + + return nil +} + +// WithDenseVector sets the dense vector of the document. +// can use doc.DenseVector() to get the dense vector. +func (d *Document) WithDenseVector(vector []float64) *Document { + if d.MetaData == nil { + d.MetaData = make(map[string]any) + } + + d.MetaData[docMetaDataKeyDenseVector] = vector + + return d +} + +// DenseVector returns the dense vector of the document. +// can use doc.WithDenseVector() to set the dense vector. +func (d *Document) DenseVector() []float64 { + if d.MetaData == nil { + return nil + } + + vector, ok := d.MetaData[docMetaDataKeyDenseVector].([]float64) + if ok { + return vector + } + + return nil +} + +// WithSparseVector sets the sparse vector of the document, key indices -> value vector. +// can use doc.SparseVector() to get the sparse vector. +func (d *Document) WithSparseVector(sparse map[int]float64) *Document { + if d.MetaData == nil { + d.MetaData = make(map[string]any) + } + + d.MetaData[docMetaDataKeySparseVector] = sparse + + return d +} + +// SparseVector returns the sparse vector of the document, key indices -> value vector. +// can use doc.WithSparseVector() to set the sparse vector. +func (d *Document) SparseVector() map[int]float64 { + if d.MetaData == nil { + return nil + } + + sparse, ok := d.MetaData[docMetaDataKeySparseVector].(map[int]float64) + if ok { + return sparse + } + + return nil +} diff --git a/schema/document_test.go b/schema/document_test.go new file mode 100644 index 0000000..db2ba37 --- /dev/null +++ b/schema/document_test.go @@ -0,0 +1,53 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "testing" + + "github.com/smartystreets/goconvey/convey" +) + +func TestDocument(t *testing.T) { + convey.Convey("test document", t, func() { + var ( + subIndexes = []string{"hello", "bye"} + score = 1.1 + extraInfo = "asd" + dslInfo = map[string]any{"hello": true} + vector = []float64{1.1, 2.2} + ) + + d := &Document{ + ID: "asd", + Content: "qwe", + MetaData: nil, + } + + d.WithSubIndexes(subIndexes). + WithDenseVector(vector). + WithScore(score). + WithExtraInfo(extraInfo). + WithDSLInfo(dslInfo) + + convey.So(d.SubIndexes(), convey.ShouldEqual, subIndexes) + convey.So(d.Score(), convey.ShouldEqual, score) + convey.So(d.ExtraInfo(), convey.ShouldEqual, extraInfo) + convey.So(d.DSLInfo(), convey.ShouldEqual, dslInfo) + convey.So(d.DenseVector(), convey.ShouldEqual, vector) + }) +} diff --git a/schema/gemini/extension.go b/schema/gemini/extension.go new file mode 100644 index 0000000..efbc4f4 --- /dev/null +++ b/schema/gemini/extension.go @@ -0,0 +1,115 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package gemini defines the extension for gemini. +package gemini + +import ( + "fmt" +) + +type ResponseMetaExtension struct { + ID string `json:"id,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` + GroundingMeta *GroundingMetadata `json:"grounding_meta,omitempty"` +} + +type GroundingMetadata struct { + // List of supporting references retrieved from specified grounding source. + GroundingChunks []*GroundingChunk `json:"grounding_chunks,omitempty"` + // Optional. List of grounding support. + GroundingSupports []*GroundingSupport `json:"grounding_supports,omitempty"` + // Optional. Google search entry for the following-up web searches. + SearchEntryPoint *SearchEntryPoint `json:"search_entry_point,omitempty"` + // Optional. Web search queries for the following-up web search. + WebSearchQueries []string `json:"web_search_queries,omitempty"` +} + +type GroundingChunk struct { + // Grounding chunk from the web. + Web *GroundingChunkWeb `json:"web,omitempty"` +} + +// GroundingChunkWeb is the chunk from the web. +type GroundingChunkWeb struct { + // Domain of the (original) URI. This field is not supported in Gemini API. + Domain string `json:"domain,omitempty"` + // Title of the chunk. + Title string `json:"title,omitempty"` + // URI reference of the chunk. + URI string `json:"uri,omitempty"` +} + +type GroundingSupport struct { + // Confidence score of the support references. Ranges from 0 to 1. 1 is the most confident. + // For Gemini 2.0 and before, this list must have the same size as the grounding_chunk_indices. + // For Gemini 2.5 and after, this list will be empty and should be ignored. + ConfidenceScores []float32 `json:"confidence_scores,omitempty"` + // A list of indices (into 'grounding_chunk') specifying the citations associated with + // the claim. For instance [1,3,4] means that grounding_chunk[1], grounding_chunk[3], + // grounding_chunk[4] are the retrieved content attributed to the claim. + GroundingChunkIndices []int `json:"grounding_chunk_indices,omitempty"` + // Segment of the content this support belongs to. + Segment *Segment `json:"segment,omitempty"` +} + +// Segment of the content. +type Segment struct { + // Output only. End index in the given Part, measured in bytes. Offset from the start + // of the Part, exclusive, starting at zero. + EndIndex int `json:"end_index,omitempty"` + // Output only. The index of a Part object within its parent Content object. + PartIndex int `json:"part_index,omitempty"` + // Output only. Start index in the given Part, measured in bytes. Offset from the start + // of the Part, inclusive, starting at zero. + StartIndex int `json:"start_index,omitempty"` + // Output only. The text corresponding to the segment from the response. + Text string `json:"text,omitempty"` +} + +// SearchEntryPoint is the Google search entry point. +type SearchEntryPoint struct { + // Optional. Web content snippet that can be embedded in a web page or an app webview. + RenderedContent string `json:"rendered_content,omitempty"` + // Optional. Base64 encoded JSON representing array of tuple. + SDKBlob []byte `json:"sdk_blob,omitempty"` +} + +// ConcatResponseMetaExtensions concatenates multiple ResponseMetaExtension chunks into a single one. +func ConcatResponseMetaExtensions(chunks []*ResponseMetaExtension) (*ResponseMetaExtension, error) { + if len(chunks) == 0 { + return nil, fmt.Errorf("no response meta extension found") + } + if len(chunks) == 1 { + return chunks[0], nil + } + + ret := &ResponseMetaExtension{} + + for _, ext := range chunks { + if ext.ID != "" { + ret.ID = ext.ID + } + if ext.FinishReason != "" { + ret.FinishReason = ext.FinishReason + } + if ext.GroundingMeta != nil { + ret.GroundingMeta = ext.GroundingMeta + } + } + + return ret, nil +} diff --git a/schema/gemini/extension_test.go b/schema/gemini/extension_test.go new file mode 100644 index 0000000..56f390a --- /dev/null +++ b/schema/gemini/extension_test.go @@ -0,0 +1,79 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gemini + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConcatResponseMetaExtensions(t *testing.T) { + t.Run("multiple extensions - takes last non-empty values", func(t *testing.T) { + meta1 := &GroundingMetadata{WebSearchQueries: []string{"query1"}} + meta2 := &GroundingMetadata{WebSearchQueries: []string{"query2"}} + + exts := []*ResponseMetaExtension{ + { + ID: "resp_1", + FinishReason: "STOP", + GroundingMeta: meta1, + }, + { + ID: "resp_2", + FinishReason: "", + GroundingMeta: nil, + }, + { + ID: "", + FinishReason: "MAX_TOKENS", + GroundingMeta: meta2, + }, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "resp_2", result.ID) + assert.Equal(t, "MAX_TOKENS", result.FinishReason) + assert.Equal(t, meta2, result.GroundingMeta) + }) + + t.Run("streaming scenario", func(t *testing.T) { + meta := &GroundingMetadata{ + GroundingChunks: []*GroundingChunk{ + { + Web: &GroundingChunkWeb{ + Title: "Example", + URI: "https://example.com", + }, + }, + }, + } + + exts := []*ResponseMetaExtension{ + {ID: "stream_123", FinishReason: "", GroundingMeta: nil}, + {ID: "", FinishReason: "", GroundingMeta: nil}, + {ID: "", FinishReason: "STOP", GroundingMeta: meta}, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "stream_123", result.ID) + assert.Equal(t, "STOP", result.FinishReason) + assert.Equal(t, meta, result.GroundingMeta) + }) +} diff --git a/schema/message.go b/schema/message.go new file mode 100644 index 0000000..892abe7 --- /dev/null +++ b/schema/message.go @@ -0,0 +1,1937 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "context" + "fmt" + "io" + "reflect" + "sort" + "strings" + "sync" + "text/template" + + "github.com/nikolalohinski/gonja" + "github.com/nikolalohinski/gonja/config" + "github.com/nikolalohinski/gonja/exec" + "github.com/nikolalohinski/gonja/nodes" + "github.com/nikolalohinski/gonja/parser" + "github.com/slongfield/pyfmt" + + "github.com/cloudwego/eino/internal" + "github.com/cloudwego/eino/internal/generic" +) + +func init() { + internal.RegisterStreamChunkConcatFunc(ConcatMessages) + internal.RegisterStreamChunkConcatFunc(ConcatMessageArray) + + internal.RegisterStreamChunkConcatFunc(ConcatAgenticMessages) + internal.RegisterStreamChunkConcatFunc(ConcatAgenticMessagesArray) + + internal.RegisterStreamChunkConcatFunc(ConcatToolResults) +} + +func buildConcatGenericArray[T any](f func([]*T) (*T, error)) func([][]*T) ([]*T, error) { + return func(mas [][]*T) ([]*T, error) { + arrayLen := len(mas[0]) + + ret := make([]*T, arrayLen) + slicesToConcat := make([][]*T, arrayLen) + + for _, ma := range mas { + if len(ma) != arrayLen { + return nil, fmt.Errorf("unexpected array length. "+ + "Got %d, expected %d", len(ma), arrayLen) + } + + for i := 0; i < arrayLen; i++ { + m := ma[i] + if m != nil { + slicesToConcat[i] = append(slicesToConcat[i], m) + } + } + } + + for i, slice := range slicesToConcat { + if len(slice) == 0 { + ret[i] = nil + } else if len(slice) == 1 { + ret[i] = slice[0] + } else { + cm, err := f(slice) + if err != nil { + return nil, err + } + + ret[i] = cm + } + } + + return ret, nil + } +} + +// ConcatMessageArray merges aligned slices of messages into a single slice, +// concatenating messages at the same index across the input arrays. +func ConcatMessageArray(mas [][]*Message) ([]*Message, error) { + return buildConcatGenericArray[Message](ConcatMessages)(mas) +} + +// FormatType used by MessageTemplate.Format +type FormatType uint8 + +const ( + // FString Supported by pyfmt(github.com/slongfield/pyfmt), which is an implementation of https://peps.python.org/pep-3101/. + FString FormatType = 0 + // GoTemplate https://pkg.go.dev/text/template. + GoTemplate FormatType = 1 + // Jinja2 Supported by gonja(github.com/nikolalohinski/gonja), which is a implementation of https://jinja.palletsprojects.com/en/3.1.x/templates/. + Jinja2 FormatType = 2 +) + +// RoleType is the type of the role of a message. +type RoleType string + +const ( + // Assistant is the role of an assistant, means the message is returned by ChatModel. + Assistant RoleType = "assistant" + // User is the role of a user, means the message is a user message. + User RoleType = "user" + // System is the role of a system, means the message is a system message. + System RoleType = "system" + // Tool is the role of a tool, means the message is a tool call output. + Tool RoleType = "tool" +) + +// FunctionCall is the function call in a message. +// It's used in Assistant Message. +type FunctionCall struct { + // Name is the name of the function to call, it can be used to identify the specific function. + Name string `json:"name,omitempty"` + // Arguments is the arguments to call the function with, in JSON format. + Arguments string `json:"arguments,omitempty"` +} + +// ToolCall is the tool call in a message. +// It's used in Assistant Message when there are tool calls should be made. +type ToolCall struct { + // Index is used when there are multiple tool calls in a message. + // In stream mode, it's used to identify the chunk of the tool call for merging. + Index *int `json:"index,omitempty"` + // ID is the id of the tool call, it can be used to identify the specific tool call. + ID string `json:"id"` + // Type is the type of the tool call, default is "function". + Type string `json:"type"` + // Function is the function call to be made. + Function FunctionCall `json:"function"` + // Extra is used to store extra information for the tool call. + Extra map[string]any `json:"extra,omitempty"` +} + +// ImageURLDetail is the detail of the image url. +type ImageURLDetail string + +const ( + // ImageURLDetailHigh means the high quality image url. + ImageURLDetailHigh ImageURLDetail = "high" + // ImageURLDetailLow means the low quality image url. + ImageURLDetailLow ImageURLDetail = "low" + // ImageURLDetailAuto means the auto quality image url. + ImageURLDetailAuto ImageURLDetail = "auto" +) + +// MessagePartCommon represents the common abstract components for input and output of multi-modal types. +type MessagePartCommon struct { + // URL is primarily used for HTTP or HTTPS access links. + // For data in the format 'data:[][;base64],' (the 'data' URL Schema of RFC-2397 (https://www.rfc-editor.org/rfc/rfc2397)), + // it is recommended to use Base64Data and MIMEType fields separately instead. + URL *string `json:"url,omitempty"` + + // Base64Data represents the binary data in Base64 encoded string format. + Base64Data *string `json:"base64data,omitempty"` + + // MIMEType is the mime type , eg."image/png",""audio/wav" etc. + MIMEType string `json:"mime_type,omitempty"` + + // Deprecated: Use MessageOutputPart.Extra or MessageInputPart.Extra to set additional metadata instead. + Extra map[string]any `json:"extra,omitempty"` +} + +// MessageInputImage is used to represent an image part in message. +// Choose either URL or Base64Data. +type MessageInputImage struct { + MessagePartCommon + + // Detail is the quality of the image url. + Detail ImageURLDetail `json:"detail,omitempty"` +} + +// MessageInputAudio is used to represent an audio part in message. +// Choose either URL or Base64Data. +type MessageInputAudio struct { + MessagePartCommon +} + +// MessageInputVideo is used to represent a video part in message. +// Choose either URL or Base64Data. +type MessageInputVideo struct { + MessagePartCommon +} + +// MessageInputFile is used to represent a file part in message. +// Choose either URL or Base64Data. +type MessageInputFile struct { + MessagePartCommon + + // Name represents the filename. + // Optional. + Name string `json:"name,omitempty"` +} + +// MessageInputPart represents the input part of message. +type MessageInputPart struct { + Type ChatMessagePartType `json:"type"` + + Text string `json:"text,omitempty"` + + // Image is the image input of the part, it's used when Type is "image_url". + Image *MessageInputImage `json:"image,omitempty"` + + // Audio is the audio input of the part, it's used when Type is "audio_url". + Audio *MessageInputAudio `json:"audio,omitempty"` + + // Video is the video input of the part, it's used when Type is "video_url". + Video *MessageInputVideo `json:"video,omitempty"` + + // File is the file input of the part, it's used when Type is "file_url". + File *MessageInputFile `json:"file,omitempty"` + + // ToolSearchResult holds the result of a tool search request, containing the matched tool names and their definitions. + ToolSearchResult *ToolSearchResult `json:"tool_search_result,omitempty"` + + // Extra is used to store extra information. + Extra map[string]any `json:"extra,omitempty"` +} + +// MessageOutputImage is used to represent an image part in message. +type MessageOutputImage struct { + MessagePartCommon +} + +// MessageOutputAudio is used to represent an audio part in message. +type MessageOutputAudio struct { + MessagePartCommon +} + +// MessageOutputVideo is used to represent a video part in message. +type MessageOutputVideo struct { + MessagePartCommon +} + +// MessageOutputReasoning represents the reasoning content generated by reasoning models. +// Some models produce reasoning steps before generating the final response. +// This struct captures that reasoning output. +type MessageOutputReasoning struct { + // Text is either the thought summary or the raw reasoning text itself. + Text string `json:"text,omitempty"` + + // Signature contains encrypted reasoning tokens. + // Required by some models when passing reasoning context back in subsequent requests. + Signature string `json:"signature,omitempty"` +} + +// MessageStreamingMeta contains metadata for streaming responses. +// It is used to track position of part when the model outputs multiple parts in a single response. +type MessageStreamingMeta struct { + // Index specifies the index position of this part in the final response. + // This is useful for reassembling multiple reasoning/content parts in correct order. + Index int `json:"index,omitempty"` +} + +// MessageOutputPart represents a part of an assistant-generated message. +// It can contain text, or multimedia content like images, audio, or video. +type MessageOutputPart struct { + // Type is the type of the part, e.g. "text", "image_url", "audio_url", "video_url". + Type ChatMessagePartType `json:"type"` + + // Text is the text of the part, it's used when Type is "text". + Text string `json:"text,omitempty"` + + // Image is the image output of the part, used when Type is ChatMessagePartTypeImageURL. + Image *MessageOutputImage `json:"image,omitempty"` + + // Audio is the audio output of the part, used when Type is ChatMessagePartTypeAudioURL. + Audio *MessageOutputAudio `json:"audio,omitempty"` + + // Video is the video output of the part, used when Type is ChatMessagePartTypeVideoURL. + Video *MessageOutputVideo `json:"video,omitempty"` + + // Reasoning contains the reasoning content generated by the model. + // Used when Type is ChatMessagePartTypeReasoning. + Reasoning *MessageOutputReasoning `json:"reasoning,omitempty"` + + // Extra is used to store extra information. + Extra map[string]any `json:"extra,omitempty"` + + // StreamingMeta contains metadata for streaming responses. + // This field is typically used at runtime and not serialized. + StreamingMeta *MessageStreamingMeta `json:"-"` +} + +// Deprecated: This struct is deprecated as the MultiContent field is deprecated. +// For the image input part of the model, use MessageInputImage. +// For the image output part of the model, use MessageOutputImage. +// Choose either URL or URI. +// If your model implementation supports it, URL could embed inline image data +// as defined in RFC-2397. +type ChatMessageImageURL struct { + // URL can either be a traditional URL or a special URL conforming to RFC-2397 (https://www.rfc-editor.org/rfc/rfc2397). + // double check with model implementations for detailed instructions on how to use this. + URL string `json:"url,omitempty"` + + URI string `json:"uri,omitempty"` + // Detail is the quality of the image url. + Detail ImageURLDetail `json:"detail,omitempty"` + + // MIMEType is the mime type of the image, eg. "image/png". + MIMEType string `json:"mime_type,omitempty"` + // Extra is used to store extra information for the image url. + Extra map[string]any `json:"extra,omitempty"` +} + +// ChatMessagePartType is the type of the part in a chat message. +type ChatMessagePartType string + +const ( + // ChatMessagePartTypeText means the part is a text. + ChatMessagePartTypeText ChatMessagePartType = "text" + // ChatMessagePartTypeImageURL means the part is an image url. + ChatMessagePartTypeImageURL ChatMessagePartType = "image_url" + // ChatMessagePartTypeAudioURL means the part is an audio url. + ChatMessagePartTypeAudioURL ChatMessagePartType = "audio_url" + // ChatMessagePartTypeVideoURL means the part is a video url. + ChatMessagePartTypeVideoURL ChatMessagePartType = "video_url" + // ChatMessagePartTypeFileURL means the part is a file url. + ChatMessagePartTypeFileURL ChatMessagePartType = "file_url" + // ChatMessagePartTypeReasoning means the part is a reasoning block. + ChatMessagePartTypeReasoning ChatMessagePartType = "reasoning" + + // ChatMessagePartTypeToolSearchResult means the part contains tool search results. + ChatMessagePartTypeToolSearchResult ChatMessagePartType = "tool_search_result" +) + +// Deprecated: This struct is deprecated as the MultiContent field is deprecated. +// For the audio input part of the model, use MessageInputAudio. +// For the audio output part of the model, use MessageOutputAudio. +// Choose either URL or URI. +// If supported, URL may embed inline audio data per RFC-2397. +type ChatMessageAudioURL struct { + // URL can either be a traditional URL or a special URL conforming to RFC-2397 (https://www.rfc-editor.org/rfc/rfc2397). + // double check with model implementations for detailed instructions on how to use this. + URL string `json:"url,omitempty"` + URI string `json:"uri,omitempty"` + + // MIMEType is the mime type of the audio, eg. "audio/wav" or "audio/ogg". + MIMEType string `json:"mime_type,omitempty"` + // Extra is used to store extra information for the audio url. + Extra map[string]any `json:"extra,omitempty"` +} + +// Deprecated: This struct is deprecated as the MultiContent field is deprecated. +// For the video input part of the model, use MessageInputVideo. +// For the video output part of the model, use MessageOutputVideo. +// Choose either URL or URI. +// If supported, URL may embed inline video data per RFC-2397. +type ChatMessageVideoURL struct { + // URL can either be a traditional URL or a special URL conforming to RFC-2397 (https://www.rfc-editor.org/rfc/rfc2397). + // double check with model implementations for detailed instructions on how to use this. + URL string `json:"url,omitempty"` + URI string `json:"uri,omitempty"` + + // MIMEType is the mime type of the video, eg. "video/mp4". + MIMEType string `json:"mime_type,omitempty"` + // Extra is used to store extra information for the video url. + Extra map[string]any `json:"extra,omitempty"` +} + +// Deprecated: This struct is deprecated as the MultiContent field is deprecated. +// For the file input part of the model, use MessageInputFile. +// Choose either URL or URI. +type ChatMessageFileURL struct { + URL string `json:"url,omitempty"` + URI string `json:"uri,omitempty"` + + // MIMEType is the mime type of the file, eg. "application/pdf", "text/plain". + MIMEType string `json:"mime_type,omitempty"` + // Name is the name of the file. + Name string `json:"name,omitempty"` + + // Extra is used to store extra information for the file url. + Extra map[string]any `json:"extra,omitempty"` +} + +// Deprecated: This struct is deprecated as the MultiContent field is deprecated. +// For model input, use MessageInputPart. For model output, use MessageOutputPart. +type ChatMessagePart struct { + // Type is the type of the part, eg. "text", "image_url", "audio_url", "video_url", "file_url". + Type ChatMessagePartType `json:"type,omitempty"` + + // Text is the text of the part, it's used when Type is "text". + Text string `json:"text,omitempty"` + + // ImageURL is the image url of the part, it's used when Type is "image_url". + ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` + // AudioURL is the audio url of the part, it's used when Type is "audio_url". + AudioURL *ChatMessageAudioURL `json:"audio_url,omitempty"` + // VideoURL is the video url of the part, it's used when Type is "video_url". + VideoURL *ChatMessageVideoURL `json:"video_url,omitempty"` + // FileURL is the file url of the part, it's used when Type is "file_url". + FileURL *ChatMessageFileURL `json:"file_url,omitempty"` +} + +// LogProbs is the top-level structure containing the log probability information. +type LogProbs struct { + // Content is a list of message content tokens with log probability information. + Content []LogProb `json:"content"` +} + +// LogProb represents the probability information for a token. +type LogProb struct { + // Token represents the text of the token, which is a contiguous sequence of characters + // (e.g., a word, part of a word, or punctuation) as understood by the tokenization process used by the language model. + Token string `json:"token"` + // LogProb is the log probability of this token, if it is within the top 20 most likely tokens. + // Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. + LogProb float64 `json:"logprob"` + // Bytes is a list of integers representing the UTF-8 bytes representation of the token. + // Useful in instances where characters are represented by multiple tokens and + // their byte representations must be combined to generate the correct text + // representation. Can be `null` if there is no bytes representation for the token. + Bytes []int64 `json:"bytes,omitempty"` // Omitting the field if it is null + // TopLogProbs is a list of the most likely tokens and their log probability, at this token position. + // In rare cases, there may be fewer than the number of requested top_logprobs returned. + TopLogProbs []TopLogProb `json:"top_logprobs"` +} + +// TopLogProb describes a likely token and its log probability at a position. +type TopLogProb struct { + // Token represents the text of the token, which is a contiguous sequence of characters + // (e.g., a word, part of a word, or punctuation) as understood by the tokenization process used by the language model. + Token string `json:"token"` + // LogProb is the log probability of this token, if it is within the top 20 most likely tokens. + // Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. + LogProb float64 `json:"logprob"` + // Bytes is a list of integers representing the UTF-8 bytes representation of the token. + // Useful in instances where characters are represented by multiple tokens and + // their byte representations must be combined to generate the correct text + // representation. Can be `null` if there is no bytes representation for the token. + Bytes []int64 `json:"bytes,omitempty"` +} + +// ResponseMeta collects meta information about a chat response. +type ResponseMeta struct { + // FinishReason is the reason why the chat response is finished. + // It's usually "stop", "length", "tool_calls", "content_filter", "null". This is defined by chat model implementation. + FinishReason string `json:"finish_reason,omitempty"` + // Usage is the token usage of the chat response, whether usage exists depends on whether the chat model implementation returns. + Usage *TokenUsage `json:"usage,omitempty"` + // LogProbs is Log probability information. + LogProbs *LogProbs `json:"logprobs,omitempty"` +} + +// Message denotes the data structure for model input and output, originating from either user input or model return. +// It supports both text-only and multimodal content. +// +// For text-only input from a user, use the Content field: +// +// &schema.Message{ +// Role: schema.User, +// Content: "What is the capital of France?", +// } +// +// For multimodal input from a user, use the UserInputMultiContent field. +// This allows combining text with other media like images: +// +// &schema.Message{ +// Role: schema.User, +// UserInputMultiContent: []schema.MessageInputPart{ +// {Type: schema.ChatMessagePartTypeText, Text: "What is in this image?"}, +// {Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageInputImage{ +// MessagePartCommon: schema.MessagePartCommon{ +// URL: toPtr("https://example.com/cat.jpg"), +// }, +// Detail: schema.ImageURLDetailHigh, +// }}, +// }, +// } +// +// When the model returns multimodal content, it is available in the AssistantGenMultiContent field: +// +// &schema.Message{ +// Role: schema.Assistant, +// AssistantGenMultiContent: []schema.MessageOutputPart{ +// {Type: schema.ChatMessagePartTypeText, Text: "Here is the generated image:"}, +// {Type: schema.ChatMessagePartTypeImage, Image: &schema.MessageOutputImage{ +// MessagePartCommon: schema.MessagePartCommon{ +// Base64Data: toPtr("base64_image_binary"), +// MIMEType: "image/png", +// }, +// }}, +// }, +// } +type Message struct { + Role RoleType `json:"role"` + + // Content is for user text input and model text output. + Content string `json:"content"` + + // if MultiContent is not empty, use this instead of Content + // if MultiContent is empty, use Content + // Deprecated: Use UserInputMultiContent for user multimodal inputs and AssistantGenMultiContent for model multimodal outputs. + MultiContent []ChatMessagePart `json:"multi_content,omitempty"` + + // UserInputMultiContent passes multimodal content provided by the user to the model. + UserInputMultiContent []MessageInputPart `json:"user_input_multi_content,omitempty"` + + // AssistantGenMultiContent is for receiving multimodal output from the model. + AssistantGenMultiContent []MessageOutputPart `json:"assistant_output_multi_content,omitempty"` + + Name string `json:"name,omitempty"` + + // only for AssistantMessage + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + + // only for ToolMessage + ToolCallID string `json:"tool_call_id,omitempty"` + // only for ToolMessage + ToolName string `json:"tool_name,omitempty"` + + ResponseMeta *ResponseMeta `json:"response_meta,omitempty"` + + // ReasoningContent is the thinking process of the model, which will be included when the model returns reasoning content. + ReasoningContent string `json:"reasoning_content,omitempty"` + + // customized information for model implementation + Extra map[string]any `json:"extra,omitempty"` +} + +// TokenUsage Represents the token usage of chat model request. +type TokenUsage struct { + // PromptTokens is the number of prompt tokens, including all the input tokens of this request. + PromptTokens int `json:"prompt_tokens"` + // PromptTokenDetails is a breakdown of the prompt tokens. + PromptTokenDetails PromptTokenDetails `json:"prompt_token_details"` + // CompletionTokens is the number of completion tokens. + CompletionTokens int `json:"completion_tokens"` + // TotalTokens is the total number of tokens. + TotalTokens int `json:"total_tokens"` + // CompletionTokensDetails is breakdown of completion tokens. + CompletionTokensDetails CompletionTokensDetails `json:"completion_token_details"` +} + +type CompletionTokensDetails struct { + // ReasoningTokens tokens generated by the model for reasoning. + // This is currently supported by OpenAI, Gemini, ARK and Qwen chat models. + // For other models, this field will be 0. + ReasoningTokens int `json:"reasoning_tokens,omitempty"` +} + +// PromptTokenDetails provides a breakdown of prompt token usage. +type PromptTokenDetails struct { + // Cached tokens present in the prompt. + CachedTokens int `json:"cached_tokens"` +} + +var _ MessagesTemplate = &Message{} +var _ MessagesTemplate = MessagesPlaceholder("", false) + +// MessagesTemplate is the interface for messages template. +// It's used to render a template to a list of messages. +// e.g. +// +// chatTemplate := prompt.FromMessages( +// schema.SystemMessage("you are an eino helper"), +// schema.MessagesPlaceholder("history", false), // <= this will use the value of "history" in params +// ) +// msgs, err := chatTemplate.Format(ctx, params) +type MessagesTemplate interface { + Format(ctx context.Context, vs map[string]any, formatType FormatType) ([]*Message, error) +} + +type messagesPlaceholder struct { + key string + optional bool +} + +// MessagesPlaceholder can render a placeholder to a list of messages in params. +// e.g. +// +// placeholder := MessagesPlaceholder("history", false) +// params := map[string]any{ +// "history": []*schema.Message{{Role: "user", Content: "what is eino?"}, {Role: "assistant", Content: "eino is a great framework to build llm apps"}}, +// "query": "how to use eino?", +// } +// chatTemplate := chatTpl := prompt.FromMessages( +// schema.SystemMessage("you are eino helper"), +// schema.MessagesPlaceholder("history", false), // <= this will use the value of "history" in params +// ) +// msgs, err := chatTemplate.Format(ctx, params) +func MessagesPlaceholder(key string, optional bool) MessagesTemplate { + return &messagesPlaceholder{ + key: key, + optional: optional, + } +} + +// Format just return the messages of specified key. +// because it's a placeholder. +// e.g. +// +// placeholder := MessagesPlaceholder("history", false) +// params := map[string]any{ +// "history": []*schema.Message{{Role: "user", Content: "what is eino?"}, {Role: "assistant", Content: "eino is a great freamwork to build llm apps"}}, +// "query": "how to use eino?", +// } +// msgs, err := placeholder.Format(ctx, params) // <= this will return the value of "history" in params +func (p *messagesPlaceholder) Format(_ context.Context, vs map[string]any, _ FormatType) ([]*Message, error) { + v, ok := vs[p.key] + if !ok { + if p.optional { + return []*Message{}, nil + } + + return nil, fmt.Errorf("message placeholder format: %s not found", p.key) + } + + msgs, ok := v.([]*Message) + if !ok { + return nil, fmt.Errorf("only messages can be used to format message placeholder, key: %v, actual type: %v", p.key, reflect.TypeOf(v)) + } + + return msgs, nil +} + +func formatContent(content string, vs map[string]any, formatType FormatType) (string, error) { + switch formatType { + case FString: + return pyfmt.Fmt(content, vs) + case GoTemplate: + parsedTmpl, err := template.New("template"). + Option("missingkey=error"). + Parse(content) + if err != nil { + return "", err + } + sb := new(strings.Builder) + err = parsedTmpl.Execute(sb, vs) + if err != nil { + return "", err + } + return sb.String(), nil + case Jinja2: + env, err := getJinjaEnv() + if err != nil { + return "", err + } + tpl, err := env.FromString(content) + if err != nil { + return "", err + } + out, err := tpl.Execute(vs) + if err != nil { + return "", err + } + return out, nil + default: + return "", fmt.Errorf("unknown format type: %v", formatType) + } +} + +// Format returns the messages after rendering by the given formatType. +// e.g. +// +// msg := schema.UserMessage("hello world, {name}") +// msgs, err := msg.Format(ctx, map[string]any{"name": "eino"}, schema.FString) // <= this will render the content of msg by pyfmt +// // msgs[0].Content will be "hello world, eino" +func (m *Message) Format(_ context.Context, vs map[string]any, formatType FormatType) ([]*Message, error) { + c, err := formatContent(m.Content, vs, formatType) + if err != nil { + return nil, err + } + copied := *m + copied.Content = c + + if len(m.MultiContent) > 0 { + copied.MultiContent, err = formatMultiContent(m.MultiContent, vs, formatType) + if err != nil { + return nil, err + } + } + + if len(m.UserInputMultiContent) > 0 { + copied.UserInputMultiContent, err = formatUserInputMultiContent(m.UserInputMultiContent, vs, formatType) + if err != nil { + return nil, err + } + } + + return []*Message{&copied}, nil +} + +func formatMultiContent(multiContent []ChatMessagePart, vs map[string]any, formatType FormatType) ([]ChatMessagePart, error) { + copiedMC := make([]ChatMessagePart, len(multiContent)) + copy(copiedMC, multiContent) + + for i, mc := range copiedMC { + switch mc.Type { + case ChatMessagePartTypeText: + nmc, err := formatContent(mc.Text, vs, formatType) + if err != nil { + return nil, err + } + copiedMC[i].Text = nmc + case ChatMessagePartTypeImageURL: + if mc.ImageURL == nil { + continue + } + url, err := formatContent(mc.ImageURL.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedMC[i].ImageURL.URL = url + case ChatMessagePartTypeAudioURL: + if mc.AudioURL == nil { + continue + } + url, err := formatContent(mc.AudioURL.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedMC[i].AudioURL.URL = url + case ChatMessagePartTypeVideoURL: + if mc.VideoURL == nil { + continue + } + url, err := formatContent(mc.VideoURL.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedMC[i].VideoURL.URL = url + case ChatMessagePartTypeFileURL: + if mc.FileURL == nil { + continue + } + url, err := formatContent(mc.FileURL.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedMC[i].FileURL.URL = url + } + } + + return copiedMC, nil +} + +func formatUserInputMultiContent(userInputMultiContent []MessageInputPart, vs map[string]any, formatType FormatType) ([]MessageInputPart, error) { + copiedUIMC := make([]MessageInputPart, len(userInputMultiContent)) + copy(copiedUIMC, userInputMultiContent) + + for i, uimc := range copiedUIMC { + switch uimc.Type { + case ChatMessagePartTypeText: + text, err := formatContent(uimc.Text, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Text = text + case ChatMessagePartTypeImageURL: + if uimc.Image == nil { + continue + } + if uimc.Image.URL != nil && *uimc.Image.URL != "" { + url, err := formatContent(*uimc.Image.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Image.URL = &url + } + if uimc.Image.Base64Data != nil && *uimc.Image.Base64Data != "" { + base64data, err := formatContent(*uimc.Image.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Image.Base64Data = &base64data + } + case ChatMessagePartTypeAudioURL: + if uimc.Audio == nil { + continue + } + if uimc.Audio.URL != nil && *uimc.Audio.URL != "" { + url, err := formatContent(*uimc.Audio.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Audio.URL = &url + } + if uimc.Audio.Base64Data != nil && *uimc.Audio.Base64Data != "" { + base64data, err := formatContent(*uimc.Audio.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Audio.Base64Data = &base64data + } + case ChatMessagePartTypeVideoURL: + if uimc.Video == nil { + continue + } + if uimc.Video.URL != nil && *uimc.Video.URL != "" { + url, err := formatContent(*uimc.Video.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Video.URL = &url + } + if uimc.Video.Base64Data != nil && *uimc.Video.Base64Data != "" { + base64data, err := formatContent(*uimc.Video.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].Video.Base64Data = &base64data + } + case ChatMessagePartTypeFileURL: + if uimc.File == nil { + continue + } + if uimc.File.URL != nil && *uimc.File.URL != "" { + url, err := formatContent(*uimc.File.URL, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].File.URL = &url + } + if uimc.File.Base64Data != nil && *uimc.File.Base64Data != "" { + base64data, err := formatContent(*uimc.File.Base64Data, vs, formatType) + if err != nil { + return nil, err + } + copiedUIMC[i].File.Base64Data = &base64data + } + } + } + + return copiedUIMC, nil +} + +// String returns the string representation of the message. +// e.g. +// +// msg := schema.UserMessage("hello world") +// fmt.Println(msg.String()) // Output will be: `user: hello world`` +// +// msg := schema.Message{ +// Role: schema.Tool, +// Content: "{...}", +// ToolCallID: "callxxxx" +// } +// fmt.Println(msg.String()) +// Output will be: +// tool: {...} +// call_id: callxxxx +func (m *Message) String() string { + sb := &strings.Builder{} + sb.WriteString(fmt.Sprintf("%s: %s", m.Role, m.Content)) + + if len(m.UserInputMultiContent) > 0 { + sb.WriteString("\nuser_input_multi_content:") + for i, part := range m.UserInputMultiContent { + sb.WriteString(fmt.Sprintf("\n [%d] %s", i, formatInputPart(part))) + } + } + + if len(m.AssistantGenMultiContent) > 0 { + sb.WriteString("\nassistant_gen_multi_content:") + for i, part := range m.AssistantGenMultiContent { + sb.WriteString(fmt.Sprintf("\n [%d] %s", i, formatOutputPart(part))) + } + } + + if len(m.MultiContent) > 0 { + sb.WriteString("\nmulti_content:") + for i, part := range m.MultiContent { + sb.WriteString(fmt.Sprintf("\n [%d] %s", i, formatChatMessagePart(part))) + } + } + + if len(m.ReasoningContent) > 0 { + sb.WriteString("\nreasoning content:\n") + sb.WriteString(m.ReasoningContent) + } + if len(m.ToolCalls) > 0 { + sb.WriteString("\ntool_calls:\n") + for _, tc := range m.ToolCalls { + if tc.Index != nil { + sb.WriteString(fmt.Sprintf("index[%d]:", *tc.Index)) + } + sb.WriteString(fmt.Sprintf("%+v\n", tc)) + } + } + if m.ToolCallID != "" { + sb.WriteString(fmt.Sprintf("\ntool_call_id: %s", m.ToolCallID)) + } + if m.ToolName != "" { + sb.WriteString(fmt.Sprintf("\ntool_call_name: %s", m.ToolName)) + } + if m.ResponseMeta != nil { + sb.WriteString(fmt.Sprintf("\nfinish_reason: %s", m.ResponseMeta.FinishReason)) + if m.ResponseMeta.Usage != nil { + sb.WriteString(fmt.Sprintf("\nusage: %v", m.ResponseMeta.Usage)) + } + } + + return sb.String() +} + +func formatInputPart(part MessageInputPart) string { + switch part.Type { + case ChatMessagePartTypeText: + return fmt.Sprintf("text: %s", part.Text) + case ChatMessagePartTypeImageURL: + return fmt.Sprintf("image: %s", formatMessageInputMedia(part.Image)) + case ChatMessagePartTypeAudioURL: + return fmt.Sprintf("audio: %s", formatMessageInputMedia(part.Audio)) + case ChatMessagePartTypeVideoURL: + return fmt.Sprintf("video: %s", formatMessageInputMedia(part.Video)) + case ChatMessagePartTypeFileURL: + return fmt.Sprintf("file: %s", formatMessageInputFile(part.File)) + default: + return fmt.Sprintf("unknown type: %s", part.Type) + } +} + +func formatMessageInputMedia[T MessageInputImage | MessageInputAudio | MessageInputVideo](media *T) string { + if media == nil { + return "" + } + var parts []string + switch v := any(media).(type) { + case *MessageInputImage: + if v.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *v.URL)) + } + if v.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*v.Base64Data))) + } + if v.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", v.MIMEType)) + } + if v.Detail != "" { + parts = append(parts, fmt.Sprintf("detail=%s", v.Detail)) + } + if len(v.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", v.Extra)) + } + case *MessageInputAudio: + if v.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *v.URL)) + } + if v.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*v.Base64Data))) + } + if v.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", v.MIMEType)) + } + if len(v.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", v.Extra)) + } + case *MessageInputVideo: + if v.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *v.URL)) + } + if v.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*v.Base64Data))) + } + if v.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", v.MIMEType)) + } + if len(v.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", v.Extra)) + } + } + if len(parts) == 0 { + return "" + } + return strings.Join(parts, ", ") +} + +func formatMessageInputFile(file *MessageInputFile) string { + if file == nil { + return "" + } + var parts []string + if file.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *file.URL)) + } + if file.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*file.Base64Data))) + } + if file.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", file.MIMEType)) + } + if file.Name != "" { + parts = append(parts, fmt.Sprintf("name=%s", file.Name)) + } + if len(file.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", file.Extra)) + } + if len(parts) == 0 { + return "" + } + return strings.Join(parts, ", ") +} + +func formatOutputPart(part MessageOutputPart) string { + switch part.Type { + case ChatMessagePartTypeText: + return fmt.Sprintf("text: %s", part.Text) + case ChatMessagePartTypeImageURL: + return fmt.Sprintf("image: %s", formatMessageOutputMedia(part.Image)) + case ChatMessagePartTypeAudioURL: + return fmt.Sprintf("audio: %s", formatMessageOutputMedia(part.Audio)) + case ChatMessagePartTypeVideoURL: + return fmt.Sprintf("video: %s", formatMessageOutputMedia(part.Video)) + default: + return fmt.Sprintf("unknown type: %s", part.Type) + } +} + +func formatMessageOutputMedia[T MessageOutputImage | MessageOutputAudio | MessageOutputVideo](media *T) string { + if media == nil { + return "" + } + var parts []string + switch v := any(media).(type) { + case *MessageOutputImage: + if v.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *v.URL)) + } + if v.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*v.Base64Data))) + } + if v.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", v.MIMEType)) + } + if len(v.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", v.Extra)) + } + case *MessageOutputAudio: + if v.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *v.URL)) + } + if v.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*v.Base64Data))) + } + if v.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", v.MIMEType)) + } + if len(v.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", v.Extra)) + } + case *MessageOutputVideo: + if v.URL != nil { + parts = append(parts, fmt.Sprintf("url=%s", *v.URL)) + } + if v.Base64Data != nil { + parts = append(parts, fmt.Sprintf("base64[%d bytes]", len(*v.Base64Data))) + } + if v.MIMEType != "" { + parts = append(parts, fmt.Sprintf("mime=%s", v.MIMEType)) + } + if len(v.Extra) > 0 { + parts = append(parts, fmt.Sprintf("extra=%v", v.Extra)) + } + } + if len(parts) == 0 { + return "" + } + return strings.Join(parts, ", ") +} + +func formatChatMessagePart(part ChatMessagePart) string { + switch part.Type { + case ChatMessagePartTypeText: + return fmt.Sprintf("text: %s", part.Text) + case ChatMessagePartTypeImageURL: + if part.ImageURL != nil { + return fmt.Sprintf("image_url: %s", part.ImageURL.URL) + } + return "image_url: " + case ChatMessagePartTypeAudioURL: + if part.AudioURL != nil { + return fmt.Sprintf("audio_url: %s", part.AudioURL.URL) + } + return "audio_url: " + case ChatMessagePartTypeVideoURL: + if part.VideoURL != nil { + return fmt.Sprintf("video_url: %s", part.VideoURL.URL) + } + return "video_url: " + case ChatMessagePartTypeFileURL: + if part.FileURL != nil { + return fmt.Sprintf("file_url: %s", part.FileURL.URL) + } + return "file_url: " + default: + return fmt.Sprintf("unknown type: %s", part.Type) + } +} + +// SystemMessage represents a message with Role "system". +func SystemMessage(content string) *Message { + return &Message{ + Role: System, + Content: content, + } +} + +// AssistantMessage represents a message with Role "assistant". +func AssistantMessage(content string, toolCalls []ToolCall) *Message { + return &Message{ + Role: Assistant, + Content: content, + ToolCalls: toolCalls, + } +} + +// UserMessage represents a message with Role "user". +func UserMessage(content string) *Message { + return &Message{ + Role: User, + Content: content, + } + +} + +type toolMessageOptions struct { + toolName string +} + +// ToolMessageOption defines a option for ToolMessage +type ToolMessageOption func(*toolMessageOptions) + +// WithToolName returns a ToolMessageOption that sets the tool call name. +func WithToolName(name string) ToolMessageOption { + return func(o *toolMessageOptions) { + o.toolName = name + } +} + +// ToolMessage represents a message with Role "tool". +func ToolMessage(content string, toolCallID string, opts ...ToolMessageOption) *Message { + o := &toolMessageOptions{} + for _, opt := range opts { + opt(o) + } + return &Message{ + Role: Tool, + Content: content, + ToolCallID: toolCallID, + ToolName: o.toolName, + } +} + +// ConcatToolResults merges multiple ToolResult chunks into a single ToolResult. +// It collects all ToolOutputParts from the input chunks and merges contiguous text parts within each chunk. +// +// Merge rules: +// - Text parts: Contiguous text parts within each chunk are concatenated into a single text part. +// - Non-text parts (image, audio, video, file): These parts are kept as-is without merging. +// Each non-text part type can only appear in one chunk; if the same non-text type appears +// in multiple chunks, an error is returned. +// +// This function is primarily used in streaming scenarios where tool output is delivered +// in multiple chunks that need to be merged into a complete result. +// +// Parameters: +// - chunks: A slice of ToolResult pointers representing sequential chunks from a stream. +// Nil chunks and chunks with empty Parts are safely ignored. +// +// Returns: +// - *ToolResult: The merged ToolResult containing all content from the chunks. +// Returns an empty ToolResult if chunks is empty or all chunks are nil/empty. +// - error: An error if the same non-text part type appears in multiple chunks. +func ConcatToolResults(chunks []*ToolResult) (*ToolResult, error) { + if len(chunks) == 0 { + return &ToolResult{}, nil + } + + nonTextPartTypes := make(map[ToolPartType]int) + + var allParts []ToolOutputPart + for chunkIdx, chunk := range chunks { + if chunk == nil || len(chunk.Parts) == 0 { + continue + } + + for _, part := range chunk.Parts { + if part.Type != ToolPartTypeText { + // This restricts non-text modal content only appear once. + if prevChunkIdx, exists := nonTextPartTypes[part.Type]; exists { + return nil, fmt.Errorf("conflicting %s parts found in chunk %d and chunk %d: "+ + "non-text modality parts cannot appear in multiple chunks", part.Type, prevChunkIdx, chunkIdx) + } + nonTextPartTypes[part.Type] = chunkIdx + } + } + + allParts = append(allParts, chunk.Parts...) + } + + mergedChunkParts, err := concatToolOutputParts(allParts) + if err != nil { + return nil, fmt.Errorf("failed to merge parts: %w", err) + } + + return &ToolResult{Parts: mergedChunkParts}, nil +} + +func concatToolOutputParts(parts []ToolOutputPart) ([]ToolOutputPart, error) { + if len(parts) == 0 { + return nil, nil + } + + groups := groupToolOutputParts(parts) + + merged := make([]ToolOutputPart, 0, len(groups)) + for _, group := range groups { + if len(group) == 1 { + merged = append(merged, group...) + continue + } + switch group[0].Type { + case ToolPartTypeText: + mergedPart, err := mergeToolTextParts(group) + if err != nil { + return nil, err + } + merged = append(merged, mergedPart) + default: + merged = append(merged, group...) + } + } + + return merged, nil +} + +func groupToolOutputParts(parts []ToolOutputPart) [][]ToolOutputPart { + groups := make([][]ToolOutputPart, 0) + i := 0 + for i < len(parts) { + if parts[i].Type == ToolPartTypeText { + end := i + 1 + for end < len(parts) && parts[end].Type == ToolPartTypeText { + end++ + } + groups = append(groups, parts[i:end]) + i = end + } else { + groups = append(groups, parts[i:i+1]) + i++ + } + } + return groups +} + +func mergeToolTextParts(group []ToolOutputPart) (ToolOutputPart, error) { + var sb strings.Builder + extraList := make([]map[string]any, 0, len(group)) + for _, part := range group { + sb.WriteString(part.Text) + if len(part.Extra) > 0 { + extraList = append(extraList, part.Extra) + } + } + var mergedExtra map[string]any + if len(extraList) > 0 { + var err error + mergedExtra, err = concatExtra(extraList) + if err != nil { + return ToolOutputPart{}, fmt.Errorf("failed to concat tool output text part extra: %w", err) + } + } + return ToolOutputPart{ + Type: ToolPartTypeText, + Text: sb.String(), + Extra: mergedExtra, + }, nil +} + +func concatToolCalls(chunks []ToolCall) ([]ToolCall, error) { + var merged []ToolCall + m := make(map[int][]int) + for i := range chunks { + index := chunks[i].Index + if index == nil { + merged = append(merged, chunks[i]) + } else { + m[*index] = append(m[*index], i) + } + } + + var args strings.Builder + for k, v := range m { + index := k + toolCall := ToolCall{Index: &index} + if len(v) > 0 { + toolCall = chunks[v[0]] + } + + args.Reset() + toolID, toolType, toolName := "", "", "" // these field will output atomically in any chunk + + for _, n := range v { + chunk := chunks[n] + if chunk.ID != "" { + if toolID == "" { + toolID = chunk.ID + } else if toolID != chunk.ID { + return nil, fmt.Errorf("cannot concat ToolCalls with different tool id: '%s' '%s'", toolID, chunk.ID) + } + + } + + if chunk.Type != "" { + if toolType == "" { + toolType = chunk.Type + } else if toolType != chunk.Type { + return nil, fmt.Errorf("cannot concat ToolCalls with different tool type: '%s' '%s'", toolType, chunk.Type) + } + } + + if chunk.Function.Name != "" { + if toolName == "" { + toolName = chunk.Function.Name + } else if toolName != chunk.Function.Name { + return nil, fmt.Errorf("cannot concat ToolCalls with different tool name: '%s' '%s'", toolName, chunk.Function.Name) + } + } + + if chunk.Function.Arguments != "" { + _, err := args.WriteString(chunk.Function.Arguments) + if err != nil { + return nil, err + } + } + } + + toolCall.ID = toolID + toolCall.Type = toolType + toolCall.Function.Name = toolName + toolCall.Function.Arguments = args.String() + + merged = append(merged, toolCall) + } + + if len(merged) > 1 { + sort.SliceStable(merged, func(i, j int) bool { + iVal, jVal := merged[i].Index, merged[j].Index + if iVal == nil && jVal == nil { + return false + } else if iVal == nil && jVal != nil { + return true + } else if iVal != nil && jVal == nil { + return false + } + + return *iVal < *jVal + }) + } + + return merged, nil +} + +func concatAssistantMultiContent(parts []MessageOutputPart) ([]MessageOutputPart, error) { + if len(parts) == 0 { + return parts, nil + } + + groups := groupOutputParts(parts) + + merged := make([]MessageOutputPart, 0, len(groups)) + for _, group := range groups { + mergedPart, err := mergeOutputPartGroup(group) + if err != nil { + return nil, err + } + merged = append(merged, mergedPart) + } + + return merged, nil +} + +func groupOutputParts(parts []MessageOutputPart) [][]MessageOutputPart { + if len(parts) == 0 { + return nil + } + + groups := make([][]MessageOutputPart, 0) + currentGroup := []MessageOutputPart{parts[0]} + + for i := 1; i < len(parts); i++ { + if canMergeOutputParts(currentGroup[0], parts[i]) { + currentGroup = append(currentGroup, parts[i]) + } else { + groups = append(groups, currentGroup) + currentGroup = []MessageOutputPart{parts[i]} + } + } + groups = append(groups, currentGroup) + + return groups +} + +func canMergeOutputParts(current, next MessageOutputPart) bool { + if current.Type != next.Type { + return false + } + + if !isMergeableOutputPartType(current) { + return false + } + + if current.StreamingMeta != nil && next.StreamingMeta != nil { + return current.StreamingMeta.Index == next.StreamingMeta.Index + } + + return current.StreamingMeta == nil && next.StreamingMeta == nil +} + +func isMergeableOutputPartType(part MessageOutputPart) bool { + switch part.Type { + case ChatMessagePartTypeText, ChatMessagePartTypeReasoning: + return true + case ChatMessagePartTypeAudioURL: + return isBase64MessageOutputAudioPart(part) + default: + return false + } +} + +func mergeOutputPartGroup(group []MessageOutputPart) (MessageOutputPart, error) { + if len(group) == 0 { + return MessageOutputPart{}, nil + } + + if len(group) == 1 { + return group[0], nil + } + + first := group[0] + switch first.Type { + case ChatMessagePartTypeText: + return mergeTextParts(group) + case ChatMessagePartTypeReasoning: + return mergeReasoningParts(group) + case ChatMessagePartTypeAudioURL: + if isBase64MessageOutputAudioPart(first) { + return mergeAudioParts(group) + } + } + + return first, nil +} + +func mergeTextParts(group []MessageOutputPart) (MessageOutputPart, error) { + var sb strings.Builder + extraList := make([]map[string]any, 0, len(group)) + for _, part := range group { + sb.WriteString(part.Text) + if len(part.Extra) > 0 { + extraList = append(extraList, part.Extra) + } + } + var mergedExtra map[string]any + if len(extraList) > 0 { + var err error + mergedExtra, err = concatExtra(extraList) + if err != nil { + return MessageOutputPart{}, fmt.Errorf("failed to concat text part extra: %w", err) + } + } + return MessageOutputPart{ + Type: ChatMessagePartTypeText, + Text: sb.String(), + Extra: mergedExtra, + StreamingMeta: group[0].StreamingMeta, + }, nil +} + +func mergeReasoningParts(group []MessageOutputPart) (MessageOutputPart, error) { + var textBuilder strings.Builder + var signature string + extraList := make([]map[string]any, 0, len(group)) + for _, part := range group { + if part.Reasoning != nil { + textBuilder.WriteString(part.Reasoning.Text) + if part.Reasoning.Signature != "" { + signature = part.Reasoning.Signature + } + } + if len(part.Extra) > 0 { + extraList = append(extraList, part.Extra) + } + } + var mergedExtra map[string]any + if len(extraList) > 0 { + var err error + mergedExtra, err = concatExtra(extraList) + if err != nil { + return MessageOutputPart{}, fmt.Errorf("failed to concat reasoning part extra: %w", err) + } + } + return MessageOutputPart{ + Type: ChatMessagePartTypeReasoning, + Reasoning: &MessageOutputReasoning{ + Text: textBuilder.String(), + Signature: signature, + }, + Extra: mergedExtra, + StreamingMeta: group[0].StreamingMeta, + }, nil +} + +func mergeAudioParts(group []MessageOutputPart) (MessageOutputPart, error) { + var b64Builder strings.Builder + var mimeType string + audioExtraList := make([]map[string]any, 0, len(group)) + partExtraList := make([]map[string]any, 0, len(group)) + + for _, part := range group { + audioPart := part.Audio + if audioPart.Base64Data != nil { + b64Builder.WriteString(*audioPart.Base64Data) + } + if mimeType == "" { + mimeType = audioPart.MIMEType + } + if len(audioPart.Extra) > 0 { + audioExtraList = append(audioExtraList, audioPart.Extra) + } + if len(part.Extra) > 0 { + partExtraList = append(partExtraList, part.Extra) + } + } + + var mergedAudioExtra map[string]any + var err error + if len(audioExtraList) > 0 { + mergedAudioExtra, err = concatExtra(audioExtraList) + if err != nil { + return MessageOutputPart{}, fmt.Errorf("failed to concat audio extra: %w", err) + } + } + + var mergedPartExtra map[string]any + if len(partExtraList) > 0 { + mergedPartExtra, err = concatExtra(partExtraList) + if err != nil { + return MessageOutputPart{}, fmt.Errorf("failed to concat audio part extra: %w", err) + } + } + + mergedB64 := b64Builder.String() + return MessageOutputPart{ + Type: ChatMessagePartTypeAudioURL, + Audio: &MessageOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &mergedB64, + MIMEType: mimeType, + Extra: mergedAudioExtra, + }, + }, + Extra: mergedPartExtra, + StreamingMeta: group[0].StreamingMeta, + }, nil +} + +func isBase64MessageOutputAudioPart(part MessageOutputPart) bool { + return part.Type == ChatMessagePartTypeAudioURL && + part.Audio != nil && + part.Audio.Base64Data != nil && + part.Audio.URL == nil +} + +func concatUserMultiContent(parts []MessageInputPart) ([]MessageInputPart, error) { + if len(parts) == 0 { + return parts, nil + } + + merged := make([]MessageInputPart, 0, len(parts)) + i := 0 + for i < len(parts) { + currentPart := parts[i] + + if currentPart.Type == ChatMessagePartTypeText { + end := i + 1 + for end < len(parts) && parts[end].Type == ChatMessagePartTypeText { + end++ + } + + if end == i+1 { + merged = append(merged, currentPart) + } else { + var sb strings.Builder + for k := i; k < end; k++ { + sb.WriteString(parts[k].Text) + } + mergedPart := MessageInputPart{ + Type: ChatMessagePartTypeText, + Text: sb.String(), + } + merged = append(merged, mergedPart) + } + i = end + } else { + + merged = append(merged, currentPart) + i++ + } + } + + return merged, nil +} + +func concatExtra(extraList []map[string]any) (map[string]any, error) { + if len(extraList) == 1 { + return generic.CopyMap(extraList[0]), nil + } + + return internal.ConcatItems(extraList) +} + +// ConcatMessages concat messages with the same role and name. +// It will concat tool calls with the same index. +// It will return an error if the messages have different roles or names. +// It's useful for concatenating messages from a stream. +// e.g. +// +// msgs := []*Message{} +// for { +// msg, err := stream.Recv() +// if errors.Is(err, io.EOF) { +// break +// } +// if err != nil {...} +// msgs = append(msgs, msg) +// } +// +// concatedMsg, err := ConcatMessages(msgs) // concatedMsg.Content will be full content of all messages +func ConcatMessages(msgs []*Message) (*Message, error) { + var ( + contents []string + contentLen int + reasoningContents []string + reasoningContentLen int + toolCalls []ToolCall + multiContentParts []ChatMessagePart + assistantGenMultiContentParts []MessageOutputPart + userInputMultiContentParts []MessageInputPart + ret = Message{} + extraList = make([]map[string]any, 0, len(msgs)) + ) + + for idx, msg := range msgs { + if msg == nil { + return nil, fmt.Errorf("unexpected nil chunk in message stream, index: %d", idx) + } + + if msg.Role != "" { + if ret.Role == "" { + ret.Role = msg.Role + } else if ret.Role != msg.Role { + return nil, fmt.Errorf("cannot concat messages with "+ + "different roles: '%s' '%s'", ret.Role, msg.Role) + } + } + + if msg.Name != "" { + if ret.Name == "" { + ret.Name = msg.Name + } else if ret.Name != msg.Name { + return nil, fmt.Errorf("cannot concat messages with"+ + " different names: '%s' '%s'", ret.Name, msg.Name) + } + } + + if msg.ToolCallID != "" { + if ret.ToolCallID == "" { + ret.ToolCallID = msg.ToolCallID + } else if ret.ToolCallID != msg.ToolCallID { + return nil, fmt.Errorf("cannot concat messages with"+ + " different toolCallIDs: '%s' '%s'", ret.ToolCallID, msg.ToolCallID) + } + } + if msg.ToolName != "" { + if ret.ToolName == "" { + ret.ToolName = msg.ToolName + } else if ret.ToolName != msg.ToolName { + return nil, fmt.Errorf("cannot concat messages with"+ + " different toolNames: '%s' '%s'", ret.ToolCallID, msg.ToolCallID) + } + } + + if msg.Content != "" { + contents = append(contents, msg.Content) + contentLen += len(msg.Content) + } + if msg.ReasoningContent != "" { + reasoningContents = append(reasoningContents, msg.ReasoningContent) + reasoningContentLen += len(msg.ReasoningContent) + } + + if len(msg.ToolCalls) > 0 { + toolCalls = append(toolCalls, msg.ToolCalls...) + } + + if len(msg.Extra) > 0 { + extraList = append(extraList, msg.Extra) + } + + // The 'MultiContent' field is deprecated but is kept for backward compatibility. + if len(msg.MultiContent) > 0 { + multiContentParts = append(multiContentParts, msg.MultiContent...) + } + + if len(msg.AssistantGenMultiContent) > 0 { + assistantGenMultiContentParts = append(assistantGenMultiContentParts, msg.AssistantGenMultiContent...) + } + if len(msg.UserInputMultiContent) > 0 { + userInputMultiContentParts = append(userInputMultiContentParts, msg.UserInputMultiContent...) + } + if msg.ResponseMeta != nil && ret.ResponseMeta == nil { + ret.ResponseMeta = &ResponseMeta{} + } + + if msg.ResponseMeta != nil && ret.ResponseMeta != nil { + // keep the last FinishReason with a valid value. + if msg.ResponseMeta.FinishReason != "" { + ret.ResponseMeta.FinishReason = msg.ResponseMeta.FinishReason + } + + if msg.ResponseMeta.Usage != nil { + if ret.ResponseMeta.Usage == nil { + ret.ResponseMeta.Usage = &TokenUsage{} + } + + if msg.ResponseMeta.Usage.PromptTokens > ret.ResponseMeta.Usage.PromptTokens { + ret.ResponseMeta.Usage.PromptTokens = msg.ResponseMeta.Usage.PromptTokens + } + if msg.ResponseMeta.Usage.CompletionTokens > ret.ResponseMeta.Usage.CompletionTokens { + ret.ResponseMeta.Usage.CompletionTokens = msg.ResponseMeta.Usage.CompletionTokens + } + + if msg.ResponseMeta.Usage.TotalTokens > ret.ResponseMeta.Usage.TotalTokens { + ret.ResponseMeta.Usage.TotalTokens = msg.ResponseMeta.Usage.TotalTokens + } + + if msg.ResponseMeta.Usage.PromptTokenDetails.CachedTokens > ret.ResponseMeta.Usage.PromptTokenDetails.CachedTokens { + ret.ResponseMeta.Usage.PromptTokenDetails.CachedTokens = msg.ResponseMeta.Usage.PromptTokenDetails.CachedTokens + } + + if msg.ResponseMeta.Usage.CompletionTokensDetails.ReasoningTokens > ret.ResponseMeta.Usage.CompletionTokensDetails.ReasoningTokens { + ret.ResponseMeta.Usage.CompletionTokensDetails.ReasoningTokens = msg.ResponseMeta.Usage.CompletionTokensDetails.ReasoningTokens + } + } + + if msg.ResponseMeta.LogProbs != nil { + if ret.ResponseMeta.LogProbs == nil { + ret.ResponseMeta.LogProbs = &LogProbs{} + } + + ret.ResponseMeta.LogProbs.Content = append(ret.ResponseMeta.LogProbs.Content, msg.ResponseMeta.LogProbs.Content...) + } + + } + } + + if len(contents) > 0 { + var sb strings.Builder + sb.Grow(contentLen) + for _, content := range contents { + _, err := sb.WriteString(content) + if err != nil { + return nil, err + } + } + + ret.Content = sb.String() + } + if len(reasoningContents) > 0 { + var sb strings.Builder + sb.Grow(reasoningContentLen) + for _, rc := range reasoningContents { + _, err := sb.WriteString(rc) + if err != nil { + return nil, err + } + } + + ret.ReasoningContent = sb.String() + } + + if len(toolCalls) > 0 { + merged, err := concatToolCalls(toolCalls) + if err != nil { + return nil, err + } + + ret.ToolCalls = merged + } + + if len(extraList) > 0 { + extra, err := concatExtra(extraList) + if err != nil { + return nil, fmt.Errorf("failed to concat message's extra: %w", err) + } + + if len(extra) > 0 { + ret.Extra = extra + } + } + + if len(multiContentParts) > 0 { + ret.MultiContent = multiContentParts + } + + if len(assistantGenMultiContentParts) > 0 { + merged, err := concatAssistantMultiContent(assistantGenMultiContentParts) + if err != nil { + return nil, fmt.Errorf("failed to concat message's assistant multicontent: %w", err) + } + ret.AssistantGenMultiContent = merged + } + + if len(userInputMultiContentParts) > 0 { + merged, err := concatUserMultiContent(userInputMultiContentParts) + if err != nil { + return nil, fmt.Errorf("failed to concat message's user multicontent: %w", err) + } + ret.UserInputMultiContent = merged + } + + return &ret, nil +} + +// ConcatMessageStream drains a stream of messages and returns a single +// concatenated message representing the merged content. +func ConcatMessageStream(s *StreamReader[*Message]) (*Message, error) { + defer s.Close() + + var msgs []*Message + for { + msg, err := s.Recv() + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + msgs = append(msgs, msg) + } + + return ConcatMessages(msgs) +} + +// custom jinja env +var jinjaEnvOnce sync.Once +var jinjaEnv *gonja.Environment +var envInitErr error + +const ( + jinjaInclude = "include" + jinjaExtends = "extends" + jinjaImport = "import" + jinjaFrom = "from" + jinjaFile = "file" + jinjaFileSet = "fileset" +) + +func getJinjaEnv() (*gonja.Environment, error) { + jinjaEnvOnce.Do(func() { + jinjaEnv = gonja.NewEnvironment(config.DefaultConfig, gonja.DefaultLoader) + formatInitError := "init jinja env fail: %w" + var err error + if jinjaEnv.Statements.Exists(jinjaInclude) { + err = jinjaEnv.Statements.Replace(jinjaInclude, func(parser *parser.Parser, args *parser.Parser) (nodes.Statement, error) { + return nil, fmt.Errorf("keyword[include] has been disabled") + }) + if err != nil { + envInitErr = fmt.Errorf(formatInitError, err) + return + } + } + if jinjaEnv.Statements.Exists(jinjaExtends) { + err = jinjaEnv.Statements.Replace(jinjaExtends, func(parser *parser.Parser, args *parser.Parser) (nodes.Statement, error) { + return nil, fmt.Errorf("keyword[extends] has been disabled") + }) + if err != nil { + envInitErr = fmt.Errorf(formatInitError, err) + return + } + } + if jinjaEnv.Statements.Exists(jinjaFrom) { + err = jinjaEnv.Statements.Replace(jinjaFrom, func(parser *parser.Parser, args *parser.Parser) (nodes.Statement, error) { + return nil, fmt.Errorf("keyword[from] has been disabled") + }) + if err != nil { + envInitErr = fmt.Errorf(formatInitError, err) + return + } + } + if jinjaEnv.Statements.Exists(jinjaImport) { + err = jinjaEnv.Statements.Replace(jinjaImport, func(parser *parser.Parser, args *parser.Parser) (nodes.Statement, error) { + return nil, fmt.Errorf("keyword[import] has been disabled") + }) + if err != nil { + envInitErr = fmt.Errorf(formatInitError, err) + return + } + } + if jinjaEnv.Filters.Exists(jinjaFile) { + err = jinjaEnv.Filters.Replace(jinjaFile, func(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value { + return exec.AsValue(fmt.Errorf("keyword[file] has been disabled")) + }) + if err != nil { + envInitErr = fmt.Errorf(formatInitError, err) + return + } + } + if jinjaEnv.Filters.Exists(jinjaFileSet) { + err = jinjaEnv.Filters.Replace(jinjaFileSet, func(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value { + return exec.AsValue(fmt.Errorf("keyword[fileset] has been disabled")) + }) + if err != nil { + envInitErr = fmt.Errorf(formatInitError, err) + return + } + } + }) + return jinjaEnv, envInitErr +} diff --git a/schema/message_parser.go b/schema/message_parser.go new file mode 100644 index 0000000..7bd95b8 --- /dev/null +++ b/schema/message_parser.go @@ -0,0 +1,141 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "context" + "fmt" + "strings" + + "github.com/bytedance/sonic" +) + +// MessageParser parses a Message into a strongly typed value. +type MessageParser[T any] interface { + Parse(ctx context.Context, m *Message) (T, error) +} + +// MessageParseFrom determines the source of the data to be parsed. default is content (Message.Content). +type MessageParseFrom string + +// MessageParseFrom indicates the source data used by the parser. +const ( + MessageParseFromContent MessageParseFrom = "content" + MessageParseFromToolCall MessageParseFrom = "tool_call" +) + +// MessageJSONParseConfig configures JSON parsing behavior for Message. +type MessageJSONParseConfig struct { + // parse from content or tool call, default is content. + ParseFrom MessageParseFrom `json:"parse_from,omitempty"` + + // parse key path, default is empty. + // must be a valid json path expression, eg: field.sub_field + ParseKeyPath string `json:"parse_key_path,omitempty"` +} + +// NewMessageJSONParser creates a new MessageJSONParser. +func NewMessageJSONParser[T any](config *MessageJSONParseConfig) MessageParser[T] { + if config == nil { + config = &MessageJSONParseConfig{} + } + + if config.ParseFrom == "" { + config.ParseFrom = MessageParseFromContent + } + + return &MessageJSONParser[T]{ + ParseFrom: config.ParseFrom, + ParseKeyPath: config.ParseKeyPath, + } +} + +// MessageJSONParser is a parser that parses a message into an object T, using json unmarshal. +// eg of parse to single struct: +// +// config := &MessageJSONParseConfig{ +// ParseFrom: MessageParseFromToolCall, +// } +// parser := NewMessageJSONParser[GetUserParam](config) +// param, err := parser.Parse(ctx, message) +// +// eg of parse to slice of struct: +// +// config := &MessageJSONParseConfig{ +// ParseFrom: MessageParseFromToolCall, +// } +// +// parser := NewMessageJSONParser[GetUserParam](config) +// param, err := parser.Parse(ctx, message) +type MessageJSONParser[T any] struct { + ParseFrom MessageParseFrom + ParseKeyPath string +} + +// Parse parses a message into an object T. +func (p *MessageJSONParser[T]) Parse(ctx context.Context, m *Message) (parsed T, err error) { + if p.ParseFrom == MessageParseFromContent { + return p.parse(m.Content) + } else if p.ParseFrom == MessageParseFromToolCall { + if len(m.ToolCalls) == 0 { + return parsed, fmt.Errorf("no tool call found") + } + + return p.parse(m.ToolCalls[0].Function.Arguments) + } + + return parsed, fmt.Errorf("invalid parse from type: %s", p.ParseFrom) +} + +// extractData extracts data from a string using the parse key path. +func (p *MessageJSONParser[T]) extractData(data string) (string, error) { + if p.ParseKeyPath == "" { + return data, nil + } + + keys := strings.Split(p.ParseKeyPath, ".") + interfaceKeys := make([]any, len(keys)) + for i, key := range keys { + interfaceKeys[i] = key + } + + node, err := sonic.GetFromString(data, interfaceKeys...) + if err != nil { + return "", fmt.Errorf("failed to get parse key path: %w", err) + } + + bytes, err := node.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal node: %w", err) + } + + return string(bytes), nil +} + +// parse parses a string into an object T. +func (p *MessageJSONParser[T]) parse(data string) (parsed T, err error) { + parsedData, err := p.extractData(data) + if err != nil { + return parsed, err + } + + if err := sonic.UnmarshalString(parsedData, &parsed); err != nil { + return parsed, fmt.Errorf("failed to unmarshal content: %w", err) + } + + return parsed, nil +} diff --git a/schema/message_parser_test.go b/schema/message_parser_test.go new file mode 100644 index 0000000..e645f87 --- /dev/null +++ b/schema/message_parser_test.go @@ -0,0 +1,179 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestStructForParse struct { + ID int `json:"id"` + Name string `json:"name"` + XX struct { + YY int `json:"yy"` + } `json:"xx"` +} + +func TestMessageJSONParser(t *testing.T) { + ctx := context.Background() + + t.Run("parse from content", func(t *testing.T) { + parser := NewMessageJSONParser[TestStructForParse](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromContent, + }) + + parsed, err := parser.Parse(ctx, &Message{ + Content: `{"id": 1, "name": "test", "xx": {"yy": 2}}`, + }) + assert.Nil(t, err) + assert.Equal(t, 1, parsed.ID) + }) + + t.Run("parse from tool call", func(t *testing.T) { + t.Run("only one tool call, default use first tool call", func(t *testing.T) { + parser := NewMessageJSONParser[TestStructForParse](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromToolCall, + }) + + parsed, err := parser.Parse(ctx, &Message{ + ToolCalls: []ToolCall{ + {Function: FunctionCall{Arguments: `{"id": 1, "name": "test", "xx": {"yy": 2}}`}}, + }, + }) + assert.Nil(t, err) + assert.Equal(t, 1, parsed.ID) + }) + + t.Run("parse key path", func(t *testing.T) { + type TestStructForParse2 struct { + YY int `json:"yy"` + } + + parser := NewMessageJSONParser[TestStructForParse2](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromToolCall, + ParseKeyPath: "xx", + }) + + parsed, err := parser.Parse(ctx, &Message{ + ToolCalls: []ToolCall{ + {Function: FunctionCall{Arguments: `{"id": 1, "name": "test", "xx": {"yy": 2}}`}}, + }, + }) + assert.Nil(t, err) + assert.Equal(t, 2, parsed.YY) + }) + + t.Run("parse key path, deep level", func(t *testing.T) { + type TestStructForParse3 struct { + ZZ int `json:"zz"` + } + + parser := NewMessageJSONParser[TestStructForParse3](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromToolCall, + ParseKeyPath: "xx.yy", + }) + + parsed, err := parser.Parse(ctx, &Message{ + ToolCalls: []ToolCall{ + {Function: FunctionCall{Arguments: `{"id": 1, "name": "test", "xx": {"yy": {"zz": 3}}}`}}, + }, + }) + assert.Nil(t, err) + assert.Equal(t, 3, parsed.ZZ) + }) + + t.Run("parse key with pointer", func(t *testing.T) { + type TestStructForParse4 struct { + ZZ *int `json:"zz"` + } + + parser := NewMessageJSONParser[**TestStructForParse4](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromToolCall, + }) + + parsed, err := parser.Parse(ctx, &Message{ + ToolCalls: []ToolCall{{Function: FunctionCall{Arguments: `{"zz": 3}`}}}, + }) + assert.Nil(t, err) + assert.Equal(t, 3, *((**parsed).ZZ)) + }) + }) + + t.Run("parse of slice", func(t *testing.T) { + t.Run("valid slice string, not multiple tool calls", func(t *testing.T) { + parser := NewMessageJSONParser[[]map[string]any](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromToolCall, + }) + + parsed, err := parser.Parse(ctx, &Message{ + ToolCalls: []ToolCall{{Function: FunctionCall{Arguments: `[{"id": 1}, {"id": 2}]`}}}, + }) + assert.Nil(t, err) + assert.Equal(t, 2, len(parsed)) + }) + + t.Run("invalid slice string, not multiple tool calls", func(t *testing.T) { + parser := NewMessageJSONParser[[]map[string]any](&MessageJSONParseConfig{ + ParseFrom: MessageParseFromToolCall, + }) + + _, err := parser.Parse(ctx, &Message{ + ToolCalls: []ToolCall{ + {Function: FunctionCall{Arguments: `{"id": 1}`}}, + {Function: FunctionCall{Arguments: `{"id": 2}`}}, + }, + }) + assert.NotNil(t, err) + }) + }) + + t.Run("invalid configs", func(t *testing.T) { + parser := NewMessageJSONParser[TestStructForParse](nil) + _, err := parser.Parse(ctx, &Message{ + Content: "", + }) + assert.NotNil(t, err) + }) + + t.Run("invalid parse key path", func(t *testing.T) { + parser := NewMessageJSONParser[TestStructForParse](&MessageJSONParseConfig{ + ParseKeyPath: "...invalid", + }) + _, err := parser.Parse(ctx, &Message{}) + assert.NotNil(t, err) + }) + + t.Run("invalid parse from", func(t *testing.T) { + parser := NewMessageJSONParser[TestStructForParse](&MessageJSONParseConfig{ + ParseFrom: "invalid", + }) + _, err := parser.Parse(ctx, &Message{}) + assert.NotNil(t, err) + }) + + t.Run("invalid parse from type", func(t *testing.T) { + parser := NewMessageJSONParser[int](&MessageJSONParseConfig{ + ParseFrom: MessageParseFrom("invalid"), + }) + _, err := parser.Parse(ctx, &Message{}) + assert.NotNil(t, err) + }) + +} diff --git a/schema/message_test.go b/schema/message_test.go new file mode 100644 index 0000000..bb086b8 --- /dev/null +++ b/schema/message_test.go @@ -0,0 +1,2189 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "context" + "reflect" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/generic" +) + +func TestMessageTemplate(t *testing.T) { + pyFmtMessage := UserMessage("input: {question}") + jinja2Message := UserMessage("input: {{question}}") + goTemplateMessage := UserMessage("input: {{.question}}") + ctx := context.Background() + question := "what's the weather today" + expected := []*Message{UserMessage("input: " + question)} + + ms, err := pyFmtMessage.Format(ctx, map[string]any{"question": question}, FString) + assert.Nil(t, err) + assert.True(t, reflect.DeepEqual(expected, ms)) + ms, err = jinja2Message.Format(ctx, map[string]any{"question": question}, Jinja2) + assert.Nil(t, err) + assert.True(t, reflect.DeepEqual(expected, ms)) + ms, err = goTemplateMessage.Format(ctx, map[string]any{"question": question}, GoTemplate) + assert.Nil(t, err) + assert.True(t, reflect.DeepEqual(expected, ms)) + + mp := MessagesPlaceholder("chat_history", false) + m1 := UserMessage("how are you?") + m2 := AssistantMessage("I'm good. how about you?", nil) + ms, err = mp.Format(ctx, map[string]any{"chat_history": []*Message{m1, m2}}, FString) + assert.Nil(t, err) + + // len(ms) == 2 + assert.Equal(t, 2, len(ms)) + assert.Equal(t, ms[0], m1) + assert.Equal(t, ms[1], m2) +} + +func TestConcatMessage(t *testing.T) { + t.Run("tool_call_normal_append", func(t *testing.T) { + expectMsg := &Message{ + Role: "assistant", + Content: "", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "i_am_a_too_call_id", + Type: "function", + Function: FunctionCall{ + Name: "i_am_a_tool_name", + Arguments: "{}", + }, + }, + }, + } + givenMsgList := []*Message{ + { + Role: "", + Content: "", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + }, + }, + { + Role: "assistant", + Content: "", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "i_am_a_too_call_id", + Type: "function", + Function: FunctionCall{ + Name: "i_am_a_tool_name", + }, + }, + }, + }, + { + Role: "", + Content: "", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "", + Type: "", + Function: FunctionCall{ + Name: "", + Arguments: "{}", + }, + }, + }, + }, + } + + msg, err := ConcatMessages(givenMsgList) + assert.NoError(t, err) + assert.EqualValues(t, expectMsg, msg) + }) + + t.Run("exist_nil_message", func(t *testing.T) { + givenMsgList := []*Message{ + nil, + { + Role: "assistant", + Content: "", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "i_am_a_too_call_id", + Type: "function", + Function: FunctionCall{ + Name: "i_am_a_tool_name", + }, + }, + }, + }, + } + + _, err := ConcatMessages(givenMsgList) + assert.ErrorContains(t, err, "unexpected nil chunk in message stream") + }) + + t.Run("response_meta", func(t *testing.T) { + expectedMsg := &Message{ + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + Usage: &TokenUsage{ + CompletionTokens: 15, + PromptTokens: 30, + PromptTokenDetails: PromptTokenDetails{ + CachedTokens: 15, + }, + CompletionTokensDetails: CompletionTokensDetails{ + ReasoningTokens: 8, + }, + TotalTokens: 45, + }, + }, + } + + givenMsgList := []*Message{ + { + Role: "assistant", + }, + { + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "", + Usage: &TokenUsage{ + CompletionTokens: 10, + PromptTokens: 20, + PromptTokenDetails: PromptTokenDetails{ + CachedTokens: 10, + }, + CompletionTokensDetails: CompletionTokensDetails{ + ReasoningTokens: 5, + }, + TotalTokens: 30, + }, + }, + }, + { + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + }, + }, + { + Role: "assistant", + ResponseMeta: &ResponseMeta{ + Usage: &TokenUsage{ + CompletionTokens: 15, + PromptTokens: 30, + PromptTokenDetails: PromptTokenDetails{ + CachedTokens: 15, + }, + CompletionTokensDetails: CompletionTokensDetails{ + ReasoningTokens: 8, + }, + TotalTokens: 45, + }, + }, + }, + } + + msg, err := ConcatMessages(givenMsgList) + assert.NoError(t, err) + assert.Equal(t, expectedMsg, msg) + + givenMsgList = append(givenMsgList, &Message{ + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "tool_calls", + }, + }) + msg, err = ConcatMessages(givenMsgList) + assert.NoError(t, err) + expectedMsg.ResponseMeta.FinishReason = "tool_calls" + assert.Equal(t, expectedMsg, msg) + + }) + + t.Run("err: different roles", func(t *testing.T) { + msgs := []*Message{ + {Role: User}, + {Role: Assistant}, + } + + msg, err := ConcatMessages(msgs) + if assert.Error(t, err) { + assert.ErrorContains(t, err, "cannot concat messages with different roles") + assert.Nil(t, msg) + } + }) + + t.Run("err: different name", func(t *testing.T) { + msgs := []*Message{ + {Role: Assistant, Name: "n", Content: "1"}, + {Role: Assistant, Name: "a", Content: "2"}, + } + + msg, err := ConcatMessages(msgs) + if assert.Error(t, err) { + assert.ErrorContains(t, err, "cannot concat messages with different names") + assert.Nil(t, msg) + } + }) + + t.Run("err: different tool name", func(t *testing.T) { + msgs := []*Message{ + { + Role: "", + Content: "", + ToolCallID: "123", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "abc", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + }, + }, + { + Role: "assistant", + Content: "", + ToolCallID: "321", + ToolCalls: []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "abc", + Type: "function", + Function: FunctionCall{ + Name: "i_am_a_tool_name", + }, + }, + }, + }, + } + + msg, err := ConcatMessages(msgs) + if assert.Error(t, err) { + assert.ErrorContains(t, err, "cannot concat messages with different toolCallIDs") + assert.Nil(t, msg) + } + }) + + t.Run("first response meta usage is nil", func(t *testing.T) { + exp := &Message{ + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + Usage: &TokenUsage{ + CompletionTokens: 15, + PromptTokens: 30, + TotalTokens: 45, + }, + }, + } + + msgs := []*Message{ + { + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "", + Usage: nil, + }, + }, + { + Role: "assistant", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + }, + }, + { + Role: "assistant", + ResponseMeta: &ResponseMeta{ + Usage: &TokenUsage{ + CompletionTokens: 15, + PromptTokens: 30, + TotalTokens: 45, + }, + }, + }, + } + + msg, err := ConcatMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, exp, msg) + }) + + t.Run("concurrent concat", func(t *testing.T) { + content := "i_am_a_good_concat_message" + exp := &Message{Role: Assistant, Content: content} + var msgs []*Message + for i := 0; i < len(content); i++ { + msgs = append(msgs, &Message{Role: Assistant, Content: content[i : i+1]}) + } + + wg := sync.WaitGroup{} + size := 100 + wg.Add(size) + for i := 0; i < size; i++ { + go func() { + defer wg.Done() + msg, err := ConcatMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, exp, msg) + }() + } + + wg.Wait() + }) + + t.Run("concat logprobs", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + Content: "🚀", + ResponseMeta: &ResponseMeta{ + LogProbs: &LogProbs{ + Content: []LogProb{ + { + Token: "\\xf0\\x9f\\x9a", + LogProb: -0.0000073458323, + Bytes: []int64{240, 159, 154}, + }, + { + Token: "\\x80", + LogProb: 0, + Bytes: []int64{128}, + }, + }, + }, + }, + }, + { + Role: "", + Content: "❤️", + ResponseMeta: &ResponseMeta{ + LogProbs: &LogProbs{ + Content: []LogProb{ + { + Token: "❤️", + LogProb: -0.0011431955, + Bytes: []int64{226, 157, 164, 239, 184, 143}, + }, + }, + }, + }, + }, + { + Role: "", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + Usage: &TokenUsage{ + PromptTokens: 7, + CompletionTokens: 3, + TotalTokens: 10, + }, + }, + }, + } + + msg, err := ConcatMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, 3, len(msg.ResponseMeta.LogProbs.Content)) + assert.Equal(t, msgs[0].ResponseMeta.LogProbs.Content[0], msg.ResponseMeta.LogProbs.Content[0]) + assert.Equal(t, msgs[0].ResponseMeta.LogProbs.Content[1], msg.ResponseMeta.LogProbs.Content[1]) + assert.Equal(t, msgs[1].ResponseMeta.LogProbs.Content[0], msg.ResponseMeta.LogProbs.Content[2]) + }) + + t.Run("fix unexpected setting ResponseMeta of the first element in slice after ConcatMessages", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + Content: "🚀", + // ResponseMeta: &ResponseMeta{}, + }, + { + Role: "", + Content: "❤️", + ResponseMeta: &ResponseMeta{}, + }, + { + Role: "", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + Usage: &TokenUsage{ + PromptTokens: 7, + CompletionTokens: 3, + TotalTokens: 10, + }, + }, + }, + } + + msg, err := ConcatMessages(msgs) + assert.NoError(t, err) + assert.Equal(t, msgs[2].ResponseMeta, msg.ResponseMeta) + assert.Nil(t, msgs[0].ResponseMeta) + }) + + t.Run("concat assistant multi content", func(t *testing.T) { + base64Audio1 := "dGVzdF9hdWRpb18x" + base64Audio2 := "dGVzdF9hdWRpb18y" + imageURL1 := "https://example.com/image1.png" + imageURL2 := "https://example.com/image2.png" + + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello, "}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "world!"}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio1}}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio2, MIMEType: "audio/wav"}}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeImageURL, Image: &MessageOutputImage{MessagePartCommon: MessagePartCommon{URL: &imageURL1}}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeImageURL, Image: &MessageOutputImage{MessagePartCommon: MessagePartCommon{URL: &imageURL2}}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + mergedBase64Audio := base64Audio1 + base64Audio2 + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello, world!"}, + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &mergedBase64Audio, MIMEType: "audio/wav"}}}, + {Type: ChatMessagePartTypeImageURL, Image: &MessageOutputImage{MessagePartCommon: MessagePartCommon{URL: &imageURL1}}}, + {Type: ChatMessagePartTypeImageURL, Image: &MessageOutputImage{MessagePartCommon: MessagePartCommon{URL: &imageURL2}}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat assistant multi content with extra", func(t *testing.T) { + base64Audio1 := "dGVzdF9hdWRpb18x" + base64Audio2 := "dGVzdF9hdWRpb18y" + + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio1, Extra: map[string]any{"key1": "val1"}}}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio2, Extra: map[string]any{"key2": "val2"}}}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + mergedBase64Audio := base64Audio1 + base64Audio2 + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &mergedBase64Audio, Extra: map[string]any{"key1": "val1", "key2": "val2"}}}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat assistant multi content with single extra", func(t *testing.T) { + base64Audio1 := "dGVzdF9hdWRpb18x" + base64Audio2 := "dGVzdF9hdWRpb18y" + + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio1, Extra: map[string]any{"key1": "val1"}}}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio2}}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + mergedBase64Audio := base64Audio1 + base64Audio2 + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &mergedBase64Audio, Extra: map[string]any{"key1": "val1"}}}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat text parts with extra", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello ", Extra: map[string]any{"key1": "val1"}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "World", Extra: map[string]any{"key2": "val2"}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello World", Extra: map[string]any{"key1": "val1", "key2": "val2"}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat text parts with single extra", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello ", Extra: map[string]any{"key1": "val1"}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "World"}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello World", Extra: map[string]any{"key1": "val1"}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat reasoning parts with extra", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "First, "}, Extra: map[string]any{"key1": "val1"}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "I need to think."}, Extra: map[string]any{"key2": "val2"}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "First, I need to think."}, Extra: map[string]any{"key1": "val1", "key2": "val2"}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat audio parts with outer extra", func(t *testing.T) { + base64Audio1 := "dGVzdF9hdWRpb18x" + base64Audio2 := "dGVzdF9hdWRpb18y" + + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio1}}, Extra: map[string]any{"outer1": "val1"}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &base64Audio2}}, Extra: map[string]any{"outer2": "val2"}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + mergedBase64Audio := base64Audio1 + base64Audio2 + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{MessagePartCommon: MessagePartCommon{Base64Data: &mergedBase64Audio}}, Extra: map[string]any{"outer1": "val1", "outer2": "val2"}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat reasoning parts", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "First, "}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "I need to think."}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Here is my answer."}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "First, I need to think."}}, + {Type: ChatMessagePartTypeText, Text: "Here is my answer."}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat reasoning parts with signature", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "Step 1: "}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "analyze.", Signature: "sig_abc"}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: " Step 2: ", Signature: "sig_xyz"}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "conclude."}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "Step 1: analyze. Step 2: conclude.", Signature: "sig_xyz"}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat with streaming meta index grouping", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "Think "}, StreamingMeta: &MessageStreamingMeta{Index: 0}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "more."}, StreamingMeta: &MessageStreamingMeta{Index: 0}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Hello ", StreamingMeta: &MessageStreamingMeta{Index: 1}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "world!", StreamingMeta: &MessageStreamingMeta{Index: 1}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeReasoning, Reasoning: &MessageOutputReasoning{Text: "Think more."}, StreamingMeta: &MessageStreamingMeta{Index: 0}}, + {Type: ChatMessagePartTypeText, Text: "Hello world!", StreamingMeta: &MessageStreamingMeta{Index: 1}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat with different streaming meta index should not merge", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "First block ", StreamingMeta: &MessageStreamingMeta{Index: 0}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Second block ", StreamingMeta: &MessageStreamingMeta{Index: 1}}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "continues.", StreamingMeta: &MessageStreamingMeta{Index: 0}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "First block ", StreamingMeta: &MessageStreamingMeta{Index: 0}}, + {Type: ChatMessagePartTypeText, Text: "Second block ", StreamingMeta: &MessageStreamingMeta{Index: 1}}, + {Type: ChatMessagePartTypeText, Text: "continues.", StreamingMeta: &MessageStreamingMeta{Index: 0}}, + } + + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat empty parts", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{}, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + assert.Empty(t, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat single part no merge needed", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Single"}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Single"}, + } + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat multiple consecutive text parts", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "One "}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Two "}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Three "}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Four"}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "One Two Three Four"}, + } + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat without streaming meta should not merge with streaming meta parts", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "No meta "}, + }, + }, + { + Role: Assistant, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "With meta", StreamingMeta: &MessageStreamingMeta{Index: 0}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedContent := []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "No meta "}, + {Type: ChatMessagePartTypeText, Text: "With meta", StreamingMeta: &MessageStreamingMeta{Index: 0}}, + } + assert.Equal(t, expectedContent, mergedMsg.AssistantGenMultiContent) + }) + + t.Run("concat multi content (deprecated)", func(t *testing.T) { + msgs := []*Message{ + { + Role: Assistant, + MultiContent: []ChatMessagePart{ + {Type: ChatMessagePartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: "image1.jpg"}}, + }, + }, + { + Role: Assistant, + MultiContent: []ChatMessagePart{ + {Type: ChatMessagePartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: "image2.jpg"}}, + }, + }, + } + + mergedMsg, err := ConcatMessages(msgs) + assert.NoError(t, err) + + expectedMultiContent := []ChatMessagePart{ + {Type: ChatMessagePartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: "image1.jpg"}}, + {Type: ChatMessagePartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: "image2.jpg"}}, + } + + assert.Equal(t, expectedMultiContent, mergedMsg.MultiContent) + }) +} + +func TestConcatToolCalls(t *testing.T) { + t.Run("atomic_field_in_first_chunk", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + }, + }, + { + Index: generic.PtrOf(0), + Function: FunctionCall{ + Arguments: "call me please", + }, + }, + } + + expectedToolCall := ToolCall{ + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + Arguments: "call me please", + }, + } + + tc, err := concatToolCalls(givenToolCalls) + assert.NoError(t, err) + assert.Len(t, tc, 1) + assert.EqualValues(t, expectedToolCall, tc[0]) + }) + + t.Run("atomic_field_in_every_chunk", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + Arguments: "call me please", + }, + }, + } + + expectedToolCall := ToolCall{ + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + Arguments: "call me please", + }, + } + + tc, err := concatToolCalls(givenToolCalls) + assert.NoError(t, err) + assert.Len(t, tc, 1) + assert.EqualValues(t, expectedToolCall, tc[0]) + }) + + t.Run("atomic_field_in_interval", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + { + Index: generic.PtrOf(0), + ID: "", + Type: "function", + Function: FunctionCall{ + Name: "", + Arguments: "call me please", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "", + Function: FunctionCall{ + Name: "", + Arguments: "", + }, + }, + } + + expectedToolCall := ToolCall{ + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "", + Arguments: "call me please", + }, + } + + tc, err := concatToolCalls(givenToolCalls) + assert.NoError(t, err) + assert.Len(t, tc, 1) + assert.EqualValues(t, expectedToolCall, tc[0]) + }) + + t.Run("different_tool_id", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id_1", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + Arguments: "call me please", + }, + }, + } + + _, err := concatToolCalls(givenToolCalls) + assert.ErrorContains(t, err, "cannot concat ToolCalls with different tool id") + }) + + t.Run("different_tool_type", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function_1", + Function: FunctionCall{ + Name: "tool_name", + Arguments: "call me please", + }, + }, + } + + _, err := concatToolCalls(givenToolCalls) + assert.ErrorContains(t, err, "cannot concat ToolCalls with different tool type") + }) + + t.Run("different_tool_name", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "tool_name_1", + Arguments: "call me please", + }, + }, + } + + _, err := concatToolCalls(givenToolCalls) + assert.ErrorContains(t, err, "cannot concat ToolCalls with different tool name") + }) + + t.Run("multi_tool_call", func(t *testing.T) { + givenToolCalls := []ToolCall{ + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + { + Index: generic.PtrOf(0), + ID: "", + Type: "function", + Function: FunctionCall{ + Name: "", + Arguments: "call me please", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "", + Function: FunctionCall{ + Name: "", + Arguments: "", + }, + }, + { + Index: generic.PtrOf(1), + ID: "tool_call_id", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + { + Index: generic.PtrOf(1), + ID: "", + Type: "function", + Function: FunctionCall{ + Name: "", + Arguments: "call me please", + }, + }, + { + Index: generic.PtrOf(1), + ID: "tool_call_id", + Type: "", + Function: FunctionCall{ + Name: "", + Arguments: "", + }, + }, + { + Index: nil, + ID: "22", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + { + Index: nil, + ID: "44", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + } + + expectedToolCall := []ToolCall{ + { + Index: nil, + ID: "22", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + { + Index: nil, + ID: "44", + Type: "", + Function: FunctionCall{ + Name: "", + }, + }, + { + Index: generic.PtrOf(0), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "", + Arguments: "call me please", + }, + }, + { + Index: generic.PtrOf(1), + ID: "tool_call_id", + Type: "function", + Function: FunctionCall{ + Name: "", + Arguments: "call me please", + }, + }, + } + + tc, err := concatToolCalls(givenToolCalls) + assert.NoError(t, err) + assert.EqualValues(t, expectedToolCall, tc) + }) +} + +func TestFormatMultiContent(t *testing.T) { + vs := map[string]any{ + "name": "eino", + "url": "https://example.com/img.png", + "id": "42", + } + + t.Run("empty input", func(t *testing.T) { + out, err := formatMultiContent(nil, vs, FString) + assert.NoError(t, err) + assert.Equal(t, []ChatMessagePart{}, out) + }) + + t.Run("render text and urls with FString", func(t *testing.T) { + in := []ChatMessagePart{ + {Type: ChatMessagePartTypeText, Text: "hello {name}"}, + {Type: ChatMessagePartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: "{url}"}}, + {Type: ChatMessagePartTypeAudioURL, AudioURL: &ChatMessageAudioURL{URL: "http://audio/{id}.wav"}}, + {Type: ChatMessagePartTypeVideoURL, VideoURL: &ChatMessageVideoURL{URL: "http://video/{id}.mp4"}}, + {Type: ChatMessagePartTypeFileURL, FileURL: &ChatMessageFileURL{URL: "http://file/{id}.txt"}}, + } + + out, err := formatMultiContent(in, vs, FString) + assert.NoError(t, err) + if assert.Len(t, out, len(in)) { + assert.Equal(t, "hello eino", out[0].Text) + assert.Equal(t, "https://example.com/img.png", out[1].ImageURL.URL) + assert.Equal(t, "http://audio/42.wav", out[2].AudioURL.URL) + assert.Equal(t, "http://video/42.mp4", out[3].VideoURL.URL) + assert.Equal(t, "http://file/42.txt", out[4].FileURL.URL) + } + }) + + t.Run("nil nested pointer should be skipped", func(t *testing.T) { + in := []ChatMessagePart{ + {Type: ChatMessagePartTypeImageURL, ImageURL: nil}, + {Type: ChatMessagePartTypeAudioURL, AudioURL: nil}, + {Type: ChatMessagePartTypeVideoURL, VideoURL: nil}, + {Type: ChatMessagePartTypeFileURL, FileURL: nil}, + } + out, err := formatMultiContent(in, vs, FString) + assert.NoError(t, err) + assert.Equal(t, in, out) + }) + + t.Run("missing var should error in GoTemplate", func(t *testing.T) { + in := []ChatMessagePart{{Type: ChatMessagePartTypeText, Text: "hi {{.who}}"}} + _, err := formatMultiContent(in, map[string]any{"name": "x"}, GoTemplate) + assert.Error(t, err) + }) + +} + +func TestFormatUserInputMultiContent(t *testing.T) { + makeStrPtr := func(s string) *string { return &s } + + vs := map[string]any{ + "x": "world", + "img": "https://example.com/i.png", + "b64": "YmFzZTY0", + "aid": "99", + "vid": "77", + "file": "abc", + } + + t.Run("empty input", func(t *testing.T) { + out, err := formatUserInputMultiContent(nil, vs, FString) + assert.NoError(t, err) + assert.Equal(t, []MessageInputPart{}, out) + }) + + t.Run("render text and both URL/Base64 for each type", func(t *testing.T) { + in := []MessageInputPart{ + {Type: ChatMessagePartTypeText, Text: "hello {x}"}, + {Type: ChatMessagePartTypeImageURL, Image: &MessageInputImage{MessagePartCommon: MessagePartCommon{URL: makeStrPtr("{img}"), Base64Data: makeStrPtr("{b64}")}}}, + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageInputAudio{MessagePartCommon: MessagePartCommon{URL: makeStrPtr("http://a/{aid}.wav"), Base64Data: makeStrPtr("{b64}")}}}, + {Type: ChatMessagePartTypeVideoURL, Video: &MessageInputVideo{MessagePartCommon: MessagePartCommon{URL: makeStrPtr("http://v/{vid}.mp4"), Base64Data: makeStrPtr("{b64}")}}}, + {Type: ChatMessagePartTypeFileURL, File: &MessageInputFile{MessagePartCommon: MessagePartCommon{URL: makeStrPtr("/f/{file}.txt"), Base64Data: makeStrPtr("{b64}")}}}, + } + + out, err := formatUserInputMultiContent(in, vs, FString) + assert.NoError(t, err) + if assert.Len(t, out, len(in)) { + assert.Equal(t, "hello world", out[0].Text) + assert.Equal(t, "https://example.com/i.png", *out[1].Image.URL) + assert.Equal(t, "YmFzZTY0", *out[1].Image.Base64Data) + assert.Equal(t, "http://a/99.wav", *out[2].Audio.URL) + assert.Equal(t, "YmFzZTY0", *out[2].Audio.Base64Data) + assert.Equal(t, "http://v/77.mp4", *out[3].Video.URL) + assert.Equal(t, "YmFzZTY0", *out[3].Video.Base64Data) + assert.Equal(t, "/f/abc.txt", *out[4].File.URL) + assert.Equal(t, "YmFzZTY0", *out[4].File.Base64Data) + } + }) + + t.Run("empty string pointer should not be formatted", func(t *testing.T) { + empty := "" + in := []MessageInputPart{ + {Type: ChatMessagePartTypeImageURL, Image: &MessageInputImage{MessagePartCommon: MessagePartCommon{URL: &empty, Base64Data: &empty}}}, + } + out, err := formatUserInputMultiContent(in, vs, FString) + assert.NoError(t, err) + if assert.Len(t, out, 1) { + assert.NotNil(t, out[0].Image.URL) + assert.NotNil(t, out[0].Image.Base64Data) + assert.Equal(t, "", *out[0].Image.URL) + assert.Equal(t, "", *out[0].Image.Base64Data) + } + }) +} + +func TestConcatToolResults(t *testing.T) { + t.Run("empty_chunks", func(t *testing.T) { + result, err := ConcatToolResults([]*ToolResult{}) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result.Parts) + }) + + t.Run("nil_chunks", func(t *testing.T) { + result, err := ConcatToolResults([]*ToolResult{nil, nil}) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result.Parts) + }) + + t.Run("single_text_part", func(t *testing.T) { + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Hello World"}, + }, + }, + } + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "Hello World", result.Parts[0].Text) + }) + + t.Run("multiple_text_parts_merge", func(t *testing.T) { + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Hello "}, + }, + }, + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "World"}, + }, + }, + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "!"}, + }, + }, + } + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "Hello World!", result.Parts[0].Text) + }) + + t.Run("multiple_text_parts_merge_with_extra", func(t *testing.T) { + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Hello ", Extra: map[string]any{"k1": "v1"}}, + {Type: ToolPartTypeText, Text: "World", Extra: map[string]any{"k2": "v2"}}, + }, + }, + } + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, "Hello World", result.Parts[0].Text) + assert.Equal(t, map[string]any{"k1": "v1", "k2": "v2"}, result.Parts[0].Extra) + }) + + t.Run("multiple_text_parts_merge_with_single_extra", func(t *testing.T) { + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Hello ", Extra: map[string]any{"k1": "v1"}}, + {Type: ToolPartTypeText, Text: "World"}, + }, + }, + } + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, "Hello World", result.Parts[0].Text) + assert.Equal(t, map[string]any{"k1": "v1"}, result.Parts[0].Extra) + }) + + t.Run("cross_chunk_audio_conflict_error", func(t *testing.T) { + base64Data1 := "YXVkaW8x" + base64Data2 := "YXVkaW8y" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data1, + MIMEType: "audio/wav", + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data2, + MIMEType: "audio/wav", + }, + }, + }, + }, + }, + } + + _, err := ConcatToolResults(chunks) + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting") + assert.Contains(t, err.Error(), "audio") + }) + + t.Run("mixed_types_no_merge", func(t *testing.T) { + imageURL := "https://example.com/image.png" + videoURL := "https://example.com/video.mp4" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Text part"}, + { + Type: ToolPartTypeImage, + Image: &ToolOutputImage{ + MessagePartCommon: MessagePartCommon{ + URL: &imageURL, + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeVideo, + Video: &ToolOutputVideo{ + MessagePartCommon: MessagePartCommon{ + URL: &videoURL, + }, + }, + }, + }, + }, + } + + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 3) + assert.Equal(t, ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, ToolPartTypeImage, result.Parts[1].Type) + assert.Equal(t, ToolPartTypeVideo, result.Parts[2].Type) + }) + + t.Run("mixed_text_and_single_audio", func(t *testing.T) { + base64Data1 := "YXVkaW8x" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Part 1 "}, + {Type: ToolPartTypeText, Text: "Part 2"}, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data1, + MIMEType: "audio/wav", + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: " Part 3"}, + }, + }, + } + + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 3) + + assert.Equal(t, ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "Part 1 Part 2", result.Parts[0].Text) + + assert.Equal(t, ToolPartTypeAudio, result.Parts[1].Type) + assert.NotNil(t, result.Parts[1].Audio) + assert.NotNil(t, result.Parts[1].Audio.Base64Data) + assert.Equal(t, "YXVkaW8x", *result.Parts[1].Audio.Base64Data) + + assert.Equal(t, ToolPartTypeText, result.Parts[2].Type) + assert.Equal(t, " Part 3", result.Parts[2].Text) + }) + + t.Run("cross_chunk_audio_url_and_base64_conflict_error", func(t *testing.T) { + audioURL := "https://example.com/audio.wav" + base64Data := "YXVkaW8x" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + URL: &audioURL, + MIMEType: "audio/wav", + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data, + MIMEType: "audio/wav", + }, + }, + }, + }, + }, + } + + _, err := ConcatToolResults(chunks) + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting") + assert.Contains(t, err.Error(), "audio") + }) + + t.Run("single_audio_with_extra_fields", func(t *testing.T) { + base64Data1 := "YXVkaW8x" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data1, + MIMEType: "audio/wav", + Extra: map[string]any{ + "key1": "value1", + }, + }, + }, + }, + }, + }, + } + + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, ToolPartTypeAudio, result.Parts[0].Type) + assert.NotNil(t, result.Parts[0].Audio) + assert.NotNil(t, result.Parts[0].Audio.Base64Data) + assert.Equal(t, "YXVkaW8x", *result.Parts[0].Audio.Base64Data) + assert.NotNil(t, result.Parts[0].Audio.Extra) + assert.Equal(t, "value1", result.Parts[0].Audio.Extra["key1"]) + }) + + t.Run("cross_chunk_image_conflict_error", func(t *testing.T) { + imageURL1 := "https://example.com/image1.png" + imageURL2 := "https://example.com/image2.png" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeImage, + Image: &ToolOutputImage{ + MessagePartCommon: MessagePartCommon{ + URL: &imageURL1, + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeImage, + Image: &ToolOutputImage{ + MessagePartCommon: MessagePartCommon{ + URL: &imageURL2, + }, + }, + }, + }, + }, + } + + _, err := ConcatToolResults(chunks) + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting") + assert.Contains(t, err.Error(), "image") + }) + + t.Run("cross_chunk_video_conflict_error", func(t *testing.T) { + videoURL1 := "https://example.com/video1.mp4" + videoURL2 := "https://example.com/video2.mp4" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeVideo, + Video: &ToolOutputVideo{ + MessagePartCommon: MessagePartCommon{ + URL: &videoURL1, + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeVideo, + Video: &ToolOutputVideo{ + MessagePartCommon: MessagePartCommon{ + URL: &videoURL2, + }, + }, + }, + }, + }, + } + + _, err := ConcatToolResults(chunks) + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting") + assert.Contains(t, err.Error(), "video") + }) + + t.Run("cross_chunk_file_conflict_error", func(t *testing.T) { + fileURL1 := "https://example.com/file1.pdf" + fileURL2 := "https://example.com/file2.pdf" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeFile, + File: &ToolOutputFile{ + MessagePartCommon: MessagePartCommon{ + URL: &fileURL1, + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeFile, + File: &ToolOutputFile{ + MessagePartCommon: MessagePartCommon{ + URL: &fileURL2, + }, + }, + }, + }, + }, + } + + _, err := ConcatToolResults(chunks) + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting") + assert.Contains(t, err.Error(), "file") + }) + + t.Run("same_chunk_text_merged", func(t *testing.T) { + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + {Type: ToolPartTypeText, Text: "Hello "}, + {Type: ToolPartTypeText, Text: "World"}, + }, + }, + } + + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 1) + assert.Equal(t, ToolPartTypeText, result.Parts[0].Type) + assert.Equal(t, "Hello World", result.Parts[0].Text) + }) + + t.Run("different_non_text_types_across_chunks_allowed", func(t *testing.T) { + imageURL := "https://example.com/image.png" + videoURL := "https://example.com/video.mp4" + base64Audio := "YXVkaW8x" + + chunks := []*ToolResult{ + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeImage, + Image: &ToolOutputImage{ + MessagePartCommon: MessagePartCommon{ + URL: &imageURL, + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeVideo, + Video: &ToolOutputVideo{ + MessagePartCommon: MessagePartCommon{ + URL: &videoURL, + }, + }, + }, + }, + }, + { + Parts: []ToolOutputPart{ + { + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Audio, + MIMEType: "audio/wav", + }, + }, + }, + }, + }, + } + + result, err := ConcatToolResults(chunks) + assert.NoError(t, err) + assert.Len(t, result.Parts, 3) + assert.Equal(t, ToolPartTypeImage, result.Parts[0].Type) + assert.Equal(t, ToolPartTypeVideo, result.Parts[1].Type) + assert.Equal(t, ToolPartTypeAudio, result.Parts[2].Type) + }) +} + +func TestMessageString(t *testing.T) { + t.Run("basic message", func(t *testing.T) { + msg := &Message{ + Role: User, + Content: "Hello, world!", + } + result := msg.String() + assert.Contains(t, result, "user: Hello, world!") + }) + + t.Run("message with UserInputMultiContent", func(t *testing.T) { + imageURL := "https://example.com/image.png" + msg := &Message{ + Role: User, + Content: "", + UserInputMultiContent: []MessageInputPart{ + {Type: ChatMessagePartTypeText, Text: "Describe this image:"}, + {Type: ChatMessagePartTypeImageURL, Image: &MessageInputImage{ + MessagePartCommon: MessagePartCommon{URL: &imageURL}, + }}, + }, + } + result := msg.String() + assert.Contains(t, result, "user_input_multi_content:") + assert.Contains(t, result, "[0] text: Describe this image:") + assert.Contains(t, result, "[1] image: url=https://example.com/image.png") + }) + + t.Run("message with AssistantGenMultiContent", func(t *testing.T) { + base64Data := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + msg := &Message{ + Role: Assistant, + Content: "", + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Here is the generated image:"}, + {Type: ChatMessagePartTypeImageURL, Image: &MessageOutputImage{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data, + MIMEType: "image/png", + }, + }}, + }, + } + result := msg.String() + assert.Contains(t, result, "assistant_gen_multi_content:") + assert.Contains(t, result, "[0] text: Here is the generated image:") + assert.Contains(t, result, "[1] image: base64[") + assert.Contains(t, result, "mime=image/png") + }) + + t.Run("message with MultiContent (deprecated)", func(t *testing.T) { + msg := &Message{ + Role: User, + Content: "", + MultiContent: []ChatMessagePart{ + {Type: ChatMessagePartTypeText, Text: "What is this?"}, + {Type: ChatMessagePartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: "https://example.com/photo.jpg"}}, + }, + } + result := msg.String() + assert.Contains(t, result, "multi_content:") + assert.Contains(t, result, "[0] text: What is this?") + assert.Contains(t, result, "[1] image_url: https://example.com/photo.jpg") + }) + + t.Run("message with ToolCalls", func(t *testing.T) { + idx := 0 + msg := &Message{ + Role: Assistant, + Content: "", + ToolCalls: []ToolCall{ + { + Index: &idx, + ID: "call_123", + Type: "function", + Function: FunctionCall{ + Name: "get_weather", + Arguments: `{"location": "Beijing"}`, + }, + }, + }, + } + result := msg.String() + assert.Contains(t, result, "tool_calls:") + assert.Contains(t, result, "index[0]:") + assert.Contains(t, result, "get_weather") + }) + + t.Run("tool message", func(t *testing.T) { + msg := &Message{ + Role: Tool, + Content: `{"temperature": 25}`, + ToolCallID: "call_123", + ToolName: "get_weather", + } + result := msg.String() + assert.Contains(t, result, "tool: {\"temperature\": 25}") + assert.Contains(t, result, "tool_call_id: call_123") + assert.Contains(t, result, "tool_call_name: get_weather") + }) + + t.Run("message with reasoning content", func(t *testing.T) { + msg := &Message{ + Role: Assistant, + Content: "The answer is 42.", + ReasoningContent: "Let me think about this step by step...", + } + result := msg.String() + assert.Contains(t, result, "reasoning content:") + assert.Contains(t, result, "Let me think about this step by step...") + }) + + t.Run("message with response meta", func(t *testing.T) { + msg := &Message{ + Role: Assistant, + Content: "Hello!", + ResponseMeta: &ResponseMeta{ + FinishReason: "stop", + Usage: &TokenUsage{ + PromptTokens: 10, + CompletionTokens: 5, + TotalTokens: 15, + }, + }, + } + result := msg.String() + assert.Contains(t, result, "finish_reason: stop") + assert.Contains(t, result, "usage:") + }) + + t.Run("message with audio input", func(t *testing.T) { + audioURL := "https://example.com/audio.wav" + msg := &Message{ + Role: User, + UserInputMultiContent: []MessageInputPart{ + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageInputAudio{ + MessagePartCommon: MessagePartCommon{URL: &audioURL}, + }}, + }, + } + result := msg.String() + assert.Contains(t, result, "[0] audio: url=https://example.com/audio.wav") + }) + + t.Run("message with video input", func(t *testing.T) { + videoURL := "https://example.com/video.mp4" + msg := &Message{ + Role: User, + UserInputMultiContent: []MessageInputPart{ + {Type: ChatMessagePartTypeVideoURL, Video: &MessageInputVideo{ + MessagePartCommon: MessagePartCommon{URL: &videoURL}, + }}, + }, + } + result := msg.String() + assert.Contains(t, result, "[0] video: url=https://example.com/video.mp4") + }) + + t.Run("message with file input", func(t *testing.T) { + fileURL := "https://example.com/document.pdf" + msg := &Message{ + Role: User, + UserInputMultiContent: []MessageInputPart{ + {Type: ChatMessagePartTypeFileURL, File: &MessageInputFile{ + MessagePartCommon: MessagePartCommon{URL: &fileURL}, + }}, + }, + } + result := msg.String() + assert.Contains(t, result, "[0] file: url=https://example.com/document.pdf") + }) + + t.Run("nil media parts", func(t *testing.T) { + msg := &Message{ + Role: User, + UserInputMultiContent: []MessageInputPart{ + {Type: ChatMessagePartTypeImageURL, Image: nil}, + }, + } + result := msg.String() + assert.Contains(t, result, "[0] image: ") + }) + + t.Run("combined multi-content types", func(t *testing.T) { + imageURL := "https://example.com/image.png" + base64Audio := "YXVkaW9kYXRh" + msg := &Message{ + Role: User, + Content: "Main content", + UserInputMultiContent: []MessageInputPart{ + {Type: ChatMessagePartTypeText, Text: "User input text"}, + {Type: ChatMessagePartTypeImageURL, Image: &MessageInputImage{ + MessagePartCommon: MessagePartCommon{URL: &imageURL}, + }}, + }, + AssistantGenMultiContent: []MessageOutputPart{ + {Type: ChatMessagePartTypeText, Text: "Assistant output text"}, + {Type: ChatMessagePartTypeAudioURL, Audio: &MessageOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Audio, + MIMEType: "audio/wav", + }, + }}, + }, + } + result := msg.String() + assert.Contains(t, result, "user: Main content") + assert.Contains(t, result, "user_input_multi_content:") + assert.Contains(t, result, "assistant_gen_multi_content:") + }) +} + +func TestConvToolOutputPartToMessageInputPart(t *testing.T) { + t.Run("text part", func(t *testing.T) { + toolPart := ToolOutputPart{ + Type: ToolPartTypeText, + Text: "test text", + Extra: map[string]any{"key": "value"}, + } + result, err := convToolOutputPartToMessageInputPart(toolPart) + assert.NoError(t, err) + assert.Equal(t, ChatMessagePartTypeText, result.Type) + assert.Equal(t, "test text", result.Text) + assert.Equal(t, map[string]any{"key": "value"}, result.Extra) + }) + + t.Run("image part", func(t *testing.T) { + url := "https://example.com/image.png" + toolPart := ToolOutputPart{ + Type: ToolPartTypeImage, + Image: &ToolOutputImage{ + MessagePartCommon: MessagePartCommon{ + URL: &url, + MIMEType: "image/png", + }, + }, + Extra: map[string]any{"img_key": "img_value"}, + } + result, err := convToolOutputPartToMessageInputPart(toolPart) + assert.NoError(t, err) + assert.Equal(t, ChatMessagePartTypeImageURL, result.Type) + assert.NotNil(t, result.Image) + assert.Equal(t, url, *result.Image.URL) + assert.Equal(t, "image/png", result.Image.MIMEType) + assert.Equal(t, map[string]any{"img_key": "img_value"}, result.Extra) + }) + + t.Run("image part nil content", func(t *testing.T) { + toolPart := ToolOutputPart{ + Type: ToolPartTypeImage, + Image: nil, + } + result, err := convToolOutputPartToMessageInputPart(toolPart) + assert.Error(t, err) + assert.ErrorContains(t, err, "image content is nil") + assert.Equal(t, MessageInputPart{}, result) + }) + + t.Run("audio part", func(t *testing.T) { + base64Data := "dGVzdF9hdWRpbw==" + toolPart := ToolOutputPart{ + Type: ToolPartTypeAudio, + Audio: &ToolOutputAudio{ + MessagePartCommon: MessagePartCommon{ + Base64Data: &base64Data, + MIMEType: "audio/wav", + }, + }, + } + result, err := convToolOutputPartToMessageInputPart(toolPart) + assert.NoError(t, err) + assert.Equal(t, ChatMessagePartTypeAudioURL, result.Type) + assert.NotNil(t, result.Audio) + assert.Equal(t, base64Data, *result.Audio.Base64Data) + assert.Equal(t, "audio/wav", result.Audio.MIMEType) + }) + + t.Run("audio part nil content", func(t *testing.T) { + toolPart := ToolOutputPart{ + Type: ToolPartTypeAudio, + Audio: nil, + } + _, err := convToolOutputPartToMessageInputPart(toolPart) + assert.Error(t, err) + assert.ErrorContains(t, err, "audio content is nil") + }) + + t.Run("video part", func(t *testing.T) { + url := "https://example.com/video.mp4" + toolPart := ToolOutputPart{ + Type: ToolPartTypeVideo, + Video: &ToolOutputVideo{ + MessagePartCommon: MessagePartCommon{ + URL: &url, + MIMEType: "video/mp4", + }, + }, + } + result, err := convToolOutputPartToMessageInputPart(toolPart) + assert.NoError(t, err) + assert.Equal(t, ChatMessagePartTypeVideoURL, result.Type) + assert.NotNil(t, result.Video) + assert.Equal(t, url, *result.Video.URL) + assert.Equal(t, "video/mp4", result.Video.MIMEType) + }) + + t.Run("video part nil content", func(t *testing.T) { + toolPart := ToolOutputPart{ + Type: ToolPartTypeVideo, + Video: nil, + } + _, err := convToolOutputPartToMessageInputPart(toolPart) + assert.Error(t, err) + assert.ErrorContains(t, err, "video content is nil") + }) + + t.Run("file part", func(t *testing.T) { + url := "https://example.com/file.pdf" + toolPart := ToolOutputPart{ + Type: ToolPartTypeFile, + File: &ToolOutputFile{ + MessagePartCommon: MessagePartCommon{ + URL: &url, + MIMEType: "application/pdf", + }, + }, + Extra: map[string]any{"file_key": "file_value"}, + } + result, err := convToolOutputPartToMessageInputPart(toolPart) + assert.NoError(t, err) + assert.Equal(t, ChatMessagePartTypeFileURL, result.Type) + assert.NotNil(t, result.File) + assert.Equal(t, url, *result.File.URL) + assert.Equal(t, "application/pdf", result.File.MIMEType) + assert.Equal(t, map[string]any{"file_key": "file_value"}, result.Extra) + }) + + t.Run("file part nil content", func(t *testing.T) { + toolPart := ToolOutputPart{ + Type: ToolPartTypeFile, + File: nil, + } + _, err := convToolOutputPartToMessageInputPart(toolPart) + assert.Error(t, err) + assert.ErrorContains(t, err, "file content is nil") + }) + + t.Run("unknown type", func(t *testing.T) { + toolPart := ToolOutputPart{ + Type: "unknown_type", + } + _, err := convToolOutputPartToMessageInputPart(toolPart) + assert.Error(t, err) + assert.ErrorContains(t, err, "unknown tool part type") + }) +} diff --git a/schema/openai/consts.go b/schema/openai/consts.go new file mode 100644 index 0000000..5958cef --- /dev/null +++ b/schema/openai/consts.go @@ -0,0 +1,95 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package openai defines constants for openai. +package openai + +type TextAnnotationType string + +const ( + TextAnnotationTypeFileCitation TextAnnotationType = "file_citation" + TextAnnotationTypeURLCitation TextAnnotationType = "url_citation" + TextAnnotationTypeContainerFileCitation TextAnnotationType = "container_file_citation" + TextAnnotationTypeFilePath TextAnnotationType = "file_path" +) + +type ReasoningEffort string + +const ( + ReasoningEffortMinimal ReasoningEffort = "minimal" + ReasoningEffortLow ReasoningEffort = "low" + ReasoningEffortMedium ReasoningEffort = "medium" + ReasoningEffortHigh ReasoningEffort = "high" +) + +type ReasoningSummary string + +const ( + ReasoningSummaryAuto ReasoningSummary = "auto" + ReasoningSummaryConcise ReasoningSummary = "concise" + ReasoningSummaryDetailed ReasoningSummary = "detailed" +) + +type ServiceTier string + +const ( + ServiceTierAuto ServiceTier = "auto" + ServiceTierDefault ServiceTier = "default" + ServiceTierFlex ServiceTier = "flex" + ServiceTierScale ServiceTier = "scale" + ServiceTierPriority ServiceTier = "priority" +) + +type PromptCacheRetention string + +const ( + PromptCacheRetentionInMemory PromptCacheRetention = "in-memory" + PromptCacheRetention24h PromptCacheRetention = "24h" +) + +type ResponseStatus string + +const ( + ResponseStatusCompleted ResponseStatus = "completed" + ResponseStatusFailed ResponseStatus = "failed" + ResponseStatusInProgress ResponseStatus = "in_progress" + ResponseStatusCancelled ResponseStatus = "cancelled" + ResponseStatusQueued ResponseStatus = "queued" + ResponseStatusIncomplete ResponseStatus = "incomplete" +) + +type ResponseErrorCode string + +const ( + ResponseErrorCodeServerError ResponseErrorCode = "server_error" + ResponseErrorCodeRateLimitExceeded ResponseErrorCode = "rate_limit_exceeded" + ResponseErrorCodeInvalidPrompt ResponseErrorCode = "invalid_prompt" + ResponseErrorCodeVectorStoreTimeout ResponseErrorCode = "vector_store_timeout" + ResponseErrorCodeInvalidImage ResponseErrorCode = "invalid_image" + ResponseErrorCodeInvalidImageFormat ResponseErrorCode = "invalid_image_format" + ResponseErrorCodeInvalidBase64Image ResponseErrorCode = "invalid_base64_image" + ResponseErrorCodeInvalidImageURL ResponseErrorCode = "invalid_image_url" + ResponseErrorCodeImageTooLarge ResponseErrorCode = "image_too_large" + ResponseErrorCodeImageTooSmall ResponseErrorCode = "image_too_small" + ResponseErrorCodeImageParseError ResponseErrorCode = "image_parse_error" + ResponseErrorCodeImageContentPolicyViolation ResponseErrorCode = "image_content_policy_violation" + ResponseErrorCodeInvalidImageMode ResponseErrorCode = "invalid_image_mode" + ResponseErrorCodeImageFileTooLarge ResponseErrorCode = "image_file_too_large" + ResponseErrorCodeUnsupportedImageMediaType ResponseErrorCode = "unsupported_image_media_type" + ResponseErrorCodeEmptyImageFile ResponseErrorCode = "empty_image_file" + ResponseErrorCodeFailedToDownloadImage ResponseErrorCode = "failed_to_download_image" + ResponseErrorCodeImageFileNotFound ResponseErrorCode = "image_file_not_found" +) diff --git a/schema/openai/extension.go b/schema/openai/extension.go new file mode 100644 index 0000000..3bee170 --- /dev/null +++ b/schema/openai/extension.go @@ -0,0 +1,283 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openai + +import ( + "fmt" + "sort" +) + +type ResponseMetaExtension struct { + ID string `json:"id,omitempty"` + Status ResponseStatus `json:"status,omitempty"` + Error *ResponseError `json:"error,omitempty"` + IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"` + PreviousResponseID string `json:"previous_response_id,omitempty"` + Reasoning *Reasoning `json:"reasoning,omitempty"` + ServiceTier ServiceTier `json:"service_tier,omitempty"` + CreatedAt int64 `json:"created_at,omitempty"` + PromptCacheRetention PromptCacheRetention `json:"prompt_cache_retention,omitempty"` +} + +type AssistantGenTextExtension struct { + Refusal *OutputRefusal `json:"refusal,omitempty"` + Annotations []*TextAnnotation `json:"annotations,omitempty"` +} + +type ReasoningExtension struct { + // Content is the reasoning text content. + Content []*ReasoningContent `json:"content,omitempty"` +} + +type ReasoningContent struct { + // Index specifies the index position of this content in the final response. + // Only available in streaming response. + Index *int `json:"index,omitempty"` + + Text string `json:"text,omitempty"` +} + +type ResponseError struct { + Code ResponseErrorCode `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +type IncompleteDetails struct { + Reason string `json:"reason,omitempty"` +} + +type Reasoning struct { + Effort ReasoningEffort `json:"effort,omitempty"` + Summary ReasoningSummary `json:"summary,omitempty"` +} + +type OutputRefusal struct { + Reason string `json:"reason,omitempty"` +} + +type TextAnnotation struct { + // Index specifies the index position of this annotation in the final response. + // Only available in streaming response. + Index int `json:"index,omitempty"` + + Type TextAnnotationType `json:"type,omitempty"` + + FileCitation *TextAnnotationFileCitation `json:"file_citation,omitempty"` + URLCitation *TextAnnotationURLCitation `json:"url_citation,omitempty"` + ContainerFileCitation *TextAnnotationContainerFileCitation `json:"container_file_citation,omitempty"` + FilePath *TextAnnotationFilePath `json:"file_path,omitempty"` +} + +type TextAnnotationFileCitation struct { + // The ID of the file. + FileID string `json:"file_id,omitempty"` + // The filename of the file cited. + Filename string `json:"filename,omitempty"` + + // The index of the file in the list of files. + Index int `json:"index,omitempty"` +} + +type TextAnnotationURLCitation struct { + // The title of the web resource. + Title string `json:"title,omitempty"` + // The URL of the web resource. + URL string `json:"url,omitempty"` + + // The index of the first character of the URL citation in the message. + StartIndex int `json:"start_index,omitempty"` + // The index of the last character of the URL citation in the message. + EndIndex int `json:"end_index,omitempty"` +} + +type TextAnnotationContainerFileCitation struct { + // The ID of the container file. + ContainerID string `json:"container_id,omitempty"` + + // The ID of the file. + FileID string `json:"file_id,omitempty"` + // The filename of the container file cited. + Filename string `json:"filename,omitempty"` + + // The index of the first character of the container file citation in the message. + StartIndex int `json:"start_index,omitempty"` + // The index of the last character of the container file citation in the message. + EndIndex int `json:"end_index,omitempty"` +} + +type TextAnnotationFilePath struct { + // The ID of the file. + FileID string `json:"file_id,omitempty"` + + // The index of the file in the list of files. + Index int `json:"index,omitempty"` +} + +// ConcatAssistantGenTextExtensions concatenates multiple AssistantGenTextExtension chunks into a single one. +func ConcatAssistantGenTextExtensions(chunks []*AssistantGenTextExtension) (*AssistantGenTextExtension, error) { + if len(chunks) == 0 { + return nil, fmt.Errorf("no assistant generated text extension found") + } + + ret := &AssistantGenTextExtension{} + + var allAnnotations []*TextAnnotation + for _, ext := range chunks { + allAnnotations = append(allAnnotations, ext.Annotations...) + } + + var ( + indices []int + indexToAnnotation = map[int]*TextAnnotation{} + ) + + for _, an := range allAnnotations { + if an == nil { + continue + } + if indexToAnnotation[an.Index] == nil { + indexToAnnotation[an.Index] = an + indices = append(indices, an.Index) + } else { + return nil, fmt.Errorf("duplicate annotation index %d", an.Index) + } + } + + sort.Slice(indices, func(i, j int) bool { + return indices[i] < indices[j] + }) + + ret.Annotations = make([]*TextAnnotation, 0, len(indices)) + for _, idx := range indices { + an := *indexToAnnotation[idx] + an.Index = 0 // clear index + ret.Annotations = append(ret.Annotations, &an) + } + + for _, ext := range chunks { + if ext.Refusal == nil { + continue + } + if ret.Refusal == nil { + ret.Refusal = ext.Refusal + } else { + ret.Refusal.Reason += ext.Refusal.Reason + } + } + + return ret, nil +} + +// ConcatReasoningExtensions concatenates multiple ReasoningExtension chunks into a single one. +func ConcatReasoningExtensions(chunks []*ReasoningExtension) (*ReasoningExtension, error) { + if len(chunks) == 0 { + return nil, fmt.Errorf("no reasoning extension found") + } + + ret := &ReasoningExtension{} + + var ( + indices []int + indexToContent = map[int]*ReasoningContent{} + hasIndexed bool + hasUnindexed bool + ) + + for _, ext := range chunks { + if ext == nil { + continue + } + for _, c := range ext.Content { + if c == nil { + continue + } + + if c.Index == nil { + hasUnindexed = true + ret.Content = append(ret.Content, &ReasoningContent{Text: c.Text}) + continue + } + + hasIndexed = true + idx := *c.Index + if existing, ok := indexToContent[idx]; ok { + existing.Text += c.Text + } else { + indexToContent[idx] = &ReasoningContent{Text: c.Text} + indices = append(indices, idx) + } + } + } + + if hasIndexed && hasUnindexed { + return nil, fmt.Errorf("reasoning content chunks mix indexed and non-indexed content") + } + + if hasIndexed { + sort.Ints(indices) + ret.Content = make([]*ReasoningContent, 0, len(indices)) + for _, idx := range indices { + ret.Content = append(ret.Content, indexToContent[idx]) + } + } + + return ret, nil +} + +// ConcatResponseMetaExtensions concatenates multiple ResponseMetaExtension chunks into a single one. +func ConcatResponseMetaExtensions(chunks []*ResponseMetaExtension) (*ResponseMetaExtension, error) { + if len(chunks) == 0 { + return nil, fmt.Errorf("no response meta extension found") + } + if len(chunks) == 1 { + return chunks[0], nil + } + + ret := &ResponseMetaExtension{} + + for _, ext := range chunks { + if ext.ID != "" { + ret.ID = ext.ID + } + if ext.Status != "" { + ret.Status = ext.Status + } + if ext.Error != nil { + ret.Error = ext.Error + } + if ext.IncompleteDetails != nil { + ret.IncompleteDetails = ext.IncompleteDetails + } + if ext.PreviousResponseID != "" { + ret.PreviousResponseID = ext.PreviousResponseID + } + if ext.Reasoning != nil { + ret.Reasoning = ext.Reasoning + } + if ext.ServiceTier != "" { + ret.ServiceTier = ext.ServiceTier + } + if ext.CreatedAt != 0 { + ret.CreatedAt = ext.CreatedAt + } + if ext.PromptCacheRetention != "" { + ret.PromptCacheRetention = ext.PromptCacheRetention + } + } + + return ret, nil +} diff --git a/schema/openai/extension_test.go b/schema/openai/extension_test.go new file mode 100644 index 0000000..5ee57ca --- /dev/null +++ b/schema/openai/extension_test.go @@ -0,0 +1,268 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openai + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/generic" +) + +func TestConcatResponseMetaExtensions(t *testing.T) { + t.Run("multiple extensions - takes last non-empty values", func(t *testing.T) { + err1 := &ResponseError{Code: "err1", Message: "msg1"} + incomplete := &IncompleteDetails{Reason: "max_tokens"} + + exts := []*ResponseMetaExtension{ + { + ID: "id_1", + Status: "in_progress", + Error: err1, + IncompleteDetails: nil, + }, + { + ID: "id_2", + Status: "", + Error: nil, + IncompleteDetails: nil, + }, + { + ID: "", + Status: "completed", + Error: nil, + IncompleteDetails: incomplete, + }, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "id_2", result.ID) + assert.Equal(t, ResponseStatus("completed"), result.Status) + assert.Equal(t, err1, result.Error) + assert.Equal(t, incomplete, result.IncompleteDetails) + }) + + t.Run("streaming scenario", func(t *testing.T) { + exts := []*ResponseMetaExtension{ + {ID: "chatcmpl_stream", Status: "", Error: nil, IncompleteDetails: nil}, + {ID: "", Status: ResponseStatus("in_progress"), Error: nil, IncompleteDetails: nil}, + {ID: "", Status: ResponseStatus("completed"), Error: nil, IncompleteDetails: nil}, + } + + result, err := ConcatResponseMetaExtensions(exts) + assert.NoError(t, err) + assert.Equal(t, "chatcmpl_stream", result.ID) + assert.Equal(t, ResponseStatus("completed"), result.Status) + }) +} + +func TestConcatAssistantGenTextExtensions(t *testing.T) { + t.Run("single extension with annotations", func(t *testing.T) { + ext := &AssistantGenTextExtension{ + Annotations: []*TextAnnotation{ + { + Index: 0, + Type: "file_citation", + FileCitation: &TextAnnotationFileCitation{ + FileID: "file_123", + Filename: "doc.pdf", + }, + }, + }, + } + + result, err := ConcatAssistantGenTextExtensions([]*AssistantGenTextExtension{ext}) + assert.NoError(t, err) + assert.Len(t, result.Annotations, 1) + assert.Equal(t, "file_123", result.Annotations[0].FileCitation.FileID) + }) + + t.Run("multiple extensions - merges annotations by index", func(t *testing.T) { + exts := []*AssistantGenTextExtension{ + { + Annotations: []*TextAnnotation{ + { + Index: 0, + Type: "file_citation", + FileCitation: &TextAnnotationFileCitation{ + FileID: "file_1", + }, + }, + }, + }, + { + Annotations: []*TextAnnotation{ + { + Index: 2, + Type: "url_citation", + URLCitation: &TextAnnotationURLCitation{ + URL: "https://example.com", + }, + }, + }, + }, + { + Annotations: []*TextAnnotation{ + { + Index: 1, + Type: "file_path", + FilePath: &TextAnnotationFilePath{ + FileID: "file_2", + }, + }, + }, + }, + } + + result, err := ConcatAssistantGenTextExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Annotations, 3) + assert.Equal(t, "file_1", result.Annotations[0].FileCitation.FileID) + assert.Equal(t, "file_2", result.Annotations[1].FilePath.FileID) + assert.Equal(t, "https://example.com", result.Annotations[2].URLCitation.URL) + }) + + t.Run("streaming scenario - annotations arrive in chunks", func(t *testing.T) { + exts := []*AssistantGenTextExtension{ + { + Annotations: []*TextAnnotation{ + {Index: 0, Type: "file_citation", FileCitation: &TextAnnotationFileCitation{FileID: "f1"}}, + }, + }, + { + Annotations: []*TextAnnotation{ + {Index: 1, Type: "url_citation", URLCitation: &TextAnnotationURLCitation{URL: "url1"}}, + }, + }, + { + Annotations: []*TextAnnotation{ + {Index: 2, Type: "file_path", FilePath: &TextAnnotationFilePath{FileID: "f2"}}, + }, + }, + } + + result, err := ConcatAssistantGenTextExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Annotations, 3) + assert.Equal(t, "f1", result.Annotations[0].FileCitation.FileID) + assert.Equal(t, "url1", result.Annotations[1].URLCitation.URL) + assert.Equal(t, "f2", result.Annotations[2].FilePath.FileID) + }) + + t.Run("multiple extensions - concatenates refusal reason", func(t *testing.T) { + ext1 := &AssistantGenTextExtension{Refusal: &OutputRefusal{Reason: "A"}} + ext2 := &AssistantGenTextExtension{Refusal: &OutputRefusal{Reason: "B"}} + + result, err := ConcatAssistantGenTextExtensions([]*AssistantGenTextExtension{ext1, ext2}) + assert.NoError(t, err) + assert.NotNil(t, result.Refusal) + assert.Equal(t, "AB", result.Refusal.Reason) + }) + + t.Run("duplicate index - error occurrence", func(t *testing.T) { + exts := []*AssistantGenTextExtension{ + { + Annotations: []*TextAnnotation{ + {Index: 0, Type: "file_citation", FileCitation: &TextAnnotationFileCitation{FileID: "first"}}, + }, + }, + { + Annotations: []*TextAnnotation{ + {Index: 0, Type: "url_citation", URLCitation: &TextAnnotationURLCitation{URL: "second"}}, + }, + }, + } + + _, err := ConcatAssistantGenTextExtensions(exts) + assert.Error(t, err) + }) +} + +func TestConcatReasoningExtensions(t *testing.T) { + t.Run("empty chunks - error", func(t *testing.T) { + _, err := ConcatReasoningExtensions(nil) + assert.Error(t, err) + }) + + t.Run("single extension", func(t *testing.T) { + ext := &ReasoningExtension{ + Content: []*ReasoningContent{ + {Text: "hello", Index: generic.PtrOf(0)}, + }, + } + + result, err := ConcatReasoningExtensions([]*ReasoningExtension{ext}) + assert.NoError(t, err) + assert.Len(t, result.Content, 1) + assert.Equal(t, "hello", result.Content[0].Text) + }) + + t.Run("streaming scenario - same index concatenated, ordered by index", func(t *testing.T) { + exts := []*ReasoningExtension{ + {Content: []*ReasoningContent{{Text: "he", Index: generic.PtrOf(0)}}}, + {Content: []*ReasoningContent{{Text: "first", Index: generic.PtrOf(1)}}}, + {Content: []*ReasoningContent{{Text: "llo", Index: generic.PtrOf(0)}}}, + } + + result, err := ConcatReasoningExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Content, 2) + assert.Nil(t, result.Content[0].Index) + assert.Equal(t, "hello", result.Content[0].Text) + assert.Nil(t, result.Content[1].Index) + assert.Equal(t, "first", result.Content[1].Text) + }) + + t.Run("nil extension and nil content skipped", func(t *testing.T) { + exts := []*ReasoningExtension{ + nil, + {Content: []*ReasoningContent{nil, {Text: "ok", Index: generic.PtrOf(0)}}}, + } + + result, err := ConcatReasoningExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Content, 1) + assert.Equal(t, "ok", result.Content[0].Text) + }) + + t.Run("all unindexed - appended in arrival order", func(t *testing.T) { + exts := []*ReasoningExtension{ + {Content: []*ReasoningContent{{Text: "he"}}}, + {Content: []*ReasoningContent{{Text: "llo"}}}, + } + + result, err := ConcatReasoningExtensions(exts) + assert.NoError(t, err) + assert.Len(t, result.Content, 2) + assert.Nil(t, result.Content[0].Index) + assert.Equal(t, "he", result.Content[0].Text) + assert.Nil(t, result.Content[1].Index) + assert.Equal(t, "llo", result.Content[1].Text) + }) + + t.Run("mixed indexed and unindexed - error", func(t *testing.T) { + exts := []*ReasoningExtension{ + {Content: []*ReasoningContent{{Text: "indexed", Index: generic.PtrOf(0)}}}, + {Content: []*ReasoningContent{{Text: "no meta"}}}, + } + + _, err := ConcatReasoningExtensions(exts) + assert.Error(t, err) + }) +} diff --git a/schema/select.go b/schema/select.go new file mode 100644 index 0000000..ed22dfc --- /dev/null +++ b/schema/select.go @@ -0,0 +1,73 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +const maxSelectNum = 5 + +func receiveN[T any](chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) { + return []func(chosenList []int, ss []*stream[T]) (index int, item *streamItem[T], ok bool){ + nil, + func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) { + item, ok := <-ss[chosenList[0]].items + return chosenList[0], &item, ok + }, + func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) { + select { + case item, ok := <-ss[chosenList[0]].items: + return chosenList[0], &item, ok + case item, ok := <-ss[chosenList[1]].items: + return chosenList[1], &item, ok + } + }, + func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) { + select { + case item, ok := <-ss[chosenList[0]].items: + return chosenList[0], &item, ok + case item, ok := <-ss[chosenList[1]].items: + return chosenList[1], &item, ok + case item, ok := <-ss[chosenList[2]].items: + return chosenList[2], &item, ok + } + }, + func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) { + select { + case item, ok := <-ss[chosenList[0]].items: + return chosenList[0], &item, ok + case item, ok := <-ss[chosenList[1]].items: + return chosenList[1], &item, ok + case item, ok := <-ss[chosenList[2]].items: + return chosenList[2], &item, ok + case item, ok := <-ss[chosenList[3]].items: + return chosenList[3], &item, ok + } + }, + func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) { + select { + case item, ok := <-ss[chosenList[0]].items: + return chosenList[0], &item, ok + case item, ok := <-ss[chosenList[1]].items: + return chosenList[1], &item, ok + case item, ok := <-ss[chosenList[2]].items: + return chosenList[2], &item, ok + case item, ok := <-ss[chosenList[3]].items: + return chosenList[3], &item, ok + case item, ok := <-ss[chosenList[4]].items: + return chosenList[4], &item, ok + } + }, + }[len(chosenList)](chosenList, ss) +} diff --git a/schema/serialization.go b/schema/serialization.go new file mode 100644 index 0000000..169bf9e --- /dev/null +++ b/schema/serialization.go @@ -0,0 +1,148 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "encoding/gob" + "reflect" + + "github.com/cloudwego/eino/internal/generic" + "github.com/cloudwego/eino/internal/serialization" +) + +func init() { + RegisterName[*Message]("_eino_message") + RegisterName[[]*Message]("_eino_message_slice") + RegisterName[*AgenticMessage]("_eino_agentic_message") + RegisterName[[]*AgenticMessage]("_eino_agentic_message_slice") + RegisterName[Document]("_eino_document") + RegisterName[RoleType]("_eino_role_type") + RegisterName[ToolCall]("_eino_tool_call") + RegisterName[FunctionCall]("_eino_function_call") + RegisterName[ResponseMeta]("_eino_response_meta") + RegisterName[TokenUsage]("_eino_token_usage") + RegisterName[LogProbs]("_eino_log_probs") + RegisterName[ChatMessagePart]("_eino_chat_message_part") + RegisterName[ChatMessagePartType]("_eino_chat_message_type") + RegisterName[ChatMessageImageURL]("_eino_chat_message_image_url") + RegisterName[ChatMessageAudioURL]("_eino_chat_message_audio_url") + RegisterName[ChatMessageVideoURL]("_eino_chat_message_video_url") + RegisterName[ChatMessageFileURL]("_eino_chat_message_file_url") + RegisterName[MessageInputPart]("_eino_message_input_part") + RegisterName[MessageInputImage]("_eino_message_input_image") + RegisterName[MessageInputAudio]("_eino_message_input_audio") + RegisterName[MessageInputVideo]("_eino_message_input_video") + RegisterName[MessageInputFile]("_eino_message_input_file") + RegisterName[MessageOutputPart]("_eino_message_output_part") + RegisterName[MessageOutputImage]("_eino_message_output_image") + RegisterName[MessageOutputAudio]("_eino_message_output_audio") + RegisterName[MessageOutputVideo]("_eino_message_output_video") + RegisterName[MessagePartCommon]("_eino_message_part_common") + RegisterName[ImageURLDetail]("_eino_image_url_detail") + RegisterName[PromptTokenDetails]("_eino_prompt_token_details") +} + +// RegisterName registers a type with a specific name for serialization. This is +// required for any type you intend to persist in a graph or ADK checkpoint. +// Use this function to maintain backward compatibility by mapping a type to a +// previously used name. For new types, `Register` is preferred. +// +// It is recommended to call this in an `init()` function in the file where the +// type is declared. +// +// What to Register: +// - Top-level types used as state (e.g., structs). +// - Concrete types that are assigned to interface fields. +// +// What NOT to Register: +// - Struct fields with concrete types (e.g., `string`, `int`, other structs). +// These are inferred via reflection. +// +// Serialization Rules: +// +// The serialization behavior is based on Go's standard `encoding/gob` package. +// See https://pkg.go.dev/encoding/gob for detailed rules. +// - Only exported struct fields are serialized. +// - Functions and channels are not supported and will be ignored. +// +// This function panics if registration fails. +func RegisterName[T any](name string) { + gob.RegisterName(name, generic.NewInstance[T]()) + + err := serialization.GenericRegister[T](name) + if err != nil { + panic(err) + } +} + +func getTypeName(rt reflect.Type) string { + name := rt.String() + + // But for named types (or pointers to them), qualify with import path. + // Dereference one pointer looking for a named type. + star := "" + if rt.Name() == "" { + if pt := rt; pt.Kind() == reflect.Pointer { + star = "*" + rt = pt.Elem() + } + } + if rt.Name() != "" { + if rt.PkgPath() == "" { + name = star + rt.Name() + } else { + name = star + rt.PkgPath() + "." + rt.Name() + } + } + return name +} + +// Register registers a type for serialization. This is required for any type +// you intend to persist in a graph or ADK checkpoint. It automatically determines +// the type name and is the recommended method for registering new types. +// +// It is recommended to call this in an `init()` function in the file where the +// type is declared. +// +// What to Register: +// - Top-level types used as state (e.g., structs). +// - Concrete types that are assigned to interface fields. +// +// What NOT to Register: +// - Struct fields with concrete types (e.g., `string`, `int`, other structs). +// These are inferred via reflection. +// +// Serialization Rules: +// +// The serialization behavior is based on Go's standard `encoding/gob` package. +// See https://pkg.go.dev/encoding/gob for detailed rules. +// - Only exported struct fields are serialized. +// - Functions and channels are not supported and will be ignored. +// +// This function panics if registration fails. +func Register[T any]() { + value := generic.NewInstance[T]() + + gob.Register(value) + + name := getTypeName(reflect.TypeOf(value)) + + err := serialization.GenericRegister[T](name) + if err != nil { + panic(err) + } +} diff --git a/schema/serialization_test.go b/schema/serialization_test.go new file mode 100644 index 0000000..d17cc40 --- /dev/null +++ b/schema/serialization_test.go @@ -0,0 +1,191 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "bytes" + "encoding/gob" + "fmt" + "reflect" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/internal/serialization" +) + +type testStruct struct{} + +func TestGetTypeName(t *testing.T) { + type localNamedType struct{} + + testCases := []struct { + name string + input reflect.Type + expected string + }{ + { + name: "named type from current package", + input: reflect.TypeOf(testStruct{}), + expected: "github.com/cloudwego/eino/schema.testStruct", + }, + { + name: "pointer to named type from current package", + input: reflect.TypeOf(&testStruct{}), + expected: "*github.com/cloudwego/eino/schema.testStruct", + }, + { + name: "unnamed map type", + input: reflect.TypeOf(map[string]int{}), + expected: "map[string]int", + }, + { + name: "pointer to unnamed map type", + input: reflect.TypeOf(new(map[string]int)), + expected: "*map[string]int", + }, + { + name: "built-in type", + input: reflect.TypeOf(0), + expected: "int", + }, + { + name: "pointer to built-in type", + input: reflect.TypeOf(new(int)), + expected: "*int", + }, + { + name: "named type from standard library", + input: reflect.TypeOf(bytes.Buffer{}), + expected: "bytes.Buffer", + }, + { + name: "pointer to named type from standard library", + input: reflect.TypeOf(&bytes.Buffer{}), + expected: "*bytes.Buffer", + }, + { + name: "local named type", + input: reflect.TypeOf(localNamedType{}), + expected: "github.com/cloudwego/eino/schema.localNamedType", + }, + { + name: "pointer to local named type", + input: reflect.TypeOf(&localNamedType{}), + expected: "*github.com/cloudwego/eino/schema.localNamedType", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := getTypeName(tc.input) + if actual != tc.expected { + t.Errorf("getTypeName() got %q, want %q", actual, tc.expected) + } + }) + } +} + +func TestRegister(t *testing.T) { + type testStruct1 struct { + A any + B any + C any + D any + E any + F any + } + + type testStruct2 struct{} + + Register[*testStruct1]() + Register[*testStruct2]() + Register[[]Message]() + Register[[]*testStruct2]() + Register[[]testStruct2]() + + t1 := testStruct1{A: []*Message{{}}, B: []Message{{}}, C: []*testStruct2{{}}, D: []testStruct2{{}}, + E: &testStruct1{}, F: []int{1}} + + in := &serialization.InternalSerializer{} + mar, err := in.Marshal(t1) + if err != nil { + panic(err) + } + var t2 testStruct1 + err = in.Unmarshal(mar, &t2) + if err != nil { + panic(err) + } + assert.Equal(t, t1, t2) + + buf := new(bytes.Buffer) + err = gob.NewEncoder(buf).Encode(t1) + if err != nil { + panic(err) + } + err = gob.NewDecoder(buf).Decode(&t2) + if err != nil { + panic(err) + } + assert.Equal(t, t1, t2) + + f := func() (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + + Register[[]int]() + Register[map[string]any]() + Register[[]*testStruct1]() + Register[[]testStruct1]() + + return nil + } + + err = f() + assert.NoError(t, err) +} + +// TestRegisterStructWithUUIDField reproduces issue #607 +// uuid.UUID is a [16]byte array. Prior to the fix, calling schema.RegisterName on +// a struct with a uuid.UUID field would panic during deserialization. +func TestRegisterStructWithUUIDField(t *testing.T) { + type Item struct { + ID uuid.UUID + } + + RegisterName[Item]("test_item") + + original := Item{ + ID: uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), + } + + s := &serialization.InternalSerializer{} + + data, err := s.Marshal(original) + assert.NoError(t, err) + + var result Item + err = s.Unmarshal(data, &result) + assert.NoError(t, err) + + assert.Equal(t, original.ID, result.ID) +} diff --git a/schema/stream.go b/schema/stream.go new file mode 100644 index 0000000..5625efe --- /dev/null +++ b/schema/stream.go @@ -0,0 +1,1024 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "errors" + "fmt" + "io" + "reflect" + "runtime" + "runtime/debug" + "sync" + "sync/atomic" + + "github.com/cloudwego/eino/internal/safe" +) + +// ErrNoValue is a sentinel returned from the convert function passed to +// [StreamReaderWithConvert] to skip a stream element — the element is dropped +// and the next one is read without surfacing an error to the caller. +// +// Use it to filter out empty or irrelevant chunks: +// +// outStream = schema.StreamReaderWithConvert(s, +// func(src string) (string, error) { +// if len(src) == 0 { +// return "", schema.ErrNoValue // skip empty chunks +// } +// return src, nil +// }) +// +// DO NOT use ErrNoValue in any other context. +var ErrNoValue = errors.New("no value") + +// ErrRecvAfterClosed indicates that StreamReader.Recv was unexpectedly called after StreamReader.Close. +// This error should not occur during normal use of StreamReader.Recv. If it does, please check your application code. +var ErrRecvAfterClosed = errors.New("recv after stream closed") + +// SourceEOF represents an EOF error from a specific source stream. +// It is only returned by the method Recv() of StreamReader created +// with MergeNamedStreamReaders when one of its source streams reaches EOF. +type SourceEOF struct { + sourceName string +} + +func (e *SourceEOF) Error() string { + return fmt.Sprintf("EOF from source stream: %s", e.sourceName) +} + +// GetSourceName extracts the source stream name from a SourceEOF error. +// It returns the source name and a boolean indicating whether the error was a SourceEOF. +// If the error is not a SourceEOF, it returns an empty string and false. +func GetSourceName(err error) (string, bool) { + var sErr *SourceEOF + if errors.As(err, &sErr) { + return sErr.sourceName, true + } + + return "", false +} + +// Pipe creates a new stream with the given capacity that represented with StreamWriter and StreamReader. +// The capacity is the maximum number of items that can be buffered in the stream. +// e.g. +// +// sr, sw := schema.Pipe[string](3) +// go func() { // send data +// defer sw.Close() +// for i := 0; i < 10; i++ { +// sw.Send(i, nil) +// } +// } +// +// defer sr.Close() +// for { +// chunk, err := sr.Recv() +// if errors.Is(err, io.EOF) { +// break +// } +// if err != nil { +// panic(err) +// } +// fmt.Println(chunk) +// } +func Pipe[T any](cap int) (*StreamReader[T], *StreamWriter[T]) { + stm := newStream[T](cap) + return stm.asReader(), &StreamWriter[T]{stm: stm} +} + +// StreamWriter the sender of a stream. +// created by Pipe function. +// eg. +// +// sr, sw := schema.Pipe[string](3) +// go func() { // send data +// defer sw.Close() +// for i := 0; i < 10; i++ { +// sw.Send(i, nil) +// } +// } +type StreamWriter[T any] struct { + stm *stream[T] +} + +// Send sends a value to the stream. +// e.g. +// +// closed := sw.Send(i, nil) +// if closed { +// // the stream is closed +// } +func (sw *StreamWriter[T]) Send(chunk T, err error) (closed bool) { + return sw.stm.send(chunk, err) +} + +// Close notify the receiver that the stream sender has finished. +// The stream receiver will get an error of io.EOF from StreamReader.Recv(). +// Notice: always remember to call Close() after sending all data. +// eg. +// +// defer sw.Close() +// for i := 0; i < 10; i++ { +// sw.Send(i, nil) +// } +func (sw *StreamWriter[T]) Close() { + sw.stm.closeSend() +} + +// StreamReader is the consumer side of an Eino stream. +// +// A StreamReader is read-once: only one goroutine should call Recv, and the +// reader must be closed exactly once (whether the loop finishes normally or +// exits early via break or return). +// +// Typical usage: +// +// defer sr.Close() // always close, even after io.EOF +// for { +// chunk, err := sr.Recv() +// if errors.Is(err, io.EOF) { +// break +// } +// if err != nil { +// return err +// } +// process(chunk) +// } +// +// To fan-out a single stream to N independent consumers, call [StreamReader.Copy] +// before any Recv; the original reader becomes unusable after the call. +// +// StreamReaders are created by [Pipe], [StreamReaderFromArray], +// [MergeStreamReaders], [MergeNamedStreamReaders], and [StreamReaderWithConvert]. +type StreamReader[T any] struct { + typ readerType + + st *stream[T] + + ar *arrayReader[T] + + msr *multiStreamReader[T] + + srw *streamReaderWithConvert[T] + + csr *childStreamReader[T] +} + +// Recv receives a value from the stream. +// eg. +// +// for { +// chunk, err := sr.Recv() +// if errors.Is(err, io.EOF) { +// break +// } +// if err != nil { +// panic(err) +// } +// fmt.Println(chunk) +// } +func (sr *StreamReader[T]) Recv() (T, error) { + switch sr.typ { + case readerTypeStream: + return sr.st.recv() + case readerTypeArray: + return sr.ar.recv() + case readerTypeMultiStream: + return sr.msr.recv() + case readerTypeWithConvert: + return sr.srw.recv() + case readerTypeChild: + return sr.csr.recv() + default: + panic("impossible") + } +} + +// Close safely closes the StreamReader. +// It should be called only once, as multiple calls may not work as expected. +// Notice: always remember to call Close() after using Recv(). +// e.g. +// +// defer sr.Close() +// +// for { +// chunk, err := sr.Recv() +// if errors.Is(err, io.EOF) { +// break +// } +// if err != nil { +// panic(err) +// } +// fmt.Println(chunk) +// } +func (sr *StreamReader[T]) Close() { + switch sr.typ { + case readerTypeStream: + sr.st.closeRecv() + case readerTypeArray: + + case readerTypeMultiStream: + sr.msr.close() + case readerTypeWithConvert: + sr.srw.close() + case readerTypeChild: + sr.csr.close() + default: + panic("impossible") + } +} + +// Copy creates n independent StreamReaders that each receive every element of +// the original stream. The original StreamReader becomes unusable after Copy. +// +// Use Copy when two or more pipeline branches need the same stream — +// for example, when a stream must be fed to both a callback handler and the +// next node in a graph: +// +// copies := sr.Copy(2) +// sr1, sr2 := copies[0], copies[1] +// defer sr1.Close() +// defer sr2.Close() +// +// // sr1 and sr2 independently read the same elements +// +// n must be at least 1. If n < 2, the original reader is returned unchanged. +func (sr *StreamReader[T]) Copy(n int) []*StreamReader[T] { + if n < 2 { + return []*StreamReader[T]{sr} + } + + if sr.typ == readerTypeArray { + ret := make([]*StreamReader[T], n) + for i, ar := range sr.ar.copy(n) { + ret[i] = &StreamReader[T]{typ: readerTypeArray, ar: ar} + } + return ret + } + + return copyStreamReaders[T](sr, n) +} + +// SetAutomaticClose sets the StreamReader to automatically close when it's no longer reachable and ready to be GCed. +// NOT concurrency safe. +func (sr *StreamReader[T]) SetAutomaticClose() { + switch sr.typ { + case readerTypeStream: + if !sr.st.automaticClose { + sr.st.automaticClose = true + var flag uint32 + sr.st.closedFlag = &flag + runtime.SetFinalizer(sr, func(s *StreamReader[T]) { + s.Close() + }) + } + case readerTypeMultiStream: + for _, s := range sr.msr.nonClosedStreams() { + if !s.automaticClose { + s.automaticClose = true + var flag uint32 + s.closedFlag = &flag + runtime.SetFinalizer(s, func(st *stream[T]) { + st.closeRecv() + }) + } + } + case readerTypeChild: + parent := sr.csr.parent.sr + parent.SetAutomaticClose() + case readerTypeWithConvert: + sr.srw.sr.SetAutomaticClose() + case readerTypeArray: + // no need to clean up + default: + } +} + +func (sr *StreamReader[T]) recvAny() (any, error) { + return sr.Recv() +} + +func (sr *StreamReader[T]) copyAny(n int) []iStreamReader { + ret := make([]iStreamReader, n) + + srs := sr.Copy(n) + + for i := 0; i < n; i++ { + ret[i] = srs[i] + } + + return ret +} + +func arrToStream[T any](arr []T) *stream[T] { + s := newStream[T](len(arr)) + for i := range arr { + s.send(arr[i], nil) + } + s.closeSend() + + return s +} + +func (sr *StreamReader[T]) toStream() *stream[T] { + switch sr.typ { + case readerTypeStream: + return sr.st + case readerTypeArray: + return sr.ar.toStream() + case readerTypeMultiStream: + return sr.msr.toStream() + case readerTypeWithConvert: + return sr.srw.toStream() + case readerTypeChild: + return sr.csr.toStream() + default: + panic("impossible") + } +} + +type readerType int + +const ( + readerTypeStream readerType = iota + readerTypeArray + readerTypeMultiStream + readerTypeWithConvert + readerTypeChild +) + +type iStreamReader interface { + recvAny() (any, error) + copyAny(int) []iStreamReader + Close() + SetAutomaticClose() +} + +// stream is a channel-based stream with 1 sender and 1 receiver. +// The sender calls closeSend() to notify the receiver that the stream sender has finished. +// The receiver calls closeRecv() to notify the sender that the receiver stop receiving. +type stream[T any] struct { + items chan streamItem[T] + + closed chan struct{} + + automaticClose bool + closedFlag *uint32 // 0 = not closed, 1 = closed, only used when automaticClose is set +} + +type streamItem[T any] struct { + chunk T + err error +} + +func newStream[T any](cap int) *stream[T] { + return &stream[T]{ + items: make(chan streamItem[T], cap), + closed: make(chan struct{}), + } +} + +func (s *stream[T]) asReader() *StreamReader[T] { + return &StreamReader[T]{typ: readerTypeStream, st: s} +} + +func (s *stream[T]) recv() (chunk T, err error) { + item, ok := <-s.items + + if !ok { + item.err = io.EOF + } + + return item.chunk, item.err +} + +func (s *stream[T]) send(chunk T, err error) (closed bool) { + // if the stream is closed, return immediately + select { + case <-s.closed: + return true + default: + } + + item := streamItem[T]{chunk, err} + + select { + case <-s.closed: + return true + case s.items <- item: + return false + } +} + +func (s *stream[T]) closeSend() { + close(s.items) +} + +func (s *stream[T]) closeRecv() { + if s.automaticClose { + if atomic.CompareAndSwapUint32(s.closedFlag, 0, 1) { + close(s.closed) + } + return + } + + close(s.closed) +} + +// StreamReaderFromArray creates a StreamReader from a given slice of elements. +// It takes an array of type T and returns a pointer to a StreamReader[T]. +// This allows for streaming the elements of the array in a controlled manner. +// eg. +// +// sr := schema.StreamReaderFromArray([]int{1, 2, 3}) +// defer sr.Close() +// +// for { +// chunk, err := sr.Recv() +// if errors.Is(err, io.EOF) { +// break +// } +// if err != nil { +// panic(err) +// } +// fmt.Println(chunk) +// } +func StreamReaderFromArray[T any](arr []T) *StreamReader[T] { + return &StreamReader[T]{ar: &arrayReader[T]{arr: arr}, typ: readerTypeArray} +} + +type arrayReader[T any] struct { + arr []T + index int +} + +func (ar *arrayReader[T]) recv() (T, error) { + if ar.index < len(ar.arr) { + ret := ar.arr[ar.index] + ar.index++ + + return ret, nil + } + + var t T + return t, io.EOF +} + +func (ar *arrayReader[T]) copy(n int) []*arrayReader[T] { + ret := make([]*arrayReader[T], n) + + for i := 0; i < n; i++ { + ret[i] = &arrayReader[T]{ + arr: ar.arr, + index: ar.index, + } + } + + return ret +} + +func (ar *arrayReader[T]) toStream() *stream[T] { + return arrToStream(ar.arr[ar.index:]) +} + +type multiArrayReader[T any] struct { + ars []*arrayReader[T] + index int +} + +type multiStreamReader[T any] struct { + sts []*stream[T] + + itemsCases []reflect.SelectCase + + nonClosed []int + + sourceReaderNames []string +} + +func newMultiStreamReader[T any](sts []*stream[T]) *multiStreamReader[T] { + var itemsCases []reflect.SelectCase + if len(sts) > maxSelectNum { + itemsCases = make([]reflect.SelectCase, len(sts)) + for i, st := range sts { + itemsCases[i] = reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(st.items), + } + } + } + + nonClosed := make([]int, len(sts)) + for i := range sts { + nonClosed[i] = i + } + + return &multiStreamReader[T]{ + sts: sts, + itemsCases: itemsCases, + nonClosed: nonClosed, + } +} + +func (msr *multiStreamReader[T]) recv() (T, error) { + for len(msr.nonClosed) > 0 { + var chosen int + var ok bool + if len(msr.nonClosed) > maxSelectNum { + var recv reflect.Value + chosen, recv, ok = reflect.Select(msr.itemsCases) + if ok { + item := recv.Interface().(streamItem[T]) + return item.chunk, item.err + } + msr.itemsCases[chosen].Chan = reflect.Value{} + } else { + var item *streamItem[T] + chosen, item, ok = receiveN(msr.nonClosed, msr.sts) + if ok { + return item.chunk, item.err + } + } + + // delete the closed stream + for i := range msr.nonClosed { + if msr.nonClosed[i] == chosen { + msr.nonClosed = append(msr.nonClosed[:i], msr.nonClosed[i+1:]...) + break + } + } + + if len(msr.sourceReaderNames) > 0 { + var t T + return t, &SourceEOF{msr.sourceReaderNames[chosen]} + } + } + + var t T + return t, io.EOF +} + +func (msr *multiStreamReader[T]) nonClosedStreams() []*stream[T] { + ret := make([]*stream[T], len(msr.nonClosed)) + + for i, idx := range msr.nonClosed { + ret[i] = msr.sts[idx] + } + + return ret +} + +func (msr *multiStreamReader[T]) close() { + for _, s := range msr.sts { + s.closeRecv() + } +} + +func (msr *multiStreamReader[T]) toStream() *stream[T] { + return toStream[T, *multiStreamReader[T]](msr) +} + +type streamReaderWithConvert[T any] struct { + sr iStreamReader + + convert func(any) (T, error) + + errWrapper func(error) error + onEOF func() (T, error) + eofDone bool +} + +func newStreamReaderWithConvert[T any](origin iStreamReader, convert func(any) (T, error), opts ...ConvertOption) *StreamReader[T] { + opt := &convertOptions{} + for _, o := range opts { + o(opt) + } + + srw := &streamReaderWithConvert[T]{ + sr: origin, + convert: convert, + errWrapper: opt.ErrWrapper, + } + + if opt.OnEOF != nil { + typedOnEOF := opt.OnEOF + srw.onEOF = func() (T, error) { + v, err := typedOnEOF() + if err != nil { + var t T + return t, err + } + if v == nil { + var t T + return t, nil + } + return v.(T), nil + } + } + + return &StreamReader[T]{ + typ: readerTypeWithConvert, + srw: srw, + } +} + +type convertOptions struct { + ErrWrapper func(error) error + OnEOF func() (any, error) +} + +type ConvertOption func(*convertOptions) + +// WithErrWrapper wraps non-EOF errors from the underlying stream reader during +// conversion by StreamReaderWithConvert. Errors returned by the convert function +// itself are not wrapped. +// If the wrapper returns nil, the errored chunk is skipped and the next chunk +// is read. If the wrapper returns a non-nil error, that error is surfaced to +// the caller. +func WithErrWrapper(wrapper func(error) error) ConvertOption { + return func(o *convertOptions) { + o.ErrWrapper = wrapper + } +} + +// WithOnEOF registers a callback that fires once when the stream reaches EOF. +// The callback can inject an error or a value before the final io.EOF is returned. +// If the callback returns (nil, io.EOF), the stream ends normally. +// If it returns a non-EOF error, that error is delivered first, then subsequent Recv returns io.EOF. +// If it returns a non-nil value with nil error, that value is delivered first, then io.EOF. +func WithOnEOF(fn func() (any, error)) ConvertOption { + return func(o *convertOptions) { + o.OnEOF = fn + } +} + +// StreamReaderWithConvert returns a new StreamReader[D] that wraps sr and +// applies convert to every element. The original reader sr must not be used +// after calling this function. +// +// Filtering: if convert returns [ErrNoValue], the element is silently dropped +// and the next element is read. This lets you strip empty or irrelevant chunks +// without surfacing an error to the caller. +// +// Error wrapping: use [WithErrWrapper] to wrap non-convert errors (e.g. those +// arriving from an upstream source) before they reach the caller. +// +// intReader := schema.StreamReaderFromArray([]int{0, 1, 2, 3}) +// strReader := schema.StreamReaderWithConvert(intReader, +// func(i int) (string, error) { +// if i == 0 { +// return "", schema.ErrNoValue // skip zero +// } +// return fmt.Sprintf("val_%d", i), nil +// }) +// defer strReader.Close() +// // Recv yields "val_1", "val_2", "val_3" +func StreamReaderWithConvert[T, D any](sr *StreamReader[T], convert func(T) (D, error), opts ...ConvertOption) *StreamReader[D] { + c := func(a any) (D, error) { + return convert(a.(T)) + } + + return newStreamReaderWithConvert(sr, c, opts...) +} + +func (srw *streamReaderWithConvert[T]) recv() (T, error) { + for { + out, err := srw.sr.recvAny() + + if err != nil { + var t T + if err == io.EOF { + if srw.onEOF != nil && !srw.eofDone { + srw.eofDone = true + val, onEOFErr := srw.onEOF() + if onEOFErr != io.EOF { + return val, onEOFErr + } + } + return t, io.EOF + } + if srw.errWrapper != nil { + err = srw.errWrapper(err) + if err != nil { + return t, err + } + + continue + } + + return t, err + } + + t, err := srw.convert(out) + if err == nil { + return t, nil + } + + if !errors.Is(err, ErrNoValue) { + return t, err + } + } +} + +func (srw *streamReaderWithConvert[T]) close() { + srw.sr.Close() +} + +type reader[T any] interface { + recv() (T, error) + close() +} + +func toStream[T any, Reader reader[T]](r Reader) *stream[T] { + ret := newStream[T](5) + + go func() { + defer func() { + panicErr := recover() + if panicErr != nil { + e := safe.NewPanicErr(panicErr, debug.Stack()) + + var chunk T + _ = ret.send(chunk, e) + } + + ret.closeSend() + r.close() + }() + + for { + out, err := r.recv() + if err == io.EOF { + break + } + + closed := ret.send(out, err) + if closed { + break + } + } + }() + + return ret +} + +func (srw *streamReaderWithConvert[T]) toStream() *stream[T] { + return toStream[T, *streamReaderWithConvert[T]](srw) +} + +type cpStreamElement[T any] struct { + once sync.Once + next *cpStreamElement[T] + item streamItem[T] +} + +// copyStreamReaders creates multiple independent StreamReaders from a single StreamReader. +// Each child StreamReader can read from the original stream independently. +func copyStreamReaders[T any](sr *StreamReader[T], n int) []*StreamReader[T] { + cpsr := &parentStreamReader[T]{ + sr: sr, + subStreamList: make([]*cpStreamElement[T], n), + closedNum: 0, + } + + // Initialize subStreamList with an empty element, which acts like a tail node. + // A nil element (used for dereference) represents that the child has been closed. + // It is challenging to link the previous and current elements when the length of the original channel is unknown. + // Additionally, using a previous pointer complicates dereferencing elements, possibly requiring reference counting. + elem := &cpStreamElement[T]{} + + for i := range cpsr.subStreamList { + cpsr.subStreamList[i] = elem + } + + ret := make([]*StreamReader[T], n) + for i := range ret { + ret[i] = &StreamReader[T]{ + csr: &childStreamReader[T]{ + parent: cpsr, + index: i, + }, + typ: readerTypeChild, + } + } + + return ret +} + +type parentStreamReader[T any] struct { + // sr is the original StreamReader. + sr *StreamReader[T] + + // subStreamList maps each child's index to its latest read chunk. + // Each value comes from a hidden linked list of cpStreamElement. + subStreamList []*cpStreamElement[T] + + // closedNum is the count of closed children. + closedNum uint32 +} + +// peek is not safe for concurrent use with the same idx but is safe for different idx. +// Ensure that each child StreamReader uses a for-loop in a single goroutine. +func (p *parentStreamReader[T]) peek(idx int) (t T, err error) { + elem := p.subStreamList[idx] + if elem == nil { + // Unexpected call to receive after the child has been closed. + return t, ErrRecvAfterClosed + } + + // The sync.Once here is used to: + // 1. Write the content of this cpStreamElement. + // 2. Initialize the 'next' field of this cpStreamElement with an empty cpStreamElement, + // similar to the initialization in copyStreamReaders. + elem.once.Do(func() { + t, err = p.sr.Recv() + elem.item = streamItem[T]{chunk: t, err: err} + if err != io.EOF { + elem.next = &cpStreamElement[T]{} + p.subStreamList[idx] = elem.next + } + }) + + // The element has been set and will not be modified again. + // Therefore, children can read this element's content and 'next' pointer concurrently. + t = elem.item.chunk + err = elem.item.err + if err != io.EOF { + p.subStreamList[idx] = elem.next + } + + return t, err +} + +func (p *parentStreamReader[T]) close(idx int) { + if p.subStreamList[idx] == nil { + return // avoid close multiple times + } + + p.subStreamList[idx] = nil + + curClosedNum := atomic.AddUint32(&p.closedNum, 1) + + allClosed := int(curClosedNum) == len(p.subStreamList) + if allClosed { + p.sr.Close() + } +} + +type childStreamReader[T any] struct { + parent *parentStreamReader[T] + index int +} + +func (csr *childStreamReader[T]) recv() (T, error) { + return csr.parent.peek(csr.index) +} + +func (csr *childStreamReader[T]) toStream() *stream[T] { + return toStream[T, *childStreamReader[T]](csr) +} + +func (csr *childStreamReader[T]) close() { + csr.parent.close(csr.index) +} + +// MergeStreamReaders fans in multiple StreamReaders into a single StreamReader. +// Elements from all source streams are interleaved in arrival order (non-deterministic). +// The merged reader reaches EOF only after every source stream has been exhausted. +// +// Callers must still close the merged reader; it propagates the close signal +// to all underlying sources. +// +// Use [MergeNamedStreamReaders] instead when you need to know which source +// stream ended first (it emits a [SourceEOF] per-source EOF rather than +// silently discarding them). +// +// Returns nil if srs is empty. +func MergeStreamReaders[T any](srs []*StreamReader[T]) *StreamReader[T] { + if len(srs) < 1 { + return nil + } + + if len(srs) < 2 { + return srs[0] + } + + var arr []T + var ss []*stream[T] + + for _, sr := range srs { + switch sr.typ { + case readerTypeStream: + ss = append(ss, sr.st) + case readerTypeArray: + arr = append(arr, sr.ar.arr[sr.ar.index:]...) + case readerTypeMultiStream: + ss = append(ss, sr.msr.nonClosedStreams()...) + case readerTypeWithConvert: + ss = append(ss, sr.srw.toStream()) + case readerTypeChild: + ss = append(ss, sr.csr.toStream()) + default: + panic("impossible") + } + } + + if len(ss) == 0 { + return &StreamReader[T]{ + typ: readerTypeArray, + ar: &arrayReader[T]{ + arr: arr, + index: 0, + }, + } + } + + if len(arr) != 0 { + s := arrToStream(arr) + ss = append(ss, s) + } + + return &StreamReader[T]{ + typ: readerTypeMultiStream, + msr: newMultiStreamReader(ss), + } +} + +// MergeNamedStreamReaders merges multiple named StreamReaders into one. +// Unlike [MergeStreamReaders], when a source stream reaches EOF the merged +// reader emits a [SourceEOF] error (instead of silently continuing) so you can +// detect exactly which source finished. Use [GetSourceName] to retrieve the +// name from a SourceEOF error. The merged reader itself signals io.EOF only +// after all named sources are exhausted. +// +// This is useful when downstream logic must react differently to each source +// completing — for example, draining one agent's output before proceeding: +// +// namedStreams := map[string]*schema.StreamReader[string]{ +// "agent_a": srA, +// "agent_b": srB, +// } +// merged := schema.MergeNamedStreamReaders(namedStreams) +// defer merged.Close() +// for { +// chunk, err := merged.Recv() +// if errors.Is(err, io.EOF) { break } +// if name, ok := schema.GetSourceName(err); ok { +// fmt.Printf("%s finished\n", name) +// continue +// } +// if err != nil { return err } +// process(chunk) +// } +// +// Returns nil if srs is empty. +func MergeNamedStreamReaders[T any](srs map[string]*StreamReader[T]) *StreamReader[T] { + if len(srs) < 1 { + return nil + } + + ss := make([]*StreamReader[T], len(srs)) + names := make([]string, len(srs)) + + i := 0 + for name, sr := range srs { + ss[i] = sr + names[i] = name + i++ + } + + return InternalMergeNamedStreamReaders(ss, names) +} + +// InternalMergeNamedStreamReaders merges multiple readers with their names +// into a single multi-stream reader. +func InternalMergeNamedStreamReaders[T any](srs []*StreamReader[T], names []string) *StreamReader[T] { + ss := make([]*stream[T], len(srs)) + + for i, sr := range srs { + ss[i] = sr.toStream() + } + + msr := newMultiStreamReader(ss) + msr.sourceReaderNames = names + + return &StreamReader[T]{ + typ: readerTypeMultiStream, + msr: msr, + } +} diff --git a/schema/stream_copy_external_test.go b/schema/stream_copy_external_test.go new file mode 100644 index 0000000..fc9cc47 --- /dev/null +++ b/schema/stream_copy_external_test.go @@ -0,0 +1,155 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "fmt" + "io" + "runtime" + "sort" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestStream1(t *testing.T) { + runtime.GOMAXPROCS(1) + + sr, sw := Pipe[int](0) + go func() { + for i := 0; i < 100; i++ { + sw.Send(i, nil) + time.Sleep(3 * time.Millisecond) + } + sw.Close() + }() + copied := sr.Copy(2) + var ( + now = time.Now().UnixMilli() + ts = []int64{now, now} + tsOld = []int64{now, now} + ) + var count int32 + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + i := 0 + s := copied[0] + for { + n, e := s.Recv() + if e != nil { + if e == io.EOF { + break + } + } + tsOld[0] = ts[0] + ts[0] = time.Now().UnixMilli() + interval := ts[0] - tsOld[0] + if interval >= 6 { + atomic.AddInt32(&count, 1) + } + assert.Equal(t, i, n) + i++ + } + wg.Done() + }() + go func() { + i := 0 + s := copied[1] + for { + n, e := s.Recv() + if e != nil { + if e == io.EOF { + break + } + } + tsOld[1] = ts[1] + ts[1] = time.Now().UnixMilli() + interval := ts[1] - tsOld[1] + if interval >= 6 { + atomic.AddInt32(&count, 1) + } + assert.Equal(t, i, n) + i++ + } + wg.Done() + }() + wg.Wait() + t.Logf("count= %d", count) +} + +type info struct { + idx int + ts int64 + after int64 + content string +} + +func TestCopyDelay(t *testing.T) { + runtime.GOMAXPROCS(10) + n := 3 + //m := 100 + s := newStream[string](0) + scp := s.asReader().Copy(n) + go func() { + s.send("1", nil) + s.send("2", nil) + time.Sleep(time.Second) + s.send("3", nil) + s.closeSend() + }() + wg := sync.WaitGroup{} + wg.Add(n) + infoList := make([][]info, n) + for i := 0; i < n; i++ { + j := i + go func() { + defer func() { + scp[j].Close() + wg.Done() + }() + for { + lastTime := time.Now() + str, err := scp[j].Recv() + if err == io.EOF { + break + } + now := time.Now() + infoList[j] = append(infoList[j], info{ + idx: j, + ts: now.UnixMicro(), + after: now.Sub(lastTime).Milliseconds(), + content: str, + }) + } + }() + } + wg.Wait() + infos := make([]info, 0) + for _, infoL := range infoList { + infos = append(infos, infoL...) + } + sort.Slice(infos, func(i, j int) bool { + return infos[i].ts < infos[j].ts + }) + for _, info := range infos { + fmt.Printf("child[%d] ts[%d] after[%5dms] content[%s]\n", info.idx, info.ts, info.after, info.content) + } +} diff --git a/schema/stream_oneof_test.go b/schema/stream_oneof_test.go new file mode 100644 index 0000000..740836d --- /dev/null +++ b/schema/stream_oneof_test.go @@ -0,0 +1,324 @@ +/* + * Copyright 2026 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema_test + +import ( + "errors" + "io" + "testing" + "time" + + "github.com/cloudwego/eino/schema" +) + +func recvAll(t *testing.T, sr *schema.StreamReader[string]) ([]string, []error) { + t.Helper() + var vals []string + var errs []error + for { + v, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + errs = append(errs, err) + continue + } + vals = append(vals, v) + } + return vals, errs +} + +func makeStream(items []string, opts ...schema.ConvertOption) *schema.StreamReader[string] { + return schema.StreamReaderWithConvert( + schema.StreamReaderFromArray(items), + func(s string) (string, error) { return s, nil }, + opts..., + ) +} + +func TestWithOnEOF_PassThroughEOF(t *testing.T) { + items := []string{"a", "b", "c", "d"} + sr := makeStream(items, schema.WithOnEOF(func() (any, error) { + return nil, io.EOF + })) + defer sr.Close() + + vals, errs := recvAll(t, sr) + if len(errs) != 0 { + t.Fatalf("expected no errors, got %v", errs) + } + if len(vals) != 4 { + t.Fatalf("expected 4 values, got %d: %v", len(vals), vals) + } + for i, want := range items { + if vals[i] != want { + t.Errorf("vals[%d] = %q, want %q", i, vals[i], want) + } + } +} + +func TestWithOnEOF_InjectError(t *testing.T) { + items := []string{"a", "b", "c", "d"} + customErr := errors.New("validation failed") + sr := makeStream(items, schema.WithOnEOF(func() (any, error) { + return nil, customErr + })) + defer sr.Close() + + var vals []string + var gotCustomErr bool + for { + v, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + if errors.Is(err, customErr) { + gotCustomErr = true + continue + } + t.Fatalf("unexpected error: %v", err) + } + vals = append(vals, v) + } + + if len(vals) != 4 { + t.Fatalf("expected 4 values, got %d: %v", len(vals), vals) + } + if !gotCustomErr { + t.Fatalf("expected custom error from onEOF, did not receive it") + } +} + +func TestWithOnEOF_InjectValue(t *testing.T) { + items := []string{"a", "b", "c", "d"} + sr := makeStream(items, schema.WithOnEOF(func() (any, error) { + return "extra", nil + })) + defer sr.Close() + + var vals []string + for { + v, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + vals = append(vals, v) + } + + if len(vals) != 5 { + t.Fatalf("expected 5 values, got %d: %v", len(vals), vals) + } + if vals[4] != "extra" { + t.Errorf("vals[4] = %q, want %q", vals[4], "extra") + } +} + +func TestWithOnEOF_BlockingCallback(t *testing.T) { + sr, sw := schema.Pipe[string](0) + + unblock := make(chan struct{}) + converted := schema.StreamReaderWithConvert(sr, + func(s string) (string, error) { return s, nil }, + schema.WithOnEOF(func() (any, error) { + <-unblock + return "after-block", nil + }), + ) + defer converted.Close() + + go func() { + sw.Send("x", nil) + sw.Close() + }() + + v, err := converted.Recv() + if err != nil { + t.Fatalf("first Recv error: %v", err) + } + if v != "x" { + t.Fatalf("first Recv = %q, want %q", v, "x") + } + + done := make(chan struct{}) + var recvVal string + var recvErr error + go func() { + recvVal, recvErr = converted.Recv() + close(done) + }() + + select { + case <-done: + t.Fatal("Recv returned before unblock signal") + case <-time.After(50 * time.Millisecond): + } + + close(unblock) + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("Recv did not return after unblock signal") + } + + if recvErr != nil { + t.Fatalf("second Recv error: %v", recvErr) + } + if recvVal != "after-block" { + t.Errorf("second Recv = %q, want %q", recvVal, "after-block") + } + + v3, err3 := converted.Recv() + if !errors.Is(err3, io.EOF) { + t.Fatalf("third Recv: got (%q, %v), want EOF", v3, err3) + } +} + +func TestWithOnEOF_EmptyStream(t *testing.T) { + customErr := errors.New("empty stream error") + sr := makeStream(nil, schema.WithOnEOF(func() (any, error) { + return nil, customErr + })) + defer sr.Close() + + v, err := sr.Recv() + if !errors.Is(err, customErr) { + t.Fatalf("first Recv: got (%q, %v), want customErr", v, err) + } + + v2, err2 := sr.Recv() + if !errors.Is(err2, io.EOF) { + t.Fatalf("second Recv: got (%q, %v), want EOF", v2, err2) + } +} + +func TestWithOnEOF_WithErrWrapper_ErrorPath(t *testing.T) { + sr, sw := schema.Pipe[string](0) + + streamErr := errors.New("stream error") + onEOFCalled := false + + converted := schema.StreamReaderWithConvert(sr, + func(s string) (string, error) { return s, nil }, + schema.WithErrWrapper(func(err error) error { + return err + }), + schema.WithOnEOF(func() (any, error) { + onEOFCalled = true + return nil, errors.New("should not happen") + }), + ) + defer converted.Close() + + go func() { + sw.Send("a", nil) + sw.Send("", streamErr) + sw.Close() + }() + + v, err := converted.Recv() + if err != nil { + t.Fatalf("first Recv error: %v", err) + } + if v != "a" { + t.Fatalf("first Recv = %q, want %q", v, "a") + } + + _, err = converted.Recv() + if !errors.Is(err, streamErr) { + t.Fatalf("second Recv: got %v, want streamErr", err) + } + + if onEOFCalled { + t.Fatal("onEOF should not have been called when stream errored") + } +} + +func TestWithOnEOF_WithErrWrapper_EOFPath(t *testing.T) { + items := []string{"a", "b", "c"} + errWrapperCalled := false + + sr := schema.StreamReaderWithConvert( + schema.StreamReaderFromArray(items), + func(s string) (string, error) { return s, nil }, + schema.WithErrWrapper(func(err error) error { + errWrapperCalled = true + return err + }), + schema.WithOnEOF(func() (any, error) { + return "oneof-val", nil + }), + ) + defer sr.Close() + + var vals []string + for { + v, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + vals = append(vals, v) + } + + if len(vals) != 4 { + t.Fatalf("expected 4 values, got %d: %v", len(vals), vals) + } + if vals[3] != "oneof-val" { + t.Errorf("vals[3] = %q, want %q", vals[3], "oneof-val") + } + if errWrapperCalled { + t.Fatal("errWrapper should not have been called for clean stream") + } +} + +func TestWithOnEOF_MultipleRecvAfterEOF(t *testing.T) { + items := []string{"a"} + customErr := errors.New("oneof error") + + sr := makeStream(items, schema.WithOnEOF(func() (any, error) { + return nil, customErr + })) + defer sr.Close() + + v, err := sr.Recv() + if err != nil { + t.Fatalf("first Recv error: %v", err) + } + if v != "a" { + t.Fatalf("first Recv = %q, want %q", v, "a") + } + + _, err = sr.Recv() + if !errors.Is(err, customErr) { + t.Fatalf("second Recv: got %v, want customErr", err) + } + + for i := 0; i < 5; i++ { + _, err = sr.Recv() + if !errors.Is(err, io.EOF) { + t.Fatalf("Recv #%d after onEOF: got %v, want io.EOF", i+3, err) + } + } +} diff --git a/schema/stream_test.go b/schema/stream_test.go new file mode 100644 index 0000000..9282bf9 --- /dev/null +++ b/schema/stream_test.go @@ -0,0 +1,905 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "errors" + "fmt" + "io" + "math/rand" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestStream(t *testing.T) { + s := newStream[int](0) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + closed := s.send(i, nil) + if closed { + break + } + } + s.closeSend() + }() + + i := 0 + for { + i++ + if i > 5 { + s.closeRecv() + break + } + v, err := s.recv() + if err != nil { + assert.ErrorIs(t, err, io.EOF) + break + } + t.Log(v) + } + + wg.Wait() +} + +func TestStreamCopy(t *testing.T) { + s := newStream[string](10) + srs := s.asReader().Copy(2) + + s.send("a", nil) + s.send("b", nil) + s.send("c", nil) + s.closeSend() + + defer func() { + for _, sr := range srs { + sr.Close() + } + }() + + for { + v, err := srs[0].Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + t.Fatal(err) + } + + t.Log("copy 01 recv", v) + } + + for { + v, err := srs[1].Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + t.Fatal(err) + } + + t.Log("copy 02 recv", v) + } + + for { + v, err := s.recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + t.Fatal(err) + } + + t.Log("recv origin", v) + } + + t.Log("done") +} + +func TestNewStreamCopy(t *testing.T) { + t.Run("test one index recv channel blocked while other indexes could recv", func(t *testing.T) { + s := newStream[string](1) + scp := s.asReader().Copy(2) + + var t1, t2 time.Time + + go func() { + s.send("a", nil) + t1 = time.Now() + time.Sleep(time.Millisecond * 200) + s.send("a", nil) + s.closeSend() + }() + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + defer func() { + scp[0].Close() + wg.Done() + }() + + for { + str, err := scp[0].Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, str, "a") + } + }() + + go func() { + defer func() { + scp[1].Close() + wg.Done() + }() + + time.Sleep(time.Millisecond * 100) + for { + str, err := scp[1].Recv() + if err == io.EOF { + break + } + + if t2.IsZero() { + t2 = time.Now() + } + + assert.NoError(t, err) + assert.Equal(t, str, "a") + } + }() + + wg.Wait() + + assert.True(t, t2.Sub(t1) < time.Millisecond*200) + }) + + t.Run("test one index recv channel blocked and other index closed", func(t *testing.T) { + s := newStream[string](1) + scp := s.asReader().Copy(2) + + go func() { + s.send("a", nil) + time.Sleep(time.Millisecond * 200) + s.send("a", nil) + s.closeSend() + }() + + wg := sync.WaitGroup{} + wg.Add(2) + + //buf := scp[0].csr.parent.mem.buf + go func() { + defer func() { + scp[0].Close() + wg.Done() + }() + + for { + str, err := scp[0].Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, str, "a") + } + }() + + go func() { + time.Sleep(time.Millisecond * 100) + scp[1].Close() + scp[1].Close() // try close multiple times + wg.Done() + }() + + wg.Wait() + + //assert.Equal(t, 0, buf.Len()) + }) + + t.Run("test long time recv", func(t *testing.T) { + s := newStream[int](2) + n := 1000 + go func() { + for i := 0; i < n; i++ { + s.send(i, nil) + } + + s.closeSend() + }() + + m := 100 + wg := sync.WaitGroup{} + wg.Add(m) + copies := s.asReader().Copy(m) + for i := 0; i < m; i++ { + idx := i + go func() { + cp := copies[idx] + l := 0 + defer func() { + assert.Equal(t, 1000, l) + cp.Close() + wg.Done() + }() + + for { + exp, err := cp.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, exp, l) + l++ + } + }() + } + + wg.Wait() + //memo := copies[0].csr.parent.mem + //assert.Equal(t, true, memo.hasFinished) + //assert.Equal(t, 0, memo.buf.Len()) + }) + + t.Run("test closes", func(t *testing.T) { + s := newStream[int](20) + n := 1000 + go func() { + for i := 0; i < n; i++ { + s.send(i, nil) + } + + s.closeSend() + }() + + m := 100 + wg := sync.WaitGroup{} + wg.Add(m) + + wgEven := sync.WaitGroup{} + wgEven.Add(m / 2) + + sr := s.asReader() + sr.SetAutomaticClose() + copies := sr.Copy(m) + for i := 0; i < m; i++ { + idx := i + go func() { + cp := copies[idx] + l := 0 + defer func() { + cp.Close() + wg.Done() + if idx%2 == 0 { + wgEven.Done() + } + }() + + for { + if idx%2 == 0 && l == idx { + break + } + + exp, err := cp.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, exp, l) + l++ + } + }() + } + + wgEven.Wait() + wg.Wait() + assert.Equal(t, m, int(copies[0].csr.parent.closedNum)) + }) + + t.Run("test reader do no close", func(t *testing.T) { + s := newStream[int](20) + n := 1000 + go func() { + for i := 0; i < n; i++ { + s.send(i, nil) + } + + s.closeSend() + }() + + m := 4 + wg := sync.WaitGroup{} + wg.Add(m) + + copies := s.asReader().Copy(m) + for i := 0; i < m; i++ { + idx := i + cp := copies[idx] + cp.SetAutomaticClose() + go func() { + l := 0 + defer func() { + wg.Done() + }() + + for { + exp, err := cp.Recv() + if err == io.EOF { + break + } + + assert.NoError(t, err) + assert.Equal(t, exp, l) + l++ + } + }() + } + + wg.Wait() + assert.Equal(t, 0, int(copies[0].csr.parent.closedNum)) // not closed + }) + +} + +func checkStream(s *StreamReader[int]) error { + defer s.Close() + + for i := 0; i < 10; i++ { + chunk, err := s.Recv() + if err != nil { + return err + } + if chunk != i { + return fmt.Errorf("receive err, expected:%d, actual: %d", i, chunk) + } + } + _, err := s.Recv() + if err != io.EOF { + return fmt.Errorf("close chan fail") + } + return nil +} + +func testStreamN(cap, n int) error { + s := newStream[int](cap) + go func() { + for i := 0; i < 10; i++ { + s.send(i, nil) + } + s.closeSend() + }() + + vs := s.asReader().Copy(n) + err := checkStream(vs[0]) + if err != nil { + return err + } + + vs = vs[1].Copy(n) + err = checkStream(vs[0]) + if err != nil { + return err + } + vs = vs[1].Copy(n) + err = checkStream(vs[0]) + if err != nil { + return err + } + return nil +} + +func TestCopy(t *testing.T) { + for i := 0; i < 10; i++ { + for j := 2; j < 10; j++ { + err := testStreamN(i, j) + if err != nil { + t.Fatal(err) + } + } + } +} + +func TestCopy5(t *testing.T) { + s := newStream[int](0) + go func() { + for i := 0; i < 10; i++ { + closed := s.send(i, nil) + if closed { + fmt.Printf("has closed") + } + } + s.closeSend() + }() + vs := s.asReader().Copy(5) + time.Sleep(time.Second) + defer func() { + for _, v := range vs { + v.Close() + } + }() + for i := 0; i < 10; i++ { + chunk, err := vs[0].Recv() + if err != nil { + t.Fatal(err) + } + if chunk != i { + t.Fatalf("receive err, expected:%d, actual: %d", i, chunk) + } + } + _, err := vs[0].Recv() + if err != io.EOF { + t.Fatalf("copied stream reader cannot return EOF") + } + _, err = vs[0].Recv() + if err != io.EOF { + t.Fatalf("copied stream reader cannot return EOF repeatedly") + } +} + +func TestStreamReaderWithConvert(t *testing.T) { + s := newStream[int](2) + + var cntA int + var e error + + convA := func(src int) (int, error) { + if src == 1 { + return 0, fmt.Errorf("mock err") + } + + return src, nil + } + + sta := StreamReaderWithConvert[int, int](s.asReader(), convA) + sta.SetAutomaticClose() + + s.send(1, nil) + s.send(2, nil) + s.closeSend() + + for { + item, err := sta.Recv() + if err != nil { + if err == io.EOF { + break + } + + e = err + continue + } + + cntA += item + } + + assert.NotNil(t, e) + assert.Equal(t, cntA, 2) +} + +func TestStreamReaderWithConvert_ErrWrapperContinue(t *testing.T) { + s := newStream[int](5) + + s.send(1, nil) + s.send(0, fmt.Errorf("transient error 1")) + s.send(2, nil) + s.send(0, fmt.Errorf("transient error 2")) + s.send(3, nil) + s.closeSend() + + wrapperCalls := 0 + sr := StreamReaderWithConvert[int, int](s.asReader(), func(v int) (int, error) { + return v, nil + }, WithErrWrapper(func(err error) error { + wrapperCalls++ + return nil + })) + + var results []int + for { + v, err := sr.Recv() + if err == io.EOF { + break + } + assert.NoError(t, err) + results = append(results, v) + } + + assert.Equal(t, []int{1, 2, 3}, results) + assert.Equal(t, 2, wrapperCalls) + + s2 := newStream[int](3) + s2.send(0, fmt.Errorf("skip me")) + s2.send(42, nil) + s2.closeSend() + + sr2 := StreamReaderWithConvert[int, int](s2.asReader(), func(v int) (int, error) { + return v, nil + }, WithErrWrapper(func(err error) error { + return nil + })) + + v, err := sr2.Recv() + assert.NoError(t, err) + assert.Equal(t, 42, v) + sr2.Close() +} + +func TestArrayStreamCombined(t *testing.T) { + asr := &StreamReader[int]{ + typ: readerTypeArray, + ar: &arrayReader[int]{ + arr: []int{0, 1, 2}, + index: 0, + }, + } + + s := newStream[int](3) + for i := 3; i < 6; i++ { + s.send(i, nil) + } + s.closeSend() + + nSR := MergeStreamReaders([]*StreamReader[int]{asr, s.asReader()}) + nSR.SetAutomaticClose() + + record := make([]bool, 6) + for i := 0; i < 6; i++ { + chunk, err := nSR.Recv() + if err != nil { + t.Fatal(err) + } + if record[chunk] { + t.Fatal("record duplicated") + } + record[chunk] = true + } + + _, err := nSR.Recv() + if err != io.EOF { + t.Fatal("reader haven't finish correctly") + } + + for i := range record { + if !record[i] { + t.Fatal("record missing") + } + } +} + +func TestMultiStream(t *testing.T) { + var sts []*stream[int] + sum := 0 + for i := 0; i < 10; i++ { + size := rand.Intn(10) + 1 + sum += size + st := newStream[int](size) + for j := 1; j <= size; j++ { + st.send(j&0xffff+i<<16, nil) + } + st.closeSend() + sts = append(sts, st) + } + mst := newMultiStreamReader(sts) + receiveList := make([]int, 10) + for i := 0; i < sum; i++ { + chunk, err := mst.recv() + if err != nil { + t.Fatal(err) + } + if receiveList[chunk>>16] >= chunk&0xffff { + t.Fatal("out of order") + } + receiveList[chunk>>16] = chunk & 0xffff + } + _, err := mst.recv() + if err != io.EOF { + t.Fatal("end stream haven't return EOF") + } +} + +// TestMergeNamedStreamReaders tests the functionality of MergeNamedStreamReaders +// with a focus on SourceEOF error handling. +func TestMergeNamedStreamReaders(t *testing.T) { + t.Run("BasicSourceEOF", func(t *testing.T) { + // Create two named streams + sr1, sw1 := Pipe[string](2) + sr2, sw2 := Pipe[string](2) + + // Merge the streams with names + namedStreams := map[string]*StreamReader[string]{ + "stream1": sr1, + "stream2": sr2, + } + mergedSR := MergeNamedStreamReaders(namedStreams) + mergedSR.SetAutomaticClose() + + // Send data to the first stream and close it immediately + go func() { + defer sw1.Close() + sw1.Send("data1-1", nil) + sw1.Send("data1-2", nil) + // First stream ends + }() + + // Send data to the second stream with a delay before closing + go func() { + defer sw2.Close() + sw2.Send("data2-1", nil) + sw2.Send("data2-2", nil) + sw2.Send("data2-3", nil) + // Second stream ends + }() + + // Track received data and EOF sources + receivedData := make(map[string][]string) + eofSources := make([]string, 0, 2) + + for { + chunk, err := mergedSR.Recv() + if err != nil { + // Check if it's a SourceEOF error + if sourceName, ok := GetSourceName(err); ok { + eofSources = append(eofSources, sourceName) + t.Logf("Received EOF from source: %s", sourceName) + continue // Continue receiving from other streams + } + + // If it's a regular EOF, all streams have ended + if errors.Is(err, io.EOF) { + break + } + + // Handle other errors + t.Errorf("Error receiving data: %v", err) + break + } + + // Categorize data by prefix + if len(chunk) >= 5 { + prefix := chunk[:5] + if prefix == "data1" { + receivedData["stream1"] = append(receivedData["stream1"], chunk) + } else if prefix == "data2" { + receivedData["stream2"] = append(receivedData["stream2"], chunk) + } + } + } + + // Verify we received both SourceEOF errors + if len(eofSources) != 2 { + t.Errorf("Expected 2 SourceEOF errors, got %d", len(eofSources)) + } + + // Verify the source names are correct + expectedSources := map[string]bool{"stream1": false, "stream2": false} + for _, source := range eofSources { + if _, exists := expectedSources[source]; !exists { + t.Errorf("Unexpected source name: %s", source) + } else { + expectedSources[source] = true + } + } + + // Verify all expected sources were seen + for source, seen := range expectedSources { + if !seen { + t.Errorf("Did not receive SourceEOF for %s", source) + } + } + + // Verify we received all expected data + if len(receivedData["stream1"]) != 2 { + t.Errorf("Expected 2 items from stream1, got %d", len(receivedData["stream1"])) + } + + if len(receivedData["stream2"]) != 3 { + t.Errorf("Expected 3 items from stream2, got %d", len(receivedData["stream2"])) + } + }) + + t.Run("EmptyStream", func(t *testing.T) { + // Create two streams, one will be empty + sr1, sw1 := Pipe[string](2) + sr2, sw2 := Pipe[string](2) + + // Close the first stream immediately to make it empty + sw1.Close() + + // Merge the streams with names + namedStreams := map[string]*StreamReader[string]{ + "empty": sr1, + "data": sr2, + } + mergedSR := MergeNamedStreamReaders(namedStreams) + mergedSR.SetAutomaticClose() + + // Send data to the second stream + go func() { + defer sw2.Close() + sw2.Send("test-data", nil) + }() + + // Track received EOFs and data + eofSources := make(map[string]bool, 2) + receivedData := make([]string, 0, 1) + + for { + chunk, err := mergedSR.Recv() + if err != nil { + if sourceName, ok := GetSourceName(err); ok { + eofSources[sourceName] = true + continue + } + + if errors.Is(err, io.EOF) { + break + } + + t.Errorf("Error receiving data: %v", err) + break + } + + receivedData = append(receivedData, chunk) + } + + // Verify we received EOF from the empty stream + if len(eofSources) != 2 { + t.Errorf("Expected 2 SourceEOF errors, got %d", len(eofSources)) + } + + if _, exist := eofSources["empty"]; !exist { + t.Errorf("Expected EOF from 'empty' stream, got '%v'", eofSources) + } + if _, exist := eofSources["data"]; !exist { + t.Errorf("Expected EOF from 'data' stream, got '%v'", eofSources) + } + + // Verify we received the data from the non-empty stream + if len(receivedData) != 1 || receivedData[0] != "test-data" { + t.Errorf("Expected to receive 'test-data', got %v", receivedData) + } + }) + + t.Run("ArraySource", func(t *testing.T) { + // Create three named streams + sr1, sw1 := Pipe[string](2) + sr2, sw2 := Pipe[string](2) + sr3 := StreamReaderFromArray([]string{"data3-1", "data3-2", "data3-3"}) + + // Merge the streams with names + namedStreams := map[string]*StreamReader[string]{ + "stream1": sr1, + "stream2": sr2, + "stream3": sr3, + } + mergedSR := MergeNamedStreamReaders(namedStreams) + mergedSR.SetAutomaticClose() + + // Send data and close streams in sequence + go func() { + // First stream sends one item then closes + sw1.Send("data1", nil) + sw1.Close() + + // Second stream sends two items then closes + sw2.Send("data2-1", nil) + sw2.Send("data2-2", nil) + sw2.Close() + }() + + // Track EOF order and data count + eofOrder := make([]string, 0, 3) + dataCount := 0 + + for { + _, err := mergedSR.Recv() + if err != nil { + if sourceName, ok := GetSourceName(err); ok { + eofOrder = append(eofOrder, sourceName) + continue + } + + if errors.Is(err, io.EOF) { + break + } + + t.Errorf("Error receiving data: %v", err) + break + } + + dataCount++ + } + + // Verify EOF count + if len(eofOrder) != 3 { + t.Errorf("Expected 3 SourceEOF errors, got %d", len(eofOrder)) + } + + // Verify data count + if dataCount != 6 { + t.Errorf("Expected 6 data items, got %d", dataCount) + } + }) + + t.Run("ErrorPropagation", func(t *testing.T) { + // Create two streams + sr1, sw1 := Pipe[string](2) + sr2, sw2 := Pipe[string](2) + + // Merge the streams with names + namedStreams := map[string]*StreamReader[string]{ + "normal": sr1, + "error": sr2, + } + mergedSR := MergeNamedStreamReaders(namedStreams) + defer mergedSR.Close() + + testError := errors.New("test error") + + // Send normal data to first stream + go func() { + defer sw1.Close() + sw1.Send("normal-data", nil) + }() + + // Send error to second stream + go func() { + defer sw2.Close() + sw2.Send("", testError) + }() + + // Track received errors + var receivedError error + + for { + _, err := mergedSR.Recv() + if err != nil { + // Skip SourceEOF errors + if _, ok := GetSourceName(err); ok { + continue + } + + if errors.Is(err, io.EOF) { + break + } + + // Store the first non-EOF error + receivedError = err + break + } + } + + // Verify we received the test error + if receivedError == nil || receivedError.Error() != testError.Error() { + t.Errorf("Expected error '%v', got '%v'", testError, receivedError) + } + }) +} diff --git a/schema/tool.go b/schema/tool.go new file mode 100644 index 0000000..170db11 --- /dev/null +++ b/schema/tool.go @@ -0,0 +1,580 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/eino-contrib/jsonschema" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +// DataType is the type of the parameter. +// It must be one of the following values: "object", "number", "integer", "string", "array", "null", "boolean", which is the same as the type of the parameter in JSONSchema. +type DataType string + +// Supported JSONSchema data types for tool parameters. +const ( + Object DataType = "object" + Number DataType = "number" + Integer DataType = "integer" + String DataType = "string" + Array DataType = "array" + Null DataType = "null" + Boolean DataType = "boolean" +) + +// ToolChoice controls how the model uses the tools provided to it. +// Pass as part of the model option via [model.WithToolChoice]. +type ToolChoice string + +const ( + // ToolChoiceForbidden instructs the model not to call any tools, even if + // tools are bound. The model responds with a plain text message instead. + // Corresponds to "none" in OpenAI Chat Completion. + ToolChoiceForbidden ToolChoice = "forbidden" + + // ToolChoiceAllowed lets the model decide: it may generate a plain message + // or call one or more tools. This is the default when tools are provided. + // Corresponds to "auto" in OpenAI Chat Completion. + ToolChoiceAllowed ToolChoice = "allowed" + + // ToolChoiceForced requires the model to call at least one tool. Use this + // when you want to guarantee structured output via tool calling. + // Corresponds to "required" in OpenAI Chat Completion. + ToolChoiceForced ToolChoice = "forced" +) + +type AgenticToolChoice struct { + // Type is the tool choice mode. + Type ToolChoice + + // Allowed optionally specifies the list of tools that the model is permitted to call. + // Optional. + Allowed *AgenticAllowedToolChoice + + // Forced optionally specifies the list of tools that the model is required to call. + // Optional. + Forced *AgenticForcedToolChoice +} + +// AgenticAllowedToolChoice specifies a list of allowed tools for the model. +type AgenticAllowedToolChoice struct { + // Tools is the list of allowed tools for the model to call. + // Optional. + Tools []*AllowedTool +} + +// AgenticForcedToolChoice specifies a list of tools that the model must call. +type AgenticForcedToolChoice struct { + // Tools is the list of tools that the model must call. + // Optional. + Tools []*AllowedTool +} + +// AllowedTool represents a tool that the model is allowed or forced to call. +// Exactly one of FunctionName, MCPTool, or ServerTool must be specified. +type AllowedTool struct { + // FunctionName specifies a function tool by name. + FunctionName string + + // MCPTool specifies an MCP tool. + MCPTool *AllowedMCPTool + + // ServerTool specifies a server tool. + ServerTool *AllowedServerTool +} + +// AllowedMCPTool contains the information for identifying an MCP tool. +type AllowedMCPTool struct { + // ServerLabel is the label of the MCP server. + ServerLabel string + // Name is the name of the MCP tool. + Name string +} + +// AllowedServerTool contains the information for identifying a server tool. +type AllowedServerTool struct { + // Name is the name of the server tool. + Name string +} + +// ToolInfo is the information of a tool. +// ToolInfo describes a tool that can be passed to a ChatModel via +// [ToolCallingChatModel.WithTools] or [ChatModel.BindTools]. +// +// Name should be concise and unique within the tool set. Desc should explain +// when and why to use the tool; few-shot examples in Desc significantly improve +// model accuracy. ParamsOneOf may be nil for tools that take no arguments. +type ToolInfo struct { + // The unique name of the tool that clearly communicates its purpose. + Name string + // Used to tell the model how/when/why to use the tool. + // You can provide few-shot examples as a part of the description. + Desc string + // Extra is the extra information for the tool. + Extra map[string]any + + // The parameters the functions accepts (different models may require different parameter types). + // can be described in two ways: + // - use params: schema.NewParamsOneOfByParams(params) + // - use jsonschema: schema.NewParamsOneOfByJSONSchema(jsonschema) + // If is nil, signals that the tool does not need any input parameter + *ParamsOneOf +} + +type toolInfoForJSON struct { + Name string `json:"name,omitempty"` + Desc string `json:"desc,omitempty"` + Extra map[string]any `json:"extra,omitempty"` + HasParamsOneOf bool `json:"has_params_one_of,omitempty"` + Params map[string]*ParameterInfo `json:"params,omitempty"` + JSONSchema *jsonschema.Schema `json:"json_schema,omitempty"` +} + +type toolInfoForGob struct { + Name string + Desc string + Extra map[string]any + HasParamsOneOf bool + Params map[string]*ParameterInfo + JSONSchema *string +} + +func (t *ToolInfo) MarshalJSON() ([]byte, error) { + tmp := &toolInfoForJSON{ + Name: t.Name, + Desc: t.Desc, + Extra: t.Extra, + } + if t.ParamsOneOf != nil { + tmp.HasParamsOneOf = true + tmp.Params = t.ParamsOneOf.params + tmp.JSONSchema = t.ParamsOneOf.jsonschema + } + return json.Marshal(tmp) +} + +func (t *ToolInfo) UnmarshalJSON(data []byte) error { + tmp := &toolInfoForJSON{} + if err := json.Unmarshal(data, tmp); err != nil { + return err + } + t.Name = tmp.Name + t.Desc = tmp.Desc + t.Extra = tmp.Extra + if tmp.HasParamsOneOf { + t.ParamsOneOf = &ParamsOneOf{ + params: tmp.Params, + jsonschema: tmp.JSONSchema, + } + // An empty-but-non-nil params map is dropped by `omitempty` during + // marshaling. When jsonschema is also absent, the params form was the + // chosen representation, so restore the empty map to preserve the + // roundtrip invariant. + if t.ParamsOneOf.params == nil && t.ParamsOneOf.jsonschema == nil { + t.ParamsOneOf.params = map[string]*ParameterInfo{} + } + } + return nil +} + +func (t *ToolInfo) GobEncode() ([]byte, error) { + tmp := &toolInfoForGob{ + Name: t.Name, + Desc: t.Desc, + Extra: t.Extra, + } + if t.ParamsOneOf != nil { + tmp.HasParamsOneOf = true + tmp.Params = t.ParamsOneOf.params + if t.ParamsOneOf.jsonschema != nil { + b, err := json.Marshal(t.ParamsOneOf.jsonschema) + if err != nil { + return nil, err + } + str := string(b) + tmp.JSONSchema = &str + } + } + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(tmp); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (t *ToolInfo) GobDecode(b []byte) error { + tmp := &toolInfoForGob{} + if err := gob.NewDecoder(bytes.NewBuffer(b)).Decode(tmp); err != nil { + return err + } + t.Name = tmp.Name + t.Desc = tmp.Desc + t.Extra = tmp.Extra + if !tmp.HasParamsOneOf { + return nil + } + t.ParamsOneOf = &ParamsOneOf{ + params: tmp.Params, + } + if tmp.JSONSchema != nil { + s := &jsonschema.Schema{} + if err := json.Unmarshal([]byte(*tmp.JSONSchema), s); err != nil { + return err + } + t.ParamsOneOf.jsonschema = s + } + return nil +} + +// ParameterInfo is the information of a parameter. +// It is used to describe the parameters of a tool. +type ParameterInfo struct { + // The type of the parameter. + Type DataType + // The element type of the parameter, only for array. + ElemInfo *ParameterInfo + // The sub parameters of the parameter, only for object. + SubParams map[string]*ParameterInfo + // The description of the parameter. + Desc string + // The enum values of the parameter, only for string. + Enum []string + // Whether the parameter is required. + Required bool +} + +// ParamsOneOf holds a tool's parameter schema using exactly one of two +// representations. Choose the one that best fits your needs: +// +// 1. [NewParamsOneOfByParams] — lightweight: describe parameters as a +// map[string]*[ParameterInfo]. Covers the most common cases (scalars, +// arrays, nested objects, enums, required flags). +// +// 2. [NewParamsOneOfByJSONSchema] — powerful: supply a full +// *jsonschema.Schema (JSON Schema 2020-12). Required when you need +// features not expressible via ParameterInfo, such as anyOf, oneOf, or +// $defs references. [utils.InferTool] generates this form automatically +// from Go struct tags. +// +// You must use exactly one constructor — setting both fields is invalid. +// If ParamsOneOf is nil, the tool takes no input parameters. +type ParamsOneOf struct { + // use NewParamsOneOfByParams to set this field + params map[string]*ParameterInfo + + jsonschema *jsonschema.Schema +} + +// NewParamsOneOfByParams creates a ParamsOneOf with map[string]*ParameterInfo. +func NewParamsOneOfByParams(params map[string]*ParameterInfo) *ParamsOneOf { + return &ParamsOneOf{ + params: params, + } +} + +// NewParamsOneOfByJSONSchema creates a ParamsOneOf with *jsonschema.Schema. +func NewParamsOneOfByJSONSchema(s *jsonschema.Schema) *ParamsOneOf { + return &ParamsOneOf{ + jsonschema: s, + } +} + +// ToJSONSchema parses ParamsOneOf, converts the parameter description that user actually provides, into the format ready to be passed to Model. +func (p *ParamsOneOf) ToJSONSchema() (*jsonschema.Schema, error) { + if p == nil { + return nil, nil + } + + if p.params != nil { + sc := &jsonschema.Schema{ + Properties: orderedmap.New[string, *jsonschema.Schema](), + Type: string(Object), + Required: make([]string, 0, len(p.params)), + } + + keys := make([]string, 0, len(p.params)) + for k := range p.params { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := p.params[k] + sc.Properties.Set(k, paramInfoToJSONSchema(v)) + if v.Required { + sc.Required = append(sc.Required, k) + } + } + + return sc, nil + } + + return p.jsonschema, nil +} + +func paramInfoToJSONSchema(paramInfo *ParameterInfo) *jsonschema.Schema { + js := &jsonschema.Schema{ + Type: string(paramInfo.Type), + Description: paramInfo.Desc, + } + + if len(paramInfo.Enum) > 0 { + js.Enum = make([]any, len(paramInfo.Enum)) + for i, enum := range paramInfo.Enum { + js.Enum[i] = enum + } + } + + if paramInfo.ElemInfo != nil { + js.Items = paramInfoToJSONSchema(paramInfo.ElemInfo) + } + + if len(paramInfo.SubParams) > 0 { + required := make([]string, 0, len(paramInfo.SubParams)) + js.Properties = orderedmap.New[string, *jsonschema.Schema]() + keys := make([]string, 0, len(paramInfo.SubParams)) + for k := range paramInfo.SubParams { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := paramInfo.SubParams[k] + item := paramInfoToJSONSchema(v) + js.Properties.Set(k, item) + if v.Required { + required = append(required, k) + } + } + + js.Required = required + } + + return js +} + +// ToolPartType defines the type of content in a tool output part. +// It is used to distinguish between different types of multimodal content returned by tools. +type ToolPartType string + +const ( + // ToolPartTypeText means the part is a text. + ToolPartTypeText ToolPartType = "text" + + // ToolPartTypeImage means the part is an image url. + ToolPartTypeImage ToolPartType = "image" + + // ToolPartTypeAudio means the part is an audio url. + ToolPartTypeAudio ToolPartType = "audio" + + // ToolPartTypeVideo means the part is a video url. + ToolPartTypeVideo ToolPartType = "video" + + // ToolPartTypeFile means the part is a file url. + ToolPartTypeFile ToolPartType = "file" + + // ToolPartTypeToolSearchResult means the part contains tool search results. + ToolPartTypeToolSearchResult ToolPartType = "tool_search_result" +) + +// ToolOutputImage represents an image in tool output. +// It contains URL or Base64-encoded data along with MIME type information. +type ToolOutputImage struct { + MessagePartCommon +} + +// ToolOutputAudio represents an audio file in tool output. +// It contains URL or Base64-encoded data along with MIME type information. +type ToolOutputAudio struct { + MessagePartCommon +} + +// ToolOutputVideo represents a video file in tool output. +// It contains URL or Base64-encoded data along with MIME type information. +type ToolOutputVideo struct { + MessagePartCommon +} + +// ToolOutputFile represents a generic file in tool output. +// It contains URL or Base64-encoded data along with MIME type information. +type ToolOutputFile struct { + MessagePartCommon +} + +// ToolSearchResult represents the result of a tool search operation. +// When a model issues a tool search call, the framework searches for matching tools +// and returns the results via this struct. +type ToolSearchResult struct { + // Tools contains the full definitions of matched tools that were not previously + // registered. Their complete definitions are required so that the model can + // understand their parameters and usage. + Tools []*ToolInfo +} + +func (t *ToolSearchResult) String() string { + sb := new(strings.Builder) + sb.WriteString("ToolSearchResult[") + for _, tool := range t.Tools { + sb.WriteString(tool.Name) + sb.WriteString(",") + } + sb.WriteString("]") + return sb.String() +} + +// ToolOutputPart represents a part of tool execution output. +// It supports streaming scenarios through the Index field for chunk merging. +type ToolOutputPart struct { + + // Type is the type of the part, e.g., "text", "image_url", "audio_url", "video_url". + Type ToolPartType `json:"type"` + + // Text is the text content, used when Type is "text". + Text string `json:"text,omitempty"` + + // Image is the image content, used when Type is ToolPartTypeImage. + Image *ToolOutputImage `json:"image,omitempty"` + + // Audio is the audio content, used when Type is ToolPartTypeAudio. + Audio *ToolOutputAudio `json:"audio,omitempty"` + + // Video is the video content, used when Type is ToolPartTypeVideo. + Video *ToolOutputVideo `json:"video,omitempty"` + + // File is the file content, used when Type is ToolPartTypeFile. + File *ToolOutputFile `json:"file,omitempty"` + + // ToolSearchResult holds the tool search results, used when Type is ToolPartTypeToolSearchResult. + ToolSearchResult *ToolSearchResult `json:"tool_search_result,omitempty"` + + // Extra is used to store extra information. + Extra map[string]any `json:"extra,omitempty"` +} + +// ToolArgument contains the input information for a tool call. +// It is used to pass tool call arguments to enhanced tools. +type ToolArgument struct { + // Text contains the arguments for the tool call in JSON format. + Text string `json:"text,omitempty"` +} + +// ToolResult represents the structured multimodal output from a tool execution. +// It is used when a tool needs to return more than just a simple string, +// such as images, files, or other structured data. +type ToolResult struct { + // Parts contains the multimodal output parts. Each part can be a different + // type of content, like text, an image, or a file. + Parts []ToolOutputPart `json:"parts,omitempty"` +} + +func convToolOutputPartToMessageInputPart(toolPart ToolOutputPart) (MessageInputPart, error) { + switch toolPart.Type { + case ToolPartTypeText: + return MessageInputPart{ + Type: ChatMessagePartTypeText, + Text: toolPart.Text, + Extra: toolPart.Extra, + }, nil + case ToolPartTypeImage: + if toolPart.Image == nil { + return MessageInputPart{}, fmt.Errorf("image content is nil for tool part type %v", toolPart.Type) + } + return MessageInputPart{ + Type: ChatMessagePartTypeImageURL, + Image: &MessageInputImage{MessagePartCommon: toolPart.Image.MessagePartCommon}, + Extra: toolPart.Extra, + }, nil + case ToolPartTypeAudio: + if toolPart.Audio == nil { + return MessageInputPart{}, fmt.Errorf("audio content is nil for tool part type %v", toolPart.Type) + } + return MessageInputPart{ + Type: ChatMessagePartTypeAudioURL, + Audio: &MessageInputAudio{MessagePartCommon: toolPart.Audio.MessagePartCommon}, + Extra: toolPart.Extra, + }, nil + case ToolPartTypeVideo: + if toolPart.Video == nil { + return MessageInputPart{}, fmt.Errorf("video content is nil for tool part type %v", toolPart.Type) + } + return MessageInputPart{ + Type: ChatMessagePartTypeVideoURL, + Video: &MessageInputVideo{MessagePartCommon: toolPart.Video.MessagePartCommon}, + Extra: toolPart.Extra, + }, nil + case ToolPartTypeFile: + if toolPart.File == nil { + return MessageInputPart{}, fmt.Errorf("file content is nil for tool part type %v", toolPart.Type) + } + return MessageInputPart{ + Type: ChatMessagePartTypeFileURL, + File: &MessageInputFile{MessagePartCommon: toolPart.File.MessagePartCommon}, + Extra: toolPart.Extra, + }, nil + case ToolPartTypeToolSearchResult: + if toolPart.ToolSearchResult == nil { + return MessageInputPart{}, fmt.Errorf("tool search result is nil for tool part type %v", toolPart.Type) + } + return MessageInputPart{ + Type: ChatMessagePartTypeToolSearchResult, + ToolSearchResult: toolPart.ToolSearchResult, + }, nil + default: + return MessageInputPart{}, fmt.Errorf("unknown tool part type: %v", toolPart.Type) + } +} + +// ToMessageInputParts converts ToolOutputPart slice to MessageInputPart slice. +// This is used when passing tool results as input to the model. +// +// Parameters: +// - None (method receiver is *ToolResult) +// +// Returns: +// - []MessageInputPart: The converted message input parts that can be used in a Message. +// - error: An error if conversion fails due to unknown part types or nil content fields. +// +// Example: +// +// toolResult := &schema.ToolResult{ +// Parts: []schema.ToolOutputPart{ +// {Type: schema.ToolPartTypeText, Text: "Result text"}, +// {Type: schema.ToolPartTypeImage, Image: &schema.ToolOutputImage{...}}, +// }, +// } +// inputParts, err := toolResult.ToMessageInputParts() +func (tr *ToolResult) ToMessageInputParts() ([]MessageInputPart, error) { + if tr == nil || len(tr.Parts) == 0 { + return nil, nil + } + result := make([]MessageInputPart, len(tr.Parts)) + for i, part := range tr.Parts { + var err error + result[i], err = convToolOutputPartToMessageInputPart(part) + if err != nil { + return nil, err + } + } + return result, nil +} diff --git a/schema/tool_test.go b/schema/tool_test.go new file mode 100644 index 0000000..76a6a25 --- /dev/null +++ b/schema/tool_test.go @@ -0,0 +1,235 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package schema + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "testing" + + "github.com/eino-contrib/jsonschema" + "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParamsOneOfToJSONSchema(t *testing.T) { + convey.Convey("ParamsOneOfToJSONSchema", t, func() { + var ( + oneOf ParamsOneOf + converted any + err error + ) + + convey.Convey("user provides JSON schema directly, use what the user provides", func() { + oneOf.jsonschema = &jsonschema.Schema{ + Type: "string", + Description: "this is the only argument", + } + converted, err = oneOf.ToJSONSchema() + convey.So(err, convey.ShouldBeNil) + convey.So(converted, convey.ShouldResemble, oneOf.jsonschema) + }) + + convey.Convey("user provides map[string]ParameterInfo, converts to json schema", func() { + oneOf.params = map[string]*ParameterInfo{ + "arg1": { + Type: String, + Desc: "this is the first argument", + Required: true, + Enum: []string{"1", "2"}, + }, + "arg2": { + Type: Object, + Desc: "this is the second argument", + SubParams: map[string]*ParameterInfo{ + "sub_arg1": { + Type: String, + Desc: "this is the sub argument", + Required: true, + Enum: []string{"1", "2"}, + }, + "sub_arg2": { + Type: String, + Desc: "this is the sub argument 2", + }, + }, + Required: true, + }, + "arg3": { + Type: Array, + Desc: "this is the third argument", + ElemInfo: &ParameterInfo{ + Type: String, + Desc: "this is the element of the third argument", + Required: true, + Enum: []string{"1", "2"}, + }, + Required: true, + }, + } + converted, err = oneOf.ToJSONSchema() + convey.So(err, convey.ShouldBeNil) + }) + + convey.Convey("user provides map[string]ParameterInfo, converts to json schema in order", func() { + params := &ParamsOneOf{ + params: map[string]*ParameterInfo{ + "c": { + Type: "string", + }, + "a": { + Type: "object", + SubParams: map[string]*ParameterInfo{ + "z": { + Type: "number", + }, + "y": { + Type: "string", + }, + }, + }, + "b": { + Type: "array", + ElemInfo: &ParameterInfo{ + Type: "object", + SubParams: map[string]*ParameterInfo{ + "p": { + Type: "integer", + }, + "o": { + Type: "boolean", + }, + }, + }, + }, + }, + } + + schema1, err := params.ToJSONSchema() + assert.NoError(t, err) + json1, err := json.Marshal(schema1) + assert.NoError(t, err) + + schema2, err := params.ToJSONSchema() + assert.NoError(t, err) + json2, err := json.Marshal(schema2) + assert.NoError(t, err) + + assert.Equal(t, string(json1), string(json2)) + }) + + }) +} + +func TestToolInfoSerialization(t *testing.T) { + ti1 := &ToolInfo{ + ParamsOneOf: NewParamsOneOfByParams(map[string]*ParameterInfo{ + "a": { + Type: String, + Desc: "desc", + }, + }), + } + ti2 := &ToolInfo{ + ParamsOneOf: NewParamsOneOfByJSONSchema(&jsonschema.Schema{ + Type: "string", + }), + } + + // json + b, err := json.Marshal(ti1) + assert.NoError(t, err) + result := &ToolInfo{} + err = json.Unmarshal(b, result) + assert.NoError(t, err) + assert.Equal(t, ti1, result) + b, err = json.Marshal(ti2) + assert.NoError(t, err) + result = &ToolInfo{} + err = json.Unmarshal(b, result) + assert.NoError(t, err) + assert.Equal(t, ti2, result) + + // gob + buf := new(bytes.Buffer) + err = gob.NewEncoder(buf).Encode(ti1) + assert.NoError(t, err) + result = &ToolInfo{} + err = gob.NewDecoder(buf).Decode(result) + assert.NoError(t, err) + assert.Equal(t, ti1, result) + buf = new(bytes.Buffer) + err = gob.NewEncoder(buf).Encode(ti2) + assert.NoError(t, err) + result = &ToolInfo{} + err = gob.NewDecoder(buf).Decode(result) + assert.NoError(t, err) + assert.Equal(t, ti2, result) + + // json roundtrip with empty-but-non-nil params map: must not collapse to nil, + // otherwise the params form is silently dropped. + tiEmpty := &ToolInfo{ + ParamsOneOf: NewParamsOneOfByParams(map[string]*ParameterInfo{}), + } + b, err = json.Marshal(tiEmpty) + assert.NoError(t, err) + result = &ToolInfo{} + err = json.Unmarshal(b, result) + assert.NoError(t, err) + assert.NotNil(t, result.ParamsOneOf) + assert.NotNil(t, result.ParamsOneOf.params) + assert.Equal(t, tiEmpty, result) +} + +func TestMCPToolResult_NilErrorCode(t *testing.T) { + result := &MCPToolResult{ + CallID: "test-call", + Name: "test-tool", + Content: "some result", + Error: &MCPToolCallError{ + Code: nil, + Message: "something went wrong", + }, + } + + require.NotPanics(t, func() { + s := result.String() + t.Logf("String output: %s", s) + assert.Contains(t, s, "something went wrong") + }, "BUG: MCPToolResult.String() should not panic when Error.Code is nil") +} + +func TestMCPToolResult_WithErrorCode(t *testing.T) { + code := int64(500) + result := &MCPToolResult{ + CallID: "test-call", + Name: "test-tool", + Content: "", + Error: &MCPToolCallError{ + Code: &code, + Message: "internal server error", + }, + } + + require.NotPanics(t, func() { + s := result.String() + assert.Contains(t, s, "500") + assert.Contains(t, s, "internal server error") + }) +} diff --git a/scripts/dev_setup.sh b/scripts/dev_setup.sh new file mode 100755 index 0000000..0be6017 --- /dev/null +++ b/scripts/dev_setup.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# dev_setup.sh — set up a local multi-module workspace for eino development. +# +# BACKGROUND +# eino, eino-ext, and eino-examples live in separate GitHub repositories to +# keep their Go modules, versioning, and maintenance independent. However, +# working across them is inconvenient: editors and AI coding tools lack +# cross-repo type information and can't navigate between them. +# +# This script brings all three repos together locally so that a single +# go.work file provides full cross-module LSP (go-to-definition, type +# inference, autocomplete) across all ~83 modules — without touching any +# remote repository. +# +# WHAT IT DOES +# 1. Clones eino-ext → ext/ +# 2. Clones eino-examples → examples/ +# 3. Registers ext/ and examples/ in .git/info/exclude so eino's git +# never sees them (local-only, never committed) +# 4. Creates go.work at the repo root covering eino + all modules in +# ext/ and examples/ (go.work is already in .gitignore) +# +# RESULTING LAYOUT +# eino/ ← you are here (github.com/cloudwego/eino) +# eino/ext/ ← github.com/cloudwego/eino-ext (full git repo) +# eino/examples/ ← github.com/cloudwego/eino-examples (full git repo) +# eino/go.work ← wires all modules together (gitignored) +# +# WORKING ACROSS REPOS +# Each subdirectory is a full independent git repo tracking its own remote. +# To contribute to eino-ext or eino-examples, work inside that directory: +# +# cd ext +# git checkout -b feat/my-feature +# # make changes — editor has full cross-repo type info via go.work +# git commit -m "feat: ..." +# git push origin feat/my-feature # pushes to cloudwego/eino-ext +# +# KEEPING REPOS UP TO DATE +# git -C ext pull +# git -C examples pull +# +# USAGE +# bash scripts/dev_setup.sh # first-time setup +# bash scripts/dev_setup.sh --reset # re-clone everything from scratch + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +EXT_DIR="ext" +EXAMPLES_DIR="examples" +EINO_EXT_REPO="https://github.com/cloudwego/eino-ext" +EINO_EXAMPLES_REPO="https://github.com/cloudwego/eino-examples" + +# Parse flags +RESET=false +for arg in "$@"; do + case $arg in + --reset) RESET=true ;; + esac +done + +echo "==> Setting up eino dev workspace in: $REPO_ROOT" + +# --reset: remove existing dirs +if [ "$RESET" = true ]; then + echo "==> --reset: removing $EXT_DIR/ and $EXAMPLES_DIR/" + rm -rf "$EXT_DIR" "$EXAMPLES_DIR" go.work go.work.sum +fi + +# Clone repos if not already present +if [ ! -d "$EXT_DIR/.git" ]; then + echo "==> Cloning eino-ext into $EXT_DIR/" + git clone "$EINO_EXT_REPO" "$EXT_DIR" +else + echo "==> $EXT_DIR/ already exists, skipping clone" +fi + +if [ ! -d "$EXAMPLES_DIR/.git" ]; then + echo "==> Cloning eino-examples into $EXAMPLES_DIR/" + git clone "$EINO_EXAMPLES_REPO" "$EXAMPLES_DIR" +else + echo "==> $EXAMPLES_DIR/ already exists, skipping clone" +fi + +# Exclude dirs from eino's git tracking (local only, not committed) +EXCLUDE_FILE=".git/info/exclude" +add_exclude() { + local entry="$1" + if ! grep -qxF "$entry" "$EXCLUDE_FILE" 2>/dev/null; then + echo "$entry" >> "$EXCLUDE_FILE" + echo "==> Added '$entry' to $EXCLUDE_FILE" + fi +} +add_exclude "$EXT_DIR/" +add_exclude "$EXAMPLES_DIR/" + +# Build go.work covering eino root + every go.mod found in ext/ and examples/ +if [ ! -f "go.work" ]; then + echo "==> Creating go.work" + go work init . + + # Collect all module directories (directories containing a go.mod) + while IFS= read -r modfile; do + dir="$(dirname "$modfile")" + go work use "$dir" + done < <(find "$EXT_DIR" "$EXAMPLES_DIR" -name "go.mod" | sort) + + echo "==> go.work created with $(grep -c '^\s\+\.' go.work || true) module(s)" +else + echo "==> go.work already exists, skipping (use --reset to recreate)" +fi + +echo "" +echo "Done. Your workspace includes:" +echo " . — github.com/cloudwego/eino" +echo " $EXT_DIR/ — github.com/cloudwego/eino-ext ($(find "$EXT_DIR" -name "go.mod" | wc -l | tr -d ' ') modules)" +echo " $EXAMPLES_DIR/ — github.com/cloudwego/eino-examples ($(find "$EXAMPLES_DIR" -name "go.mod" | wc -l | tr -d ' ') modules)" +echo "" +echo "Run 'go build ./...' or open this directory in your editor." diff --git a/scripts/eino_setup.sh b/scripts/eino_setup.sh new file mode 100755 index 0000000..5b99bed --- /dev/null +++ b/scripts/eino_setup.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# eino_setup.sh — fetch eino framework source into your project for AI-assisted development. +# +# BACKGROUND +# When building applications with eino, your AI coding assistant (Claude Code, +# Cursor, Copilot, etc.) only sees your code. It cannot navigate into eino's +# source to understand how components work, what patterns are idiomatic, or +# how to wire things together correctly. +# +# This script clones eino, eino-ext, and eino-examples into a _eino/ directory +# inside your project. Your AI assistant can then browse the actual source, +# examples, and extensions — giving it full context to help you build correctly. +# +# WHAT IT DOES +# 1. Clones eino → _eino/eino/ +# 2. Clones eino-ext → _eino/eino-ext/ +# 3. Clones eino-examples → _eino/eino-examples/ +# 4. Adds _eino/ to .gitignore (read-only reference, never committed) +# 5. Writes a _eino/README.md explaining the directory to future readers +# +# RESULTING LAYOUT +# your-project/ +# ├── _eino/ +# │ ├── eino/ ← github.com/cloudwego/eino (core framework) +# │ ├── eino-ext/ ← github.com/cloudwego/eino-ext (components & integrations) +# │ └── eino-examples/ ← github.com/cloudwego/eino-examples (patterns & recipes) +# └── ... your code +# +# NOTE: _eino/ is read-only reference material. Do not edit files inside it. +# Your go.mod is unchanged — eino remains a normal dependency. +# +# KEEPING UP TO DATE +# bash eino_setup.sh --update # pull latest on all three repos +# +# USAGE +# bash eino_setup.sh # first-time setup +# bash eino_setup.sh --reset # re-clone everything from scratch +# bash eino_setup.sh --update # pull latest without re-cloning +# +# SYSTEM PROMPT +# After running this script, add the following to your AI assistant's project +# instructions (CLAUDE.md, .cursorrules, .github/copilot-instructions.md, etc.): +# +# --- +# ## eino Framework Reference +# +# This project uses the eino framework (github.com/cloudwego/eino). +# The full framework source is available locally in `_eino/`: +# +# - `_eino/eino/` — core framework (components, graph, compose, callbacks) +# - `_eino/eino-ext/` — official components and integrations (models, tools, retrievers, etc.) +# - `_eino/eino-examples/` — working examples and patterns +# +# When answering questions about eino APIs, component wiring, graph construction, +# callbacks, or any eino-specific patterns: explore `_eino/` first. +# Prefer examples from `_eino/eino-examples/` as the canonical reference for +# idiomatic usage. +# --- + +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$PROJECT_ROOT" + +EINO_DIR="_eino" +EINO_REPO="https://github.com/cloudwego/eino" +EINO_EXT_REPO="https://github.com/cloudwego/eino-ext" +EINO_EXAMPLES_REPO="https://github.com/cloudwego/eino-examples" + +# Parse flags +RESET=false +UPDATE=false +for arg in "$@"; do + case $arg in + --reset) RESET=true ;; + --update) UPDATE=true ;; + esac +done + +echo "==> eino setup in: $PROJECT_ROOT" + +# --reset: remove and re-clone +if [ "$RESET" = true ]; then + echo "==> --reset: removing $EINO_DIR/" + rm -rf "$EINO_DIR" +fi + +# --update: pull latest on existing clones +if [ "$UPDATE" = true ]; then + for repo in eino eino-ext eino-examples; do + dir="$EINO_DIR/$repo" + if [ -d "$dir/.git" ]; then + echo "==> Updating $dir/" + git -C "$dir" pull --ff-only + else + echo "==> $dir/ not found, skipping update (run without --update to clone)" + fi + done + echo "" + echo "Done. Run 'bash eino_setup.sh' to clone any missing repos." + exit 0 +fi + +mkdir -p "$EINO_DIR" + +# Clone repos (shallow — we only need source to read, not full history) +clone_if_missing() { + local repo_url="$1" + local dest="$2" + if [ ! -d "$dest/.git" ]; then + echo "==> Cloning $(basename "$dest")/" + git clone --depth=1 "$repo_url" "$dest" + else + echo "==> $dest/ already exists, skipping clone" + fi +} + +clone_if_missing "$EINO_REPO" "$EINO_DIR/eino" +clone_if_missing "$EINO_EXT_REPO" "$EINO_DIR/eino-ext" +clone_if_missing "$EINO_EXAMPLES_REPO" "$EINO_DIR/eino-examples" + +# Add _eino/ to .gitignore +GITIGNORE=".gitignore" +if ! grep -qxF "$EINO_DIR/" "$GITIGNORE" 2>/dev/null; then + echo "" >> "$GITIGNORE" + echo "# eino framework source (AI coding reference — see eino_setup.sh)" >> "$GITIGNORE" + echo "$EINO_DIR/" >> "$GITIGNORE" + echo "==> Added '$EINO_DIR/' to $GITIGNORE" +fi + +# Write a README so the directory is self-explanatory +cat > "$EINO_DIR/README.md" <<'EOF' +# _eino — eino framework source reference + +This directory contains read-only clones of the eino framework repositories, +checked out for use by AI coding assistants (Claude Code, Cursor, Copilot, etc.). + +| Directory | Repository | Purpose | +|----------------|-----------------------------------------|--------------------------------| +| `eino/` | github.com/cloudwego/eino | Core framework source | +| `eino-ext/` | github.com/cloudwego/eino-ext | Components and integrations | +| `eino-examples/` | github.com/cloudwego/eino-examples | Patterns, recipes, and samples | + +**Do not edit files here.** This directory is in `.gitignore` and is never committed. + +To update to the latest: + + bash eino_setup.sh --update + +To re-clone from scratch: + + bash eino_setup.sh --reset +EOF + +echo "" +echo "Done. Your AI assistant now has full eino context in $EINO_DIR/:" +echo " $EINO_DIR/eino/ — core framework ($(find "$EINO_DIR/eino" -name "*.go" | wc -l | tr -d ' ') .go files)" +echo " $EINO_DIR/eino-ext/ — components & integrations ($(find "$EINO_DIR/eino-ext" -name "*.go" | wc -l | tr -d ' ') .go files)" +echo " $EINO_DIR/eino-examples/ — patterns & recipes ($(find "$EINO_DIR/eino-examples" -name "*.go" | wc -l | tr -d ' ') .go files)" +echo "" +echo "Add the following to your AI assistant's system prompt or project instructions" +echo "(e.g. CLAUDE.md, .cursorrules, .github/copilot-instructions.md):" +echo "" +echo "---" +cat <<'PROMPT' +## eino Framework Reference + +This project uses the eino framework (github.com/cloudwego/eino). +The full framework source is available locally in `_eino/`: + +- `_eino/eino/` — core framework (components, graph, compose, callbacks) +- `_eino/eino-ext/` — official components and integrations (models, tools, retrievers, etc.) +- `_eino/eino-examples/` — working examples and patterns + +When answering questions about eino APIs, component wiring, graph construction, +callbacks, or any eino-specific patterns: explore `_eino/` first. +Prefer examples from `_eino/eino-examples/` as the canonical reference for +idiomatic usage. +PROMPT +echo "---" diff --git a/utils/callbacks/template.go b/utils/callbacks/template.go new file mode 100644 index 0000000..850e301 --- /dev/null +++ b/utils/callbacks/template.go @@ -0,0 +1,793 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package callbacks provides ready-to-use callback handler templates for components. +package callbacks + +import ( + "context" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// NewHandlerHelper creates a new component template handler builder. +// This builder can be used to configure and build a component template handler, +// which can handle callback events for different components with its own struct definition, +// and fallbackTemplate can be used to handle scenarios where none of the cases are hit as a fallback. +func NewHandlerHelper() *HandlerHelper { + return &HandlerHelper{ + composeTemplates: map[components.Component]callbacks.Handler{}, + } +} + +// HandlerHelper is a builder for creating a callbacks.Handler with specific handlers for different component types. +// create a handler with callbacks.NewHandlerHelper(). +// eg. +// +// helper := template.NewHandlerHelper(). +// ChatModel(&model.IndexerCallbackHandler{}). +// Prompt(&prompt.IndexerCallbackHandler{}). +// Handler() +// +// then use the handler with runnable.Invoke(ctx, input, compose.WithCallbacks(handler)) +type HandlerHelper struct { + promptHandler *PromptCallbackHandler + chatModelHandler *ModelCallbackHandler + embeddingHandler *EmbeddingCallbackHandler + indexerHandler *IndexerCallbackHandler + retrieverHandler *RetrieverCallbackHandler + loaderHandler *LoaderCallbackHandler + transformerHandler *TransformerCallbackHandler + toolHandler *ToolCallbackHandler + toolsNodeHandler *ToolsNodeCallbackHandlers + agentHandler *AgentCallbackHandler + agenticAgentHandler *AgenticAgentCallbackHandler + agenticPromptHandler *AgenticPromptCallbackHandler + agenticModelHandler *AgenticModelCallbackHandler + agenticToolsNodeHandler *AgenticToolsNodeCallbackHandlers + composeTemplates map[components.Component]callbacks.Handler +} + +// Handler returns the callbacks.Handler created by HandlerHelper. +func (c *HandlerHelper) Handler() callbacks.Handler { + return &handlerTemplate{c} +} + +// Prompt sets the prompt handler for the handler helper, which will be called when the prompt component is executed. +func (c *HandlerHelper) Prompt(handler *PromptCallbackHandler) *HandlerHelper { + c.promptHandler = handler + return c +} + +// ChatModel sets the chat model handler for the handler helper, which will be called when the chat model component is executed. +func (c *HandlerHelper) ChatModel(handler *ModelCallbackHandler) *HandlerHelper { + c.chatModelHandler = handler + return c +} + +// Embedding sets the embedding handler for the handler helper, which will be called when the embedding component is executed. +func (c *HandlerHelper) Embedding(handler *EmbeddingCallbackHandler) *HandlerHelper { + c.embeddingHandler = handler + return c +} + +// Indexer sets the indexer handler for the handler helper, which will be called when the indexer component is executed. +func (c *HandlerHelper) Indexer(handler *IndexerCallbackHandler) *HandlerHelper { + c.indexerHandler = handler + return c +} + +// Retriever sets the retriever handler for the handler helper, which will be called when the retriever component is executed. +func (c *HandlerHelper) Retriever(handler *RetrieverCallbackHandler) *HandlerHelper { + c.retrieverHandler = handler + return c +} + +// Loader sets the loader handler for the handler helper, which will be called when the loader component is executed. +func (c *HandlerHelper) Loader(handler *LoaderCallbackHandler) *HandlerHelper { + c.loaderHandler = handler + return c +} + +// Transformer sets the transformer handler for the handler helper, which will be called when the transformer component is executed. +func (c *HandlerHelper) Transformer(handler *TransformerCallbackHandler) *HandlerHelper { + c.transformerHandler = handler + return c +} + +// Tool sets the tool handler for the handler helper, which will be called when the tool component is executed. +func (c *HandlerHelper) Tool(handler *ToolCallbackHandler) *HandlerHelper { + c.toolHandler = handler + return c +} + +// ToolsNode sets the tools node handler for the handler helper, which will be called when the tools node is executed. +func (c *HandlerHelper) ToolsNode(handler *ToolsNodeCallbackHandlers) *HandlerHelper { + c.toolsNodeHandler = handler + return c +} + +// AgenticPrompt sets the agentic prompt handler for the handler helper, which will be called when the agentic prompt component is executed. +func (c *HandlerHelper) AgenticPrompt(handler *AgenticPromptCallbackHandler) *HandlerHelper { + c.agenticPromptHandler = handler + return c +} + +// AgenticModel sets the agentic chat model handler for the handler helper, which will be called when the agentic chat model component is executed. +func (c *HandlerHelper) AgenticModel(handler *AgenticModelCallbackHandler) *HandlerHelper { + c.agenticModelHandler = handler + return c +} + +// AgenticToolsNode sets the agentic tools node handler for the handler helper, which will be called when the agentic tools node is executed. +func (c *HandlerHelper) AgenticToolsNode(handler *AgenticToolsNodeCallbackHandlers) *HandlerHelper { + c.agenticToolsNodeHandler = handler + return c +} + +// Agent sets the agent handler for the handler helper, which will be called when the agent is executed. +func (c *HandlerHelper) Agent(handler *AgentCallbackHandler) *HandlerHelper { + c.agentHandler = handler + return c +} + +// AgenticAgent sets the agentic agent callback handler for the handler helper, which will be called when an agentic agent is executed. +func (c *HandlerHelper) AgenticAgent(handler *AgenticAgentCallbackHandler) *HandlerHelper { + c.agenticAgentHandler = handler + return c +} + +// Graph sets the graph handler for the handler helper, which will be called when the graph is executed. +func (c *HandlerHelper) Graph(handler callbacks.Handler) *HandlerHelper { + c.composeTemplates[compose.ComponentOfGraph] = handler + return c +} + +// Chain sets the chain handler for the handler helper, which will be called when the chain is executed. +func (c *HandlerHelper) Chain(handler callbacks.Handler) *HandlerHelper { + c.composeTemplates[compose.ComponentOfChain] = handler + return c +} + +// Lambda sets the lambda handler for the handler helper, which will be called when the lambda is executed. +func (c *HandlerHelper) Lambda(handler callbacks.Handler) *HandlerHelper { + c.composeTemplates[compose.ComponentOfLambda] = handler + return c +} + +type handlerTemplate struct { + *HandlerHelper +} + +// OnStart is the callback function for the start event of a component. +// implement the callbacks Handler interface. +func (c *handlerTemplate) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + switch info.Component { + case components.ComponentOfPrompt: + return c.promptHandler.OnStart(ctx, info, prompt.ConvCallbackInput(input)) + case components.ComponentOfAgenticPrompt: + return c.agenticPromptHandler.OnStart(ctx, info, prompt.ConvCallbackInput(input)) + case components.ComponentOfChatModel: + return c.chatModelHandler.OnStart(ctx, info, model.ConvCallbackInput(input)) + case components.ComponentOfAgenticModel: + return c.agenticModelHandler.OnStart(ctx, info, model.ConvAgenticCallbackInput(input)) + case components.ComponentOfEmbedding: + return c.embeddingHandler.OnStart(ctx, info, embedding.ConvCallbackInput(input)) + case components.ComponentOfIndexer: + return c.indexerHandler.OnStart(ctx, info, indexer.ConvCallbackInput(input)) + case components.ComponentOfRetriever: + return c.retrieverHandler.OnStart(ctx, info, retriever.ConvCallbackInput(input)) + case components.ComponentOfLoader: + return c.loaderHandler.OnStart(ctx, info, document.ConvLoaderCallbackInput(input)) + case components.ComponentOfTransformer: + return c.transformerHandler.OnStart(ctx, info, document.ConvTransformerCallbackInput(input)) + case components.ComponentOfTool: + return c.toolHandler.OnStart(ctx, info, tool.ConvCallbackInput(input)) + case compose.ComponentOfToolsNode: + return c.toolsNodeHandler.OnStart(ctx, info, convToolsNodeCallbackInput(input)) + case compose.ComponentOfAgenticToolsNode: + return c.agenticToolsNodeHandler.OnStart(ctx, info, convAgenticToolsNodeCallbackInput(input)) + case adk.ComponentOfAgent: + return c.agentHandler.OnStart(ctx, info, adk.ConvAgentCallbackInput(input)) + case adk.ComponentOfAgenticAgent: + return c.agenticAgentHandler.OnStart(ctx, info, adk.ConvTypedCallbackInput[*schema.AgenticMessage](input)) + case compose.ComponentOfGraph, + compose.ComponentOfChain, + compose.ComponentOfLambda: + return c.composeTemplates[info.Component].OnStart(ctx, info, input) + default: + return ctx + } +} + +// OnEnd is the callback function for the end event of a component. +// implement the callbacks Handler interface. +func (c *handlerTemplate) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + switch info.Component { + case components.ComponentOfPrompt: + return c.promptHandler.OnEnd(ctx, info, prompt.ConvCallbackOutput(output)) + case components.ComponentOfAgenticPrompt: + return c.agenticPromptHandler.OnEnd(ctx, info, prompt.ConvCallbackOutput(output)) + case components.ComponentOfChatModel: + return c.chatModelHandler.OnEnd(ctx, info, model.ConvCallbackOutput(output)) + case components.ComponentOfAgenticModel: + return c.agenticModelHandler.OnEnd(ctx, info, model.ConvAgenticCallbackOutput(output)) + case components.ComponentOfEmbedding: + return c.embeddingHandler.OnEnd(ctx, info, embedding.ConvCallbackOutput(output)) + case components.ComponentOfIndexer: + return c.indexerHandler.OnEnd(ctx, info, indexer.ConvCallbackOutput(output)) + case components.ComponentOfRetriever: + return c.retrieverHandler.OnEnd(ctx, info, retriever.ConvCallbackOutput(output)) + case components.ComponentOfLoader: + return c.loaderHandler.OnEnd(ctx, info, document.ConvLoaderCallbackOutput(output)) + case components.ComponentOfTransformer: + return c.transformerHandler.OnEnd(ctx, info, document.ConvTransformerCallbackOutput(output)) + case components.ComponentOfTool: + return c.toolHandler.OnEnd(ctx, info, tool.ConvCallbackOutput(output)) + case compose.ComponentOfToolsNode: + return c.toolsNodeHandler.OnEnd(ctx, info, convToolsNodeCallbackOutput(output)) + case compose.ComponentOfAgenticToolsNode: + return c.agenticToolsNodeHandler.OnEnd(ctx, info, convAgenticToolsNodeCallbackOutput(output)) + case adk.ComponentOfAgent: + return c.agentHandler.OnEnd(ctx, info, adk.ConvAgentCallbackOutput(output)) + case adk.ComponentOfAgenticAgent: + return c.agenticAgentHandler.OnEnd(ctx, info, adk.ConvTypedCallbackOutput[*schema.AgenticMessage](output)) + case compose.ComponentOfGraph, + compose.ComponentOfChain, + compose.ComponentOfLambda: + return c.composeTemplates[info.Component].OnEnd(ctx, info, output) + default: + return ctx + } +} + +// OnError is the callback function for the error event of a component. +// implement the callbacks Handler interface. +func (c *handlerTemplate) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + switch info.Component { + case components.ComponentOfPrompt: + return c.promptHandler.OnError(ctx, info, err) + case components.ComponentOfAgenticPrompt: + return c.agenticPromptHandler.OnError(ctx, info, err) + case components.ComponentOfChatModel: + return c.chatModelHandler.OnError(ctx, info, err) + case components.ComponentOfAgenticModel: + return c.agenticModelHandler.OnError(ctx, info, err) + case components.ComponentOfEmbedding: + return c.embeddingHandler.OnError(ctx, info, err) + case components.ComponentOfIndexer: + return c.indexerHandler.OnError(ctx, info, err) + case components.ComponentOfRetriever: + return c.retrieverHandler.OnError(ctx, info, err) + case components.ComponentOfLoader: + return c.loaderHandler.OnError(ctx, info, err) + case components.ComponentOfTransformer: + return c.transformerHandler.OnError(ctx, info, err) + case components.ComponentOfTool: + return c.toolHandler.OnError(ctx, info, err) + case compose.ComponentOfToolsNode: + return c.toolsNodeHandler.OnError(ctx, info, err) + case compose.ComponentOfAgenticToolsNode: + return c.agenticToolsNodeHandler.OnError(ctx, info, err) + case compose.ComponentOfGraph, + compose.ComponentOfChain, + compose.ComponentOfLambda: + return c.composeTemplates[info.Component].OnError(ctx, info, err) + default: + return ctx + } +} + +// OnStartWithStreamInput is the callback function for the start event of a component with stream input. +// implement the callbacks Handler interface. +func (c *handlerTemplate) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + switch info.Component { + // currently no components.Component receive stream as input + case compose.ComponentOfGraph, + compose.ComponentOfChain, + compose.ComponentOfLambda: + return c.composeTemplates[info.Component].OnStartWithStreamInput(ctx, info, input) + default: + return ctx + } +} + +// OnEndWithStreamOutput is the callback function for the end event of a component with stream output. +// implement the callbacks Handler interface. +func (c *handlerTemplate) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context { + switch info.Component { + case components.ComponentOfChatModel: + return c.chatModelHandler.OnEndWithStreamOutput(ctx, info, + schema.StreamReaderWithConvert(output, func(item callbacks.CallbackOutput) (*model.CallbackOutput, error) { + return model.ConvCallbackOutput(item), nil + })) + case components.ComponentOfAgenticModel: + return c.agenticModelHandler.OnEndWithStreamOutput(ctx, info, + schema.StreamReaderWithConvert(output, func(item callbacks.CallbackOutput) (*model.AgenticCallbackOutput, error) { + return model.ConvAgenticCallbackOutput(item), nil + })) + case components.ComponentOfTool: + return c.toolHandler.OnEndWithStreamOutput(ctx, info, + schema.StreamReaderWithConvert(output, func(item callbacks.CallbackOutput) (*tool.CallbackOutput, error) { + return tool.ConvCallbackOutput(item), nil + })) + case compose.ComponentOfToolsNode: + return c.toolsNodeHandler.OnEndWithStreamOutput(ctx, info, + schema.StreamReaderWithConvert(output, func(item callbacks.CallbackOutput) ([]*schema.Message, error) { + return convToolsNodeCallbackOutput(item), nil + })) + case compose.ComponentOfAgenticToolsNode: + return c.agenticToolsNodeHandler.OnEndWithStreamOutput(ctx, info, + schema.StreamReaderWithConvert(output, func(item callbacks.CallbackOutput) ([]*schema.AgenticMessage, error) { + return convAgenticToolsNodeCallbackOutput(item), nil + })) + case compose.ComponentOfGraph, + compose.ComponentOfChain, + compose.ComponentOfLambda: + return c.composeTemplates[info.Component].OnEndWithStreamOutput(ctx, info, output) + default: + return ctx + } +} + +// Needed checks if the callback handler is needed for the given timing. +// +//nolint:cyclop +func (c *handlerTemplate) Needed(ctx context.Context, info *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + if info == nil { + return false + } + + switch info.Component { + case components.ComponentOfChatModel: + if c.chatModelHandler != nil && c.chatModelHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfAgenticModel: + if c.agenticModelHandler != nil && c.agenticModelHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfEmbedding: + if c.embeddingHandler != nil && c.embeddingHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfIndexer: + if c.indexerHandler != nil && c.indexerHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfLoader: + if c.loaderHandler != nil && c.loaderHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfPrompt: + if c.promptHandler != nil && c.promptHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfAgenticPrompt: + if c.agenticPromptHandler != nil && c.agenticPromptHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfRetriever: + if c.retrieverHandler != nil && c.retrieverHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfTool: + if c.toolHandler != nil && c.toolHandler.Needed(ctx, info, timing) { + return true + } + case components.ComponentOfTransformer: + if c.transformerHandler != nil && c.transformerHandler.Needed(ctx, info, timing) { + return true + } + case compose.ComponentOfToolsNode: + if c.toolsNodeHandler != nil && c.toolsNodeHandler.Needed(ctx, info, timing) { + return true + } + case compose.ComponentOfAgenticToolsNode: + if c.agenticToolsNodeHandler != nil && c.agenticToolsNodeHandler.Needed(ctx, info, timing) { + return true + } + case adk.ComponentOfAgent: + if c.agentHandler != nil && c.agentHandler.Needed(ctx, info, timing) { + return true + } + case adk.ComponentOfAgenticAgent: + if c.agenticAgentHandler != nil && c.agenticAgentHandler.Needed(ctx, info, timing) { + return true + } + case compose.ComponentOfGraph, + compose.ComponentOfChain, + compose.ComponentOfLambda: + handler := c.composeTemplates[info.Component] + if handler != nil { + checker, ok := handler.(callbacks.TimingChecker) + if !ok || checker.Needed(ctx, info, timing) { + return true + } + } + default: + return false + } + + return false +} + +// LoaderCallbackHandler is the handler for the loader callback. +type LoaderCallbackHandler struct { + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *document.LoaderCallbackInput) context.Context + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *document.LoaderCallbackOutput) context.Context + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *LoaderCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// TransformerCallbackHandler is the handler for the transformer callback. +type TransformerCallbackHandler struct { + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *document.TransformerCallbackInput) context.Context + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *document.TransformerCallbackOutput) context.Context + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *TransformerCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// EmbeddingCallbackHandler is the handler for the embedding callback. +type EmbeddingCallbackHandler struct { + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *embedding.CallbackInput) context.Context + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *embedding.CallbackOutput) context.Context + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *EmbeddingCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// IndexerCallbackHandler is the handler for the indexer callback. +type IndexerCallbackHandler struct { + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *indexer.CallbackInput) context.Context + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *indexer.CallbackOutput) context.Context + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *IndexerCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// ModelCallbackHandler is the handler for the model callback. +type ModelCallbackHandler struct { + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *model.CallbackInput) context.Context + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *model.CallbackOutput) context.Context + OnEndWithStreamOutput func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[*model.CallbackOutput]) context.Context + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *ModelCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + case callbacks.TimingOnEndWithStreamOutput: + return ch.OnEndWithStreamOutput != nil + default: + return false + } +} + +// PromptCallbackHandler is the handler for the callback. +type PromptCallbackHandler struct { + // OnStart is the callback function for the start of the callback. + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context + // OnEnd is the callback function for the end of the callback. + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *prompt.CallbackOutput) context.Context + // OnError is the callback function for the error of the callback. + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *PromptCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// RetrieverCallbackHandler is the handler for the retriever callback. +type RetrieverCallbackHandler struct { + // OnStart is the callback function for the start of the retriever. + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *retriever.CallbackInput) context.Context + // OnEnd is the callback function for the end of the retriever. + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *retriever.CallbackOutput) context.Context + // OnError is the callback function for the error of the retriever. + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *RetrieverCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// ToolCallbackHandler is the handler for the tool callback. +type ToolCallbackHandler struct { + OnStart func(ctx context.Context, info *callbacks.RunInfo, input *tool.CallbackInput) context.Context + OnEnd func(ctx context.Context, info *callbacks.RunInfo, output *tool.CallbackOutput) context.Context + OnEndWithStreamOutput func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[*tool.CallbackOutput]) context.Context + OnError func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *ToolCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnEndWithStreamOutput: + return ch.OnEndWithStreamOutput != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// ToolsNodeCallbackHandlers defines optional callbacks for the Tools node +// lifecycle events. +type ToolsNodeCallbackHandlers struct { + OnStart func(ctx context.Context, info *callbacks.RunInfo, input *schema.Message) context.Context + OnEnd func(ctx context.Context, info *callbacks.RunInfo, input []*schema.Message) context.Context + OnEndWithStreamOutput func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[[]*schema.Message]) context.Context + OnError func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context +} + +// Needed reports whether a handler is registered for the given timing. +func (ch *ToolsNodeCallbackHandlers) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnEndWithStreamOutput: + return ch.OnEndWithStreamOutput != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +func convToolsNodeCallbackInput(src callbacks.CallbackInput) *schema.Message { + switch t := src.(type) { + case *schema.Message: + return t + default: + return nil + } +} + +func convToolsNodeCallbackOutput(src callbacks.CallbackInput) []*schema.Message { + switch t := src.(type) { + case []*schema.Message: + return t + default: + return nil + } +} + +// AgentCallbackHandler handles callbacks for agents using *schema.Message. +// Use ComponentOfAgent to filter callback events to agent-related events. +type AgentCallbackHandler struct { + // OnStart is called when an agent run begins. Return a modified context to propagate values. + OnStart func(ctx context.Context, info *callbacks.RunInfo, input *adk.AgentCallbackInput) context.Context + // OnEnd is called when an agent run completes. The output's Events iterator should be + // consumed asynchronously to avoid blocking. + OnEnd func(ctx context.Context, info *callbacks.RunInfo, output *adk.AgentCallbackOutput) context.Context +} + +func (ch *AgentCallbackHandler) Needed(ctx context.Context, info *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + default: + return false + } +} + +// AgenticAgentCallbackHandler handles callbacks for agentic agents using *schema.AgenticMessage. +// Use ComponentOfAgenticAgent to filter callback events to agentic-agent-related events. +type AgenticAgentCallbackHandler struct { + // OnStart is called when an agentic agent run begins. Return a modified context to propagate values. + OnStart func(ctx context.Context, info *callbacks.RunInfo, input *adk.TypedAgentCallbackInput[*schema.AgenticMessage]) context.Context + // OnEnd is called when an agentic agent run completes. The output's Events iterator should be + // consumed asynchronously to avoid blocking. + OnEnd func(ctx context.Context, info *callbacks.RunInfo, output *adk.TypedAgentCallbackOutput[*schema.AgenticMessage]) context.Context +} + +func (ch *AgenticAgentCallbackHandler) Needed(ctx context.Context, info *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + default: + return false + } +} + +// AgenticPromptCallbackHandler is the handler for the agentic prompt callback. +type AgenticPromptCallbackHandler struct { + // OnStart is the callback function for the start of the agentic prompt. + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context + // OnEnd is the callback function for the end of the agentic prompt. + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *prompt.CallbackOutput) context.Context + // OnError is the callback function for the error of the agentic prompt. + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *AgenticPromptCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +// AgenticModelCallbackHandler is the handler for the agentic chat model callback. +type AgenticModelCallbackHandler struct { + OnStart func(ctx context.Context, runInfo *callbacks.RunInfo, input *model.AgenticCallbackInput) context.Context + OnEnd func(ctx context.Context, runInfo *callbacks.RunInfo, output *model.AgenticCallbackOutput) context.Context + OnEndWithStreamOutput func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[*model.AgenticCallbackOutput]) context.Context + OnError func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context +} + +// Needed checks if the callback handler is needed for the given timing. +func (ch *AgenticModelCallbackHandler) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnError: + return ch.OnError != nil + case callbacks.TimingOnEndWithStreamOutput: + return ch.OnEndWithStreamOutput != nil + default: + return false + } +} + +// AgenticToolsNodeCallbackHandlers defines optional callbacks for the Agentic Tools node +// lifecycle events. +type AgenticToolsNodeCallbackHandlers struct { + OnStart func(ctx context.Context, info *callbacks.RunInfo, input *schema.AgenticMessage) context.Context + OnEnd func(ctx context.Context, info *callbacks.RunInfo, input []*schema.AgenticMessage) context.Context + OnEndWithStreamOutput func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[[]*schema.AgenticMessage]) context.Context + OnError func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context +} + +// Needed reports whether a handler is registered for the given timing. +func (ch *AgenticToolsNodeCallbackHandlers) Needed(ctx context.Context, runInfo *callbacks.RunInfo, timing callbacks.CallbackTiming) bool { + switch timing { + case callbacks.TimingOnStart: + return ch.OnStart != nil + case callbacks.TimingOnEnd: + return ch.OnEnd != nil + case callbacks.TimingOnEndWithStreamOutput: + return ch.OnEndWithStreamOutput != nil + case callbacks.TimingOnError: + return ch.OnError != nil + default: + return false + } +} + +func convAgenticToolsNodeCallbackInput(src callbacks.CallbackInput) *schema.AgenticMessage { + switch t := src.(type) { + case *schema.AgenticMessage: + return t + default: + return nil + } +} + +func convAgenticToolsNodeCallbackOutput(src callbacks.CallbackInput) []*schema.AgenticMessage { + switch t := src.(type) { + case []*schema.AgenticMessage: + return t + default: + return nil + } +} diff --git a/utils/callbacks/template_test.go b/utils/callbacks/template_test.go new file mode 100644 index 0000000..79be157 --- /dev/null +++ b/utils/callbacks/template_test.go @@ -0,0 +1,807 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package callbacks + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +func TestNewComponentTemplate(t *testing.T) { + t.Run("TestNewComponentTemplate", func(t *testing.T) { + cnt := 0 + tpl := NewHandlerHelper() + tpl.ChatModel(&ModelCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *model.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *model.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnEndWithStreamOutput: func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[*model.CallbackOutput]) context.Context { + output.Close() + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }}). + Embedding(&EmbeddingCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *embedding.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *embedding.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + Prompt(&PromptCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *prompt.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + Retriever(&RetrieverCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *retriever.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *retriever.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + Tool(&ToolCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *tool.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *tool.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnEndWithStreamOutput: func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[*tool.CallbackOutput]) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + Lambda(callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + cnt++ + return ctx + }). + OnStartWithStreamInputFn(func(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context { + input.Close() + cnt++ + return ctx + }). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + cnt++ + return ctx + }). + OnEndWithStreamOutputFn(func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context { + output.Close() + cnt++ + return ctx + }). + OnErrorFn(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }).Build()). + AgenticModel(&AgenticModelCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *model.AgenticCallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *model.AgenticCallbackOutput) context.Context { + cnt++ + return ctx + }, + OnEndWithStreamOutput: func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[*model.AgenticCallbackOutput]) context.Context { + output.Close() + cnt++ + return ctx + }, + OnError: func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + AgenticPrompt(&AgenticPromptCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *prompt.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + AgenticToolsNode(&AgenticToolsNodeCallbackHandlers{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *schema.AgenticMessage) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, input []*schema.AgenticMessage) context.Context { + cnt++ + return ctx + }, + OnEndWithStreamOutput: func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[[]*schema.AgenticMessage]) context.Context { + output.Close() + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }). + Handler() + + types := []components.Component{ + components.ComponentOfPrompt, + components.ComponentOfChatModel, + components.ComponentOfEmbedding, + components.ComponentOfRetriever, + components.ComponentOfTool, + compose.ComponentOfLambda, + components.ComponentOfAgenticModel, + components.ComponentOfAgenticPrompt, + compose.ComponentOfAgenticToolsNode, + } + + handler := tpl.Handler() + ctx := context.Background() + for _, typ := range types { + handler.OnStart(ctx, &callbacks.RunInfo{Component: typ}, nil) + handler.OnEnd(ctx, &callbacks.RunInfo{Component: typ}, nil) + handler.OnError(ctx, &callbacks.RunInfo{Component: typ}, fmt.Errorf("mock err")) + + sir, siw := schema.Pipe[callbacks.CallbackInput](1) + siw.Close() + handler.OnStartWithStreamInput(ctx, &callbacks.RunInfo{Component: typ}, sir) + + sor, sow := schema.Pipe[callbacks.CallbackOutput](1) + sow.Close() + handler.OnEndWithStreamOutput(ctx, &callbacks.RunInfo{Component: typ}, sor) + } + + assert.Equal(t, 33, cnt) + + ctx = context.Background() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{Component: components.ComponentOfTransformer}, handler) + callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 33, cnt) + + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{Component: components.ComponentOfPrompt}) + ctx = callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 34, cnt) + + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{Component: components.ComponentOfIndexer}) + callbacks.OnEnd[any](ctx, nil) + assert.Equal(t, 34, cnt) + + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{Component: components.ComponentOfEmbedding}) + callbacks.OnError(ctx, nil) + assert.Equal(t, 35, cnt) + + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{Component: components.ComponentOfLoader}) + callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 35, cnt) + + tpl.Transformer(&TransformerCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *document.TransformerCallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *document.TransformerCallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }).Indexer(&IndexerCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *indexer.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *indexer.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }).Loader(&LoaderCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *document.LoaderCallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *document.LoaderCallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }).ToolsNode(&ToolsNodeCallbackHandlers{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *schema.Message) context.Context { + cnt++ + return ctx + }, + OnEndWithStreamOutput: func(ctx context.Context, runInfo *callbacks.RunInfo, output *schema.StreamReader[[]*schema.Message]) context.Context { + cnt++ + + if output == nil { + return ctx + } + + for { + _, err := output.Recv() + if err != nil { + return ctx + } + } + }, + }).AgenticPrompt(&AgenticPromptCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, runInfo *callbacks.RunInfo, output *prompt.CallbackOutput) context.Context { + cnt++ + return ctx + }, + OnError: func(ctx context.Context, runInfo *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }).AgenticToolsNode(&AgenticToolsNodeCallbackHandlers{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *schema.AgenticMessage) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, input []*schema.AgenticMessage) context.Context { + cnt++ + return ctx + }, + OnEndWithStreamOutput: func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[[]*schema.AgenticMessage]) context.Context { + output.Close() + cnt++ + return ctx + }, + OnError: func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { + cnt++ + return ctx + }, + }) + + handler = tpl.Handler() + ctx = context.Background() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{Component: components.ComponentOfTransformer}, handler) + + ctx = callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 36, cnt) + + callbacks.OnEnd[any](ctx, nil) + assert.Equal(t, 37, cnt) + + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{Component: components.ComponentOfLoader}) + callbacks.OnEnd[any](ctx, nil) + assert.Equal(t, 38, cnt) + + ctx = callbacks.ReuseHandlers(ctx, &callbacks.RunInfo{Component: compose.ComponentOfToolsNode}) + callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 39, cnt) + + sr, sw := schema.Pipe[any](0) + sw.Close() + callbacks.OnEndWithStreamOutput[any](ctx, sr) + assert.Equal(t, 40, cnt) + + sr1, sw1 := schema.Pipe[[]*schema.Message](1) + sw1.Send([]*schema.Message{{}}, nil) + sw1.Close() + callbacks.OnEndWithStreamOutput[[]*schema.Message](ctx, sr1) + // Check AgenticModel stream + sir2, siw2 := schema.Pipe[callbacks.CallbackOutput](1) + siw2.Close() + handler.OnEndWithStreamOutput(ctx, &callbacks.RunInfo{Component: components.ComponentOfAgenticModel}, sir2) + assert.Equal(t, 42, cnt) + + // Check AgenticToolsNode stream + sir3, siw3 := schema.Pipe[callbacks.CallbackOutput](1) + siw3.Close() + handler.OnEndWithStreamOutput(ctx, &callbacks.RunInfo{Component: compose.ComponentOfAgenticToolsNode}, sir3) + assert.Equal(t, 43, cnt) + + ctx = callbacks.ReuseHandlers(ctx, nil) + callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 43, cnt) + }) + + t.Run("EdgeCases", func(t *testing.T) { + ctx := context.Background() + cnt := 0 + + // 1. Test Graph and Chain Setters and Execution + tpl := NewHandlerHelper(). + Graph(callbacks.NewHandlerBuilder(). + OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + cnt++ + return ctx + }).Build()). + Chain(callbacks.NewHandlerBuilder(). + OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + cnt++ + return ctx + }).Build()) + + h := tpl.Handler() + + // Trigger Graph OnStart + h.OnStart(ctx, &callbacks.RunInfo{Component: compose.ComponentOfGraph}, nil) + assert.Equal(t, 1, cnt) + + // Trigger Chain OnEnd + h.OnEnd(ctx, &callbacks.RunInfo{Component: compose.ComponentOfChain}, nil) + assert.Equal(t, 2, cnt) + + // 2. Test Needed logic for Graph/Chain when handler is present/absent + // Graph is present (OnStart) + needed := h.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: compose.ComponentOfGraph}, callbacks.TimingOnStart) + assert.True(t, needed) + + // Chain is present (OnEnd) - but we check OnStart which is not defined in the builder above? + // NewHandlerBuilder returns a handler that usually returns true for Needed if the specific func is not nil. + // Let's verify Chain OnStart is NOT needed because we only set OnEndFn. + needed = h.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: compose.ComponentOfChain}, callbacks.TimingOnStart) + assert.False(t, needed) // Should be false because OnStartFn wasn't set for Chain + + // Lambda is NOT present + needed = h.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: compose.ComponentOfLambda}, callbacks.TimingOnStart) + assert.False(t, needed) + + // 3. Test Conversion Fallbacks (Default cases) + // We need a handler with ToolsNode and AgenticToolsNode to test their conversion fallbacks + tpl2 := NewHandlerHelper(). + ToolsNode(&ToolsNodeCallbackHandlers{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *schema.Message) context.Context { + if input == nil { + cnt++ + } + return ctx + }, + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, input []*schema.Message) context.Context { + if input == nil { + cnt++ + } + return ctx + }, + }). + AgenticToolsNode(&AgenticToolsNodeCallbackHandlers{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *schema.AgenticMessage) context.Context { + if input == nil { + cnt++ + } + return ctx + }, + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, input []*schema.AgenticMessage) context.Context { + if input == nil { + cnt++ + } + return ctx + }, + }) + + h2 := tpl2.Handler() + + // Pass wrong type (string) to trigger default case in convToolsNodeCallbackInput -> returns nil + h2.OnStart(ctx, &callbacks.RunInfo{Component: compose.ComponentOfToolsNode}, "wrong-input-type") + assert.Equal(t, 3, cnt) // +1 + + // Pass wrong type to trigger default case in convToolsNodeCallbackOutput -> returns nil + h2.OnEnd(ctx, &callbacks.RunInfo{Component: compose.ComponentOfToolsNode}, "wrong-output-type") + assert.Equal(t, 4, cnt) // +1 + + // Pass wrong type to trigger default case in convAgenticToolsNodeCallbackInput -> returns nil + h2.OnStart(ctx, &callbacks.RunInfo{Component: compose.ComponentOfAgenticToolsNode}, "wrong-input-type") + assert.Equal(t, 5, cnt) // +1 + + // Pass wrong type to trigger default case in convAgenticToolsNodeCallbackOutput -> returns nil + h2.OnEnd(ctx, &callbacks.RunInfo{Component: compose.ComponentOfAgenticToolsNode}, "wrong-output-type") + assert.Equal(t, 6, cnt) // +1 + + // 4. Test Needed for Agentic components when handlers are Set vs Unset + // tpl2 has AgenticToolsNode set + needed = h2.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: compose.ComponentOfAgenticToolsNode}, callbacks.TimingOnStart) + assert.True(t, needed) + + // tpl2 does NOT have AgenticModel set + needed = h2.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfAgenticModel}, callbacks.TimingOnStart) + assert.False(t, needed) + + // Set it now + tpl2.AgenticModel(&AgenticModelCallbackHandler{ + OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *model.AgenticCallbackInput) context.Context { + return ctx + }, + }) + + needed = h2.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfAgenticModel}, callbacks.TimingOnStart) + assert.True(t, needed) + + // Check invalid component + needed = h2.(callbacks.TimingChecker).Needed(ctx, &callbacks.RunInfo{Component: "UnknownComponent"}, callbacks.TimingOnStart) + assert.False(t, needed) + + // Check RunInfo nil + needed = h2.(callbacks.TimingChecker).Needed(ctx, nil, callbacks.TimingOnStart) + assert.False(t, needed) + + // 5. Test Needed for Transformer, Loader, Indexer, etc to ensure switch coverage + tpl3 := NewHandlerHelper(). + Transformer(&TransformerCallbackHandler{OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *document.TransformerCallbackInput) context.Context { + return ctx + }}). + Loader(&LoaderCallbackHandler{OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *document.LoaderCallbackInput) context.Context { + return ctx + }}). + Indexer(&IndexerCallbackHandler{OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *indexer.CallbackInput) context.Context { + return ctx + }}). + Retriever(&RetrieverCallbackHandler{OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *retriever.CallbackInput) context.Context { + return ctx + }}). + Embedding(&EmbeddingCallbackHandler{OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *embedding.CallbackInput) context.Context { + return ctx + }}). + Tool(&ToolCallbackHandler{OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *tool.CallbackInput) context.Context { + return ctx + }}) + + h3 := tpl3.Handler() + checker := h3.(callbacks.TimingChecker) + + assert.True(t, checker.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfTransformer}, callbacks.TimingOnStart)) + assert.True(t, checker.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfLoader}, callbacks.TimingOnStart)) + assert.True(t, checker.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfIndexer}, callbacks.TimingOnStart)) + assert.True(t, checker.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfRetriever}, callbacks.TimingOnStart)) + assert.True(t, checker.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfEmbedding}, callbacks.TimingOnStart)) + assert.True(t, checker.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfTool}, callbacks.TimingOnStart)) + + // Verify False paths (by using a helper without them) + emptyH := NewHandlerHelper().Handler().(callbacks.TimingChecker) + assert.False(t, emptyH.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfTransformer}, callbacks.TimingOnStart)) + assert.False(t, emptyH.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfLoader}, callbacks.TimingOnStart)) + assert.False(t, emptyH.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfIndexer}, callbacks.TimingOnStart)) + assert.False(t, emptyH.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfRetriever}, callbacks.TimingOnStart)) + assert.False(t, emptyH.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfEmbedding}, callbacks.TimingOnStart)) + assert.False(t, emptyH.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfTool}, callbacks.TimingOnStart)) + + // 6. Test Needed for remaining components (ChatModel, Prompt, AgenticPrompt) + tpl4 := NewHandlerHelper(). + ChatModel(&ModelCallbackHandler{OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *model.CallbackInput) context.Context { + return ctx + }}). + Prompt(&PromptCallbackHandler{OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context { + return ctx + }}). + AgenticPrompt(&AgenticPromptCallbackHandler{OnStart: func(ctx context.Context, runInfo *callbacks.RunInfo, input *prompt.CallbackInput) context.Context { + return ctx + }}) + + h4 := tpl4.Handler() + checker4 := h4.(callbacks.TimingChecker) + + assert.True(t, checker4.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfChatModel}, callbacks.TimingOnStart)) + assert.True(t, checker4.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfPrompt}, callbacks.TimingOnStart)) + assert.True(t, checker4.Needed(ctx, &callbacks.RunInfo{Component: components.ComponentOfAgenticPrompt}, callbacks.TimingOnStart)) + }) +} + +func TestAgentCallbackHandler(t *testing.T) { + t.Run("Needed returns correct values", func(t *testing.T) { + handler := &AgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.AgentCallbackInput) context.Context { + return ctx + }, + } + + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgent} + + assert.True(t, handler.Needed(ctx, info, callbacks.TimingOnStart)) + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnEnd)) + }) + + t.Run("Needed with OnEnd set", func(t *testing.T) { + handler := &AgentCallbackHandler{ + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *adk.AgentCallbackOutput) context.Context { + return ctx + }, + } + + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgent} + + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnStart)) + assert.True(t, handler.Needed(ctx, info, callbacks.TimingOnEnd)) + }) + + t.Run("Needed with nil handlers", func(t *testing.T) { + handler := &AgentCallbackHandler{} + + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgent} + + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnStart)) + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnEnd)) + }) +} + +func TestHandlerHelperWithAgent(t *testing.T) { + t.Run("Agent method sets handler correctly", func(t *testing.T) { + cnt := 0 + tpl := NewHandlerHelper() + tpl.Agent(&AgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.AgentCallbackInput) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *adk.AgentCallbackOutput) context.Context { + cnt++ + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{Component: adk.ComponentOfAgent}, handler) + + ctx = callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 1, cnt) + + callbacks.OnEnd[any](ctx, nil) + assert.Equal(t, 2, cnt) + }) +} + +func TestHandlerTemplateWithAgentComponent(t *testing.T) { + t.Run("OnStart routes to agent handler", func(t *testing.T) { + called := false + tpl := NewHandlerHelper() + tpl.Agent(&AgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.AgentCallbackInput) context.Context { + called = true + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgent, Name: "TestAgent"} + + handler.OnStart(ctx, info, &adk.AgentCallbackInput{}) + assert.True(t, called) + }) + + t.Run("OnEnd routes to agent handler", func(t *testing.T) { + called := false + tpl := NewHandlerHelper() + tpl.Agent(&AgentCallbackHandler{ + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *adk.AgentCallbackOutput) context.Context { + called = true + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgent, Name: "TestAgent"} + + handler.OnEnd(ctx, info, &adk.AgentCallbackOutput{}) + assert.True(t, called) + }) + + t.Run("Needed returns true for agent component", func(t *testing.T) { + tpl := NewHandlerHelper() + tpl.Agent(&AgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.AgentCallbackInput) context.Context { + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgent} + + checker, ok := handler.(callbacks.TimingChecker) + assert.True(t, ok, "handler should implement TimingChecker") + assert.True(t, checker.Needed(ctx, info, callbacks.TimingOnStart)) + }) +} + +func TestAgenticAgentCallbackHandler(t *testing.T) { + t.Run("Needed returns correct values", func(t *testing.T) { + handler := &AgenticAgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.TypedAgentCallbackInput[*schema.AgenticMessage]) context.Context { + return ctx + }, + } + + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent} + + assert.True(t, handler.Needed(ctx, info, callbacks.TimingOnStart)) + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnEnd)) + }) + + t.Run("Needed with OnEnd set", func(t *testing.T) { + handler := &AgenticAgentCallbackHandler{ + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *adk.TypedAgentCallbackOutput[*schema.AgenticMessage]) context.Context { + return ctx + }, + } + + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent} + + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnStart)) + assert.True(t, handler.Needed(ctx, info, callbacks.TimingOnEnd)) + }) + + t.Run("Needed with nil handlers", func(t *testing.T) { + handler := &AgenticAgentCallbackHandler{} + + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent} + + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnStart)) + assert.False(t, handler.Needed(ctx, info, callbacks.TimingOnEnd)) + }) +} + +func TestHandlerHelperWithAgenticAgent(t *testing.T) { + t.Run("AgenticAgent method sets handler correctly", func(t *testing.T) { + cnt := 0 + tpl := NewHandlerHelper() + tpl.AgenticAgent(&AgenticAgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.TypedAgentCallbackInput[*schema.AgenticMessage]) context.Context { + cnt++ + return ctx + }, + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *adk.TypedAgentCallbackOutput[*schema.AgenticMessage]) context.Context { + cnt++ + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + ctx = callbacks.InitCallbacks(ctx, &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent}, handler) + + ctx = callbacks.OnStart[any](ctx, nil) + assert.Equal(t, 1, cnt) + + callbacks.OnEnd[any](ctx, nil) + assert.Equal(t, 2, cnt) + }) +} + +func TestHandlerTemplateWithAgenticAgentComponent(t *testing.T) { + t.Run("OnStart routes to agentic agent handler", func(t *testing.T) { + called := false + tpl := NewHandlerHelper() + tpl.AgenticAgent(&AgenticAgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.TypedAgentCallbackInput[*schema.AgenticMessage]) context.Context { + called = true + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent, Name: "TestAgenticAgent"} + + handler.OnStart(ctx, info, &adk.TypedAgentCallbackInput[*schema.AgenticMessage]{}) + assert.True(t, called) + }) + + t.Run("OnEnd routes to agentic agent handler", func(t *testing.T) { + called := false + tpl := NewHandlerHelper() + tpl.AgenticAgent(&AgenticAgentCallbackHandler{ + OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *adk.TypedAgentCallbackOutput[*schema.AgenticMessage]) context.Context { + called = true + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent, Name: "TestAgenticAgent"} + + handler.OnEnd(ctx, info, &adk.TypedAgentCallbackOutput[*schema.AgenticMessage]{}) + assert.True(t, called) + }) + + t.Run("Needed returns true for agentic agent component", func(t *testing.T) { + tpl := NewHandlerHelper() + tpl.AgenticAgent(&AgenticAgentCallbackHandler{ + OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *adk.TypedAgentCallbackInput[*schema.AgenticMessage]) context.Context { + return ctx + }, + }) + + handler := tpl.Handler() + ctx := context.Background() + info := &callbacks.RunInfo{Component: adk.ComponentOfAgenticAgent} + + checker, ok := handler.(callbacks.TimingChecker) + assert.True(t, ok, "handler should implement TimingChecker") + assert.True(t, checker.Needed(ctx, info, callbacks.TimingOnStart)) + }) +}

tX_lW7 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� z%AvN%pkrBBqb$jTT%V<6q;o8gcSJ_S5C?EI%CmS%Meqe7q2N;i-w0}9HFjcIB?n73WhADUEQ&-5&`Tt_(Z?``i#*;f z-i=cYC(_Stf0Hmd2x+4_AAWcADVVdLibW^DKWbQ19M;cYUpwkP1 z$?&b0N-7LF>WF04%upm|YC@y(IiI#jWlmO-Ze^w77)=qfmt+Koqu>oTP1khR5`P%Y zycFMAUFP`|OM{^1acT`rwxs~L9&g^BP=Hs3Wh3O640w76T!xf3&7N-#<=32KXBf~g z-JWrtr$Y)J3?^I*VB}np3UC(GK~W53?TKsxqGpbtc375Rz=`hVpM)sXZRyibv?5ay z=%J)1R7Qk6Dh4<4Bj)sC4~`Tn+LnG0AMF94U+&;U>SXLN4i$b$O`hHYlBmmsTTCK= z3`YMIP%R@-;@)6J=YVw%1PSJf(&%oErzer5SZSD9c9-HcVtEOvwcQU_O;idvQGp4j zZnBh$a3l`;rwcXEIKB=S!PH^m;8IbJSfyzrCMTGx%tJ;K%Sa?k`O*KRBl?x1k0K9~ z#1_U}WM67uehw5mI+U1Xp>)*8hLNJUm}j5v&uYTsEJnr7?5Gs=Bpyy!K+4cb_GVCU zPh7<%B_(3!kYGb?(PJ?lQtW_nb?T5h;Kyl83{)ro31FDK9u;;{ifq^Tq)`feVF49W zb=iy&Jx#^zi*LT3eGDkCzNCSIDvBxkTT@c5=u0F+=fJ;oX!h6ZftZ-2xBMwI&0%d(U&9y=el8wb@XpFss6C}BYC-=fZ>8)T z^{b}Br6LN1D0HjhmZ<3hDkWN;V%_FaRVC=O?3-Mci$Yg!&MBRa*h685(1zg8cu$

ga{`#gVnoRz0hDGV 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�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 EKWZE!cmMzZ literal 0 HcmV?d00001 diff --git a/.github/static/img/eino/eino_architecture_overview.png b/.github/static/img/eino/eino_architecture_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..f2667c923222f4b97cc15a2c32c6230c0e18f77e GIT binary patch literal 802559 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%0A@f4LmT#?->H%w#A~fHsE2SJ z3@`xTT7x|F>iGebRLt9xt-x}&I+Q#IeK`cD)TzlVj`{tr*m{JCaX zt=(50j4`q8o#x@7)n5=_!~9hP9EXhsGuQ)L{Go7YYfI>g2D-ly+0i~5HM`Gi_#o4& zcIvvDRJo}I3dB3H>Ri|G27LEf&>pW-UhIgu3>%~vB*5ISny>E@2SvL{lvkD`D}b8) z#_JM7{WmCsH~v`|aY@2vRd(jwTCYDPjwNkp>1bda)79=j~czsXVQi{XKn zFj7yMi#%#pTv;o0ae(j+$)yYx-O8sucM$VRFTF|17GfS-ayz}MkB*y1(O)8G5<;vh zV;q2(y_rN@aGh>52~TMsyNaxQX$mg85mwB;$s9++oKmC(vU(P2U2chRol9}=m$0X9= zV>pT?|9m0e?=)Ae2$6d_;$3KMBJQwE{@Xt`Yt)n<9xF<569saJ>G)668mF})m*&!IrMa5I_s&_E1qB6#G(DJK zof2{gP@Y`^a)#mO=^)J@ONSBSlfUPOYLcIj@-z?q(gr0(9JcrAFf`e37Lu@Mpye{Q zHv?;y&6<@h7rTJ0)bYdS?3xVA_2X`^u3SN=xf#G{CESJbDRQHk0Byt> zbiJ>71-UKjjgfBr*7g)Sl~>5A@PNkm0Q2UMjxTX{CwIb@lenmpoo;SM=r`J=H$ox$SzBuI=`iBgU}q+6CC_G8%)h2T+h7R}!mGq-g$ zyb9v%@wRU~Z%JVI@*2{;I>7Fg<@o^nT^8}|#8p=7!tEDQ8|YP2`#PGSzq|Pp`QkM= zYNU3sVrm#`v8F!pJu0sLn^a4epccgY6}yG}y?uVU>9jMYJnVfFt^jG;Sg$3tc!8I^ zRNfJ+i6EUM&ZYYh5{;>OoA!K8d3Qp_W6_N8YaQOH`9S}LkGGV!`;;6eMJi|Syom!j zQc8eLe80?yrzz#_)6c4d%0Z7}%gxg^wyEKq)LKHtz6XkCOoV9~uQjya*yefV>~8To zm2PQnq;(CSj`Wk>YXnn3nbovf*0-2!o#|LOP}GFCc3DYl2|`J!uO$Mi@*N9mB-lCi zcu}h$H%^_wK31+#E>=B&s!Hk8_dgviS}%d#8$gRWwX)NjmWST9zK*OA-APkbz#>)tvadEK*E}jdS#4==8>`-zEVU`%5cA=rr7iK*)T#0jLgkb+7_Yqf*z#}F z10Vu_i`nN2i!|E0eo^U$neEMUYmm6Z>!5evet~p7RR;= z+(>_cR2OyOVMyDNKw0?hPH?kmMHJ(LIsV!9{@5Z;$R&$4*E!xCWqMtbJyT@y0S)i& zbdCS%VaLJH+OSFw^}Knv&N~6C;NR^)acV!}@-!?ahql#J2xtE_p<99^G3uLP*CzO5 zJ+gV|!s%WhP^YEuKrs_;@@N=(AiFsqAx|mL04>+ZvR%1f?%H2AOsr!0=LMF!#23hn zUKq?4oRDbi;=148k#D7RCf6Pkr*E1e0rkfXXkpB$JANqSsA7@uLGfY;!^K>b!BPP` zHwzx<0J;TghV1YbfVj=qW@cs{sJ^j&{4cJ%o@XwAmDiVkBFvnk$V8ptSsu9GE~FRwip=HT`kR$h8mULg^#1Jk|cyC&UVx+{#d zgmQ0kz~Ao0*~cfsj%_mOt!GL)A@EZYA;nRqZec)II1!rUfF1z$or?Mos=Y zmCa2PC=COjP7F*KdAM!TctYx+%$i)7zOuEJ>w;SyWeRONLr~Nk1jG`KdhZxZV)x3r z6>ys#u3O#w!1n%+KA^di@Maq3&t>S5pbisq!0vh5AKaL@Z+@^`zhP(%6wy zSZ(0mD$vA3&--R|sD`u}>i%7V>-qQPpRBwiE_la>Wm$4}sCn(;u$y~-)f_MS9RR&c z%LPJ%Dj8?qmzv$?mQ&fVb6ZNF`kkXYZonKHAu9b;(PQfQWpJpF>Ud9Q)e>*x(Oiec zXiR%`>h1&Znxh1F?_qfd=is2>sJV++2u@kU@$@9}bpBGZ>miyj7a^Pl@8QKvJ;Y&frM2>5A9{|ZhHWo16k0v7 z?lpaN=L$+ti;27Cxf4Uu1gu4@CA`NN(srJkP$Qird5n<=aRz8<4Uj^1v{A7)!jnkT z!I34?S079?g7k!QFJ|V#4=5L0>{m60lO`Z)h$ z$H!HH#jb{mz6FJiMeR-tt$_s7)O?_StCM|3vbE>72F55%hS+K;^vqAuiOkFytz26a zP3u;%HEFd)^HQAp#^JO-kpo^2z+|3N&vpRd)d*TM^cheWV~t~^CpXxeyXfoDQLMZ$W`HVJ)DZ0RaTU#fMn(zz8+K#YJVtiwl%%xO42m%q zs|BNZKxg1rTk^-<{4V?=Rcuu3aO(=a;~Shf-rb$Rg=DEsA!kK(1>2E! zNNk+^WZbXW2d;;`W^!qIsB*4gHqH35PjBWwJ-gYUXO3kkh{fU@%r0^(F2kS^3|jf* z=L7|HZED+tg@rDWM*Bo&^jDr%sR*EraXNQ!woa>m&6(mCsYLmg__Fc*?Ew4dnGm z)8fRgdBso0$qpL@E1|#OTts-B>Op9$8fm?5lVLN2m>YMhMThr@1c;~qa^UAeOFovt z4#H8A1R%XnQ{^=4orr%X;%tkg5r}n!i%dzsX6TJMIolEX!e;~Db)2XJi2%y{kGXF& zJWO2h)da|2(r!@#p$RoBGy-u0qonW1xsF^THPb(svb7`m&w~w1x)}e{##Y6S1OqL< z2vVEB|A(JMB_VM{mVpsd>ccR}xtCc?(MWJ26r-$OsLcJ*-OY5ch81aI)XH~n z`1Brj55T-ku*M->ljdnkBj#N90NxO&J^e{3lu-nEd8kl1)-L9$tHCvsZ53<0M+TTD z8mRtSC8wr*J>=6I%JCTtF0g0q7CqBCkjD=|tzeh5aGMK}R-;r5`{nyIr{jr7L zYTx#5t7td@P!Nz>lE(Enrzgz2Z0vKdk2$ZRbKe9K^%O!3LEO)0JNOwiLJtl%;}o|$ zTW5U7EQs49^fV5+R9{ZZ66v%p-JsK@{HbA4h&$)G_0aJ#E@m^(VJ zlr2I^Lnx`s1*=<##Q|!5tnr$0Mjg7+`_65t<&#gXGiFJP6jIxbc}2(jd^#{TR_cEi znQAhX(2Q6>z-SBE-yhylF{!Pei>BP1i)uYBH)n5hfq0*#@((n+$PJc3EV`F5I9keHe>m57i9tTC2Z~|5P+nXkrCq<-0O7^bRWTb1 z$EeVT$4E5({1ATmBCkQQ$-pk<$)?)udTr6!A7z&MHS1SbfaTF)uC|#EYX%o_Q=e@< zpT-C}%iZ*F4s6b-VRQXuHtjnaeww33?wuaKFD%lm8Z=wT zH9*)@$3_=JNPke1|K?2AXQ!f(n|(Wrdk35KJG~XMk)$iv?wTc1ZEXO&0MJG)5uNkZ z$OS(+GGSHbKm5ofWTBNSp!t%O7a$Zn*;unWD31#%nf6dHe1^q2r*@JpatA~t+O98CHRD0SS3Pfnroj+j zO@Z?iy39H0uFJmXndRVjdvhAcF;W6XG$sJ)DhZK&IsKquzQL1mVMpbOuJ6LjjkJLA zJfwwdT7vWpDYS_w+B>J=k!}#AI|T%!Q@R_5ZWReBrG){cbLj3+x{;xWX6T^@83ulb=Xu}n z`~Li$wPY<>hZFbN_rCYN_jO&{H@iU4nAN<-_jKz~N6;#u`_RfwbyT`=J9ZkaA^hge z^u~<<#U9c#vkmE+sjWRtm93#oA~!WPb${ja?cdK)QBnHIbqh`w!~6S=nQ$)O0T-&^ z>x-?lQ0VF>1rmKogOkV=keq`rE-spKzn%Ic)DVRBQ`*V8O}U*M)^a6S(-X$_H3pO+ z_HKHHZe^I$iPe?Dd&~-_%%n6KGkNNPkJ4rO2P_fAz&!CC7whnDK7Xlu8vB~=7$vYEj4D+jcWRa6vi z$Ccm;z^#d-JGiCas*_mhhpn(ULcw+}Oq1(Z{#KcQ{D(QNznJHTDyA?BI z(#q-Kd@AcDtR*EFu1ZMKX-D29a)}_auL;rex%sbIV6Ww)dF0e()%heO)NmB1 zNHD%ub1QwBM0~UQ0g45cYw2r$!5$K{yVs@iLtu-ko?}TtWZ>C8LD1J@b+%O;W0eKnqn5i2*AP&Vb|6ayP6u!S{h*(J`Po3BLB=o zgUP(`@P5xOa{mEkV~?A^a8UbG-ZqIjQtF>&@19P#^}XVn;)7~4fNQy(P*mc`L6%;oWQZ zlnLYKphmA^>9pUFA?yt0av4Z1ys2W@h6}cXMCH7Vr%(KOY^Cka=mTL%-p-B42nJH3 zSCeG0>UA4|n+>KJlVJ4WQVYgPQ7%8&YjEI>UQAa{Q(9}6Om9W9rWZ!me0+L3L<9~5 zFq(U{h9g2})Fm2B1A*lo^N#CcMM?@)l~9{pNAsM=7`i5bo+J!ISj20zLms6ZtrcTf z7Vl)A__)MaOTQ0^jJ#VA5dkKyi+Z}t$VyRadutAQ#dv^3S}+EHTL2`8oB+&c{r7UC zhqH6J{>z3b`z)WeNT3pZ0w`>xCFFWAJV^d2t3Z$6NC!-r(h#C zYS6a=CnW$CHWE5TZzpzteJuL5g^GTOSLtDZi>Q4MLrTDUU4{RF+;Zcv(9GU>JUs>e zLtP^B*QKrf zMiHrU)d775?|7H}J4xxVx0!Liy!Oc_&3mt(wg(-Gmu&ivy$uxi?Zy;cf5~LfyGx`R zC;0ix7c&4XIQ&tvl%sRTg*cqu)AnYS1H*8ikjalYQ6oQwB%nCVQtyqxn6iUs;nYAB zyNU(|7v1+1{g&zKQL$ZhCh*C20^}436*5fu%Hr+wHrmCJp*$MJui|&O+=7UNMP2P5 z0;5-pm-bPX*oN%8FJk1CSXef@ySpd5HAfj0g?V_67b^Uw0o9I+?o*RaU$hw=ZJOt|yl?Ww zn^pfTg+vCFccN6Ae~6_!g({&mwOp;YWYK|NjLp?`z#Q6?-b3h(Bdz=rMy|X_0nK?2 z$VZ-6`DZYHhn>kYrRP^!M(NEpnX=JPu4Ay9{15D9*@YO5P#)>GmrS8o`^oC2tDR}+ zmXN2<6?kk~gE*|Lv>6pAWf#)l#c!>BGA)a&$HFMAjjUgIZ!YfWnEqW_TYF%a!{Haj z`Ra$f6_`wdf(r+gPjz~Q^>t1RLE!|M)dZLazW2dO|BcXGqyJnohMWJ4;oO&#?Kks8bFnuff!?{`v_R3mj0fu4J0<8eYOINurM@QiRz0{v(BvxF z4w9r3U0u`^@p1h7L*B(F(m2`Jp;Yut*eeWN;t1fMi}r=u#X%-U>y>te&v~h_n}mt) z5@E%SubM!{4Y!h=`BLy%xXX2}M8JCB3)#;T`TF2bTuX}5*#mtN7ZY)}S+|JM43P>z zsfXLlOMsR>cAdkp9(+fr%Jj1*ksB34aC}Bj&H8Sq3FN-~XZCpH3Hi*25-CqBU$#3N zZwJ9DsG^QzEVo4qj+OF~PW;9~IS%;ak0&W#IFcon=OuHF6-EmRnNAUTI|IPIs9jDP z-=_qhVx7d>7moDMwp0u#nCT)nDCcDF_orfMkRRPhJ`n7@>%>GaSH#^7LIJ&j~5s=n}ZA7Fg% zPQuBS!Bif+d);l!1dXTCy(!_-1~&RoU}s+kumu-Sdc#rw_uPLz4DDIS8#gaR`l}zw zJWzIeZIa*axG2UGRh&7Ohn)YpE^V!jS?7y?ohUqT0HKRyU6)}&w||h z*VF&+uSzlg3Dy4htpC5uwA=fiw6`-5U>HTLi@Y3CtKL$~>FGGB46Hg~MWnQM%OlSp zmU2c$cU_nc$7rW%X4&OPgKw?hy0eskp}>?q<9 z$_~a*TQ6SX^5GuJ0G^K)Kn8+-x9vRS!i#dBvD=W4w?` z`@}|o!~{NyK|-3DgKhHCAQq8&YGzom<<1j>xE?la&ahv;ef^a|+EBWCl6n;+k!ya0 z*)fckTFY1bWybz(ewfHC2ZpbQVm^jQ#le7Othz1LK#lD{hNblg8sN3R_kDg0x#+?= z3}Rn7Oulz$6*x4M^dYqh#lC2Q*I41G*mv*Hl4S*UCY2`D{)p#-RkZS{D-!$u;(%Uf zMp!Px?DeZqB0oLBOPq9~YEdJG7$Q)EoAk%{E35O{>z?eU+T;No3|2!$M&%0n?-Gq( zV=4km9%xT{Zt_`rtLRNjmwc@#hH#WCV382HTAX!lir$)6(RzCm4$1}&2bP7xfWlvv zYj3hegVVF~Z^=3(UxAgg8Y-_|LCwaikQY!%2%5$^G;5?5W*ho=Iu5Ejj~VJDuWSO1 zE;tJH!}NCh7HyK!!)3@G-WlV%v`+^A!OG>EOCmn+JiBvgLD<~UqSNY}dgJ4{V`ujJ zl3P7aIK>HHt`rkO(fpW5YeJQ`VwNLZbv0%;KU^{&BkB@YVT*?}=H!Ic{?T{g2=MMc zoo#jx0EX2^B59*Q3uR?NCedP=(@*Sb_=fW%X{Ad_F{S>jvXri(Bben^X1+W5WFsTr zOG;SZC|)L>mLFQxPI{FVK0N3N`~?^y^wm&`%z-oe$Kefm2eIeTUWfbuSL}3ay?oS9lEvHK69Z7zqO%hp?!Z8$_y?585sQR_L%>X9C6i5f<$g!FN8) zVY-V_RB7ijbbs^W&%7(e0?X*%o*W2$;_EbRPZJr6%e9UGWHrJ_nT5ep3)3CRBC2&H#NGY$cX+PKx9C3GZRFRMAEd!WyKaCAsw2*qN{x0jdMW( zr35MMb29vVzWDUFvR(vcNYB&ILm@;fFEK6s=f+3An0C83hSXdF{vXgQDB9xH!;Gmn z^Y4Q?(?HxBKn5T$%06(60Y(|ssHx%^Zs??Gqb?P_|9S8_r^LT=C|_*61KQB<)N zIUk#-5vv_a)H}N>Zh7~ui9X;ZPgPZKoYN_>(1w2dj?DZb%jXiUu{-Y&OfEMuG%DfQ zO3QR6<&vkvv(m1yYi**IJJ3JW#asc8tOF;8Y8(|%waa=g4o4k*(Itg ztQTirmz3+jt0Oy9FgxXDijJ}R8UEWW*}g@FBw6|KH0*~#&?3tEL8@zAR!p*qqGJ(A zAty=H+x##vbqISq1u^yJg}5XzDQ>vB@t9GCQNC*6oE4VhUDD|zKT8g#kjX$9$L8e+ zM|5pMN#?JSTg{ZKX?uvtv&1ZzP~MVp%aV?>Szg^3*n@=|lzP+uv#`p11eH_g_(_PrKZp6-*sZh)WM_)3bKYN^VZ?rk zb32*BRW8AsO`Jfi6tU|1HqSb7kPm`=KI#BT;>rNA8`Kou15C+GHcfdRn_b>I`WVLB z`-S9}q5}jJ3xI7Dr_N4IE>9?zeK>7aM2VyW+CF0o+_c(bBK<$KnGD8*JbR2DXMjGH z5`lD0dwM)mtTrJ7fd|{hm0j{Uv>x~4f9X71hm@4$G-8kX?v|E(mBDLic$WQ#xhEG# zZ~w%oPoxbQ@7-hX7=7B@#6A>XWgS`XH_Jt*Ng`L8z>-hDGbh?mpLoDeo;|2>5(`pA zi7ILS)EB2J(A2c&KKd#P(b1XbQz>rHPnJ{H!x<%?u$R&f?Qv@6Vk16dRuz>20*=ZR zf~)PpcNP8>J<_fB)Y{ls@;@2EYp3aY69Q%StF{9)Umr${xrI9f`S=H~;2f~#Q&`Av z$QtyL?SgE4Q{!jK8%|%mTv=LM6*vCJZh7{)BkEWT%6Iu~?X+t;YYM===*>pZ&;Hu- zeldS*m*&URHGy{J0&`@R#%XScy1^q!*kWG;zYXrE_5cybwpmYNKJ+CeOy zSf8TCM$<&v``xnqL_dp8AyzArJ`E*6_+-(*5Ky{OQc?u;;9l=|vE&;#u;e3+9h^?V zbqlW*|JdHgc&$8xa$ zE6+-Z=Z6eN&r`>j*G%vFh&>U%%oc#F*&fhI%d0!S^Vqb~4cg*0p&m-+3EB zy!y%8whob$DlV=GX>yUi@$d69Eb-|Z+d>ly#5cFu5K!skMTYY?>(j;X$6W6I0R@le z!bb9gpx|)Eh8O%boFR}qR{mz&uASmiLHN16I_kDk^n*Mv-yi?@> ztj+)KFO=*GtZ`|5Z}4gFyU~ArmENMv*}fq|b46qk-KeDWcWWlt)*gN3xSC1oKdx(c zmkxyHZ7yEaWvr^Q43CVswNHe*UnFYm1u_TUt>9uF-vhl47ZuIcCqEMgWi)8xHwrj8 zH8NKInZnU`U+EK9=ui4w1Pe{WZoK_G^9Pu4q11!2BqD2Y)Lhc?jd4L9p!Yc7DrjkB zax=1*=>}EACyxV^WHeso?T>kfMG;%^1E4I?LZ~0-nR&;ueH|$onc)r{du=9GUoYZvzx)-hRF!D} zcj`yfE2H!crpuZrC|PK#l=3+L9g;*#i_^b9&V zrIxxIbyi3n@u3na7Y+96dW9(*sNjic|Y)=v9udVGyh4gFF zDP^G3->~K;$H$hDpH-hy7&}~gBPNEP1O*2N1-0jRO04^i`5poytts^p4bUyD@AN-w zdIC#;51&ofhxvEkL8NC%&vo?4Zs1!-8$g#EdZ7UL_1cGmaEuV1g2ZMd-Zo)5U=z2k z_#QNa{eojM3bKD_JOdujp&sz=1DAR6k(pbGtNj{pWzoLf6Lnmeanoq%y~sOER(^F; zhyEfr+EgV+)hAi^x#W3$(GS&xJZ8LZZyN7zH2)APg+s6Fw) zDMD4XdiJJ5(_EoO^aVpr2P2Fl1jz=7X~!n&l|k+fqSrdLFDgF%6T;Jm>N#*VZ@+@P zvDnuQ+KjZx@Jt4&;;54V8ob6kZ7vN9fNp{yi5mc<{`f`*=R3V3Ro`jPTyu(Kzp}|y z^_{wM|4*gcm+?d$Dtj5Ghfb_lF2gmXe7FKasbduLV-AC=V7i=xa%P~I#FPI&3 zn*V4waZ*O##cKNXVx(Hnn~xcse&OilG$&D+OvX$QNUvWF{RX-|%{1M!?frMSzsPoWBt(_m@kn$}{v1n^6|B@zMxYM$}J-dig_V<`5 zVa}%sTxN?~(s>e`B47os@3IN)%WigL3ut%E?ijFVlC!O-LdyZeLeSYIJXGufeLW;t zImn0W#F*C+wW%(4j7#;GLHi3ZjftWH~@iLW0jgh=*3Lb4AZ)O-4WsPYq~YH3CTRHZYS@Bdcv0!M*DiV(I1 z7&=Oh5F=Uxw`}J;DlIXO9vL2{x?^iJ`1exSSnA8Ow5?uIhV{@!S=iP^^9-dd6aEsa zxI))z`=0>pTca%R(Q|AxK(rTxGv~8&_l8~}&L4t6AV2`xVeKdT5Lw6NZySJ7s#8%| z!kT7eQ^#*GgQ~h1&twO_9`M+}67SUgPtv3o4_p@t!eNV4?`BB|eFX586-@jFL^nCG zCwSuCHt%T_2VHIR3e>mc2v8tqtByw4T)4II=*;+NUpAdCiX7s5hxy7$)f~xneku-m z+Ph7`lqI@!fTPxkrQHNbR>WMA8*R|$*B*d9RCV(Ayr69WqkNAmmUIjIzE8`Wzv010 zO!tLiDTOM%1#}(TAjdD?#jXsr8UyzjOJwrgb;cIRDAj7bucr(}oZo!6-mI zk7t%IrWVp1&k991Ss-2_s#5?#-zyWII}yR-b<99^8i{)(9%}(X_{+J$bG#O|nl!%> zz2*gxJkCq>M;_9G1(KFo0G=GJEEQ80>Ke}3a;72umPF1sb8K`p2pH;|L_dUKTP9DK z!jn+4d*zN{{~10U$isghhcEDl*BGx7jwfjyI3g9I&c9-b(HS%g7IZ4MoRc`e7()t|IF{ggh+)%;yeXhr$^qpb zymUggACC-;`N2Rw$$Br02}J29h?R>Nei3bTJz^}Wj-DhC6d+Ho0m&emLp9*1KOVOg zTnIBv8c265L6(dQOHKqY)cofYZa3m?R{+`YjBQoKeBEBR%doNEF*5U}jL8ZuzM7`4 zyXC(o`0y#`)61(KpQp{()D?qwkNnSY65{`p>CD#dlyPs?{z{=KP!Vl)db)fw>K%Y% zrU1ZWelOpmnaC}EGXr(OpGFlkbVMmuzJPqa&1b2n$0a2Zt#RA}v|y0ysBPfPhnB(W z``a2y6bZb%yhM2Od`W8*7PO~rwNU4U`hyq1qE@vK>w8{mf}XfNE{=?hWCS zteW7`S^*W=L=WL?&TCq+5C8qv<7uG-4#0oLu~rn(o-YgEydP}3KNyp{LJ?<3W*k6~ zy(ny}Ow@j_u(LRCNduZUN!la!Jr7pgVuLNP7|??J<-b<+^VMjKr;#8h2Qgg_c%OO3 zD?q8K)_rG&vni+c4`|$UovAc@ zS&z(DOhQCNEA4t|87&k44(Er?;K$FKC1=R6ABJ;Erlo@>3r1HRX)Hf4u0zd)*9(ztv zanC5Qwvo!w0!&m{hl934K$xAMis1HQJ$Q&5)CCGgH;oE?NKOO+93;Njb!#R-0Q~3N zX$$O})8bv}Qd(B}?Jp(?U(|t!e6Bi)ocvw^)&H7pkfNRjknJP7Vi3YGb=xn+i@^C} zRW^BOxQuSktv1k`QE9k_D#xLwkga{yALP_4^Kuj8OVF(~V~r~@G%q`aiTi@kISw#% zY(o*yY@yqCVW&0&bSg8RNs>m1g<+@NWaxdp?-YE^pAQ0P1gr@3kcpQwm0b>>t*)Ie zsSKWc()=#_TH5#QhW6=)g{Pn(v|ZL^3xFqleV?FbV!J~8qN?Mv;`Ryikn&W9(4bXK zc9@9%jV#LfE-n9$@p=URoANh!U|d{MvSv)ys+*(|zoBe$^E#hTF`j)W(+fIPl2e(p z2KEmTjO`k~^Qf8$3CR6LMcf}58Eso4%R+!2Z_7bBGKlZ_Q|UFq%&e>qpn*XRyW!%S z>x+gC&*=sr(>JS|-{#jt39Gc5Vnm2J)OF5B0HW54ETtv8=YCKF3qYV|#Zx~y)Dw1I zi0~VTf{pKZlu-w+J_4Mka#rA^B2%3KBp`@0mHDTM4K!$TT{CEO#(c z(z1Vj%3aRGl#+kanV{K;d7e@o7tLh!Z-cWD_vn?M2?LpddH1Q?cF;;czN?KYmycClo`O^0mb9e-$KCV zTpCBI7z8NA!hM0-{iz}BlLfXfk7~(yu@6fX3+%UB&t>;j>it{yR zYR490f=Zitr>gq8cTMo7i?IDRF6%}i0j@Ime=Yw%1COJ&8gh2gET!_4RhER5eaRm= z>Jq#sGAhl_>Nn+`bwz&o&|9WS;DRz$WKef%PFlJtR731WvCqG`lDqkltk)@rz|C2G zdc~TbK$N&184l_lHySV4stSB}{azQ=&3`As?4p58_M!yG`iPJFW2IX_-ZKRp~ z!>>7t>f!DLIom)>lq{x5+I! znMa$l{mCtqSTI}*Ho(M?t|l*=C6|svPxv*IQaFQP^|!6y?(b7)?w8t%er@)Gz%)ZF zUvCL?T)gRc(Z}N^*Va%SL>1uMs7CugQ}&n0J|;4WUTUrP>B9lN$8%wiN}Il8Ay=fE zvtKN07{-(q!yHgU2s$SHe~~K89m?D`$O)U5X9vMMe47Rb!hV=D7|a zbLwcbfT_W7!(H8Zzu=V?t`6(v3j!uM;D-E&*y~KPHx2AN`hD8oP|9bRQJTw?hOMjA zdVL#Dat!MJp!d6v_&F(ZRb%iKTYh5n-yG&wQV{q(PA-20aiVd{`zy-!0Se0e^!HXXYG^OFy39cQsq=0dPhT>P zcNxrD{S3qOgFbYj@yhAp>j1o>u?1PH?&WWOV@)_{?dOb<&Gg^}#^h-#BUKj9l6Vab zN;`rL-fK2WmDmV>rLFiAa0~TK%Av znkQW4{?K;&NFcbbd;M3z>hbGnTdzm7wUh=96no?ITA6V5A&2}22YL<=GdC(MAntm* z8%m^|3pzC}tOO%m_oPd4{Bi&5qk#RLaeyStKl#8ecZ2$J{ z>5;~b$cF*r4l?h~i`DOyEFvrVcN6{%)p$@$oV=N1*)GM3y{#?8A!r_#mKOQri8Qz> zTBA3$4m#o0&jZM2$$vm+9qm0;bJj9|zdzASE9&3J>CXuL)=#A^;Uq%a(AIXs&u2JV z9R7{t(6OJ#M*oy=9~D!>=O`l$_0?Q8WeLv+GqP)L4K@dKh7uMIdosHeXlVl6gdWzB zMB@3o4s+aoMICu}ej$GHeJPtvSjYS0k8z=A7(Z>9hMz;SIONc=RmMA;2D?2@{bF%t zRmlp*4v+|9X~7gj`vv;)F{X9ebdinG(<{gK7GH?0H_TZyCCqCDg`dCfm8ZRJ8-Oyc za%m?dErWZwXFRjMSs?=0z{T9&QtX}W9qzvJ(t55yzL|BPjKTPqx~%@h-dVMjIsx`8 zTHIruK5?uMn8)f3bS2f*^Yv@V#2nhJShNl>Z(Z1g3VW8*{58wtIV5yu%sS^U#p$X? z7=qwMsQ!c1{+@ko`yQSJzpN@Yozrm2Y?*ui!VvC04G@R6h5qvDdmB;aKgR4iYG<*i z*I7GPHdkH33J0%`{uwhsh*>0l!`A!hdZ1yTtIZ%YYz&CI{z>EM?WNmTX9g1Fb4nBz zdA<;=e3FQZ5-PQp>5^LuA=+QW@dd@KWpz3tK#W)fPz8XdEVA;GwsiT!r;Fq{J#IiK z{#1kJSy1>B;Z7O)6>*&oPtiEj{UA*QZtrgA6{_U;d1n5<@Y)ydL3WRmthe+NR${&gZpl78QCr*B~fFLg!Y%nK{1V0`^=8yB)~2gFmfdbrEt*KtZFNERJ{mW z_*O%80!=SD^jdqmbz6|GN|WqQvZZ~WtNePKBT%ge$*|Fc)rarxvEdtW{~w6Sf6Pio zxrlt7zEEZJcwk__{Cm@T*mR98QgXpCyMq{Y%RlT=D$y{Gj(SWw2M@3qD05i&1};P@*uRoRyHFk3i(`mU(&f z3(-P@iv)7fqF~)Zv@=%!aw0Ame6FaU99_k9Gq|4h*W)E^WWlt3pc?{QS3eL?sFFlm zb{R2_Nnp|$xMKjlp>SG{+aIDJAG69Gn&CBboVF9Vxi|UeV|T~S-TcVjN{9LuliEyG zFKmKy3l5x5+>-4Zx!%lpSfQ*XR+FQVn926-Q8E|$zRF0~@W|_& z@TBvwtO$Rzb;?x{&ESy-dyn!0+zar8ih@hP^iZr90XyKj#THB<5E23TWuK`fZg)#P5MW5(|b;_Fum5xFHFddeuI{FtEil9`b3_3 z-I)rN(yKakJ&XFM=SJ@VOZ;RP$j{$s+qWflCG*lxFJ)bXkt zrFaY7ISOy)`pu(9wP6OQFz?-U57WBMUFQQqOzRF@H?G0XewY(5TV%0^0!IiKscWH<0i{zDn>iXK z(>tWO>aT@(f@d;_mj?VOS-=`2(uSi#$@JyyG7g80M7d_L%(QCX--28xfEq{%pL)yV z*kpbAHD}&0KQ!jwxiW_0&_?TL6xRXsfI1WGv}H}9RJ`$8G^Z6Qzd3N$JW>APKoy&L z!r;-M%o~ScR|SWDjr@=3TxRh5%29tBTr*F(uFsl@vp6O1&+-l*Cv%#U)P78zsVKyV z6OjV@b@NIfe`gs5o=%B+?V zl^~t0qKI(6bA0n693my@UcY|K22vc%iY}JvnAEy?p@VlxjI!Dco=(yc?gSElRUQ!9Bz8w%-o zR*<0tsq;$a*+K*DM+Q?t;_N5%iulJ#8hoRWI>op%6gq-2x~l)(v;<*~tGJ;Oi=4rG zml`E9aw6eGYIll5qrnp95_Y=K{#pRBx|lvpN@|d?YCyLj#_$qJUuN{5MU$6F17y!q!0qm2I77u0#-R`zjL$pXN%)~;))Tl zR|Y*-;O9!zj!f(&2dL%Ld5%awxsI_PX9i3VQdiCwFNR0ls7>4fdDK978j$uxz}kTP z=Qnq`XdVLx+R8upfccwHJ!k;5=A0`$7v1{AQ2T{IJ#Q|pWa;2*s*OLgmWioNBh>wS zLHl@Ubx&X`&I=vdi7uYz%1YNMHEeYfPsTzU>*h$KuXD=Z#)5C`w6z1#4v&DJmmz}6mA3gw*vdUdTNq-F31jFB#wKC&L?KuIvQ(8;gY9iyf`|u}GTJM87?W1y5L{+B} zrT@_2ZwT+SF=;tKVlB)(N%S}tpK-g_f(Q)(=)jFruDq9P(_UTsQt&3KF|V{vw3?h z7$qQ=qnMuvw1e$yoC3IpP`Jr-OWJN3APbMRckY+lB?>oEDz1x7==ZMxvweM~2sB~5 zFB+Qz)vEQMIsE#3kjl#-JE{G$JGft*5K51A{C$!#DNvAQ&b zA(}@QX&FCZlw3e;MWJ4G$eV>e0s5v*I8?ONHC;4&$a!Y@Pk$~9Uz525y4g~>N#3Ft zR1yN_cMO-0!US6P8Q|K_vJ*M>sV@8?CzS?g$$KP;598?{tO*s;$DrRAufAt|F=(Oo zge9dS7ZuopXS7oNeeA|pYtCTsf2+8h##l_Mpfh?#Bj>8EMCFYyCYlAD#d zn>C;>=*>2V3qS`7RxUMja90)r$4#k<>%9zpD@88E{fs=DZdNJ?oKpQfz4PnCmn7f{ z8eky$NinoA1eTgm2nmsfS;V8JazONQ^>}B_hGs!ovF0<#_^i>;rNe71)9}@t`j>2N z@uh=#M^i1S8~`IdlCf84@q|ZvSn8&q?<->gOX z1j2MGtmC(yFj)95-&@U2Dg>7)6o#l8C*_*eTUi%WpuR&yGFc_6*p)MSRR;a@Nk+Wh zD>3C_+AP+5fA`L+Z90!(ekt`}xFwW`lTNWSD$!m1Of#PoKGLifu*0wWte1TE3dp9_ zu0r~vMx>ds0tvD%cRgFa0$E9^y`mgu;a6|AW=XLv@PD)w&m6u>6Kbwp(XfELc;+Pi zRzx4srh8Rn%nFHxdlLw|{a{o~M?8;phF@nLt+jzRU;9LWr4>pVWtQd?}ZokHZ@-+OLum5^nvmMcnf{A#&i6ZrLHRdxrIl^ zGO6^_@~ajAbf;5ver+hPM1H(Fk$2$pDR} zXO=D&y&@)Vn~?Mt&X7SZgpBX{^jkH;(d{7BUMEIb@nJ#uL1ZNB(9Ndu?MX`Txh|mb z3>o?KhPwOud2mJ8DnJgzx{I)1xZe-aa4l~??Qsacn4h^mzP-HcgIy;#fNwvUWP#JF z87sn_GdK^pN`PM5{uXK1&$$_gh0aIknPc~)MmEmsbU&cJ69#Y8WVQSXjYNHtF^2FR zB|=T0pMKljRZWA0U|8@Kd_C;wE{=wM-_!J)vp|6O9DcKg1sA_HLbOc_J2mWuVZY-c z|7GU^AR@DSdYONdWA(-h761(l5<{aAY~|NCwtRkn*;MU5sYFlMh@VCWUxrtZSP6-} zNgDZ4QUcbX zsAeFM@d;}B(Dm;#ZzGS*LxAmQ^}CZ?vv|^=#mQQp<-CkZ%nWq_>*XK{uikC(?jfw0 z4+LNX0F^e`iuZE@2uh-UKg7AgkmSL32}_I~zy=AV@9fcl`|>^4F6bez!2q9jxz6r+ zd1flJG|+&$fey;o+X`L7_*(b3uwB-*CHkW*G5_` zH_wstK93R3h~;5anX-2?8-jB5foB8v0a&&W&&EXamE*E!E*xBWRnyZN)`f9%Y{KO^ zs@@rG;ISbTjft$%s{M8V!~Z!2%VWXg)N~!USNkH-cSf9by&Qa0kJKIX$6zS}nU-M@ zg54Z+jYW*fZ&s45?#dx&ra2nS`DxC=bU$OzhsZ;p1BcrU09|m``E9es^j(?gdt6h z{|xfax3l`9N5-gX2&Gtsi6P7TPhQs0o@1^Bzp<%?o7=2rSLZ_}q1s&-V;eTka_hwP z;7?e2@Wme#!=Ixcs4xm|Ilgi<1Hvu--UhLAJGH=@8dXw!Ft*am2N4WZfsCS?ZiT^H zCN9layQS7}HD~X6n5QJ>tV09!wBq7j-)sl9S+H=()7@}DrcR(MvSQY6tO9bopk~4& z;H`Gw5SJl39N$}L^1kE+X6HiY3RZE0hIIaH&EM@vUTRJCpARC~R^?Gk?JuiFvX`81 z=ZT3i`Y}s>TV8NpeF>-YdvX>imW4x<bJ162$4nL$?+alXSAqXCQSn4KKIXWYbv0Q2_{ z{BZgiVS)zKQ~(8jmzD#5cpC}$^9UaL1{r)*knVCbMA!k4$VFXBgu_bq6xYIKj*OKQ znWyDwfd)#~YPVG?ntR=!cA-M>TS9-EdrZ38o425uhUxj~j6-@$ENPsVhO^!L z%tb3Wu`A_*yNztZ!gSF?+t(B}sS|Esw?)%m4~QOwVyeHpe_!8H5d)VNH$HS~ z`yASWmO!+r68|ddl4QDU&$^+X*8Wgo3~0Nw!81`Ja14k}NguWM25mT1HPAeJXR44c z6sSeM{UtPK()JTLOido9V^jN>&Q;JD);rG4zj46mtUGx5b?rVt0KC<^{c{4y1NfMB zJojEI;sq+`atGf&N!U~`zQ(b+7uEWyjpwOFSzZaWgizG=R#{E77ke3tkjIwqxtilC zThdn|LtI+KO=pXZTGrbqWpB1y>807tF4%q0dtbHTwE(1zB{<%&iov;{W% zknrtwgIcj3t_Kd$4`1jo0YlOKVv-313X5v<62_=(isebVP+6&X{^uT;KMk^dLt``y zG{LtNHt%o-$tkJl_Gd5j0hrqSK#ibeYDkZi{ZIWw8+=n*LFB$KAZRUd)gc9`Bl@JF z+*s0=E>KS0ly8gm--FMtaBOwfBNm5n>UqgX*7hF?vFVa8#bHJlM zOmhjZC)ohPO)&Y*O#hUiUE-`(WV?U-JCf<+-whogGzFK)W`!|I*Gq?!g;d=h&BPij zlar{;%3?=C-m+M8{XP8lFlX%Y2~H9xQ?yr!n3&1s+HLFUhl($H1&>0?-m~@sI}D~A z7LvmUcSkT@+T^;LV4V@?6MW@E!<%A&pc*UUl3QfSyQ#4HOc%Xhl&j{m@VA0hm(^=P z5iX_f6M| zzJsm0t>za`6m)5V7w20&eaXX8pLXJ4aZCT$V%1_evXvbw34f)eGVeU?#rGQn@8dnr zCzUeH4wA1*==r7X`EyUVs9n2n#NIsqfFIqHn?C)SP<*!G3zt!&Y461Mha6o7KJ0;O zil~p+G#f>6nnO8i0Y`x}hXZYaDwQ0Kr&2z)I~Gjrm2sl(&bfWexw}?Z?H#_G9RcWa z=LWWTJPTR6atxAc9@8j{3B5;V%yPKqX@wQEZ;AApyUu=|5#wd66kByxn=yZz8U)*t zKM!@HtbZUiKE<^~Bg!?}JhC5@D#m@V$`q=|>}_9TJtDleo%YZwM6~Wnd+R_KYCxOBAvxKy=il_%okMZy>=>0ROhS`$lEi7Iyq%1W1 zo|@dAV^8w%QO*Mzp#UV1i_@TX8xRHB^$j66YHoSlU#rsAru(%c?{)WpuUsN_uFk>5`UY=7_|L5Xzb$u7_%4B|;` z3+Pmvn6byMvv<5~tXIKt9TgiJ;;qbbSLztOe!qm#egSj>s}J~lbaeFf85?IufEzic z^iQd*i^&w7L92yoopPgM-Y!85D!~R--a1bg7p{B{SgWrvXp8kdpvQG2N{B)5L!zS) zlYK}^6(`_eZ@C>nW7fMYw*yTb*vj?d>30(D_y3vy^MjZ2uOX3ivXzP!X0*lS8d-VA z$spVRBk8Q8qWZo*OoM(6H%t_LQlplUFt;^A*> z54T31W2jvX>~Tp)cf~o9ffl*t-nSLA<-qif`NQrMpuegx^4@{3`g`3})$w!XJ-_Vv z$j7=>i}W{Ll{)=IkA*eGmdre4a{Wi&wPpP_$7!`Acygm}wRPiGju8W{clldVF!=V| zs3Of=nB>jQ_co0ew2`*^PVb>G$sDOoVGV~Lb}_36&iJ=ZEBJsxiV#Snlzw*wya+%7 zF0Eqm6w3IF!mSGxjDx5xgz;TEl5oeU{ETACHfXdR-F?L`f*O=+Gt3ALj ztVdtU9TRaqo%58##(764v_$uolvU-^WpEcKieKuAOR*hYT3Ulgy%K!KE~wu zRGLJArX)fXj0I8MUJRtOuDM2_e@S*t2{^jg^j#j4`K&>jhA>CLq42 zbe%aJ{mYmMK_if++wyL7C8w$w7>iq%YfY`-i9KpRdi>gXj)5f+JtE$pI7hm9A5eB? z{U*T;A&Nhq{`{)ctxO1(`-O~DeC-EIrOLQv8Et3Kr~398XlZbN1Qh!RQHVwJp?cYb z#GZpYlnPod-r_KQS;17f;4@E)4(09&%kpIpdfI)fsV59AuNbhxc!fc;_GsRFdyrZ) zHPTi4Ev?QSmQ_>Y@26>=-<8jOWwYj;?y3sW+G>QdJm)0d-Q2 z`%e0U%yx@ZV6E5OrOQP6Fo|d5ecvU4cn6rmC*9^)5O=>YWgB3PT|s$)X-qGRDgU{5 zWIs{!@D&@BlSYO07_*YyXcQI6o4a-lQLmH(a|L?Y%Je9*8~N zD=PiPAh3OIK-}Knvw?T!19~Th%lMT^uH~A{N+pcDS)d1$k^O7jVgSAetH<>Q!eu- zD!=1NW7qcjWtXpe7ekV(5fRF7&)Vsv{jhJ^Q`i;?m>u|~KP0H}b5_YQ$E5TUh^di( z`nU$u*vat#_zzi+wI7QC8%+$}8^F7#TxT~fh*A?n3b06*R7_gDC{Zj1WN>UpGQ&YH z{?iVY_!Ft)v*{H+-N{TW+W+43wt;Q6C=9=16P(%euJBG~=ugki&K5gtng*^E$8epc+*@gv_!mzMxdw6s--nT^}>q(r<@T|^5v7aPz0(jhB z3U6b@sawpB7|MY7r+j`C9(kB5Ev^p*jB6ZB{A)f-5Q`^ujef9=!;af7Av+`NBRQow zY*wziNEq#tvX1(mHMlBy0^vL^h8d*^2+qdPm+%|Bh9PVg8wUFOar!GaM=}?rM9EMK z@la_okR~keh#gqx6*KlJ4#&};oQ@C?z=|8Z(+8}L6u zO3wcqS^Gx4d_E6NB!zEinLHjK0GSXm3qa<;Aeq`yY!T)ObL~91^xkVg53@0JrwBcp zQsqG869f4S&aa!oW4K@!NY`GpE0sNa{L=E(`A8XkATsmILqm=D`?w zg-)Q$6xui*4#!cMu;L^*YJ)(tyATU4nU1XmhEhGZ=hH?Lq`U|f6~p3#W0ye}YD=!l zjvXjZ9%G{MZ(jV7xW8b37z>5J?t`7wN;;1FzmX0zd{(rc^>CZ-U{ml=H}7$gU{TRo z6l$b_Z}~FW`_ceImbZR3!u)*qJM5@m;n8+{(kBBVIWBRQ;&-PEC}8||d4KG`Ch~@T zAPH+3{gGjD3`2q1`nAy=94zlPtpCeyZOHW2tLSuS!&RmczSiXps5=Z+DUDWW(=&n7m~olH~9VKpL|q=3+fus3iHrH14X#u zS8tqvocYNj@gtWf3O zjdByj!?TW*Q=zYjgBe+X1i`xKioDcD&l~g}Y*WsrL{O#}{{H^B5?m&D&G6ea5;Huh-Spvwij@hmZY$Ai0&Y%`rp1ST(!wlm*ulp!fBSA7>dfV5=DZn%@-SKFX<4lq>jeUUuM@V?WzE-X>8jutj z$2*R+OyBus3Wa=6BC0I_u`x=c;4?oXlf!tqo_ugQpT+^rAi}|>^pfU;N$ywodemw4 z+iv#915inW#zlE%u@WaC!LRa29vBJ;Lk4a#c6fe{ZPyrN7MKH~Ew&9=CILu$m$^4MT)g}4SUif^!32RHY4D}A<$i%AF@%8tu(L}19KQzz6MlEt35Di+=2Vq&C+xGkKeUc9ZKP*j z2Rl#{OF2%D0g@ET%bNs}>E(I*5s5%7HM1cWYTaqOxP@xV-daU!BIOZwK~F%6lk+rO zXC8dHxN_Mt*Y1MFo$PA260ld1jkJtQVaWTqT4k|neM)@Tys>6jau%lmGu{()CMC@l z7W2B@;UbKn;j-f)+J!`>byn|t2l8Qd5;BC`WxU}nd#tU+KHByYQqa3AHQk9Q0;Tj(rd;`@6yPE-7k`R zp7=7$2|y}a|6&q0HDWiY4ukC8g}A!9wk{{g4jD}PAjff3yT*PeDMZ*w!+8+{5XiRvH5~e~7#Xlo~W*+;1Fs3j*4I5kmxX_Ee3cK+i zfAq>6zM!jG`!xk#XG$Vaymw+>eQ)%sp%s9Nrf2BkIn+Dte!`!&>Rh>fX&FPVZr{57 z&Zah|>kCOst~W@ZpH;?02x2uUd(i~_=FXhT(OHHs7`*k!pv+p&N|O{6iAMQspNB$G zcm3P+&KL#dR#|8J&4jGu0EPZ{5yWy8TF0lm8t)z=x2Fk9-&OC%$sT(dcRdqb+SrU) z#_5Jlc8QSL6|n`@K@>rlJ(z!pZ`)T+j9;s6N?!ttne?DhnFPyp(?1*AYNU?l%ASc5 zWIFmn{rUTbO`{pGQa|sk@FDq+%EVG#N0nyeyB@z()aqIoZz*~Lqq37Ww8`2874q&^ z*e~gqj7rbwGjkafsH|HFBGI(dR9aEB4c@Dv`UQAw&X8g%Kweg8-$IiFb3ug&4T#L1*%j=@+{!^2Uban6f4fM@O$`=-cn-II&?zh^$s0^rKVeMz%< ztF5n+p=x`3d&_ze5a?v^Y|Eh9doQh*AS;`5oEP1+^n4svv^Q5Z9*3HQHu_zN5b9*8 zm#t~*=N1T*9IVz#-)W zlG?FxcQxQBet^ zrFlPTNfsJ{f!d1p$esn>3*2)GIRM#{O#^^uD9;iPU~agKF9CrbbzB(Azh+M)8I+Z3 z(oo?;YeE&-8p%rcqq?G$GVohKjJ`7%ys2w#-ZQ9`OaF)p$>VY zS_W_57Qi{I_&O0sx}&NR>#<?-;#qsDHC>O=ye=kl}FC|V)uiITi zcg*=dU`_kE*D@HV?=)C&zaczUR;+Ql@VF4>3r|{p6(~yY*6os)Lv)a;>9N3nBj#o2 zcTd?mz`1gP|MuI;PSt02P|6Tz)#2EKW#hX@F`e`1;230J&>$Y2GU=AGMKZ%ABUhUh zhl$_RhOx`=hX_ED75hTv#!s2CA{@kFaqy?XS%w&$@fxP15tIMP6F@==eh+?l$UAx? zvV@nd%=4(mINsTQqF6m`_X;?R*`2#S5QfrVS>@aBPQTaq(odpX!2X$1{3xmWpqCeS z6DKN4K`uZ2EGRozrrY`$KPyl$8@U<8IILj7YtG(d0UA9gd__?Ii)@1JBeArC#K3nR z1nNI%&HiG`9m+!rM-t(ogue$CvWNQ>VAD?p* z5v91yI8Ske6R589L$%_#W&C;nRB486SSpVBm1`Zc1Un`O=4sDUn2cuamUpSktvCpk02; zv$NHNOCJnK$0n4EN67DY=-;mV_QoTz54!3e30;Dc!L+@sDw-SL@ssgD>Ki~L!vKT>`Mj~q;|4Be+R8r(VAs=HI}vR zp}P&N$T!ov_ zK&(x-#=Zm&za8o$|7-GDs}JLXYm7bF>)w4D!}nUd+@|qM@lquwzJInKTsF(DJs0(y zympzZn%$&qMi}WY;DB;0o(K#ir3^I=;{N}0`2vVJOpD9YBQqi2?9P6XTwPtszOF1I zc3NzBP78B|L+AQP5W>K_hyns++aAp(fO`lIVQlQqM%F7RoZ17G{iIUB`IVEJPOXe?|cGy+7LJJnS}hT(seF`uSEk zXsdY=9smMf0a4L!49bTsz)1k{QZtH}xi^m1@9xHxjD)0`olO%}%5k=wPebZ*LY5Hm z@+*^a4{05*`9z9ltTYA=u&Hpnu5|w?#QNLdJOO-P7pcCwps@S9DVJfw&dz_a?pL__ z@BMr~EsO*p?CP#T-UWZ8!_?9=9wrs3HP!X{O$M}8;@>>$ahq3d1L6mDMioFsXE&B9 z)VBUR`FrTs^Ls$sK_;&aOlvDMnwAqA|jEeKC zO6#HvCT1XdU{;SQOK-Z(qEASU!k0=tN9}YAelshut?)v%!QYgH&A;9^OtzSd4J`01 zjcDHF@7ey%iAyg&Qo`j!-hAu{=Iu`3t~2$iseSR5)s)pB?NyXzr9k|qa3~|9t}Sp* zm1VufrFFTX{M_YvZxSdupA;9s z1}_yWMy9|0akx=Rp5yAlu<7*{wI_ilGc3qb8y{21eEKWoq&Kg^*WX1$ivi`2&0iu; zgV@$%K6jPj8YeMd%`g(jU`hXKaoryZ`Q zXT(=!S)DRQ38je2vlDCnDvcW+#u^`f*M*vA{YUpa`-+O&o#GjYP39RNiZ7=#SBL9# z%OolACOR*8nEKu+?I<>iPu-<$-rao?v~q|y^^$qkZ8`efSQQ>HL~emUD@54zSKs&N zJVhS(*CzLl+te8X?l>0Q*Ub}#>?$Xvfv^!hU0*sEdia47;Gd281x^btP+M>93S0wq zMxnWu)`wnu$%&)Sdz)X0Ni?RhE?@`_xNXLNj|P`yFb(;Xgd4+7&^@AS;!hA7S&y+g z#R$qVUMvN2O4O&vRJJX*b|(ixvx^RWYLOh-o-JkS3|@ZSKeJ>$6m}MC_IUWeUeCG> zaLvt-w^vj+kNJ0;_v1`a7jO)N^WtX*wIWNvYN&D+AD=N3WK|;!p~ts z$Hx!>j?-{~vs8&hkU(rzlyb4*j51;AjSg!nJA#bxC;L3xP?pV&{BNVX zj-0QYYRNzflm*CdZe<2$Ge!RMX>AR>Uvkg6imj>P0V-PIgiRAvhR8w34y5-~0dqh| zhfO!NDzSX?tZ=8N=S|pNOW^Kx=2;sb2~;#oYL#tw|)2`TUi5_k%+~H zSRMjECi%ykheXDO3_TUm`e?PR5aP0PsoJ(C$4V0{WVLFx9x(**)f1^4@i^r@MJ&5uSsuW+}PrPx`O_@No!pmBm7Z6)*0Bs2pJ|si_-%1-wzz_6ij4R&t8vrcBJBMg20x<7yJQ& zfDmbK+|{%bRan{B)Ee23%9Kll)^E{kdwsLfM=t1Me||1rN14Wm#g64)k9-y!JWH^< z5}0FB>jm@?cbaVs=TaE_t#2BnF)@_br|>4{=Ij<5Ambo5n9AD>jsu@x)d|HuZ_rNp zPi|Vqhp-mUap|;J1xU)v_uR&!qp;>G4a?%_7=n=_hTKOM9{((_Cwh;Un(wljj^7q% zia&Yg+;kbQ`WPiM0GWiB9EMuet~L5Q_$QUrn@%WPek&P2e3Df~N@rM6@CgY$ql;w= zq3Gb{F3k-L=m#cE3COi*Az55zF6c5G>Q~vmZoHml@?@Ad%*=xA2)uMQ3 z0%TZRt-Ij>QbLRoxck`Gn*EO!fs5N`VqV?)%iV-Ebwiknl2V`_hS&-+B#GjI|88iD z8IcD^@Q_XRNG_ykIoY}R@_h-FRjJS|hq=|3`WnLGUW5OdU9TGbd|KbMYi)r15SK=! zk6<1e#UC7a?$>W&<+lEr%a3fJ@~;ERm{KKkGVdhdz3AcQ{EEQI^9dxT{oCRK!WbD! zsXh9wI#33&33d{X_BMluf%DwB{CG}Bcqukpwfvu}!3AW}lf6*qo#Nxc!5c0_3ptFq zg^{UjYcWiM!1a%5E9Ji_H9bqynfUx4PWmk}c3q+oeC^k>>u(7;#I{ngvzjJ2MF>#c zGTx$!(u&`F|Cen8y?StH*O5wxP7}+588Ak>UYpmI@H%q3;i7e?N4(lY%h4_T>_prb zvZxTO9IYI?A>84k?KWNuuaexE*i2;Oc^+59L?-X{6wtI+$#k?hv7Wl1%N)<&b1;%~ zNK+_H#QpCZdD?S)e0FhLTxi&{d3-@9aN}dpji$bn&M>EX-KOxLmSDup+`ZMo<4VuV z1Styw=I0@4Maw7-1J&mQ*IQg)6OX$#8n$4(^542h=nDZ2PC&u3OSF5w7-iD%iNk|V zAWsyAEZW2?i&O&ZFa21aOJy>y3*XK!a+==T|Lg2H8B}H>XgjyjVWW~mp$J1c{JB0R z7X9WSAQdKYL3-KpCC%>2=LPkip*DqisOnyaxm3tukN*IrA`vEDli+(X@fHF=gZ&71 zl>JP-gAhfE9|uP!eIRJL3$HP3KPw_YsXpJ(+lOQ{FMz{;*6H$T&qS&mdx|PM((C{c*Fay_|1n+es)Me;!$|d*tKhXS$VN4wo5MmD=Z?#J+hM}R1 zN%K|f24w7)tD(s7Ug^81iG#a)N1 zp&lGUWb{|H$b?a^5_a}5LKpwI0wAtnkbb4Z+Yx%ZIxvp=p}8kTac8F#e(#qrUmztv zTk$N${42kWCz^A96+tfKu{NjMTj5E3#gmBk^2Q8Is;SX96d8Tq@ocEs=76-On5Tc( zUhunNy&9l2;zboeY{C7nzK5;lOR2H4vKhc|Xs5!LYs)f(s{Jh+0WfW|I48QJi% zHE6la}>6 zNu+oJbo?wTI@(vLY53@NX(MvNJzA{4KQ{&?%T8=5FWns1hmNpw(DiA_p^wCAQ6m}X z&IpSB!3r~~D1r_1(RhE(9f^pr`*Kvg?ZMVh+Aka{>Nr<kDw zJkXmj=8)6j1F9Khcp(Z6aqOnw&Xl6$L!z&deLYE$R-f+f1Ff`tbbs7EDp&INTo1|ggA)_+2pLUTJ` z{wSW66d)-l#c)BK7ScI39|yBge`b_cj)T~~D@CpKkw;E9{pvpK6R_YE~Zt)>4{E(Bb^*C?NqE_6gkY#-_f$NUnds3fejOp0zOJ z@J=ycdnAK8WGMU$ZW&p*sz{T;11pEYq-3teOAuN5R#eS)CEfR;|SA zOjE7nh#D^8rRSWG)i#T``1q~&3O{Fe?9*VA# z(y;8&%o@py6`m&pa$0V21{#%g+6jAJn}F<5ry6^wVzyWN| zT1^X7BvER@qTN$l3Mib!aq`uHaoLrh0(-1+AS-^}ez_Kd2@OUHW7hWj2+&50M!Oj` zN&3|EG?4tK%5+Y>xk2w}MkmlEgw zz4#A3Ok-exq;YXMFE7v882dw4G0aYxp_-UiP(H0tLRm7KL@SPG&O0C=0I(+qkV$A0 zGg!*ul+Ns-Futxq+e%wrrqmQ}B(d44Pl4cMFesOa2m-IAa5~as`Tg2p<`LN&ZC*@G z+3>B7LZ|XiMBs~nipECY%l(W44SUe<(o~<~tiF|u*?H!={o+;eO6S$ok0#}scxF!f zgX`H(r(RCcv!aXj!?9$A1)Rya@%)RK*~!ZO_C+9}omCpA>TVbsEqFi~X73`Ni1!ye zs|YV3ce_SOK+L$Xr@+lA<( zY%}@N_2#vdzDjf^vx4DL4!GA_C-pi$b1CW-Ogh#v zyf(^v&zlUIa@e$R2?Q)rFJiDr;_1PkbjmE_&}H7`->*+y7JVW4nZ1nz2n-Mnv!Zbk zax5>OFT5A?gNFE~Xzh5cvdF7E6d8uv2P^IpAhBuVptLC`^I1i+_@u{6xO0knwDZCY z_Mau2opo_0-^Dm<)=*T|Q-x==6e?o8lJnC5&ggU_&6s=tDN*%AAer;RkonJ|3XW)7 z!qW6wb?l#8OX{Uf#inY@zqyKCl}qxX6`bm;-X1lJJyUtNdIjAAge*1YF8_8;xNd$C zy&X}dAxRH_8DV&XrPe_2MP!5n%QnsJ&?zyxc zG%UJyN{k8WE2oi)S5fjW6q_8k4mvDZNH4sCOc!Yc&XkRbpN-U+J;qM+g#$B(X~Sm8 z43;E+8Y+BR?MsL8kSz&pk!tY_-w6qY{xi7caG3>d#!H#q{r)rZTopEy5cfHRH$!*{ zw_w71Uk5m!1IX7iQZ6azFl>dLZ;k5iQ=6z1oF8|yyStmMvS~MJuPSsShA9;iaKC}v zn`*m-Oj0wZwqd;{8E%3-BXMrM1e#4_qGzepPpyexRa}Vel%F^x`NjV>5iG!4^Kvat zhk60oH5|ecn}2uI`LtUWdVSXSS;*y2rcG)emKx8ac;Y^ZCXS8DGwv2$7WEv-hc-|1 zyLCLtrD>f-$tFtLAG4(uwg@(VTcOWX$f>@A#m0xzubpj=x8wl5^>EwmrB3O(AM zcpV>0?8fW;I9dxvwWYQrn@&2<^9anN>%GRDz?J6AX-LUm(Msy;6u%!4GLxDD}EjZe7ebO$oemKA#7P%)0EG%mZC`Fw` zCJ91F0xA&{uI0;|{%@gbG#eVNFZ*}u2dkO}aZOJ#z@ zw@p-rgoi$4*BNPvA|$>`R<_JA$3}3&ARdi5AS*OJD5G?bv}45p;-IPWb6sXyHqw1e zH67uVFkl?jKrHeK_tKPca=n8Mo0^jWJw?fECXz=`P%teT%iMR_`;DpBWE!pc0FQ;q z;m*Miv{juu*&mea+R=E2cX zBZs~t<##Jo@eF(ye^wl{&OI)p`!o5^Zzz5KSpsN5?A41(U*+b3Doa-h;_f!R3njJ76GHq9^z-KM*t>y69Ck4DNnK2({XZ02u_ujW33%fBfA)(x~ z{gQmO?c5O{$OW!G9gc@w*3}0H;PBf=9IaQWFV-u& zxVS)8Nj^kpuM`P+fsQn*TnH7F-d{9OfH{eBI6rbm<=2w1{$*NDwaQ0I)zsL3T?86j-dEgH(99BcDYutG3a@UZl?(Ibtf%(|p4I z($ht`xG;6-F@~sw#?kIXkm;0hz@7S_zYDsSe!AByIvJN3v8V7*d5#6yj4f$k2OC?oFj)D_~p?e8PGsI z^thg8vVzkPMu?^JWgWjZN)y^!;d?Iw>MmEkrBsF?Y5d5f8_%LF|LuqlwVIjuA&xmN zSD{PbcrTF>OixTKj*a^iJG8;x^=Spfs0TiaBz)Z5ZvQzX_g$Yt{H*FUnxR{Swg!-2GUD@gXAJX(AqzQs+JauxrgJz6wQPUX54~h-=tT>ZEhHr6Vub43pQUL0qCS@ zBRPfNT6mQRa;a1J;AgM7hjk0@=#)Voy9)gr?7h)EQ{anNObyQDDP$CBeavv<1{a_! zw|aj++ovCN)%tWgp3@0@-a_s-$?tA&(Qumje0A0cj{FA1-f-M57&tn0*9>$#-2W$b z&n+$%N-ZhOWfeB6Log25t)RV2RnT@e7IWmom=)byX&P6S_?i3 zJ=hz&YgB#oenP)YG@E$79T5?X4*mq9BZ}Zt(1ET1EP3H5_Ex9tM{;N^2>i`jAU4Y@ zBSVe^YNZBzl}N{K4AL+=GE;+Nt@-qaqqX^oL`F$I>k`Wo%^RX}Kd;#7jquGikD$ysG)a0Pcll%7a3*wL)YPCJNvoIzHkE1b~f@vF4uJ6#@1Dr zA?0P>Il?BwD?(ARABc~S5%0ZjjG!4rq+HV}t=jS|Hq`L4BD#7>@TL-b2h-(nVi~}n zY`v4cd`^u>;jq+qxdLR7Ab(813U4|Lo?#Mt^>Ww-K{2c!Tojs!P+=h*c#VvM>(jcn z(=3R)eum%mA5#wN<{~2%~U8}b-F)q zK7QW3UJ1k+m-LQ}wVr{y1|F34%!Prp9A)djjcvur+=_vHm+J!nb~deAJv&~ZA8H_Q znVoG3BruuBup%H01@^V+Qq(A3qLp9T_FISEC^7I0vlCATrkS*a`w&!)$S9C+HKne9 znVieMic+Kw{;8YuSn>2QyRMj>+{r0mvHL#k$B&ooLb5VsC_Q0e7|ZEqqE8akV!%)9 z`k%f3^0WXNf~{BmBIoJkiPc8ve9`O5TO77ixk{YOAFylwcyttU?ntJ1s`>^VG+s=TH zK1lR#farYqHkbL1<*PhhJP-Jjc!Ytm<^_3=B+1DD32xp0a>poIe; zj=4!ARm?~bdA4@1HVveq<2Q5 zeTh+uwGK&zH_OpAf^Qrjx6$8({F6Pp>D&vw9Ng423GIE(mEpTKKW`x7$*`3m?{dPh zP_r!)P)t?iOIc~ERjrvnq=5+RWyJ6@V#GjQXBO%QfmP6LUOL1vw{sBl?i{RJ^UUsy zl)Vp#RuXqt>!S_Mw7vo{HcW*Dx#huvu`o1XRIz9VVJ{s}<5_+gX-UI)|4EaoV0ojW z)&^l}y+Wb$$i+t4c=7A_e8{mvh6x|F03TM#jZHU{`#t0>eO!d_=gDFN*0;%p%ZrP5 z7*c+?=;YG&+y?a*;d>lCxBE{nEIuGtq)-P5lnWSvu64Mx7XbZ(Q z`YgdMWhlcL?iD!inxo0Q=ztlvtOrW%*FVZryU-k1Fm>huXs3km?Egdu+ntcDba`%{3!eH~cDn6vXJ@3#j;q;kz=L?_04bUg*r~+fyuK(D8FThEYV4MuOY$R&8B?%g!jWX}N$ZF&;6L-GUUVS+9E!N0lg=Y%3yWTZ0 z0&y%^pVX=9+_glS*!=R5D&wlTs zKjswYuJvJq2W#0QMh6E6N2NM$ke@jszCcQiDg?jN$2G^*2oo9YnGT%45%;ZZ+K4p- zhVH(LwRU!Pnl#k@3wkwhlA`GhXy%t&;%QM}h-nxkgoF&r%#ub0gtBbA*3jw7oi>QNJ`(v zMN@>2@6|uZKkiTZQq7n5XHVbUkpMRQGjLX^0?}ExF-nex)DB?1a_AWvAldY%Lb-gH zsHI@JIN#v|0QI4EqmpZR*lCXstD#`!>3=)U0*a{I%ibZhkv@^{Y(;(hN#I@wf9k73 zAB=l7PFsZ9FFc>l{V`yC1GP?ZIU+7JS<1b*ml{)ndTee^nk?trLOG$6@;NeHkEi9H z4bGJLux-@m{%Yv`!i45o`<|TFX8S<^eQ^CbIo;v|#iSd-Z9u>rEU{#amlp z?HC?h+7B$rM#2fxE{&;>Er>|W*4M{P#GHAcsrl1y1XpL!Sf&Ug+w!b~&qom4gC((n zh54G^&*w=^G0LcY`!0ldIQ;QJyz%zo$UJaR^|g4t$Ef1dCDceJ{ngc(I2RX+{bDCu z6&Y)P3UIpm?n2WV&Trulk2;u5T~`6_4i*~|E%$lMVy#keCZcQ&t$hO9O^)xnum5`Q z6)guCpZ7WnhVB`cb)2R~1NAKY>8>+zHMp_UR$V}YU}AfXW}kYGUv5fuDDHnlEl6;U z1*acYTn!*G-P=7fuc&`~e78D|7E(|w%Yt|54#aK>CWSB4p`163To{(ehn@b7JVi53 zhllR=daOvwqabYP{&QEqO-J)P{mMN8LP9AiJJaLhd>9~ek5`}{K4l2V7UCCfdP2nx zI>9sdDw_fR!sGyHG_-Nin}SIb(NwnimoriPf(ks?aKIeP*}7l5l_(j|qs#;-k)MEl z0RV7U8t;3Fa_u*=U+WAZY>+2x0Cpu%Q}X7oOXJMS+pzhWx&Sd%D25xVw)I$%fDe0ImnX%VJc9$t2|1jVGJTkm1U*@WF->iIjW<{lfnx^k2w-p}q#+a=%dmu~92c}#z!$D53* z;K^rP0H^-HX_psc{>1VYpHIa@cctFPx07gAqd**3yvsyNU!|dWzUC)~0^iftXUL<# zqrgpJA5$Ua7X$85EkSsdm%4qmj8JcL}SP1vMr~CP@$)H4`a& zo!78rSnc-cYf{P`Vhv;Lmzb`0O##onxU*jTO*r@9<@>j#K-$Qz1X&|JIKMpibNqd+ zu0@_eme~EL-qE#b9c%7v1pt4e<^gbu+MOLy&l^%S7Msr1@mePjk5dl2oY5xFp}se4 z^(n72#1y|30W|_tyWj@6g1a-T!k!uzcz*R0^ZY?``Zw@xFsqtVo%XV_0@NKH2hOh4 zq}2b_$iA)18pXY)zHjDeBc)q)2Wk#ZKX3eTtXgjI-a}}L9T<)qb(8FQiK>(g4Jk)z zX%lO%E0)*bgsG)uWMnYM2CbS4*6-I>1#^J6$ycux(rmbQ8Z=?^sHoZn{V8$cwY`3?W&Z*IqXTbigO#roeT$6L*)Zf%a|*YIMjkQ8lAT$r6lo=@Am{r<&w%Z~fnBvS|H z?KB&ez+94-^~eP|IrSe3fj{ZTUo+1Ka3-rlw)V7t^Q%l}`4*DyigdBScrJl+!79n7 zj`?FT?AEZdpr54RaKsB+oAG?2=bxgvclOTo3kzBpuTjC)9VeHH4h{pc|0MO}Gn;s} zsgOyxY z<+)VVQ62d9x0RZIuMisR% zee(y2$a$i1DF*<%n7ue(>n|Fc4_xRAC**aTcCJK4g#yTsm~=(f#C!6wOAMMspYO*e1w|*ewZs zvbJIT`NOC5*5BNzFu>Ecfq9n6EVd0iA^i12;l52Ny_S?dpL@;6i5)$$t6nn}>ZR$; z3hhlH8TH5+Km3s6Ug}#Jzh6X(ATTpLVN+UgA)87+zOUPY`%}IW3ubL8SMZK15!iPr zWZ=-u4ro>JEjCbkkKh5r@IS*yE>ie*$vQc3Xn~-W2bib7u8vY==ifi(Oj`5%jT8`A z>Sky;^Q^g9WYzZNJ}BuB)gus$Gw1!^a_^t#8K9t##Oc*?%(7XyzV`c81e+aAyW$O7 zSgp0uU;!q`k{NCupozfNMJRrZGSUG;I}Ij*UUX<}vh;KJ#X!ggb^!ds2j;lPV|k|k zMr;k!p3$apW<}m#iPHQWSZ}F>d2kPQ1t_tYpC+dig8u8>*Cjz-%kwz(?fb9f&UsQG z4_l8E1~UV85x~d@OCM8M&`KTMPaR2-X+BNs~oE$+DAGNf3& z-lUcl(XU(9Q#t72Dh1a53f_csw6l^*##S?IYQ2qF)zX)AK0EiL+A_j&a^0V5$v++Q zc*$hI5(a*rxz}Q}C!BC5UgaMJEV0Xg9?H_F=$_h%q355dDg?y4EZEkWK3~sHNXX*w z1UdXIy4jx##vN&RS&pX%>z^s>&;B@3niK(XP{!Tx4<}Z@?fs$uI$&$~UAm~$&B8cO zw;8BVY_1=ZUKT(yV9Nm0WJYt|SBtJvbFL7ozi>eEIEE|oGq$yOo&b9-kL5#^DatqPnx{l9JEND)1EB3}hLY2zh z#%QI5DWu%$Y1mO7PJC!n(Kpc7^t%LtS6Ag<7Uwe$`bhgm=6ywroxx+X4n==owJH#X zfdR4x?)jIRYS}V0$+MN@_$slWm}b9*EXzxus|B5J7ggs-QAtijWp8TT_P>r-5t`cbax7ffOK~~ z^SrO^{qX((Hr7#NjxqL{3jH9jTipWs$pHreetw2V5j=z_q1-#Jk9lI`TkglL52Qlj z71}Q0xMCi(o7^G0#CZ>=Ct1%0?AJK--ut9_*){w0gMsgY??qn1u;E0K2 z7LiJ#f@96EWmvB0h$CRnGKxKAk-!UtP=;FM%I=`cMKKlj#w!)d=gJ>@UIn<;=CUz| zlSQDAFBkSxzGs=HX^k?$O2+s|DcQ)+x_^Mf*vHSmO2F=mb$EqJXTsN0>ZNH*G;pT$ z0}qZO?DeR)w*i+mZ#1lj+P4)MRIc~q+`%aRpZvdjq!o>sv5v6($03E}+BUsKhAAL& z92d4{l8#^VS@-QKPSkZD*vIjhpn6pd>kU*~Dj$YFo4LBWRkQN)(j8I0Lcr7{XE$nC zcx_^YXKW^5>GK@7{_6xyIf+^`SDY%Vd%ovNSP-LR=vS+6lpgHyBa=IF1%(P1g zN#ZW>v4i@8r8NHBtAMQoSk zJ)0GDe7@8q<`ms$C5J5%K|ML1REQ+^srKJ7M8rSB2lX&z7fD1F<0VSh;z&T4FNJV8 zbIU-J!)`TOa>g6j(Yc47kk-42KwzaT6GY~&+qf5tuUqk9mJITT>th531V$Jo&J$kPOKV*C1G1I3%8ip{BluwN<}l6T00@6Qa~s!?~1#`Pjg%c4ghgjNm0qg9+b z`~e4}*Tooy{EJTcPgcI%c%vzw_)i8+JSfBhY9&%~GD#W2BHI%%A=R1aA*GzCHY zJ?DI=?!5l$u&|l~j#QFFbCQ%*fOKgWcV6`dOQZuAxqq}Vt*vP@hsA%CY*mU$ywQ-2_2NVtl; zeiOw^7G7KLAVV7v=cdOwaRIEeRnY$o*U5cB=^!;Sbm_mWhHYNCC|uVn8tZ+pttcm; zclzDH)WbC(m##4uw+gXY(rBdt{Q&xL^}2GkGAkyQEWJY*vG#zEyUN8}#)(YqmBZ^2NMP-Lnh8%GdF|vcu(ql7 zY2<^aOo&6`e)%}GJ@xWSVr9KH=I*hlRGnQ*PO14xh=iW0cu@hCIZ3V0(5sx6(X#Tr zhVevLd`@v#x8~5P#A$wgv4-mw%r_ydzypDSNQq7o z)QuyO3pMt^FQ44>WV%xr=7+KQK_dF{)g z_&8=pj6`x{EiW?24BWktWt%?lMAO&8j9O(L0>6LD4LC%o1kj+lrK9J`b-bN*-)9n0A@kuYXEc*a5Ul#7o)yG!YN-DiKm+D$$|RYxDlDVQ|Tle zh;DN0qN85KBH@ZF7taL5$UzVT4P9Mbt*w!rEjNut&Uw8%+RUf+iFAk^%i2Q1DH3F) zI?L5euUYsAzAs!@JzakZLhONHl+Wk7Tv7KpHzB%>LMg;3siW9Jm$;&s?`tgwPdl%a z@Ah8t;$Cc`r$<5XiQEGsAmL$TRE_FzCyNTGM=O&JimIOK-gD)l5m)*zvGfvE@TEO=A=h8kk;-+d-z8`du;7E!hZi z1T>PUZ(o5u-X^Nf{?DIcNv33nop06frDPF!NW?aS$bcq5$?`_*6>o;AR`mj`2mWN9 zbUfVoKd>^4g+|oTuc(P@qkJRxYO7fa%Dj2==|xQRBIR*@VBY1?Q_7qo7C*F#-xYb685G?^30cq39A2RteOD)JWxwPygNp#KyHQNA3%$My(Sly6^8+ z{)9<(2}ty>H1X=4d7QYpt@`f+p@IFGWNO3t>heqh?$9&_$8N!92!~jSpX`W!@lpAr zM4xog;t}tCZWU8W?Trz5t{^RfF9ux&`%Ok+ZYJ~KcpwE6RcAoK!)Ix%$jC_lKoq5; zb+9=Bh{X9ICGg?E*}i8o<+iCY$_G zFE*KZV)^7)htO-iOmKC42wg$WN*fh%IjAQNZy#6ZY*^2 z?%xf?5XtaT-stZ|v?Xm2a)0mj&6A*IFKlu2l#?q@vgP9k->!ALMr~l1_%+YsmVs(f z0AoYXFcL6@o+U{<#ANP(^O&`{HE%oRr93e{zJuP)`3PfJrYE=%CFV(M4o;h8XRw9^ zGhn9UFzq+Du&h+glDub=#q3NYym76?>7w@=zSOQ3q-CVqUUfBq1CJSumHV}jp}wb& z3R87`pFoj3bm!Ooki|rj(_D%2|9x85R)<%?yah2vk-{?N znT0Ly&m%P1{Tuge5VGKt|5SpP zo{^y{v+a>Zg}D2eq+{Fics`<0p?TKN*;%_}ef{v1N!@}gzUiT#WV-%*-Ne}P7qjvT z_-3-Ovp3dvbNu`5@7#ZQcnEfMg0i$P=Zr7sHg;a=%HzkYz7ZO4muO zL8UC5SiT9DpP(ddep3cUi7Q4NuP)Vhm?}Al&^!k z{u?q_5wG#HPs9RAe3p;2zK(ji%?r_62dEj><}Am||Jjn{h6^TjxfrzD>;hxIb2m7? zD1(6jcbWI39m|*qPB?3~KSd%R1Fq+Mdqks3mz$k^8w@%=B0e{H?X;&G-MkGeKj?Q;bN-0i03MUm)r4qN@z!0%lN?WTyc1M$8jsAd#c zcZ$=x=(&M!k|x7m1Rai7HVuByei9Qy1kYG#!p1ztU!`o#maZ@d+l~oO2%9K)3qvG! z`mN>bD~gvye-DHh3ybEgKo?DH@w6fl$bWkhf2B`)t`~H*HRzknHryoUiqNS-_!p`X z77=!nX3+?TEC0hiG?gtkqzgTp2G7rWvi~~}!85cqI-yZPd>VhJ;d3@(aMzpM`I!2a z@qRP}*XQ}0!Tn!9@BJ5_zu=`LK6&oL&3*(f$#-7sT}h4;WM=4n2^jrvl|SU8NUXZB zv7}&%B5%SeQ#C+M_uxO~4Wjqys)zTY2SIz0KZVIqhTvRNu+Yg2T-4{$2VeFxD zB9Bl=QHHUM&w~v}Y3m81x`r{p4=UaAR93bU(L06{q3FH6N8oteHUHRt8_%e8OeP{* z8eArDwN)QgE4*l!e>Czeginu=6loP^8B=D|O6u__`z6`Qx?oo+{}Z*bgsh250ah5I zwMrn1NgcF6=I=)u8QFnt6|3Oztq>V38|XGkKeh=2o)dJN!;)ZW`15c|UyCe6G~7^* z{CCA0$J7Z#NhI#sxDb;Rqo@%+G#>+5Vdl?pPBO9TQW|bjv_Z@?KQ~tgQj+t?u(8x} z@^)`o=*)=Ga7~dNuoar=3*tg4%7Q0V9A)syv5mu+8QA)e`#0*8-ons>04>JD7Ai2t zg(&_^`IIyrk@T6tbi+e}HV|ER%=Du;HtkO)C%k&M#xx9txTFxAK{pXCsfyZNE*VI{ zJK>A^eux7etPO5xYMg|zY6trK+$<*R`UW8_Ibv^yipp4B5xg3wWW<`!VPr)TD+k<4 zr09w}7A-YS6;=i9Ja+27>G#iiIXj4-sub(;w%&xtVsHKY_tWeLbw-cm`)*UAI0Oir zXybrV1-^{VhTEY{w!sG({N|On8rc1_@!FbSOwdl?jH8*u>~;qfYp3It=o6tM)2cXOgwkiYu=w?r5I+gU|Jw<@l2nt^9?N#d3GNT$x z&?;l28VBquN5;)-8i(;{8fy)9qlkSB?`WD+joA0&-b8hQ=4l!~7!c9!oOMf8$ZYcp zP>Ll{7puzdaVuOIvMdOY{gi6o5SEO&)3#*Et_$$X+?lY?4p9nblJRh1MVTK~5-%<5 zSMS&ucKsdq`6pKq)2e+0l0xvurcm1tE`d^;Z!}X?(?#|&nA8$#r?@2wPsl6dv!DMK zfrOJ^>FbZlSu`O`9DKSRhGt>+3WsSg-p8wB64Mcbyg@+rH!8lRTVo_@WfgR zF_%l~60&d&|KcD0%VS;65$BL}IOdUxfHM+LWq6xAh`041AQJ4P@i)-uJ@w~N<58^M zZrCiU)GhbEKLu95hB1w{nsos+Br(J}wOr;GQ+~LkjHwnT-KJ@RBVJ!WS*6c{+FWrW zy!49M>lTIZ$gS4gWqmU-=zNiOSk-~7aGV1MZAyNFvW%>ZULjNAM-glN`{#jynioCTc94URFk2ljM`o#t?*W6w zH=cSnH~4$|C<-$YKzm4UEwf}O5orhx+D9wZgpg>D9V`pdPRf}Toj=Zs1W3M@{}i6= zkHgJoNE3;qw&Z-u-+X|bSMdlJh|x1_QTlG1p7E`Hh79e7AFX)^6r(44H%SGWzo}An z@-DBH{+5uJP!L(iSf0477Jj$Pv5%@QIv|Lau8=J9L%!a6{X-XRVRwR3+n=Mp=AQfRd7fj6Hdk7PjP`hQn6}0Na{Z}nJjG%>3YMd< zQfHmmg=I!y`u7~rXsH}Gm5SFO^<1zZyc2EI>}sjy3o^V03I}f_t@TYX6WC)|Dnr)M z>ML>*K1mc{LI}NnLtLJA|IIj-ouig*GqqmVlPxc%2F(sw=O z=LW%-fU&Fga|i`T#5A0XfH@LQ0tR1w3HKMIcQ7jf_{?g89)F!AO-jCiypWu_ckWyF zTj{_8eu)WInl#mFf^W$2&M{XJpFD?pe5@O`qfL;MV7WxZ8S$J6w?nwS_*@qcNerqp@uFkM5ULKXPC@pNr4b>x~JHN_Q;QHmNRj4nmX z>qOkIi4O0PM99EU7!ptQ8pq!WH8sd1s}DD}UQp47*P98CtRNo>lScTy0SpG7{3106 ztwB9P5DYkB7h1iR-&*RghX$|@GFaM;`d~G5&xnI3Wnca_h@$bbO*o23ZNPV) z^gVE-%U8N2naa=(HU(+{CZ7B!<1%iQlEqCf+7O4lfvE2)G||S!fD0^NHoo zmZTv|%QwL)0nl$8u8}(R@_v)^ia$2P*o*}r%e_QSf|bjY_ZPt!bR}0htc=!TlTYa<3a04(KYXXd0ztii?y}2P%Gy)JzxN^mX@B$+TS3# z%sRuWj2&>zY)p?VI_{y2-J#D2$vg6sW+6}zGb%Rx;aO{Uy^zNu+bVi>srrr?3AORb*Kf z75t7@RS+;gcjvOPKdez+pkCugnkWzcr^jhqe^MtJ_-QFLSg>Xb{95U5mdvx&%GKF* z#_M;s~V`MQ_0gD`AJI&oQv4 zsiRfn{2(>Ea;cxIVME_@Z9_%7;OjaS6x2#RqjDk&;|{Qa)k)oMPcTAnzl$l2 z%lz~v6nt#Ia;S|N$7yCZ44>k(G+@Iz5#g9$AiLv*s*r8$92$!lDmFN`G<*Sw;Z(dh zu-gh`3f_NBYpRUI^wrBG^YCMLgorzATG+(cn4Zs-n%^T04Q`93W^hmDFu_ghlgq&Z z;B#1Pf1ZnoZLFHJeb@w^qiq^~H*v`9=3*|^Te%< ze=!TBqaC6?`3gn^`gTpEzu4Q)kb3Qg%WWsx z@I$5g7tQ=vZPFH^w^0(Nj=2h`a?n+iLToJN^qZXCK$m07ZuNE*fg0UvJPYP;2)hJDs? zP5*PhUh^1;l8(QfTHF$#EIeGqCGwhdVhaW|FeI&})1}=zBC zntG+sKy?vGv!Zs3&_~DYz=91b>HdL%B6E_nr{Pn9+l?U3Rr@W_cM#HKLL=V4Pa<+# zb?`fvWVB-=kjVw;Vg(Ovun`w8@5%w-Ep+?*au(oulc-j8;0gRlUR`~t0R@3t8V0_8 z$y)t-MgZ67Z_io~(YHE!TBB<;ADN-cA1y;eQ?}Kfb=^AfOLRJw53!0Cw<%xht6>lnT$}5rZZtCy>rc_@RetSLN?Y6n%Vwswj*d$P}!mrdB!)BI5B#&;OWuD+{7r2&`J1XH2`BUTfjHjXk|`dz#9 z|KF{t7C4R%3=FJaxt+lDE~~d)(2vbm>bQ-sXVn8=kr=-}TZ~-S|0rYlFr#ghv6*)Y zEcsYiU=Yw`H1Q-P@>Nt3X5-?NE##0)klz1v;WuPUn4PoyFu~Ms4~4lO-Dk2e)LA~_ zxXnpWvB$|>N}FH4@sX$`Ldl(Boe?UjJ$j`-jfa&!>yW|6$W)Ytt9h^Xa+jT5+Gxv8 zD4ixe9d*!r9b}S)>rY225dfv7s_u~{#OwC$LNkZwVtJZhUc2xYWzV>%ZrMrm@^h#r z)kMiq3Ba<8^q7x;1@zmDT_Q(3<47<4rWLg+ppk%pr|g+Fba7jXcH4?2b0kG_{X^ z!s+m6j2bg~Bwyk-#X*HnP@%uf!E9xqw2*n(g_!P~H#{$twE4-`5B;W$6`6UFOy=vB zy91HI`Q5jLYJ-;^v`4{y&3IG@^^xmH)~gY&W7x)7i-zI4;BLDN)i39JII!@DAzaaM za~9tSrgp~Ap85nI`{u`DMn^~4(rx+|`%%!=LG&-!J9{0hw_nAIW-r(_1<-+{SLyhl zLCsSZRMIfcbB0X*4Q|1cvvstz1wNBvvk)=bKV0zVwd*I#i92Rm9Gsk6tyR=oGECSI z%SaTr6n26-dZ)tRc}ycpldl-H2zCg@GbcBIGLb-ClQ-XM814fa z;%&g&U5_^%bcLue6VdAU1&HDqJf>PgZ^P(E!k!2t)h*LLxlNdu`X3_{!cmcN*!}h% zyQ!9r*Cbm&okNC)B`eCWxw{6G>@#s1%{Gnk1!uZ9RN~bL^fdA_Dd^a6uebC;tKdrj z7jV*6Utgb}zhz3Rn@pHrPhYE1LLwB5w^fifzb_F-uh@t&N1maxb;Q-7f0Op53Y@Hz z##kcZ^o(?$kX!o_j3N!3=D#HUNFsU^gj5$usUP6sjBfWrs^DhG*h zxd@Cnu`QS2)UeG=3ZAlV9`m7759=>oe}o&6wJU2DzB!RyCidd-a=cf@)~ZepBdNR= zphXDkxs5LbH?;YgzfQ^*7O#LQcTm%mFjOxFLD>H3iqma?~0x)G`ew2$t79Yxk5PIO<#8 zD>sU0x&tu?ECAnGWJ*zji%rP`gkjY^aT8yQEBlk_H=@>$!mFPhu3Do9M@f95Wfu^L z+vcVoMO+=e6MjE|6lj@0Rq@QnSl0A07|UHrrZ539u!MeuZLCMD&Zy-MzTRljpG`$1 z1%3D;Fb}w5MVHiD$ev7?KBJkjQTdWrJmbucB5WF?Pu3v;Hl z%<4Z-MGx&CJ=D3PN4V{R$LJ?K*964IJsq-D2!gYYkkBO@ABI@J3IeZn0-4z-nTHHou{YHA{p7KRc;eqOP)lZ zM%e~xQJRTXV;ggH!pA?t^Hz;AaU&w0B(<)@FqxP3v0pSbuj-!(o}MO#uVh_E)nV@YY~q9ZpV3j&nri&ys=x0oJu$=f-TZ^Gt#o;;73$1vc zd@4aX>iE5Ai00$%AgFSZg6DC0P_orFj|*#TaU{zA)c8bLhw$M0Fr%}}q*^Zv~cNI)R(FpTNjm1JkF=Sr5_VcehQvR1}gPR4FVY=#O zkn9rek9_G7pQ*Aj!#_b6A@YU4;@BiXIBO(!7B&#sT~xL~Jm=NhuY%olJqsL7CH~bq zl}Ykm$N};{hEllyQqmO)JtsP$(iD<=*sz}1Cz%@OQ!eKKjzw6j6^qT5GGN%57kiiQ+D>08OAdr}Q&p-B(Ox*Q-w{$*j=2zS|;veh57el~UQ-S-c zaDd3CLD_-{uR^cwdMDk^*J6g!Jlp3Qj67KyS*~g5pRKLs=s*@SEWLz2(%>lZj>U>< zS|Bf>0GXeMcbNsD1^K_Hw-c4?@zAOjVykcltT|IJ@luTNbBO-l|{qG$f@?S#jh#IXZEsMz%S3TTK;+XG-t$hx|b+07g zd&y(FSUEWAJTBC|7T;T@MaRT^DD=JUBb8HB)NrzjVe`mhRbR{FQv_%&lE+Q#>!VhE z7y2f|tYy9qZ9ZX`>Mbq@qL^q-&$ZtGO?=qsN6_H=(scvk&=4whL(n}>dnAFjWSI2% zbQ$e=9qoDI-8w@AOc5dutg{I4IbE}DTC)|^rZ%3_Dra2?C`q69oFw(1{y3@)qKRj) zr$@=_T{Pj0i~Att2}#%YbE%*LH2`-EvBOMy;i>}qks<2u-CKPwLP@!vcnCpfXAb?A zX(mj$_MfSwg!etG7mOo)o`Ev_<@pL24m%SK#1^55bL8w1iDQ`Hh|;!lTyVhEZ3zE_ zO+wFjv5EG!_zF8^!Ipq?T`~kAcXD!48i9GM3F#>@-rM4@C)u!t6-MMNhDJ6`4!S5SA|I?*L?^pnwks*IWf6}=i3i+d_VmlVy2=*)o~-Boa?rYe)K36TSLJ)-+`_daheI+kE=K}bl~vnTA?`Zdi2!yl;O zsMJBs;EDQtH%k|l{M!MCX5;#|;_&qKjbROQR;4Vo5rIpDDMglzakMB5qSu2wOM2b% zX0rj>d3E$L4?$bLkNKp2FAwX2M-4O90Ac3!jd9}yX<9}tgM3dec=Np&mf%tTVNX1vIZ-jqN=B=NI zs<1|)#}5mm5=u!!ZG%WoghE9caqxz~sH-I)3wiDB8BYZqWqH=fARCl@c{YvO8TBCb zh0I@~(Fm`X71bAe^i42M&MmCfEFL8BL{gJUtnMo=OYkC7>5N10;X_ ze*R_E`$9hZ?$BI;?@Y{Q20~O^H|LPCELHzC4>HZZd}LP5iJqs))5tcWV$WhU7n0j# zqhsZD*J`K(ALPDrdAUDVjs^xxtU4^Fe$X0WtiOMx&!9o0Z zXfD9Z!r};WakFp3Xr6z^gmD^R+9MaQl$@_$5i6;i%{kZHXOb9nKx0WmldP`e zPtKvm`-7S4&8lr$Q3_cNEV=-AV8L$9x>{RVh=-3~`w%#(9b~MPfKQdypv?ds zdJ~2p0pWH#(vt2u=_7xu60Ti|BZ9|AF>w(~t}@K%w>{Fd=DKp`cPc`d63Mlzr5(8D zuk^>hG1>Ap_Z5>_3Nf^|1l6DruGq)c7S&TdKnIokFk#xP{%h=ycfJ<`8N^lQl)G;S z1Id&)YLK#zX~Pl!zaiCkpmqW4Sf;d>#Gpq!KOyiV0=e@@QmT5n%j{H_TlZ^(MXP`BwxJGI7|F)#ZteuZdInoabD27XZE|Tw1?leb2OIl#VB~ zYd=fAc~*fqYz)IHa%~0!#ma&MpZ!BG6Pyp;DQa<&d;p|mRay;h~yuMN7+EO%lDrEHA100+e-~dr~a=zW= zcBw4QQbM$?4xzzUH#DZYN!eaa6b@zit1Uo`W%Hw{)kU|c@uRppaoDo*>Nk{L87n2M zyac6)2#W2DrG)8y6(#Sq0@(GN!uR$+q~ima=dcqfKFO?koiKv4O;-Q>51V}TZT3l3 zi}@9^WK&sw9C;G{jZgLsLJ-$DQL*D6Liw%*y2SBsDjzKS`z0IagL?dVE~gwms3x`W z|6sp=@?}>Dkue$=f|wRSofJ^Eej;IUyV{QMG2Z*IuuRWtbAMzG|0TVEH$&>&JGhmB zEzZL;e|+;GgR9&pTko4hqay7UYu24$s`Z1v<Nq<-m~uWRM#KdtNpP`xQW_Tm1=VzNvk3x4cQ7bN_|lMPAgA5*7PfDcw(Mxl_3MXCdRk@C?k71@{e<8=GgcyGc;SD^dH-(Sa#1fYzuEGr!sI50PJCMEY&&VA z(;&ec@ELD8Y12x&deD;^`%!EwjQ_=*T#D_LsXBJc{O|==WId1T=SyqL>;!x{ z`}pfZdKyp@JX zUrgi8SjQAtS_LJr&|%uZ(Xc^Q^}9Sjgp6XSkE!QFok}CV4k?cV@4grzLqn#27L|l! zp(Gd8ni^l}NK`rxlVv|TTC8w_Srf7VO8?O`F?sd_V;C>H<@Iim&Bp}1{(~8ep zm{}IF4_zP|9X@3B%Z1aAU{zA*UF=IlZz02fJ!pX(s6fV%w^ZqjV~I%Z*4@@&Ws^SI zG_9mJ$unxzi4As?TGBvJFd~puy0NoGeRT(omsVf0IcvyPkJ=~P3rv$KEy{$;`eMstA0mxJ&C7LqsJPiPf7 zO}2OE+h9rdrd+*}fRq$`sUa*OAUq9Q9KKUh8CyE-wY= z%O=s+7L^Kea=NWlHrcE;$2oPH48_k(a1kG3S~G(#-ywc{bW~Hp=?Mg70^2IF&BA0K zr>hC|hfa`|ov~!q2qfKugODER#0B7&nTBmG6rJnqhN1l_fR;ra0;#Hmc~!mX$PY3| zwEOc#wBYltepi!{w_VdbVhV=BnG$LUItzEp-`lMi#*t|kZ~JUAm|A|XSE593%5s9y zXJ%%6g-F)VAfm|_X;ef1EO;j_Dpxpw3orECX@#msy<4?&tg>lM7h9`o5RQdj zWw;N`?>VmPCh)%>B(Uon)GdSMvg80$ZGurCAg$LIuE_Wci~ERfig%KcbW5p z+A^oi_kt6TtVApfWo%V1v8-PXf)k&DBM%(HpLWHsV&#Og*gDm%nZ;ZVDLFzY+c*5~+-L zQeoL8(ItK9Hjp|P-4~4BwC?*Lj;qhc%zW0m&~==U+?lsz{h^TSZMM_kW|!X~sjkP| z9B7H)OPcOFNe3M8;0w{CauQ9Y!UI)w!$)_{ogkw>hhFUq>-`aW{d#N86^Oy-xgb6w zN=+*$9W@IFc=>q}4K^!D!mHm10*F=OOj8f;QQb<|BFV|$z(q0#AaNtA$!H}-Mm3t7 zsffgqHY>3vo6-uWIgyAnkc;I_Pu?hBVWr`gahC2(wL-!+}RQYb^vjlHWt zrH$daFPWNRhSd|q*HFRe&mD|cDrhW-yfn(TV$)ryU4nn#C{WK_HnSHU9j#t3oyc!w zNjqjZ*{F}TMxs5hG{5D6VqgA2H1`giA@LNrqIUu|s`*p@t~_Ovqpu#MlT0%x1TR}V ztj*rLWf(4s;m(rT_*kttXcn^)iYueHMq-(s>-z!JhqS%83>&j z$&d=%&^H5M)i{^ zblxXc>$^{dfCXW!9>B1)E9bwAdycg|AM0_j*YU`Troj{a1zL;Brr-CWob5dh8FlpO zV>@6k0mSkz_tnq$D0bdH^;4tg9>k|hElXfV^a1X#{09|Xd>j6-5|Wa_D)6Wcoa^2f zAjrK7TigNI7!kY7#%U!5t$2bK!tQtb-K9p>^VEeP4>NOC(Gt@PzzvLeY7)-X{GQ8} zmMppQK{y0Rlfar@<(p`GzW?%_jXeD0@KcjWAqf*ZWjV9mrAzPkYe3pQdHW*;7=ke$7kcg@W#}T%-DZaAZu(33#ES zBw+VpH@e{|fuE1VBXGHJ;{ix9vn0uPD#X7x(zHiQo$Q>PKwFmZR!Qch^`I&biV9|c zJZPSG=Y5Zi`SMm?CB@70O_xE^wh_oOa>$^ozWqI?q4$)Bn}z&2A$AyhfZ5c-+naA< zCW?^r*>SC-(#+}Q=?1K-#g)N2dGcqDy8mb2jG!L-3^3Lv5SttKa7pg$>|mE4#~<8g z?XEeFfbbTOLmSXd!qN{qW>r_0`6V9Mj z=j`oGQ3oA4-+NCBi&nICtJ5^fqQACCC7r}*@Y{lofBj(8#oDa!b7ZFbpReGah>9B; z?r%;h%wYc{P!MyG0ub4h>LVcA;1|~J|YCYE)lgK&lTAQ zHgC(#4*BTfzzyJEPLOr;8@4}$4n#p>j`>SUDlJ)F6KP%?Hp^g^5F|_jpK}uD&8EEY z#bEd7;oc5f4ioC?5Yg^Nb`h1jT%(0+!4p$tRou))Aq0tYs^3#fptb zlNd7FpWqY?(&ejk#tYGDvPKEq7-fC=Gi|5qx$bi_Hag0*A~>0~&rLNXLLCM$s!VK; zEW5SVtLKN4u*-`eRGvZj42XynQsS=1q9G{qltUr>d%XuIM~^I z|9E!iFNUZQ__mTO+G#4lWE2Rit(n4w zG#+veEZEoWKacF#+^jmG6^=V)KeV^lSZxo#)A=zD&|J~;AoO9S|62zjk4%v@5g0$V z?-GMI25b|aKzhv4ONEaO4tE&QE+8lyo0;ttd{+px9@{hsDDUQeS=Q*fUOVx@C3ilk zY+C+71aA!rQs-7Q@AEcL--EBK&=N^{9@qOkP~&N-yrMe+?#)H66pu;0Ri&Uu4-MnX z{I)B#hLb}N&C7rP%xAadM7PuX4h%F+Z}&*jHGbn05(ii%l0oJxv@QT_Faz!iT%!Rc zB_*!D^Cluq`)LsA<11{@%ZDd0gq*lkd+DLW?PmKu1I_A;Ril`4Z!9oyv3mqn3Cs9! z)vfa)!|8X@?sg5#)~?{o9XIf;Nh^9eD^+~j{HBJTK{V*tC3W|!wmY`(Uf?gn? z@XA|~_RZJ3=&f!At2fQP`>gO;iB}U3PtQAJ>A;e2v$VAI>({R!KBEv@EXbQNJg)@A zNVQ=PIH1FU<6^DGXlpY}&<-@=_3t>a#YVyHQI9+Wo;$OU$-jTyjecD=%ln4KE`DEI zM>a|R*u3$*QvCoe`Xy*a|15$+gQ+EY%b7<{WRRATms)-XY9&N$lw|tp6D8mofMUhU zI$Kgojzi3Qsl z*kJ$tYZ6a|{@I|lp~I+;GI|SKRKJXJl%hNXK0a{(OecM*#M@^ftsP)pP%qxta$>=6 zw{{P4h5~WairLP8QOfG&KBqll{ytUz>q)F;-OpxQzbN z6#}p)5fdOpPU;))rWr2yz&!D?w4V)J+^XSQo&I~~e1n=vI333u>%db~;3Z_@OC>OJ zI;BJv%$u^P(CIn`V8G*H2rhmQfb@3*8`y~U{ZZO4r$>MrTmRYF8F+So{NK^dulv2A zaO!4L4Gjo9Gn0Q?=z6{ays0{Frt_>_7i*cf8GJn&U_ITx65pOI+Oe-(mGH8x%PO;6_t2 z1#ARlw>=aYh%XWTTcQFpn`wjR^W;g23AdpC9Fwc{A2j+Wx%OgWV$k59Kw|_381?@m zjbEI8xe45~yuhTc`F8m&8cp~N@J{%Q=S~c%1Tgh3g2~M1$hPBZ9(Y`y_8^2AEm}1K z3V2mZON&{(R#M9r7kuvETL?4f@7sJvpSy2OtB&<{I(ud==#yF2es@3Pkh3GQ);_oGZj3>v>qCLi_3vIAoma0GWf|oSTguib?ISnJbZaNMGuJ#ny zyPjW^x zKPSIz=H_f=jy!nb*6)=?H6OWE`x=WAuqlx*x*gp>}QYPyFMiK_ATSrOqg+i zT6rtw?cHvMLz@r1--_{2js~HvE*Q9_e7JOGR>2FS%hNX zaW{uTDByS0xL}#~49>WTOgb2Z-rL*T(oZKDY##ByWJVf|lIkUTF_I|lbQ#xbjR&gq zc&qi=LXDu}%F@!(-;3o)_&w>P^W&*BD=xqKaKFVF&E~RP?YMci-0Z#c@bF+~XXp34 zbidoyAa&p9w6Z(;q$tSmeG3sr56tynvqj(!<6PHla61A187HY6N4Cn?FXz1ms6>l^ zo3l#$>3vYD5g9u08Tb}O*i}|qTDx+@K9`Hbpndso=_qUOCPjoAqj(0;jD(Gzf!U+? z?Wr)(L-%d!cX-KVb5t6K*~UqUv5fGFNo&`hKlwadk9(S^dwzdUr%}ms)wibvW&1LJ z$lL$>b&+|OF-*Uh{%lG!LSM~PraZfe{0@Bvl?-awNP^H;fzU5h(i@basi#V6nN$|Y zl+Gv)El2F^0(u&-uZ`9_J3!`emnRUM{>BfLmbW>*-BL6$Z7hcFFE>tKUcP^qNd2i; z&h~fv99Vfe+z$3!9n9n6;sPe={H(f(xw&Gx00Si@ESoF$yhJ>7@q-e!a6DHiYgw69 zL=hBeGK1Ll0o;2MKW`5C8M>sdg$Bz{|yf>HYA@Ky?1@c%dUMO79 z^IW-x-RkW-Wc)xfVqRCzKeYqX)6;BvJ;+pe=WV(l?Of)1`Vi);wE8aE*GZkZi0DUL z_ola>AC48Jr8j?dhoWTrUGL#mYB%aP{BvmI8_49i zcp7FlE+*fCHvakQ&imEOqcWR~Jds-1cZ^u!^s(tmuj?kWRX`qw22w)I(x{>9)=Lt~ zv;UOyzkK;ZJ_9?c6FEAbzUkTqASW>s)-DC9c<1xKLx8+VT992=w`^)^22LhYg(vXJ zpybHHlChxomzS4sZTc7tlebwcB!ZSE~LZrTKOIEDm^pu$k^DM-jlPff%5VbxSvF`jvsZKmDJP{Q&JXl zMG>%we~kOtKJVCZ=+NU7&u}~blwzX1`)ijSOLh0>8zYC+ER>ZMt07(q`wAftxex5V z#3hMmh&aiz*yR|0sh7ASUQcgimq-i;}6oU8YwQ3cxQ3(Ut2K~ z0kglpUu4xSS@#&8)=#{sYipl_)5-N<9tHF=3C)EbFiUTU-URZY(s~2J07|ZW_Qax` ze;X2Hxk9(`^TYK~hv${&{)D1d<$PJ$hw=08512><$57~xZ2t7F=kG3FH-}N*fF`?! z34DC`H-d+*ZEEuG$CyTNs$YM^hrr>bvCb6AxwzQq>aG^bF5tbr+DWe_<@fv>&D)>) z)IE)CL{?@C_3e5YAocU!Wb_*vwR$-AIQ|%rXz}xQ0VtfiD_0G+v^#1GFxIM!n&EzNStGUsjU%y%F93GY# z~Z}`}&`jQOOw)E1S5c9jk$_dj*-7avw=M6Gr+ z__Ojk__C*($`)M(^n;5BA29Llj_W=D8DRuJ42j1D638a?h0F6Y_ z5z%ht`EQ?v*#-$q0Ew9C>;Kk~O;%S`_2+h#DBodhzx{tqeRWjSZS*IdLytJXNFyNK zEjiLihtk~*(k1Z%N-8J~N_Us!fG8m#(p}Qsy^ni#_qU!o9{j_@%y*u+_ft1)=ogl3 zeWJn|IP;|3ax18kpQ>W`^%%WTO&2{=qk~%-G_(|$Z>1~8KM^C`$SElS+dgP5aT*u_ zdG}J$PyR}1XlvKGE}tM>Xedc!`4z)u?n{|csRItX8k}m4{f_dZY$OAysVS9P}4r$hN5Hq!4VGavkMb8~y1U8z<0>!swKw!3>xo>FAoc1u>mX1n)Z|4cp2 z-c+^Gn09<0IEb19h_&GH83H?rZg1Y{=~(S&G}{bk1p+9z?;Qntw*#{p{B@IQ*QaWW z{kDsjpflQ zkJspABd_lSidhK;21;GKiJ5}>&YzeBROi;!h134Gy}fOcF%0gYv=1Ln!I}b{>TWv6 zEQWM~+}VvqpJlVLX2J9AFaPItZ?j57)P(K=g~^(J#2scX9H3JkedDg9GhXkI4~(L= z3-EB9faf#`V75^PpABbB9{5_nCv+i0(nuhOjEX@(B;hbou`KMoFy{-V&Y$?eyG%bZ z{%jAt<}|MHSugo08%enRry z@VEW*S(m6Z-4Ox|Q99?}ZK0ZAy20=X&uY%6KU@k-XkYiw{6f*$6nRG~|H0JgwdRzB z{_XR!d~2C+79QbXzzH{&WwGh=Fh)=kA|ABvh!8uB(Onm46EEwEi_l6wbhbK<Ph?pzEPmsRR2^sBPkFAbCOO05kwf-%Zt@kOwU#|mF&3%dn$hJ*?e zzDR2+umk@NS{)LqFl-hNx((l-uYH8L+C6zWoGrQ7PE7p$o5jw`>bv@0Xwm;* zerCpA0}B4nGaMNWjZpT1C!(Iwa`a%OTjz?p-CduhnglTs<5*f-&kJq}LYP1;6MU=B z+2{5|%oA8UMuTZyoK!|IDq2U%cIdwPS+%y*^%mHpE9_>%B7ix5zyuaqTvoOw%m$+H z;v!|}(2=1;IB8iR8DLNmLn`G-T{J2?euRv3BNbw_M2V}n{jNAs9Gkc9C* zZxbLp9`11!`bX_~EZ}adpD8n=8#HKQ>(vu?uX60PP)0?=Wcfem}bxv=oX>MoKCpeFSnuLGr)W zFttQYZEFoX8%}q;zf6Rk}w70MC=B_?F=(@4v&PhTsX}*%bJGVVre)0=C-OwKqVLeo`QDdCuWhf{b+s_l1>T9w z<#~M@3Wg%MOt8XP z1s?Vg^P5oy`8nq|r*Ls^$Ue-q2tl8?+800T*f3be9^2dPsv->E8f$zx+Uzk_sM@-- z-x@N@HgNn7+gc?{Tm*{8Vf+oGYsDL+VCLH;W0;a5j>h#K&o{Z<0VzS+)uD=T*3wCv z@d01}kSDiIV&B_91N-_l&h-(?fq1XZ&;n7#{A)fbQ|S|XC-NevDt>|>W8OeFDL(K z6Hz@cQ%%>6$%Bif)PgUkA8#=aQP~|m(&&;6KnW@0aOSK}C$}f~U?yD!9l-MW!R%_; z+=RYW4Np6;$D@d7Kpf@m7hvX%D<9iBTH;lZ2_P_HZbx zsrh@V`VAvv&&J=Zxq1g1ulh#$_VQ+f z2?fs(lsmb0GvF9gNV1%5_*@{yLNw@EIA)-!S!3G8hd>}WUe)c66;=Y#%g~U-`P~uo z#?b3(cPm3fxZ=uKfvU+jpV8y>1FJWC@W+p%(#VbhYZYUxwx4C)2w<5(Vhp;2KEw2( zl*GhPvrp`Fc6N3NUy@YBFp}PJN~peXr$h@yfi1|u9$-1zxJ*!3q*1H4(;f4UpQv4MMhohPdn7l zL=tt(b0>U%Px*N;8b`cO<-s0J?!inoQxeo$h1vZBV*k#c)LHNSxn^*m#03WLrVsrK z+upyJ{62RECK;Z6k&o}`^}W70IEX&c_AeE3y0#xJjV;r4xY(DdFyV@?P(lE<=LE>V z0Hr7H<68T7$WrmX_XBM!@P&47t{&n+CBKM{95j3qGf+#Er@xAS49isUi74c~Y; z7SDO;%urHL@ByhhFW$kxx1HkBAR)eER$GwH9r^?=t&SKK7^21LhZ?K1#3V9eN^|El zB9p1tgi#c+g@PlC#~QFS#i4cGy_EWrA!c1U3YYcK;?T?7pdC4T*YH+ z+VQ;2dN}A5IbIq`^rVbcQ|=>Me?@SWeg3$8pwm`5=-|e^e75Cb^F zZ2@g2F(Ez>EL0R*mr_M8pZRSJe%5-uV|23IZ2JTMkCmuFp3)SzT) z_Si0sj#lJXmG^01`99>$X?&gRy>J95q4I~zRqFW`D@)5Neu_K&7##UabnJ79`<@I? z1M9PsOA~_`U(M#dH<*}LkGZm3rm9dCNnP5nu6tu2+YB}VoS~!rv}Ry^<%(9CI9Gl1 z^fTYRloJ?MxYAqs$=JI>`Yo2BaIgp+9Z4?){pvtwmINd8s%)pE*W65hhI`~QAB}JD z)$tq|GqY+Mul3**0EOK%9)0j8(e5LGjB4ZcL3EgKAw2vk^g4=+JcfFf+LPR}@+41q z`0@H+N$T3~>HqtWxS?w9DlhLRK*6%aaq*N~;U=x;U+NtuZ=LS0QSYt|^vUHlR8t6P zM5}zqzIz*d9)5nJ0(vSic-!LiQ&;2s`5@nLP?O)d6Z$m?tXsW~J<<&-aev370X6Xn z>Pn!KMK0qkIU}P87n%ca_A|u_R5HLQMf&S!2*ni83+I$P+nZ@rR#W?PwkgCb-;?ka zWSff9)XeTt2?Z$MX*GNK&NBp=a46^;emi>rT))P9PZZ2-D_%-g zk#RaZAT@O^NF#4A!D}I){4sJ97}Sb%z7{WqB7igZJ&>ZTg3*)~Pu&Zs+oZ?pave$- z-67+p@^3mSuiGW>aBB>c7*(@`k8-1|7n+nEToY09y#Dx92++QCQi_UR^zp%X_1ine>79F<}&I_x=YT^&WwK;|WjEo0N9d zL~>ya5)9UkAgA$|i-8K0bfJsrqt&Nrb@H(+F&;G8Hx@znS#AFEaWK@hX2DijVvH%| z!wve6%cVJ#S!EU|rHK35K;^fL)YO0NQ^RK`CRX4Y1~(Yau>gxKL*pZ2&5E zl)))^bhWvc(T(LDbEp)Gbb_zPhH4fsMURctip6%O7Px>V4NvGYZd^Ive!Na%QYOX# z5N1`tl|MjuO_l#MlLH-UYDeQYi^4dRk%yU!uuwjTcFG5$=lWVYI)I!h)iXBuy7QkR zd#6m70(eq361P8AdX<3zjMuFD>!pu8EinbJkL|+eTee;%0fBaCnt(b+Z*|_uh8$vl zyxKz5$q6*LvhsvP6ugWxUUH|gG-7Vz^vdX1gn!;`YJfS0J<(*~LLu@1OBc<*MKSp) z_`2>veI0bmL&Pf)(vZ%bl2c-jKwSM{)yn>Kg5ik}hQ~P{LuxI5I^?ReFnk%$>y7ms za+H*f3X=inppAsM!%uzJOfR1f(H&lLL3@akiJLx6%+|o zX0Vcm2Jhu=%p}HNnHN1%JQil83;qYE0HQt}+|t4(+&`EvjUTcGdpC<`HMHJAQZ(>- z5P0h!z@~o@Pr2vaD!ogEBsS7hgdfaRTYO`V+qgQ2r``wMcLWoOQG>x||FHw+m82$g zyw&Dq3@s@1-M7~i!#W>x1qpC)sOpSUOSGt_d)s(dme|ElE+NOw+0x2JLqm+5+5-aAaj@%M9Hac^%biMGpZgFsPKG6vOrj3SmSZYk#ePm<(y z+GNJuo$c2y9liij0Ky$m5=2`Ws8fpgxITno;!ECH2F^}U1YV!gx#^}nLPNSjj&AI| z59W@*=>chX!eQxlnr`vBx^a1Ob6?a^koLu{wfFkFL#2+6j_POLkDZ69#y+T>f%;n| z?ClLcqvLF;QG^tygJZ*K>5l*|`GZK(@CJ-G3;gE2rxhHeq`L-=^1X%n)ywMDQoRa& z?%>coCI5>+|EuHtTx296T$mdM0jP(rnc4C??E9}Q{dtJkyj0&0aTUUua$od<2oNM= zjV}uAW<2?<1yq{@uH3m{f8K8zL?%0)X0iYLY+w-cSxZ_y^Q$XzBux5Ye_rh2c7s_y z4sKR~W8U{GQ+d(Q5AYh5jlZ`%>5D3g`h{K#rlzFuE4lyzSPA*xLWTeNu3fdh_x{v) zu~y-1RD6iN#aIKBFK<$UV4Q$do&+wW{l1{++gKr`kn?Th*-n{mDbkD?XXU5l@X+w^ zVzb5USKL+B3@I<aY$J>{Ws%H7!D)PM7H^RXY;+;|1D&E&}NrB)NLQ* znSc5F_)RKe<0O{r_B`MP&k#+K??IatI4ass_Wjw(@#7HLkhAT*|5v!39Z5b3es_Je zh!Y#Zpt>mz#arLlpy6Bnk;q^YbhNI_)y@wQkE1f$vK<6j#I8V zqTRD7aw!+PR}(vD<}g8v@92i7tI_H#7&8K}$oCWy-?`u2*4jw(_JSh{XM1lJcpw(P*AziWzq%8ic`CpJ zkZ#|KHsR2W?vqaiTY&$r1YA>&`A=>YC8hqJ=wwTS=l&i)JdLou*N!aucpy|G9g=aY z%dSmYeQ_AlyR(f41>!;k!uOz45{aXdu>Zh5a7fsw&wh7vk!p2yn8+a5N`R8u;;nXV zAQnY%=PQOA_DRCuyE026mmO(ePe?6}^TW2jzCMJ$ccGN$%4GxtJA@Vn0Y|Xmq*vo? z;MQ*@2y&)j2yi_QnZKgcn+U4>8A$<~2CxSI<=)32cK|?TOK-?}j@TH^j?%R4 z)E&G{Kzoi%m@csx{`{HIRIGcdi$3i74v>Cv6nvDn2RSOD_J4kS{mK}VcfNl|Sn>ib zw6mdnRv{_lYD5+=-G_DnjR1r{A6sor4QU2e=mRFb+CPUFfFLYvpC~4Q4oN{(6}U4% zx5Qn;y+=eNQpTdR@hcO!eA<`_on# z@NjUMeg9D7X_PJW#@OB6ot7B5X9GhBFwsz@iBV>zek>J z1}O|U=3QZ!U^vW!EYZ+6U_GJ95;>W+$@0DWm`Z}-*7~IC&_+@e;6K2YM~)c1y}ix& zA_4%;I2s`}9i8|-Eq#5T%aLU)0?NVQeUYMVl*|jULbZIB?LS0?ri42!KwSnBOPEzY zO#y=NlclSYl2R1m6RB-H-m(!F6f*P?Rz*#fOmbba+rdhnvG~7h+S=Nn^?lPFj=RKV zq@^WG+=HyOR!sVk>`_-x5NQUB?L}i=eYnQBBI*mdzrzg!;V-f5h~YP-<1NdvT()V{ z{I-Glh6C?>zx;?Wp#AGBI|1squslsk|Zj<1T`0mh-w!9&()*bNZp%C z%w4ab?8TikYBBG3R@3q}k|jGW<4S#-ML1y)YH_*Av)IRA^1$E;Z`>--zhJLMb$82z z^jaA_58Cw+h68Ez?(gQ>l9IxgnUUE%kkysmSas)d!CDS!;-~f52g_8NS^%IGYJy-S4OJ|)OG7SyI{U#s6}&xj5Op3*=e-(VWjBRVyNs~iCIZ-oSCXSMS_ImFni!=)C3E_XBb)K6e^Vk>) z+}&pY4FMT0_gto4W zK&=jod?MiooobIXQ~AVwpD1tL*W3FsH8mIrOZjNBnr)Brzz1*oFHDd`ni!latG@S3 zU9zsYf4n5%gxM1QDH~T4<+ESEeg#yZ6fMeE{{*RUsxJfU_JHSdIVFNY+d9l6@>qz5 z{hH+CsTmP66JJ3i1PrVb{N*`S>QF=&txCG>sBNy!ILl?kTr zB^rQ!zc+Lx!yrCM;ykVzLpq?%mk-w1Q}^QuVc%S{hyU3wg+<-m^MIx;*U~=|6W^7T zmq$W<(V`yD=^P&)R~WJayJz-E5Bvv-%m5h5DGfys!G%$3?n_EI4XZj=kKoS(E(br4 zB{7!!1V{9(Ht+lrGkaW8QQ<9`**GJKPtKkJYA9@KpF?2q@!uVlx0wrm=-Ziy2&04f zsg8B-vv6|m-&61ba2)qBmC>Y_*Dg*$7a*q+L=qT_!LuX|2gh!orUSp~#rCuov==}e zi;Rm!9dz&ZU4^s!_nX12h%uu`P3SsM=3&XBvgmblm(|sI&Ncun{8y&fOW4`#IkE`|!o9Cgw}6LC_WMKVNyY!iAKm=pk8bWzyTWI+buxj>^iQ!C z7*L)o;_;U~AgJrRstLey2&5B0L4UCzIgd|7we*Am%R0DewpKrAVqQU&*iFc+Lj^b>S>BJI@w$)B_+}riiikI6nreSPz6TwYJL3_EB(KP zk&cJt$3Eiayu8F1B5x{iX#JbM?6i>A5{GBUhid(;r>^6AIO-6^d~uo@^w7a(o2IQJ z@;46F#xwQ-WM<27!~e?WnkxPTiZV_RIQ$h*3{rZQ1}Th+vAw@bbl9Yc@nT~Dn!~7= z%4G!PM%|Z3fV==5(_PRB38KMaQkOU!fJA8ID2*V^gpZe(`$K|U@5pF~WH@Xp=)a2pyMcayT$%--GT8M3V8yIv<{Q`gIgmA_14o3lf*U+0( zT_dB?9jwS@^_83*oS2o!IJ^M4N$${82)e4v?+>}n5{p5N4j0bS#6Uj*Qf_V>+~8+D z256x(i~hF{B7@Fl$Vvg7RHok5=!5Cn3#v-4wv%6W9Iq^G)2cl-;+j1++JRRD+{=s+ z3`NegY0gE$aVx@bt$FM9Qg@)Hf3_?mk&FVOm_ zYiOXBpaJ-UtKGd=(JxsoQ&a=`g@sb_w_rNb+QZVSL7w=uJ7PdxN>(F$-mI6$zGv^t z>Pix$qJV=IH=LP+d^Q9X9l3l65H8xHI5HM#^KZoB-t19U%?oYUOTy7+;RO^5r~dNT z8c-{w6al(d^PG3<>PJ0tleSkew6I79ayF{CLs;CQOoFvS$Sf8iJlqTe!4ny3Pbovp zAEzaYwOCQoKbnCIG3SG*YGCxa0iosoOnvE)^;<6+SW%vBNlEVJL4#!HalGPnTCEWz zLI%iG1{rFpn;-5el9@q4?t3ie((xo(;qHbA&gaK}+dH+0_S=mG8n%wUe#ion_J|)p z-f$HLDQtw%5*>?=(JQ;Ugh`|89H)PCLzbv0BbSeSI!E#*>gq6?P?6k~jWMh29IqQT zXB&sB%o;DY#|s@Mvob}!_I`_`GZs5c_H#o(3eoiFX26}_R^QHZz*INlkiO=AFksm~ z@@tDCl$ZYsI$iQu4%ixNjI>5Z9;|dcYk|M5p`oFvNnhi`%F2rJV=fX59@iz5)*;px zmY1Sw)>C`G6y$#MuNJJEX6`t$OXCa9S7W?@>_;OYkHXd)0&l#L+}Gzj*sN^eL>dFC zsPSdj46!$HRtB5l*};E;y>cRdCXWXnoA5tX`2;8L0fa>e2UxAs(W@XCsra8Ll;X43 z6`#>thD*Jz-(X4k`r4>N_eR^~?Ui_aIPN1YnCI94)C6go{h`4i-TGIzUGtPBRSuD& zomK#xcQ-XPyfJw#o?A*9Dzu#gH40-aHn{?-M?QvG!l?ypla;CcHUH82%?|3i9XUh! z2%m@&&p*PQ85xz0iqBzL0Md>9dH-iI4G*1Nn=isiVM@2I$90UXM@P}+yfd4WGNk-* ziMY79Nkt_w&h=AQ>P<7R{Wq5qI|7fVQ#fXQp2IUgm*hSZjU>fTizb;iuP!cGXmHZi zG!J8ZhM_iIpKabTEMlyyE3KiD`pbwl2^S-*hW` zaoilCV^qu%c4LR)b#BF*>HoS|s)N zvx^7sA2w%lOWawYfGPw**ps`zj9v0O@p-7OfI;s^O~8sAXI%2XfJOOdaPss5AqH14 zG3Xai_hx+hq_V_M69WRmu@G)(HY`b6mJF!X zRYwynj(+726#%&`n}_0ydA?YEucV(4{>`0t7Jv-xoPBq`0RmNF66LMW51ywc;)(v> zN=j;EqrAc5^4*BnQQ+R%+FJ4^^ajmZ9HoRzO7A#L*}qU5pR8=bz*nHxGi8g7fBDVr z%dNG`?XAY)z-Ye5Mi6MC0Nd!Xv3tHJ=Dj&mZ0E<9RJMATfbkwSnmKfEVbkD*Pekk! z&2RS`0}rEZeBAoXsmHqZLu#r8?63O_lVT6|;FhO-+Qe5yvXhpbFD&1`?QJIhXFI{O z>8f~R=Z_O+KPH--2UF$iUjC2q=X)vv0Z&5>Mq`{8a?%?4F)=Zw+*3`8Uw|gOTgPCM zz({Dv1#G)gHO7?<@0Zp-DWjP(f|fhf3~x}ErphCYO!pJt3Yh>Cv> zX@+=mo33=eMHTiu6zh1?ao7S-35 z23Xlb*~8eT2HY@q78aEDg_w_Qpmh#(^73Aq-H>H$0W5;91SLo@wYjnp*}$E?@}& zp&>yXZ?-8_!OF^7Tv6ftrzjD*%nkXHv$Q+pT|tbvdT7jyq0D^WNd|&Fa}fJSKDR*~ zwBb8HJWEJ-B$61x8w(_$R{*?XflB7_*5dQPn{rUiZmxo`^SPj#!|+Q+4d{Tg5BM!C z4{zEiq#%!x#I$NJZj@wg(?FAVvU>VBLN3V77h*xu)~2|5@aB(xC)#5upGWX zbq(djtf4cFLy-=PM+*=hp$pApipd*TS5j0Q^=fOnAAjOV<##6W>3#msoJCk!3(6Ej?|RKn9%}{zksyYenxsR( zt0klU3d~Q6-k+_3PIx_mW%Akl8XO)rY4t82+k*4ks=K?#%sX%RF03A*thSewY*z*c z0u_K#(%Qw%>dK0}gG1@F(vW6wd;$_4#kbLJ>?v%?OcIrgc#20qWgLR37OGEAeody| z!h@dxqknLia#mEt9jGY`x}-Z8q=;P33+|<*PsCB?i8PyQasvp-1Xh@o@-2~{|E+Q2 z$B*MV(r-&jN<21xSy|Ulw>f}8=5L)_-e(0yk&6NrMCq_Z4*SfkBK$I&@yx)G~Y&0o?kA4b( z4+uidxH5IMiAu=1&Si1olupX;Y_~bEFK#Zm+0{NAxeSB~pI#JfB{7P?)? zZw>%0h{;n>DY^fq*S6fophhFhk&mVo4f#1u(GWq$CVo(T+@OrP=6d>F_ z_S*u27%rB<;5XB}M(8ISe}H^8Q`~n2s7QdK2PJu`&Hr3xE0Pg$5!KYxWKc^Dv{t=~ zdw0$US#4y@s&H{zbpTXhN{5(T_ny!0odZP&5TiEEwps7JIw5?TcEZ?_eQA%L6NT>0 za41jgI)(cU#k;kNs!wSshaGE|$>+q|se8^VPvrrhazE0{m7GwnI>&jiTLe?OlZRB0 zf2>!UXZXqcg01duwAT5o#6;9No1)h-)H~4M;9I>EWnyA-nyCx5ml3Wr?F;GQKyt_A zh3IvuR>Ysb9K-TU3Le&&RZ-A(sthIV@AtRO#s-u_`Qz{h&5U1;u_0Y20=Rke^T#2910vu4=PoOXe;p$!zC=y-NC2`5)Jf>?#>0Q$qVS_K zMF}MhtiSv8;y+i(xGOO?ay6Z8elsuC_)l1J>WownEY)R7{JD(~SJaNvs{qwB{x0?K z4PPWTK#KJ_?)c3Q$bG18Gv32)g5SAbXvM712+1KBDM+OW_=rgECiE(xwQz!;xM>Bq zL6UK#OnOSn6^2YWG9NspWWe}K8Wd4k50;qM<=)8u&JO_qnR_B#fbP7ep1pH&g%h@< zU&_eJsOV7ARYRUUozxJ~J z0XCppBko~g`9Xw*4x~CuS2-*(008#g%746Q(BL@U{}E8zK^>CJ8?IXwHOVH*qNb4*4;{kN8wcsi57d#duwyLjWx%iyhH^$0%$h zZ zQcN;!L(-tKlG36`6H67NtMhf@TN<>xE_Z{*&2!$=V4@C$1qTx9LxeMH*azI;k(w=$ zq5TQ$S0w0K(2rK^;6d0nPVf`Yy|^Fu52b z&Pfo>c5xtC1$1vfx+|HOK2~AWdUKF&_B5P0bwI<|SS{&mM19RWMB+!=S_17vuh`yx zUR?#Oxj7wTRX)pPzn5cW6;v(ccjy?>c4<#>U!`bcTm_PHdyU9u;$zzX3XUDyy8Sz* zT7ZXb<6I&Nu6@MlBb_k_7K09=&+RuiF=E11yyY|V)2CQ(rHy2b1CG|){4Z$#OPpYF zx|SFNhZA^;8qjy%wL1ig|e7`!W)z2_W$G1siH;4jj7Qs7gJr;*G820D-c&lclOFQlh4 z9*XWl#z2Z<$%xR^)#fz1{+m+%8dLhe_;?xtci@nm2Et2l%2smvJ!+`D3>StXECGm7 za?7uZjTRJzlpfV)grgdJ)6#|yUi#leEOFIm+ZeR8v?z_$0cS*O;rw*#%i({vmxbH+ z6+0$Qx7)z%An4ZJ!~7urnD^*G1`|tthEAurgwk-2lq?ynViYzuVjNeh!Vxl5bh@Y_U?Tf92FgJM8B5Ti?6Y zNN1dcA@Xd0esxx&6;Oi)h5=@M73E*#Rld8)jC~mW@_p@Z)>Mm^JP^b~kUs_n$Pm@? z@x~a^8jLpk7R!LCQ+VleIh4Y&xDoL1YxW@Ed}{;1AfI?v@BjSKMobK5Qs4QrGKMh} zNMq{X>+0#zi2C##T!3*~wbR5s+85u!-9Qm}bQJ)1IB9YcevvjyXbr=O(=ttx1Hm~Y zqb`Lq)=by(!fJ_(vZJGw@Y!N>Ow@A_LL@;U41*8v(kI;wC%@Q` zf5S9o<9_@+EKfLQ$=ZrE*J0EswUZTNRF(RT=kwj^+Nf3Go{%@#6cSIMPo^YsaieuU zjti!9z;MiA|0IT>?sTH_Jv5ULpH;9WRO<4fZEJ86GPq2WNnwN6)7QJYNJNfb;$7oAt+3H)nf` zWd?k)y?#e4>zQKSBJc|E&{53bhzad=y?9bp0e13qUsP+2Kg5Vci@m%ebttQ1qU$Fc z*i(8uSR}a#Cuh5%V^$=k)QQ)2SjPP8_15vLcew>^$+S$&)7noM6kBH4G#LGBCC{eb z$=qw5>;4bjsX=3XyNvoqF$tE|&iYI3aSoD0o8OKY#{aNE^RzfrSkiEU{ApPqAJm^3+?=@^vRu zeL4%H6^0WoWyqX4yu&9-2|Pr@$XLJFp9~htVVC;&D0$og0Mso3S4opQPewXAuC%p4 z&}bO#v{B2q+kO*+lyOgZf>shEYfMR58HQgX`#>HGP>8#NQcH^T;wm7-3Wc1fQmM^*vRl^Yx&QWT&F;2HJac{y`+CE1XC-|L-bfKAER z5&HSjjTNLQA_V&-BWZMO>}mv%c+vI%n8&4cuK{SyGetbC7w_-V1)Z+}W2skZ9C*D| zsF3vMVZc!<^fP`j&QhGT^xJuBz7w6%K4w`>05*e%a14<8_J@1_qRRNKEZKFuG6Q|C zPW-ql+3$wrPy`7EY6%`O#=rS! zWl_@TDaQtY<~(I&d`I=B4?LF1auY3}bq7vICS`KapMtYc~K)+d@=LdzlyFo=vqI3q$o zMSvK6f_9LMPvGCVxoZ!fO_Ch+E(0+1rkiXl;9 z1iBra*#1D2AqF@@824eOmCM0w$HVCs;7o79V->Xf-7}(*MBmkh-1D{&KG$kX%*Iw7 z%fWtPRHCcQ-GlCT;~+`Ihv&+sf5|=&*51V-@LaTpG4}^$RXQLiz}KN7`SQ_U2t-RI zSyWWEr-e5M%nb>3Rd;sY-)me1*V&gHzxaI#PYzKDzl-h6^z_z;&U?_LrvNt&=rPT! zfqGOr#VU536fcsUe4ut%q7^ilK-A2Y?;HS#h+jDj2(O`dNYy+F4t&o2m^f# zTpmG>r)~auPw#%=-eu^V>bSCganGFrhZF}(AxTA9*?&3WF}440HJ6~{^=a)(2M6Si zq=AXL=lRL-^S94ZUVk*IV4LT@pthwx{IERr?}1nLxn*p;?Z{mw+<06!Jvcl{tMQ{| z7aYVl>4MxPX!&|y4Zh0g>F9KM_qf5OQpjU5mFkJLfwXD0@BIJ}gft$^hSg$P$bVLn zZ<#57-PqI9lg#XadOqLmkUDOkDeNZ04Xbk%gpnvF0f8Uz&G{dHRRkKl+vC{E_B-!4 zLHF^Xn{6<7(lHhE31rCeKx%>IuKETm=EZ>#+(F<$u%du%zUej@mvehn_rU!4-~;&OCM!4ctgcjM7HXZw%W_ zBtn%=uAigGOvVI?iiiu%?Ax`b zb33b5^*{XL_NvfohuY1JE_#WffU2^J3b0ON>SNgIrWG8aRqpt)K&xiU8K;ud=E#~z z(4NN9eqt|e$t_8HN5+C6b9&W?hY0nrRWh&&D{(iP(eFN-y85ofsUXkjZOFc70e z!Yvv-fvDD)?spsck4h>kNA*fv%Gdz>I2 zdW0#)9&*N{fvQF}e8LizF2^`8XRW9@MuNe?&dv(~>Aj$GL=A7WIiQa1!F8aguRqeFx&zGOIy!z14gsL7 zeciT=)L@Wji+M4YQ9}7JrKU?vFRW&trZK68C{!q@$SmnPF*Bv6!+8PRkZqVsU>s1% zGZfjw!T%Ir5M#Vug@y7(#sTL`$~wIn>k<)yUNc?rbG*d$=B!IcZ_VIB+hR(JF+tST z|K<;A*K>ASdd-3Mz%lY?93+I3Y=pV%3I68KYQG23^dVDXQM^;{XKZZCsUaey!dA62 z-s~ZtI);wd08;|o4)NDK+k!+?Hf4Kv%^3FL@r74u<_6z+-Fu2E+pqJ0`vfb`>2k#b zMV^+y>$b;D%LYqc1Fa7if}H1ksis!J60@Z`w%mpXBi^M8VVd6+QW@JOo?Ud9voJT{ z;h;q%4z1CbIGFnO`L`LHs()wyg8J*faqLfgq5P0KfDHnWh<$+5sD+=cG?0Z+pIs0s z#RKOPiB6)$$xV>bc!!$`!og-81>wxXart(Fj^+`g!LjD5T?XuanoA#wE!ZfqN`9m< z_NNs@AroOniL2}wYV=r2W4q&Q`2gLMA-0?rJNPG7Jy2PgOc(k`Q$u)3SCU=fTx@K7 zV1(JAifN)l8aeRmmyB}#&XIoh<2CStp+zT|>BE_~L)_87uuk0WR24?>tM+k=Ukj-E z*_97R(F?0iZbc6y**p7L>&nWjHowN;ZDxrn6jUfLE^gM)#llm_!Oq+9Lm+?|kd^gk z!b@`l@}P%{H0Fn5S{y(s0QPt{L=x~i{jS0ReN%2}_SOdc5|QZ7YS2Cah7jten&mM0>F1z~qiE8^~JP)K)H`T{CeTKh*;D({q;wdKK zhSlji>XsTM3K(d55!AX8A||*$R88+f5pUN~%uU%XG@!VBJRWcLEFw1~r(fP{Fx;3h zlRVTKiwy9V^S)waW(G#o{keMl1Ve;|`XKkb2{(}QNJ|c5n?*VPDI)pr>1RcfxB?0| z1NuENk{dW-NiT6!t5Lf6Am~3GYkP+Jxibp3V)C|Iy4+II^NdFi&{e`V)ts}=3Oeu! zR}wI!>ug8Sq!Y9xGs5Pc-M0Hi&}FElB9~9EZc#P?L*P8K zm@!ZW;0Yd{0uSxsio-P+aPhZCv}8TXB!;~cl^4hWwN+~)=tcT{P? zBeVFT1+}tQvh9r8{?iQfbLYT5yn*J)(eyI{xhxz{b=kneIFW49#6Q?r`i9-UOUbek^I?>1iV#$ZU-jn=NZ6OOH>D)Cq;sz6vvEBtC&AZ#el6qn=+4K$Bo0hfBIcGVIb?nddu}#wwty=D*Z|eFw9rk=PX4+8 zR3|$_$Yno6!~wu~Ak8n^=QjGOY8oIT^j|T}{HmB9^>)4sw%<^-dqO!vJd%T<32L50 zVGLf!cfx(RG=miFHdK-Q#@K#tm~7&8%_^MI$GS zN*8dLXi{7SbyR-Y&L`%F)jw-ffd9Ji9P-|S)>^$G-z0`PN0 z8cPfi$9?B!7ZPgxhc_^=Uuyt^0#G#ZK$0^v3DRJu3RHwqfXSotzho*3Mc25#puEVA z;g-6K!Sxz1cT>l6Z=L5ZUA}_^W9oZVWtcs(P4hllLFDAgUg0Oq(7r@}$mlf>C5qaP z0*#`9XciIr2ciV*-N7nlr0%MFb66KG%lP*qk3+ryg^)WD_V9m`UVz9DX!HT1EN?vo zf;(I~&IE@~4vnxNn=Y{L3vK@2jJc59>JU`%z$r_Xn3D4Hy|F@~O{Kurjn`nx)N=ZS ztDmE62-6okN=tR~5xU_Mu)o4+dw}>Ex%9Z&2#UZag#U1xE6fhU+qL`T9%%;jduA}* z*E?^25k<&wFF#B&{|FWykzPl1LY5n1a2>}rJ2+~g+X!v|>T`1hLK z(Tp!uDHM^oQ4a>Zoa71Q_6vC;|4o7h9g7pvICC$WBpmu?`;X$JR}}JS8Ji~T1pw!` z_d&x^^Kn#!g2c=v)Mc{RLf&XnSxFh80Vu)^Va_i)9+zvtVC@0dbN-&l5XC{RyrWy5 zcamYXq^7B@{dK3MZQIRv115PL_fa{n}n}x-A z(b4#TB)VS+Ix)AXe$_2wbfb9uFO#V=zpSF|sPSTpukbgKkz76th~=ANGF62-PLU41 zd9Pvdr>HvT`xNP};(omzbQkA)5kD0Rt!91(QpD!hpT@?vhvC*q5i%Ff%Z|T%@z)YI z5+_T-8=0TqoE!UtFs1AA#TALkw~;4tt34~#GuSgJfm>W$s`;8JR`rdI=(`u`am+=Y zLwKQDbTRZfZoo*)&(9dxk>yIW)Ho7BMN0x;QySH=%63ZM=!CO(eXA)}#U&+nlv7M# zemn^9L&PqiWs|wri4P-RA76bt0Fi-2Axi9;F@-c9gQL~@j?3K6;Qm6j%U7#leA16Y!tY?A~7Mx#tP-^96@L#yGi++#%8G(EJ_IwKKXN zL$=8fZF+s}DQr4BGZOAKS0~XV zEDbnc(I(-j#U9-Ji}>5tDIFHQDZ}S#TB;B`2)N9Rs%Y7s1h(9hzz*$asCo;N$v7l4 z7nbis@Rj-@$Pfe`^%CHXw|92fKK9=zr-t=+?JO;HYTi9QrD#CeMA!Ccx#!ZZ)bq># zzpwRRCdtc?*3Rlk<5B46+^QcQaaJWYP#;b_nG|c+|KyBa*~L!Lby5N*Nqj^ha79ar zKBzBN1m^$-|GG%Z6D@(ryuitH(e<2bM^4u^-;4WV&1cGCw&~=`d}w|Cacr?k2bi5( zxIRoF)?@re>`*#qeCdGB=)p{Xhc!0*l~E9LtR^lnhMS* z)DJf-b)hZdAp{htb2vq!6ZDd~JD*ZM@~%jSnj;2Zy4kK?_LxS0@LhHRg8xj;o3%-0 zkx3jK$!BR^wjL<>U;lU?q%<-xIJGs zOW{6Hl6{6(_jGHBBYb;p_-a}l*BvVHEMRPUdU|G|ui9E(9zLtW;PoZoDeQHPw8W!8 zLYcI$CArFgmU$;9%e36%h{*5O)9@$Y3{ziQXVd#beq6XaxZFbU;ym}2h&zt)Rb@AJ z&~u|qVehrbCd+ZXrt+^DFps-~{fc%<&nTDrJvB?VG18gs>*EdO_uwxsB-Qeh$1W5` z>upEI7yTDB~b_o6?JOxf~qdnaty<}$W|5ZJ! z2wqV0cYu$*$5n*mF$`V^^}AVSlfh}v&z7CWe4X!jrr@6l$m{AE&tH(GQ0u+% zIPKAA1vp!s$Vl_zMKO%n-fPEmT4dQ*`tu^Ajx70?C54oDgUpW}@j1w1Q-aYL951LD z^DN)v;(c50=!S)VxZcBmqB7!jME*%~$}d5=OniXKb2SR9TCSKF&;4@Ap?I7%u7~H! zLAd;ND>-!CL+8~ljR|*De7D~-q+oOU(Ti?~7+gnb(|YuyJL<2bts6*?4~YtF!$XM{ zPhBI-xbX7QAV@sJz7IJeze4BNUTg@}sh}1vZ-1?fbHJqphB2z@oFlpFy4U9Qz(s5X zb8rr6iaCm^s~6gvxyzCkg?JZ%QubTrxrTyB&!ogYmmU#E_n+O| zS8w^2Xb6}->#UZ)60u*szZrc4-Dsrj^54)UkLK7ipHQUDR3X>`_Lh=!#*Je*3JI7$ zDV5*)tCL-((&LvEgb;UR7Bc|Zz9ESa`F3^LuzLEo4q+i$)}&z zvD&5qktqi&oeVYamX78Hw6p+pR%sC)^L zEDn@K+HV}Am0Vrq-TVo!o>gyqgphkxQlo?FldbjqJF}}BL7T3Ioz)agCxfN z!})_f@Q&+sL$;#6U}9iLtc86IXs8p)}N|OuSZRrfEPphuHk5Y zFwk=|8au!0>CZg~J$VqTiLsX6M8Pm7Q!g%UsC#~kfE^2yOK#sX~|4s`&Q_w2!8rBg%C5 z9g2E3wWH74t-t)Z<>2>vW{hWu{f^GK?l9>ya#Ovi9w+?KD(JzNDEd?XBhPY*NrHaP zH@hImggnaYXn5A}Zl^n5W1t~{?Ua*{lgk7!_;r1DjOE?FpVWN5*%#JR4uTSL@@9Fw z3*^F=+~bS-`I1PZroYRJu#-YanoHa{Ix#>8;}=(sm1J|q*3Da{(;Qk&g@`}AzdyNl zOprgHe;0hID#KgDe|v^nLdI())n3^2>CaF_0ok zI?6OC`)MK|=%~FuGCQ9RTOh?)b5guqIn1#|xg8<*YWXMvYd_QLuWW(^cFNmcX<;VE zSW@WMcq(suc$pKR)sX}@eE_9&k?!jXgDXsn@oLpVZpAkZ; zaK<%k%I3`c_B^&*9-8w|tU0D;GR-pP)xrLWw!y=)R8$5pX5ML>w7xKeTf!<76o+J~_laqgvGY#*lzL#pAgBY2|=^^D6EYgUN zf|?t!=9cS&*-^f$-}}C9Hn{F$Ygm+mmQP51n22|a`{jo_OXY>7#2S_}IyKjJ(L#?J zoaw?_Q<=!2)#TCZdYf|MuMH@qS;6krt>EyRve* z1tuKCCeGudzIfCr>v@=LO@Ssmt556Li*J@Y{+bJCguhB5GHE&Ojy4c6lM4SM&csZ^ z=y;@)g7DU(Um}P6jkT5rm(ur=1SzK$fqY3g~Ox_#i9dd)r6bM{7xEf+%T>;Uqe| z`pS?l)B*4L%?-hU+Z{=0kx-Mj)Pi&kRkiIv6FO#V^h*EJ8yw*tuh2%7HE|lwm)THP zBxgzvY!gpytQa|(X$$*vBZ*8bJPPnZN_tfpaqV~7*e_{3$9VG`t}dST``v`8TceMr zzhav@lOQeckF&naqt7Jr(mf_Q%mg7HuBYG)x$g)vPLiRA2<|t5V1POsB*xG6vq&n4 zj~LtQl=o3)B&b>RmMM=LK$AWZ#srA7>n(iS5B>R19Ogqm$X;9n3zakAQu(-EE})a} znkb@5JJOg$agx2Se#I3??s?rt8p9;T6Y{4^L^64f+JIK5682@!>L-XKc%GG}4NK5Y z5CZ8aAHH0G&fq_L=2W{jQhR1)!IPdcr|q}g-1W7!h?Us(62ZnDP93vTW26uQ6ctT9 zz0?g}(RZpv8XG6qp?GwE+m8_yCjlW+7zo<@UC7?8ErO;{BhOn4NRYo4yz>RMVutVG z4Lj8B-<&_kE}TH^*T>Nm?tPf3SsaTNPwFtG%ap2(jSX8X*VCU&LqX3eVL(R5M>YnD zFQ7^21gGch>?{vAw?_sKFt`M@>W0YESw;KZGFC;)>1l(*hH)QY6IV&n`mB1Nf!<(H zSsl_>?SAqH=e-XXFr<-E1<-PfLpLM58l*6u18vxD6KnV%(0qKQo&)l(=V5{7I447y$$rMH)lwk;2}$P+P0y zw>G+s`hOWb>aXUfr)!Nm<>#$0M+A=KeojJgk>~{=TN;Y_R{T)$4k3ha>59Ew1%7CE zx!3ID9n~?gBUtg=w|Vux+}hfDlh^idBxyzK?E)|iNrdTC1CMm>w{KmYoneM;Eh^0S zI|Eeazvi`J1us?wjW>X&`)fJHv1?LBYcm|n!ngiCIA_IfzHf?e?qa72=0;9~?%akZ_j4>#MH&FjJY zIJ`#UATyUYYJX=(yXCsvftaPi!Vu>$M*jUS+e`|GP+{=u|4hQ( zmoQ~|Rf7b)^@2AJGbVoAwKM#*al_WvCf%z~k35z}ye@lF;Ku?i3gd@9tq(p7V8J+@ zk3%HND=NT$Z)o7Qe1Z8vx6-&fxbrKTIzcV)IK0TLs3<9EUhjM1)R<1We9*alr&#thBix*ZEcfUh{6^2O-GH0>^XR@`OUHe^mGh32V?0yrVO3=AHTLiZ8 zT@p9~N!vGoFWz#w@Hvi&$}EKuf`eB;-rpJc4zY4?*-uf&(K=))Y=~WfM_XH6XjFob zURF&wv;BO+P+Xtzy$Ke^LxiC|r`bSE2&Ip<=5%J7E14*GXIpo2FhL=md$scA>fvfN zlie7x-yzZPvE*~b!vSyv`};c?6xTm^V*z?-e_>H#0W|rMZ$^#<*)l@GfEDeEK-_<0W~&*f*_wHM|=;=+L7QvhF=Xiw1#|ABXto zCF{4#3r+hm5;zYg55&M>R`7?S-zPCN7v=y28w4|cKUb$leo-CW1j1-5urGLCHd=5i zHt!Xe2+j(emA*m9xCcq8U{F}E&-&FBVNQScLW6Uicck>qN1^j$iFmoKE^z9BWl&pF z1F3dAeC?F^<&n>bgf2-jdHY5#|GON+#tCmnH`sf|r>2n8tVlLqfgDnaiG}mu^S^xE zHopCLNlP9@8Vg_rr=wbiH37{1MotF@(AnR-Zp^g%R*KgQkA`kdJ zJpO0X0ytTzHZ4rp^s!Jtl5!fw2 zbo_cyNE2;?-mHs%b&GBpV&yrA&-OT4cC%T0oR=ata5|`MZuRZ@MNm^;Z|L5M^6i^q z`?W}W60d`a8G~7G-M_oTRJ18$G$mn`1T*-GX;5bd@?MO>BseZk0WM3A-^|31=7N}s z!9p!-Y9X7jRcs4(&kEoU_=tHH0wo2Q?6CslJSOPb0tuDLYMZw*-^Gvr%zDH6Gn1aW?v1-czzNb^-u!rW0SfzhYuCSF zZ@?0^8^z3~k;X-Sm+##5Y!bM19^@af#hZI{1%?G12O1~I&T5rRqp-bp2Z*;;x%*LN zH_m5k>rxFC@a5W7UcJY7-B;iaSE5T(xlE}m3)k8nG#P$yod^5-e~hU0pr`SO9au{3KG};G%6wZ> zz~lB9aTFQ)!;CKqsjZbcOlVlkD!X6iRIuVx@zG5%rQ(=z2*u+m9P{ZoL_qlN-gWO2OMDg9^|&{vOQHa2^dyVCq2)V z&G-^>EDP_yvan8!v+$4q=5vdpMkgQbAdfv$at28z^ZC!2lmb5)NvV3RR#a5r+>8E@ zoMg{cPLLFhI1^+GMf^(A?|?06Kam^#I4k6~`U^PRMVYR(f*3jycIIRSC0--ibL5XC zeJ&J*9UUb5)3(8$ejUGH`(K2;+V-7iF+}O&g8PCG7VCg-)KJk+FEDhnYpvb+6X6T2 zO5^l&b4{9Ou^K-O!&!^ zQYtpbt1y|)`A4%|%JQq$iU0jNwHd?_Uc-K%r#UaRB6BWA#&N7CmMA1!$P||^)ZMaz z5xpY7NLFA(4*>3C%U1HvBlnq7fcMzX@k-f%*2Yf3rZ z5=8VWFgjhYOADTCKF1M0UjvsS8~ySHzalXFIR@r=X-Yr+y&y`D>IDj)6<&2KkkFmEeDh_E%&EgWqQCG z*6NorNq7#Z2LAR>TI8wEUmBzr7x&flguPMz^vT-iKCB;z&8HTcASsHV|G5U*LAjs5 zRECGyTWlIr3PAK}+0(^NE+#Q7viDEh(P!QJ#P*i^P`c=V34;W4!!HNit5-OD=P_Qw zh8x;IRp)9ccpdYVnAP2OH)lw2pV-!OP@DEMvtE1>JAX?4CJf5q?8x;1zzmXWuWCt zR`^cUiz@tmYe!mZh=2492@N~x&G!As$bA!cX6xg^3-oMo4-`Oy4BnF~;BJ)Z{^#U% zYW++AHB-9t5O^8s!yWT;-jv>!{XVK^2%ttmIBoHRab5g~Q(m^n9G6dMd6?0Qcxww{ zm^KQi(eFFJpFa3FE?9~1pUNykv+n5N?EMs9?ty?3gzbI>*&N?B61*8|{9vbi4e#2hd8-=$TwoswI9jsn~j|v z{;#*w-Y~@twgL)hv#&1>8RcG+&)C#gkOaJ`UoMOu;I6;ME4Ib}cU*9DIp1GR+>}S0 zy)c-iiyxtUcMd)d3K=kMei2CO!P{;BO>y_q06e4V+1cg{)5W*z=mj?LgH_+P(s?8R zZFzyZ8W5&(-BvsRy|mvE)Ik+M>;U294p$AmPV%4odVMm@wbbdeD{uPCqxHm}IVy5??ldNH@OVc_!sj?OE=@IjW8HNI5AgVXsYq^ErZD9;SQ#*ECu< z3U`7Fu;Tz_Ln)av2$%7PY`>kV>W{G}+mwADb^jU;l@n3PRC67jglCF@<4a5Kz*2dd zbEo4KVG*JOp^Qv8U29OYM@~o5VjT*fmh5-uZeX1{N&$ju1{o52OsX2|M_R9+6pK*2b&)$pxNzZ&Z3vkqPQH3e<|glD62b23CD#25nyRq!wwhGW5WouFCX_mewpy z2)Sc6not|Cu1=x!- z*p2@HnK9Vnk`x$6MUL*j9Jc<{V43!0LCvUO#(-Lr1mhZ`p)x_Z?8y@c%_u#u*A4X@ zWdWR-argwPP-k)r&@}{50~?{g0cWFBy!I;Xj}Pp_ppdK3IV+-<1@Es2kh+=_6ebiNTg)g&E62%7L1@L7;s8{L=R?sx&Qo_sY_H>o zMg~J7hpNv;fL_+V><9AWH#W@Nk|vk z{~h+65d_h{0e;h~)gJr7Y;O@=)_v~2r9W=$qA4ch+eFa>Imq1dnP|p?P^7T}fje-; znT~ref*vb$rkDJQ~FQ$XQQAPJ)E4z5&L}d-_Hn46UTpZ4nH#=C&A8g z+9JcOuR#2gSBH+d&LyzCT_)l4DH|2KYq43p`!;{H8K-PxZT%Ly z0m^6ZzsVM0-S#rbuas-Bh^GD*sY~g5?;N>PpV)Htcqb2tkgor#`ujIz`yJ|R=f}s! z>_IwII^UPeiL1od-o7`W^i`MyULC*W^G}BeUxAZyo@@BzE?Q*2)CmUf3Pq5Rylu;I zY)l!R+ByTxj@&ubS0_EphO?GXj$sG!D(#+7jpAFHDpYcs?DghTsZ|_FvA57yPB5SO3vPfS_w{SGr$aZM&FYIWr=Wtl0iowxlC)BnlNSaz zCQk?t2D8yF^+WyPIV8dPMJJzXS!Ct3+O)N1jTT*NP4u0H?WDhvwc4)-xIJE85yG`F z0#sPyvD3WP()hMdS_U}3m+xO-?ZznyH`7dVa>}ChowOU-t_wTMY99s8Fv>s_- z>iJDKuOCy-TRLLodJbo4z0KQ_+b%E${wo=ixYz1~OyBf7G?gEP2@{HoaEEf(Um(s6 zJCLKdHE2W`{shl`IaM z?N)w`3f>K5KDC&`KX$YKx3l9Dr}=jFChA8@AjeP{NdgLL+~9A})et5cuFT9N$k5l> z+~J#g^_A)*8n}|CnRdRUx4*z}HV*jO${Y@9nQ&PK1YTU9)5**gm+f@mYwyp|cDq+H zh@k!N-@iSuaW7vjo&AbnJ@sy{b=eNaG6Yf%(EFPK;*H^G&2>9*)P}B}n<_!P(odl3 zxU^>7ziTrU$e|Aw+wnl4iBF(;%4WvssYTF13cUkI3ONXevk_PTEj7KYqM{-o4p?fD&c%`l52r;?h~5A!5fJTR+jPk7 z@SLQAm*`#Xq}rF?Q2bUZHCzm$drh7*Dv*GQ9zeXVfXwfG$(Asg}_%6#P$Xo39h$Q5*N?Od@;^e`P`NTecO@ zZzhOI_^oGa!$9oX=$UpfirV!rpyUsHu4D;h_!=w)Rx^8)LtC7XWbkrb%>3yk_-N`- zq%q-5yX#Dr&`y0EY}`aC@rb(EA#aWw;^<(AM;m4&RLO z?{BIrh4pW6lg%83`H2Nr#hc$WY(%AUS*fwapDrygQlb03lb{a|XEE?P>4ijo>)*(; zp)RpnxW75mD%Cb78NY^7@Dt!hfs^W)7qW~VI!arkCEj@i9q%8FZ=GPy^9YsDf)Pon zg2v}u@Tll#24W@q@n%4L@UGwaCdfA(5wOEP-neUHJ1#XQNmlsVU>cpyXNszwA>#ty z0?^;Xy@iN~-G~PWA4lZ%eR_PTl_V#_PbzrUXdIq1B6wE%A8ZWZ9q=-MlC`Ml*#he3 znTJlyuX!kZ;%d0rZa*=#xv2?=XUKT%sgHrhBvE!C5yNRdr?Dv;Fc>|)YH#-jz#0gb zumoz9#djGm zO;1n9)z4+#`oQ*f@?`<`bIqlb0G1H9$)?OCSNpZe0}u#n; z9uG*1PBio|2j1{(Z2G_f{8c=Lk=C2!-=I|W=z7K^y8qGgu80|9)TtZY!Z}|pncdjL z)RYbqXS4X1*A^67!^6W_mvrx<3N@V3jim#`GqypC@c6mB@e=1;K7gR8z&3W`2j3}<+QO;x2)yGR2}3v8sRK;})0#}&wgcK}{*-<)M*6}y3? z7&QMptBy`>Z`vI;-YwPv=YYiuLlHPT@PNe{TXTYIWmG z4zxkP6$K_kBYYMI?N8U@*?zbH08p#7ew+F-9y7t5%Dw{uoGH=!^M*d8H)%y0#Z}oL zLZOYgjM_i(*Ze;~hDs?&mdmHM>O2z+`wX{xI3&>(%ncG@ho9WDO4S%XF z#@pQsultM1*0Kf)xH~X*f_7V4S`KXP{X!-HsxppwGtNfm`D(d*#BZ!wzcs$o+MR(P za&Y^sxLEufW*Q@B@^2&@!3F>ob+ZF}PIgxR6*{6NBN$z>f^3%=pn&yW9o=%=b#5we z8{woAN^?D~VNhR1y?7NzSqv;#hqp1o@4i-@h}wCOA=^)%nsy6P7Z(;l=di!GhaZMa z!YZXIIQlCL9R%EM4xmwbvB%07Dct(5WxdEycH_CT|A*^23mWEL`2@dx%phSHe4%c6 zYU=#BJ9^6NZ^spZln1~!GCcn7f-d9>7{!1$-EW1yeBY;>3G(cbH4tAzG%pl5^1vRu0nUoI9PHSCR`j#Zq@@LfmQY3!DXBYRD@9Ji88qxeC-_UVlm=q~`!GO-I@)?7j<1om|Eq#Wj^x;M7JefDQc!Q~Nj z{RA|vmrR*0*7MaSRlG(WexL&f}wxW5#0_Ox0Nyk8-M+JE=)QizV10A(SN7}em8@Uxu~n=0lb4qUJNcLnygcfChhRyI|I13lJp=GK}xDI&W1+A zCvdX9cweOr#5%DX%ur}Iu4L3U3jXrszN6x>Vo~s|5B->Ow?kn`$*XiF3EmfT;#uEl z$jd){!=M>CT=0CW_jCW&BD2ikY<>Y0LUlDz{|&4$lbElE!g_CgZ+3r{h&*|9?|5Hn z{n!z1ZVJyU_qAVbd&nkap&l!l-l5Me=Ah0AlAu5^e?iYm8q$E~k&Y>`G#U7Mmsg1; zUX>spFeyMDaGrx9jL4L?4`+FuPWs|N1ow700dD?FCHx(4PqS6_b-G;W%P8{)T!(;A zOSyXMz?d8codl4=lV?t6=^C!PaYHgJ}|3TZmCKrVe8Fqp6Qny=D3DNob?ffceSv^YM2-ip_Hl4%$0=Z zw~Hf@7$CaPxbb0@#Ta9CNP|>qq=+{!KvfCi27hF~h&sT}?Y!|xlF4IiXqs_KW4w|OUtyZ^L#uKuqc%9W z+85nUT4aVit*M6w%p8j}Fid|t$!S&fR3g8$yX$`UZg&R4}gkalIthw|XsI=d~Ky}XR+^Y9))$I7=?MESzLUu_qzlDrqAz` z(|}tKR3b!QrHo9C9s_q)lw>RGYXQLO)x(Y)(>l&*haH>wPXh+zizhhEdO}-~y9;k= zzHw;6UJRP0)E!Vmkrb?QG+;!UiUDa3>X|;>(oq-zS^BI_h!nTWVT+QqozEtYV{>_< zOm*J>K^b6>`HqbDNi)Jb|0T0*$XMut0o%;c*BwuWbS2M>`q>Y3!g^;fr~IDqT&JHIa3z)ZpGHdhObXh8o#|IwO<`mHA$#Et#6Uhm}AJ(H*>+9Ws z8+~hSEq5XuShR@~NGZl1T|XSj2KlSO5+--^jJ4TOKt%;kaf56X%TJ$*-(y+Qu!alH zBXNPn5W6Y2xmL;St?(DLf4c@%8003o7{@2FnB)AyNa^?T}^(t0I^tTXt zz<)b?NQ^?C;w0l1g$ZXey2Pf(O2;D-<-_O`2u*tc-X~q`OP?!R9UIBT2b`9)41F3G z@lE827~q^KsV@vYTxe9@24x$<$ZCgwSXPH{jF*8afSg z^DH}00<_aqCxHqTv_jn>ILbYy1?K$!J&m`iK`^OkuiCht!-MGZ{aO?B>R%<{;#w@X zvz5BuwN>=@2~knlviJm2pRGti_?PGeM|{<5V$n(R{)VVbB?}xOXFoAX+&t%YYqJB1 z9`;hJ=9q4b{G3W&q@?M_+4(&nI)amxu0t(i71gFU;iz80bfSEIpIITZhNj_-Gq%MTNsf# zL758kGeiz`F3e4uVltqTXGcRMwHc_E@2ZaoyxrDe+-Lrr!k=ss)uo`-R+62aojm8H zscAENA^3(U;u(QCUzF{_d=siYioqnY20#I+wY5m#dw$^W(o8-*hwA>8{{`saUA49NSjQ><_rzAYJN(oXU zlzQJ;U;i3Z37y5!E8^p8HflY^rh)r=oMmOr6T4h5&pXMPcA8jaL;nf4Z15TwNL9Yh z*dFAhis%K&qK}}I0JD=r0M*yPK7(8@+-g5uQ-PlBXXNwbZ)5Q+ zBN1zB;6i*n(y08JV#TK7e5n2v!b7xcSQ7Tt zLhRi~Xy}SxE3HjVkR8#`^YJTa?Ch)6B6qH;*1Q!&Ywnsk<71KHC}yben{hL-Csw;F zv{!&-FYp6PTynbZz>Jd=xpNjdW33RWP@6X}A_rYeISWULF>b6OGq4^pY9sV+MbPuy z(w>#Z7TX$0^iqvg3q=g<0QLq{A2K=3F=}QL>9f_wCwFZp2myj}gnDRKrCnfk>xYC* zJ$`J~s2|ipio6Rkz>nzldrA11w_i|PEV_VsqO(|MS~UlZ&%t9w6(bKlJ{80nXAK+^ zL!&^>{M%GsVD;tC;ter!@OU*WbOX0EQ{r$nx_?8Z4iwJl!!_7RWTC~~gUMzn0U$Uu zn#ungS%9#gc3@2)b9js(WH!xwUae?KJ8^hhzJr+zd3-Q@%aY^+`Z5s)O5vG#(trZ8 zF}l$^nLfxT!j264%V64P*Q)mRL)&K=+hrdfij08=U_i$oXdrId7;hAqJ2t!4&eyeq zq1UBz<}u-S`p(MkPH5#pm3ArM1=Qn&VfFfLQOC-3bP{53is8oOq6a`;Ric4#0}o2- zl(<3qnwX%5*EC$SK}V#-zo^kG#h<0tkp$BiC1d51MfVeG28f-Zzf)6<|CBGMN^o!H z7loK#T3o#4*r+ycod2~>q5vzD5i&qNtRZu8usB<1;5YsJqcO@rsD|c+Dx`xaR0`4I z&3Lyv77Ryo6j^)NR8njkBEbWh0g68^V)~EBs&*aG(-~hG5Z(h7n*TYebU2X}`VW~i zK^z03*YH0c!f}JZXbDDSFgyJ5S5tX+yb3lV5@J3HJtK)YKN?9@R%iGB8wWvNhcJ*L zBRDsFbN0bOdCTrx%pwql6dXnD-tX%7prdLIuf`Pz{GW2Hw=q*p4)lZLSXXC)%HO3r z2*s}0)1A%2r;Reo0&naVTX2iMAVr=f_TIW^;t`b7v|>BIlzUcq>#BDS|f>jz^ilY zxXQq6Ecws?`l%RngAGUN7zgjEv!HrFrrr^W@LGYUlNw&V-=v}(qrm8YfXqmSfoNnA z_zm@~epX?Ed2&wy-fyxFUH`flma*CNQ5tVFIvEP5Wi(6parYNpxf%hji(J4Jn6fDT zj4QC(K0~Qo=`~3{W}mBdBvPEz)z$3-A(OI*G1LrJa#(J zgMsKv`hV@=v_Uk%o6a_dZ=n*C1X)L4dl9GoasQRz6(D5_AWwph)b=QIBgv}`=r?2! zC*8=mLE;#2wG*Yuf>X>QjoGrMz9PU>@jGoNJ|44P3>M@+uMe@MYHp?E9eXA$$_)jH z;qthFx3``K6&fYe+A!32B*99vE`C*C*8}{&^_qy`I=i}dC7^LX=TZzYM%NRhndQt* zLj4eeKPaBfZf}%1GJ@Z)8ZCGlN{!y1X)oCCF#fg^1QREB!TOzm`Bp z2XzyME9yVJd^LpU>%7?g@19;nzfu{+o39_!(g@nVQLD;hc2JxQ55gxfPbmnqcN1dJ zV221pPKi`xFwPE}Y#HMa>|2lST1Z~=2fwq|idDShp8CzgSIP=vf-i<8OtMi(p0Xjwp}%w@IJ18&xuAg7aUy zDt^TX--5a%*?;D>(D~VNeWwRQ zc6J?6Qi5R6#|EDusQf4xtUKUS+C-`(pY(nK1MJ+liWXOAbMW<*|8X%UUOj!h=|o$i zamEzt=Tb!CB-PYxM_vE0VIN}2-C(G4uWgtKESiclKEoJ}RXg4yg~-}dtsg^=7&z2Z zJJK6;B`9|`&_=l2YdWfAvXp7oW(cKNiRjuZbLk8XGm%K}csq}$YojdMj*#do>18|C zu|Kf!CA)nY6(z&&%|4!v(e>M40NdrGCTH@KgFP+tr!{>>ReB6l$s8YA5af$r|9j_{ z?Ko${eeJI!c_KXDd6{u3iniZA>VO>@r?kZqkFhCEpd*ET^8qq`34-ZKg12u4M?=KU zZ@x2EK@*QkA#0)Jmbs>8wI+Qv8qTmyvv3pi8lA`yN@M2&bL5m`DY6*)!dR^ijV>!> z#HHn_4p-)5HJ0T0FH@GPKMdWCALlqWz_^_r!=y;tzkU=94M5ZcHkj~bO9VRwv@G5s zH>8<1+fPJ|%yooF{bKGRQLWB{;NsB9;hXn6NFqp^Mpy-Z!`Jc5cE{Cu zj|dTC(pWSIp_fyfB)IMfVY{@r$NT9nzVq?{ckF|)45XW;;AI~dEO^~{Iq)m(Gb-kc zZJh$*$v^Ds=J+o7$o|=M@t;30Pmj$R>D3+KB`Nq>rhFug>VvAHq6R@nbmRrE2-DNfyqPVjsl^nshWQmIp>Ua;!aLzUF7Y2Wdc`>i zPCs&(t9wj)vSL1x(j~-p}V<|fFNqDI%Q#rfM&O6Rz zSMq+^-oJ=!w;$H{V}}Xyc05S3{0dEE=pr3t{uD!pQEA@C^Gci%Ty{eyxq2O1Xr0O&q zC>}|OX}$jLYM(%K2(z@f11v$lGvN7f_&;R*aGcOEG)?2{uOXGnjSx zPWMv&J8~z;uFMyOUDw6$294&2LZ92g9>kC}AvCUy*UErrZH0W4 zsK+i13sDJ2RAlOOf6T+Depcdh8a4Xq4dPTaI~33&lVvS9Oc1G>Z8zOcVl(MZ(kirG z9}DUPO}nHQ@T$h>|71C?mRX9H8Mgd@-zb-lkBVybLTT`O0n2iuM}o0-FwgJsobZbF zHfEv-p^K^KAoLYbRW_gXKP7dMOJEL~6>G4_f7gAvUtOvTl8I=4hQ_}7?14T`VoZPv z9Tup=vFc)nGH`w#pXre$@9#I^5L**sV@LlbD5ajmjBDm?M&iwe+j|&@tV0cjS0XAK zOQQIYprQw}s>zGtqdfa0YXkSEV2_)jot1i@W5c{N;w8N>Csdv!7>XlH_eptorXp_n zF0&&z+95BTf`THjQz6TWzd(wt9PnonOv4&%=1H>Iv$$Rx>$z5`4Cd|U*w8u;iWc>p zJSQQ`nbxM4wT)yh4F2ODb)2V;%(kA38)B?ow!`rn`&E>w!GLl@)QibKF}{xEAy96# z%Wz}uQLK%yJS%Pg=pm)eXFaAb;nmEM+V5f{le;2K@2c}?$c+?kK2V2MA4#pm?z`tu zj}w^gFpgq||JHgn-j?-gsJ!k{5)prqpub8b0oX#o8@N=z4XoxRWjKjCuVmA$zD#-V zPYll=I2rgmkxpR%G!X9b%w~9s9LzmXMJZkS z3vgo=e)0kYw-72Vy$DQHV+7TH6G!yY z<4J`ercAp?5J>5$kuL^)6E5hb31N{1peF-zyjq?WLI7fCM?dpK)AG(6+8Ihmh&?qT zJF@#m>**0u%J3CT2LlhCfVLBj@$`^6CFn3kz| zm+ko3U4GPz(?;lNX};F6m7l}|V*C9>7-uJ!?~lg!)vkv<*=UO0yf21C=-09x32+Y! zFUgUg>wPQqrC!QE(sJKLqx-KVlPv!-gm0h;)ZcrvlId&Y*fd&x5{T3T?2%1PY2~RM zlWGFpNPD=JYG39)a>4g(Coza2@=%(Oilo6Pf(yvGDdq36L`xy2ynZ_f!bm+Q_8HFP zfOV=_f@*ZN=GJ*}OwN7>Iq_5W6G3R8oTJy!M5#$HW~n<6wQNJ(MuWio+#pd`WsxFl zy%0{+HL?y$l5N*_7D=Y=+3D$n5XqtpMp{P+5)Rm3%L69=US`Pfi;@)7PRwO5p z?HuaGDF+MGUHgVh;rG6fy`8-OW)5k{T@B^8coB@+Y`4N*4>6J%9Wf!>f6Jqy^^*U5 zOk&mRY&gQlbsSK9fR6)27$k2T(FI})osR*AX%#j4U&=#u+(5bf94lP|?{c;B08Y_4 zl#WV!2!x)XM=#sS8mk_i4D%Y+vDx(nfmzy}r8v-6G|-fCZ3{*0_Wj?hMf`yw$PA83 zatA+4sNfE?gFZXo_%+agGOfphiFib>W21hf2CBH1k!naNe!nx5yyycps$`#B;L(-` zWe`E&H*}9sWOGvXyjtPz$^`#ZLR-S9bFN`TnYTm{Vm(+LmgN0YxE1W#V3Y)TD@6?p zu*P^+UPMpzt{V#k9`$OdOzGNSoqK#mK~dFhs3+^^f*FTyh)&_WgA9@VIk~RaZaFPO zH(LB`s+YPXMBz-cf|l~HX>4dBc$^wzqoG^?5R)8b(fS}`u3e!yM3_f|b|`foN@v>i!ThZ&XCDNxWPTwL;lQ z)QAFOcQtHIE$ab=><>nEr~f9$oGhnY#(vX@2HC@mU+GRzn3S@^{A%gXULh3W!MLPUK?!q-Q+WyB;{12CRgGCH zJzb3Od8wKlQIXpy*Z&h!sQE1Ks!=~Iu>?0h*bv$Ha|iV-%iP)ly$J(x1jBVQdUjJ2 zKfoLgb3Su<`OOV2tuj7R_zO@<1el=y^kdhj7f5l~((W8u- zR+02IXv2o8Vd1o;yd;{{ZLUsF$lIUAl&08At>0o^K~lNtUr@iq4-~FiUxmHL?8dyL z8cH=vJ}M$X*Fd?%TfOQ;k6WqJtUSwd`A)C-N>h8>ZxNy*QnMU9+lkgHbNM z7DekO_s{7P7T@_5=`N@=6<$k$`4&?QS3CG?G|D@HKi$s6d;XtJgkT&Q+YIW1pYkyT z9|z6w_v93RiId^{keSu*tS&{adQE3(D<##aFkUv9?I8jr{ zGmSpBF0ZK>MEiD5*eF}<5RG3A;75fc{w>@;dH`U}8vjgbHAEeUfXmM{Hc0vF3kCi$ zH+6ccMTB*;kb50KKk#mUWf({!XE9M{{&g0EfovQXWqf-y!~5@l6LERAR%bu}1AVI& zLB3iM;O2bmvCuh>%8 zxO#}$K*sn*qRySh9g|~E-tC}3a@$cP(U+b2hUFih3?0E~%fQ)>Z4`pu;})3=6=?Sb z6mz|E8)eajFDhGS1?OwhzzwtDQNS6VI9ER>dGtdE4o41&k#YfIig`l^4$22)GcLG2 za(lNL@2z12mh2CM{@jq&tn=i zFxJ7fKfKd}j(VzFsD$bXQ-eNfi8Sr-hjtfG;K!LapiLuw z+gm>{=8HNI?r0X48b=s^o&crrTa`sG_>$%4Z=hPk-ebHo>-em8JU5r8lAtU1YF-(4 z>+#`kzkm=pMg`_J|KVkWSX1;K$7V=A-O@naNldPBBXddp8>!W0yTyR=#Zf~F_Z>nd z{bs^IV><1>=U%wfQP0WyC*kqC|l;1Z|5+x#%P+@GW?mLk@&d8e)+}R zLBYoVSmB{jwLgwfVBMg|WB$Pql$CcS+SG~`!L4flH47&CZXkPQ-tn2i>^P7+-F-ug6< z4jve=`%6KZwinz>(HPwKt;HAXNW1NSU7%&b5{~6AH(pX{=JZPHx<)dv#Hz@LL4f2z!k%VO=dF( z!+aBR*emQhjtUl*5@?iiW@q*ag_LSm%5r3HqNl192g za;O1e=xz{@lx{>?x?5VL`?w(RMF|vz{=Oac^!n! z`{~lU)%qV(nVBzHvG_P^(XsKOYq4Gtkz3x(Takr1ONXJ5(K=hc*XSgsqm)F@g`=RL z=u1li_krR((EYy6$TO*|K+Sucrkf zZWpW$dBl4LnPmhg0!_}wqj@*d>?zcHO}gr8e)DrDZw&OL(7#(oYcUcXrKO*fxwHKq zfK4R{>I6Su*b#X)jaJ(>ewYszFVigthGrgsV8)f@<;-5}egjZ{k(ojeWP?)!*P3(j zljJw>#3w?rWFoXqE7x?dXJg##(drI=j!TkM78%|H?S0H&_02Azt{kl!dg(VU)JlaU zwQ5*J@=Q~-OwsHU=|pc9IQP=trrUHQ29;4CS{>#>nZ-vp=U1 z?CqxCf9ZrOJ*^#=cG_rxZj(i`l0C{(x*F)=f5-S83MUCjUSWpHGJ4y9 zQjlvAEkmfv9~P3{ilPd@zSG?8;wf+~gEKEYo%hyo1v z^3vYJu00?Eo?!Ghc(~BoOOEm(grr({-+PKstllt?zvt}9m?kqzq}J3o#)VKbl=WOc zIT^f-cVhIT7^Ds(ryg5^`bo=%&#-YF7Acg6u>T!43x2%R>iHRkC$tF>|)O{gVZX=R#mi#4&r`8IUh4tkc;J^mq2sQP4ohSg{oYe6AYH1`LR}~*!EWE z?3?zzdF)(he81^bZAG4oTOGcRh<#b9>O@Gy?Ik`J)!(IRrU5MN%~#}6W#(>3tFDG3 z?RpJ!kSse1SemKXLH{Lt5PrA?CfHm-XFQMab*W2CSR=Ii?EpHHAveBKCM$=I7wzDw zipTH_#+I%r$Fx0W2wJRSq03+Jm((I4HkB%@20=0x1`Un+CEmXIj)Fvw2hi;AP69gb zR-b|{Z9OdfI$5kT1yOS}4i>Bg>x*9|GZ(Zsw!?c5o|PBijvq*3(Rg|!xq}0#9*#eE z_D5Txsr8u?duXlI*jj^Jw>(`XIq-YbnSa+-=uS0q)0Ea=C621&qakny zFF$eg1Q4MEeeZNWYXXZ0My0Cosi9Bv5}1RZ2b|ISEHudWsBM4D5KgD#l%}rDYm=}! z7aCEO)SKd>-~@K_u-xwG)LePITV%7^&HOy?wR;sz(Mr@^`|<@+x~p9P4vUE}2Pu0* zuYz(aP<&rY9x&`v;ueMTbSYNF*z)^u3g}p-s(q%x_-nvZX+C_0m#@v8vVSpy;LvLI z#=peC?-}P&OY9Yh#jfZaP1qdi<%v^=mNNbRVi{4&q(r5uxcDvi57A7qKhZ~#dI!UN z%|g206l-sbfIJkLsI^S{LVFdE$4k_SB9;?7v4h&yM=Od$E>H-Rec z7r2{?6EF0p+;G;Ls_(Z=kF{?X--$Ha)q9r^92Pq_1dvFE+w7_T$_m5CAXn-;r7s@o zPt+{3mp7A)ZGcsN7d*OYu*>eMz%%NToPxzTGB~c%ls^F1v2=lS(-K;774GuHm+8r@ zL!eM7f9)}ShW(-kR;MEd6KA}nkT{L#PtV%N2&tL7gWKvxqgIYx(S@eUe$7((@hHLc zO1eppLK(vl+)GiR$8y)LIih}kf!KVIM#VvkH1BU*R5jXOCsQn2%6v()ksU&gnyA-! z-v+K)B^^MmMW@x%Gqi*&*Ts(sjwls$rGeR5{qv^gf5LYGa@p^m_&%Z3ZR&91KH|ZD z=-|{D3Vl$++Z2;s+z)JzzRaMLn@{_peMPB#{YWVj21eWBxr5;S1t$&0xNdaXPwd+1 zoF870quGm3deDey&8-Gg#tW5_<)q? zV|pxm&l&c8+CKTHNcMY2$Mtyh>-O=IwvDSxWO^FEIh>Z}`8(!tGcpgO@%d?C*e!e! znCny;zWcfDLZW?P53eSn!W|X(RPZs-Rkc#LVNelgKBoZFULGE@ixTHmsI7twK@wq5 zN3ydMf4B}gd6Rv7WPI^&_;s~Wb8SaQhe>s@{X4bzNEhN`T07$OGC}{6Z%ol-hyQ*7 z9yZv_=;Egz4nz0F+~0Smu)6F_92}k8WnpfdI9($Rk-`JpL^{m zT=>0tWw>t|IiA)U)bkQT3(@bWX1w3565(awz!q2 zSP++0Cis(jITM5%dy+n@nw<7I>J=lAc6UEx`qURSpYduM5~^%FOa%p@aBKrE2F6e{ z0Ya=Qw;m8JZH31fqUbCv>zkBT{(3^DAnBCDd_k>fl3i!qO%2?6aPk zBLj-!-pwCS?)9nzmn=*v7UG~unB`nyu?=T}sTg^1S`k4-jWb=V%C3EM71W&0B+jiQ zHA9%M52t_{~)FQ5raE+c^ zRD?;k&!uort@W=|Y_M^giBKkf^IT(JwWz2lZ#NLFbFr@jFi_-4M)mT@hVV;}Mpv$z zmUEQ=kkK~$K)XMt71gtsce=34+?Uero^0s!BKvfudVP00HtVEG%{22W>2e}G<0@V| zro$djdDCJj@rCA3U?x5IiginC!KQYoOZx3ISfKJVYmawxOB+GX1ALpu4?h5?=5DKgh+iW9%u30`XMK>(p;JP(^hMJ9IPJAIaYVrnbra%L+js2) zW}pzx?s&A9sNxVE!;^H+e5|-e2>{j6hN@Q=D2S}G3(euTR7h;S-iXFp*RQkc!q!ym zVV}pU&Zm7gRpr_B43i66nL-{XB{W14{L zj+>RQ4x>*)j-Omzmy=sd~6>+y6akLHX z|Mf;?D%T^iD8tYuG1b;|DwMiSCpW!lM=J`oUoY~z9uH6XMx{~xeq%>-?aXQ%$$uXD zr|~c1-_Kr&ItuuByninr^9(UT!ENWue8a*0ihl+~?KY*%dIl z0018VDr!x=6;4G8BA?|C_)74QC2PtGor^ZGGB~I8EdT)8B%l4}y-jxvhj)%)ofFzevsn=YaZwK`2 zQnVBkEowQhgIw`TJ0b8c-s&S{DvX<_zs08oupeJ3kR8{U#<54?wSY;DowCu`BuyLJ z&N5GYk8gJA{mv(P2t@JN$Br`Nj3fi1-x-RN?}H}!9~SGzyeq>U7N>2=>DhVA!l?<5&Pi& zwGAV>jo+YM&-f3)0}kC8&BR@b;I`L7VC5)+V*&;1cY`qi%*Mgf@?$`;@A@wuJQ?vM zWzNnVnKT;G;MqhPMjiZ72F(imNT~yxv*%#+GE?$;b|UCp!%@pIUE?Y|e#znS{t$(Q z)-Be}IPT2JC7SgxRFGx^%Bi)P-j*s__kJIW$8lKa;tzl3mWgDHAS7)3Z zt*-j}j0wJ`_xV-xa*vUW#pY*N4$=zGkW%^5%l>$f^jLVN3z*xyzj(S`$bLK@9Ddzl zw3k6OP-8>B37Lt(Cmg4FJkTr4p~=p~u)=|M^Q*)r@~coO1v6?yJ#Q)6(^O1xDfI1} z&Jb$H2(%Oi!A3DEu%%qcW~zy9`$` zGv_|aLPQ|xf^xxW{Z*@$gw>lrumg`!*t-6wrsVJa2iDkGtt1l(RNx$|T z_$aZY`>M$X3H=5kaBNx^3GJ1WxSjB5A#KoH5P96ZTcH(Rfy}@>V9KFH$OV%=&=}A& z=`!(oIPH2~{o{Dy>FNm{)OMMBo=yEEq@uJkcsFo$$rB@GgdwY{a>L*6TySu}=PGpU z^+neqzD@ie?Z8`xSm!}-FzU4?bz56oYZ)?;gV_jju;ej64^spRQjsJWVcnxxVzlq!b`?3<*`y zd*2XF!ZkftX%!$a3)4B;|HF<&4WBjwLL8Q7{TRUKb=m$0e|sc2&Iw|?xHQu8kBpBTx@HEd>uiAAT{(kMa#}Jc&MnG$L=8vc` z;?dQneqZ@<6gn)Lrlzqd4Xw(T&TE4krL+Y zrWe_O`kPnZQi9#C*Ry#>oN>oVp#-AmJfY*$?>~$P3*j-tHx>9bw9BPpU61R@E(2k7VqCfos!cFhVQt>$9GGzI09yC1;FE;19p?L-5T zir{%A4&pO{(7_rE6c4RVW`L4rQ0Z}|BE%f33Rw^Jh$Q?YUxu3RtCO**g2CcM1!Ogl zB*?oFAEA~6G8~n}C^l%B>$V=v6F;x3WG$89qeqeqScwFtL+$j!4S3&Y{W?oROwcvHuR#^(Cnk6S-##!hq-wW!P0w-+x4T4Si(Ji%%>E;SUAKZr0T0PoNs}B;*=}kGD*$XZ5>Fh!jG|F z;&opX&r?@e@Q_&M1e(dTD^cvCKB(WEb@q@g`RjM&ep*~0$`U` z1|ph6$Aiq1rp5f@F!fdqynK;7#V>!G)|rqh?O7ON8w#+Fyf;PEip@+#PJ`7505=BluWMs)L!2< z2E0umf)ZO2PsK9r8Zn_N8I20(G8xV@B@}UxY%Vdc_MQo_9`T@oE3hNOr+4@M$fbi& zZzbG*xr>+O9+oLql-{xms7D-H71l@&I?E%Y*lDkSzE_o+lekS~268G3%}IK_$}n>d zhm)xz$4kR_1xqfcrzD<=pP#he^go$+{s~^b6v2ZnedI5jwk48oaPWD#>P*`|Q_dNp zp%LR~zqBR#C4F&btG{;MD->28 zAS{@lS=SsO?sYRY4Y*T7>~zi6v+pwNhzL4dk7o&C(X6TW+odvW66x$e_N5Y0sAMy5x=$@W(x1S5^J#Rg&@i6})x9Egln?JYWBj-ICU zM6C6(m6S(4KPa`-XOhJzcH8H*ZoR*bvI}q>A#GfCS~H`2)rBuEhc(g76%$O%wW!S} zKm-j+Xq)j89CSu#^1htI3p;m{NB4|zPEESM z`B_6T8t(#jqkgyjz4nH88(r&dvK~$v%cg~#wP8%zm~1(?ZWSims^m@T3&8;9P+vaX6svIa_lVJ%Xt6=G^f(Dyyd0pL9ePXTiuiJqg^wrgwOF?G1EQ2QBYS+^2z&XPpY|_Rpt%|7jV* zMCu(u{nIP(rapw;Pll z*MO!ZrvXe}y&cO($ZBYp>h53Mru(D_H)w;xQ82@a@X)|y_jn_YT2YJ`o z$>fO`Ek8-+m=~2RbzJ8~M`vbMbL&rj6a;RHJ?c~Ziw((o$A6rRKi_Z--|1d?cz75) zJ>I0q7*_W3yVjS}rlUg#&9o>sAaI0Bd1b?G|L6ATP|y3*0W;J>#Qx7?E+I@$V`V|L zmjeZ6TXi+MjCFc$n~=wy#`eeYcLARNO09u|HI?ChZ89s1et-kF3me?v5VB;JDE$qQ ziI~})~|M?8J;^y4U{I@7P^QyPQFR?m}1H?{_mL4$zF1z{`^ z%QnHHdg0ozsQT^Re>>GT@QbiL zuG9uMd=p>unIPl>(9_vg)Af;MT+#OM`==-FxP}WAmx^u*avi8wxx7Qrh8KK&ew5P&jt0^`+ATf_`^9gCmdb=TvC5V3k0Fc`gcuq{3bV-~M8H;|zx^73i&QJf&KWOd*9J{iOt@;T`m_}?J4A46I@q`yhhe0(Wz)#Hrc8Q~^yXmX9 zw;b7bO8|o43@Eyf_gQxpkgU*e;6|<127g>zaXk_VrzbzG)2GLwZR$Rb%_;}=sL9hG zgWH3rE%BQBB0AJ5Ot5&$$g zDgE&1>yVTE@gKk=Rh;_Wa0I|QIFmtY9EBONk4A)!pY2SUcOa_e(~5~(A1+r8Xm))! zai3264yu26Y?nO(|J#zJ1t`{HgVZEgMN7frP5>;-0egeut`z^{x4=j(_q8>!5ZZlj zs8-+Z0{y}n{Id^F7op5>2ov5r8AljEk7th1DGa& zDR=_KS?fXe(-7giBAky+W%v0}y+ZT5HHE;0JiF(^Cc{$%kqE%13}GDPDsXL8k>oGH z)7GflBggqwlR3v`76Ati8?DEO)u+q944+qS+GM-_`enOTk$lZ36i`U~ty}R2Ia#;b zepeV1R8oCX-1}=Yjh2*f+g*(Z!1yus<{?-WG`Xn(-f-*vM)dRN&#gx?_6XIYZ^F+H z0Bv{o<2wsF=veo-Zn+>Jjl_vvj?0PPukTX66uH;>a$GZeC-HA0&i8M~^Y=e3$Idjf z!qmfMdO^6wv`M)AOeu^dDs|lD)zv2{>7QBw4)WJm`UNyQD?YEAc4lYgN0BP4FCi&5 zIO6R!wMLv6K_8qBW%K=)>pLEQ0)QyD^{asMkZ6Cu>12kxxdB3v>v%vroNrL!HIML$ zHAO3Epmjv`BuT*ylZk%1EtYtD--&kh@ObqZH|5vjW9HS<`jw(J>#Jl%=Rt$mmIH|S zVX?uiI~X8ZJJZT(0IXurBf801!I+TQCsBb$1Ejh~?(TQI4>7Hh#AvZXN40&-=A(!}@l(dXo*?{z>VPjvU%-Rh6O z*QD?P&5vG}oC7;3AYaq5u`9nE4QpoH=&HM;>M_xiU?u;*Aq*cb9~K|4`t#$&J~b}y zjHQW=jplH@L~2*!G}R&ock?5qX}?q*DGmXx%@&o?cH6$FzG%u-uYn_>RbN+-AaUZX zk>AMk%cnrgKN@7QI)2=RZpMuhAD_W;0P^hT&#w&|4oR~`F1sY|*U#AK>+XUvfFlhI zq*O1$MGx2Rf^6nKRx;fzsBHZ2MNR#gN~#Rm)l8Cqmod;j3k{a|yM`9fbOo%p_8;>` zT1S6c5323}@bc8hdYmw5YSD#v+o6@bzADqQr~LYXcIacy$EqsCvOb^BVIrxFLRRk3 zsmHT^dGa|YHbWDE5FSplpVTt`EFS3{!>~B9I192cfikm9^HEZV!Cu)`+33I*VCbQr z&o9bgVL^OmR!fS>z#bJiEh#Qo8p)*cj&c1DgdZ#7q>j+v( zDvdlq?Pptf&#ya|MMsZbn0>~Tso6Vkj8PJ7%VWfgoJ9O z6VxbHZBkvmXZ!-=UBq6CS9iz5qD^sD!^KP;Zk|r4CBcgXZvdv1==SJg1IFOre9Fb4(ITE+#cCR%P(xYoDiKm>RcP3^r55#*3h{ z`I3QBj>W`GkA>0&sjq~B4vQ&R^9{*^Vp&7Hr{nI%dwL&>Y(mX?g$oflU_hmg#`5wS zB5@fH`m0(t1&NYV0;K)u{}vM+8O~_g3OQV>MEmE66_-lV+3V{aed}zGZJmzhGa%0k z2$=6QJeyoSP+I6q_UiUK>mA&uk10NPV{jXnVz~J$ce(i4v4k9sMY)rY{-(77imKK*gQm zSBb77M@K2_uM+Lp?Wd{o0wTTvxbqGTKo))-;Af?h-B@%&4qWW!2^c7LIT94YQkInf zuMNAj853lxq1?@{HvPHj-vpcn2OtBy$R?46 z8~f#OcZ&b72{~FI^X$Jy4sdGU?Uz^6gDNVGCJgHLU-Z?|Y7}M0Ns!{?ktP($ll0Uk zvdhRLQ`(k23U^Pxl(aw7)g8l}Bv5M8kx7kp|Mo?>YpyNhw7Iy>HP z%rVic17Hd80kpNEr?#Vp|5B^JRZvn&#Qw#y%BD>q>GMJbj5$Qeu-SZ41m3yx#jTRe1thBJqID2k}#A9EQSCzj|NNn)! z{mvZ-dG z()hveZoJZ1dUvEdg4Mzi>{*lgdnactKa@-mcrRDZjyh@6pGkIWrHF+yO*=dFz5Yte zqN#oIl}Uh|LM#Dc$|R>twX<}2k-)a(T7fop=I!kr%=iy&wZVbtRRJQ)sT(`{VaYlP zq7N*6ph5*Q!--g2@vlZmQ-Pu*^CJMq1Ul&ek~v5_Of}NM@WNYV)_sZ}h7rTty-82I z2`SPBxB=L(s7Tf%7JZ;9tom{6z2dd|#jFX)wFCP13Ot2~UDmXuC^#H$TQBl?MRYP$ zCieuQnhy+=mur+u`GxFyrC0{d@qUw&lOdByR(yf?4n%<{j;gYu9`7>!I&jtsl-Q5B zA0XT3Q)BX=5AFBK+rwYE_DEuhx(kS)ABCSgyX1mO#nu&ja{miH0CpAg8g|fglXrbsRU2z3nJ5sU{@& zE$8cPftNH;C_Do#NqiMkODyS9rHMHWJSlxXYciZ2(@Nb+U8?9{#&MoEcs;nmiuJ*u z^4Wtb8%@O!_B@!|0&O+!*qQQ_y&!VXJ(qvRgSI{>p?Jo%^#VoTSy3Q{cpB)4(V$do zXCU(J9|T$lDsZGckwpujF&QFsc?}@p@H8*f_0wYbZlBkK3S1?KU&Z)+925A3=BRHd zZx_fFhoBo|gG8XFJZZu}o4)!TX7(N^==4_YTex{DF~qneJS4gt?~R{_jT|;|D5+nf zE0o#E<7CNFOHCE4q=`HHWN8o`fP70C_==Oj8OKB~+g^zyEVUCx4q! zg~W;}frmx>E@P>2a2TLlt=WX1fEr3ic}vybf+PJx<83rvk!Z0c0v=eNl;H7>eLQqA z>;dXP@H9pmF{moY45hEzwACh^KNfAd(b9U{_UkU7313c2Q^!51xaeBfqa=X_3vmrgYnv zUE{qYoET?hk*$xTe14$oE8;H?Fw5S7gf(e$%db(oHY*K_V~e_e{3rbBc=<7GiG?W8 zULFN%YanP^mby;^q^T(6#1;X)EGSP08UwlNYuxq9%yB8VB;#Te-^@}) zpiB)fqwb4JwL(aOLJ{VWU%r*k(O|#$0nA;{Gz!c=*Yrgs(Y#7-*>{e5Zl$RR#C+^W z{#M)iH81BvdqG2v64#h*a$b$V1Vw#ZBr-ghQP-xlBBYm>5{ifd?gyqw)2VThS+Ubwd;k>+w za<&Pvh!qiY%|9(RO@c}ZRm`JIEHD;WKM9m4VLlE_NTO3LaIX3An?sg~SnNg5! zzu&vFy{2)0i?#6XcLmZbwQK})BN?uJwNSHE zJivXn%FZ>*_+vkW_IVzG7USY5%GfE@84VW!b)QiJ5+6- zwgMB!lt7~SwATy+y+u3ucZzi5Zn_-<2fc0_W-)&Btv+91Xc3@M4^Tws27*Wnq}ikr z)4OqWUVbsD%X6h=!_!KZkzvkT&!GDHAB8}8McXu{i>9>g1zxMRpR19AV;2t~Uu?A$ z{WQXre+V_R1+^$H3|cr$UP9pX15gLNb&p5wsXm-89GV(CnM#Q_DE-}l=1zBVk!##{3>ElI9=27IZkO7C&a5+D*N@Af$YR_@l zL|gHM>vUC>9w5yLRCLkUND;Q01j41Au~Y80px;-d#?o`X64Wv zmZ28*LTLwITVyyyj>fYcaYd++{4oEHIy`s3L{GyD3vVX__Ye6I(}~Atow4i1TD7fI zh`omys@o^M_snSzLf?lGR~Oj*-m|XgV}4{?$YMi^88e@@OB39t;dhPa^@oWZg14+&W`7u%k5k@tZbpRQIgn+_Odw{UcR|$T65Af3bf4Z8arn z#6^aJvx3R>rliElm=bDoq2DLeAxS=X>!Ukj|9xlS zBH)8xa{i^$c44E%bs=lS4ncdH^34m?*>4^@XVrL{P-Lii3KTz z#ifBfR$4;+XXf=_=>kLyuPKkytG}QuDugZ6V&bc19{+TXcYZ+Umo^j*UPOY1BIK(E z;@O3npJ`0EUl=e8+b$vfW!9bU-yt%bibioq66r(wA#wr*{m9InGWIMtaLRpzU&s6_ zs*FZOOU8DKu87&rKGBzF6Mns>L18~#d4nOkehj$y%)#I5opFEp<;*0s`Hw`rnEt@} z$K}ln4(!iHiije-DomCf;av@WZNgaIf34W%&p@xcc|2&7T*(GFmS!L(N=y0-Hb3!u z0u^yNun*H08cIDr#1p+y2=@qAo*L&b5X;&&Xp3DB`dE8=w1dfQCeK#3TRI?4!zJri z?j3B=Zt^Z2)}2Z8<`7qczgALZz@m}szx4wwwwlwxi*&Mg!;ykgcofs$Izy&I%x$D> znz4T_XE-yeCD_M`&RZw`|7ueW#A7YgwizBwV8Rn-C1RvfMmS*>vcqL)i{pvQ5h{_& zV=(7mWTa^e4OB9v4vYSb+{5P&j*n8$3>Q#VPF@ z`ogMmI|Hc*75aL18?q?gH!7r)tU0$3-8|YiTtIS-EcuwRkV<#zF@Trqc!mKom9o#t zX8V~Rt(j*dpG-*~J{3RN3A8Ll9CRB(_M1pOQ zE~+b^uxA?BiO6LCh)iEph)y4jxHMZJk+*L#-4i?*(#a(0Fn5=nxn(@nxRpX|Q!jw? zn^wyKTwsb?(13`aRAY@|tPb)fhlwemThJ}q@;NqsiieS!)hRW(j+SbUB#~&umOa=D z&c}MEo3b2uvHV*Ieo5>k6ci*b^2M0obSZK{f??G-#mSdF3KS*-(HwS{R&r6M7X04R zN`OLqya(B27k2ZlxvZlRm#%6Q9n!0oh-v+>e|lhauE$huZy#4XYS$9#?!Q)#Yo zC068?N=#Hm_eC&0JtN2t3Bj_U9mqI976}VhO$WEPx0~1Kv6EqeG2S0A{0;IcM|To* z_*tmOFZLSHF#%*pR34!FX_oR^D!Oy;#X;b71KbpJqr`>s3XRN)HkCUlSHzIJsDf8Q zj6k9D$BZ2q7h-_Sf~&bdw$Tsn%yAy^>!g zt;h!OX|R3k;sdX8MgBC$hAim$lSm(_t21GLdG@}_xH-w)PI}P5pZ8vZ_rR{uUNwi{ znV?2aPH}#IC{fw$TMo;51H51stYA1C2bRP)K^OEGoU=QXrE5-k5wKWh+a7rc3LPc$e;9mChXTJK}ddiyldd&W64t;y@P> zb%~dLwuq8$oYR?1kaW=E>uF?CXQi?-?E2p^CkKB?t@(3hWyM2x*_oFA@N?*Iw`z6+ zD<6F%i$dKxDU_$n;r+_c+pzzw)KfupC-4XTc3+58l8V{Qty}lRygm_>{Y0_uC;>E3 zq5ZpSv(m^TG1P~?jxYWTga5xo2BUuf{u4uLNuz!W@f^htm)B9m(n5C6Wcg9$8Ia+FDdb_Csd?Vm@$zwAEx=>= zWg~iS=utd-RIs{v44w7lg0Q~)Z&s#4ERSYoEg_Y%e#Kh78qc zfSeh~e;C0tVp$QZLcK;bxp~ojSf6}W@&)-?Ytkh~zZV%9o-)eW-j+LCMT|9#Qtq~y zyYSZT%fxs_4In95F%=TUo|euW$_4=gbs`Hd-a98anX7uGYX%gh51-|Qxy>yA930D* z$mWBeE@X&~-l@S81_yw1xrkm(_La`RmyNWWz3S7$3t4ppjeZZ0!m?j5CJ(06-Y%<( zt7Z#2 z#V2^4HKfD+jXNg3>Yh(4^xUe>eYNc>Gob6H%5cvA?r8NC{uu(tCN5eyN$X@p$s@kd zs?gf2PiYa?ev(RHA?eXE5H0VPj*^>jk|Qly|D<2E;9?rXOJsX;WLyVeXGH@(?*b~$ z&Yi!t^)BE=ClKFSd$?S5G;epw2YN%hOSSeKmH`1%0N)%OYxPDK1ADOgovgTqrj}+N z04AV`?FVqv_h0TFzuC{ffer$RScJegTy;D>K(Tsw7|$MyvX@exDd;4V{N`LIesDZu zPNGbxiW_;guF9b@<+lI3^rx+YY3K*9y>0fW3LrRo?7tlXuBLh~ zyo6$LaKL^~O(iM)REAjnN_D=Y`&?k-(wiXX=I-wNiG!)RFO>zoQS1_EyFJ_@KU}$8 zW!xn5aTEf)4y5c45z+NcV`IxvKn+g|OlL=zLX{y*DZ~wuWI@J*>6~6@BmzQv!#@6u zuAxJ{0!{uc8;!@$u5dHGl)`(K0mOn{jd`DA^~1~EC3jq@wU9KHQ7E^^Qq2IJ>mhHQ zwVFy|?vj$B7tF8Zu(&@pg;SdoesYJ{F9NWG_^1SeV&Oq~^0j9Fdeh@{xeiFebNTuU zrHgDRlw<~+`BeZ1ryw4)3WhzNd}$Ekrr^74ZRgE+e+=O2$nT5(djF5IqxD}IS0-N7 zZoO`mK{Mc*HT=|O+2MC}2lS=qr>6Kgj@*oAd474fQ4BcC$#E!157X3!P#-XZU^@L$ zZpN?4{&WTg+#NN3a2X;zxIhx+sk7sNQ`*1(m#uNvHx}T06ZiT(!%!Vun3W_c#p&y$ zE*D&Q_{(8sN-TBjLiV0JO2eVuM{h@?b%pX)#YB@tGkGMUunrbY%N0 z;RdY#?(^ZX)N1{*{v9s&XnLDVpRFSED)zf#vE!jmeo8WSof9t z<_fnV)GE=`_lC2yR50-eXq-Ryu*19VRcGN@3+t-Byg z{T9`39u$q829`!5=o>h85kl5X+8u|9ExrOxA3MSpowCT1r=MebgFRQI5W&N0Fk&TV z9mY!~tKkJm7-X0=mgzCBWoBK8{EMn~b)W6@({ucRB z=KL!#iXezoNx#OT9+_a1=71f|qN->qs$fAT3%65Bdk9xBLt2r^RM{2V%$Uwj)5Lz$ z`+nd)=58FL0lEp#ZEVFHS>V6*GgBxdHQ-1IaOqPM8|wje0QUX=B5>bcE%{yOblmp% zT>%C|OGC#xe5EwRq=)hqh!2Jz#|XdngN{5VWxD@|T*%AG1g@TZ&Ps>@S-HtM;WIvj`Y zON(y&RcpU`E2_g)xTp1_htb6d7ltSbHmxu#AXdh@G-x-YKAKA`{8c*wQH-c1cMxcU zk3ti3*nfR8LKaX@=is6QAc&w^q~PD*YgbGnYF`l_{H)b!GNK?-Q=$06Xc}xTg{AY@ z5@T`n-ch<;@uNl%78ER#3wNvX=3pg-43j`j)4@QJNaf``sl<=6&(jEwWuWbI2z1Ps z=O@-%j@tmhcb{67=V5}}r}opLF(94io4b>(zBetOadA7t9LDn$Oyw?~7!UHWP0&Tl z`$`I+meh1Mtc=j)q-wP1!(&<=SJA@lA?poLPMWcMxmc6@+v3t^wCPCCERnG*stUHp zPmp*R>{_LPPf8M&U}-4%(?%eD8qNt5Q`IcgR`8YJPCnG-YFoR<{nWb03LVVCy%gKq zO0pWwNGsZ#EL3{^IP~fYEwwi*nrxso zW96ZQq%*!qT*JT`!~SjY(Mxy(3lI^4KY`@&b)GZ0FauO0jU^JdGF3-*xK_`s(CErMI+E8&8j+J>XVw{ zR^n5MpDjSpH~jqukwUk&+oY-L@HaJORM8qc1CY)QTQ3Uc?OjGyrqBr}v3@w(iWvT{ zVD%gZ#OD_rl0j*9NLH{OmK4^_Xk!OmxD6R^Y|hP(jz*u$!y%m^Po-ej8o-mGu6WXZ z_sKLmBq9GN+Bl0smhZnI@3wXzJHGM}cw}nyDsI1tFQvI?sbwq7 zaE8hwnaZXX)^eEim13Nm%?Z!TKMuSBzbMwIRR7j_#EJ(~6zCbZ6Zl|B(gV%SNz6cP zpaMc{VtCKMoK#;#MMPH4jssRU{Z1BLyN>_MFl}CaI2M>S0`e7rSiP%lEj4l|q$-Fg zs=N35FW{QOMULx|1H~F`0;YE(Y>+Y7@@~kKG?qvkq%-aNG7l2A?OKIoc9q14PD03T zOKv05jD!smG$*v8g?70ePzx{3=<)8a!YKCbFzM%KBx~?>be3Q%jB9$6W4KbMR_(K&e&#c>T#YF z_o%5%b@L=cT}G#;)^*BchV%B#tq8T7&1KA#u}~bJfzhW+XKj;*t;l2*GUnjh^oMm18LJGK-S6Uqbi6b31 zM~>~c)7AcaN*}!4`=|x5{;Wwp%benIbGO!LGhqPzCCJLA0!0BOE@v7ff_IUFC z0VAO-VHRMRWrEid&p2l?$K7sKS2gn)x-eh#|l&iiw}@B6y0`?}sgUuKe=!Z|+M zMt_D;f+9@e;V4)R)oWs<~2x*@dDF1TV| z@2wRGv`EpnXEO-Vv_ps4*=PGL!Sy$V9E3wP&;B;yJ!`0Ra!D)tTE!bif=}o9>r;@| zykPH%)5ABDZYD!suh;H8y25RK_Ct-A+k@67R+7?oG_LjUhfgIulxp+tvVCpBZ`8cK z4bDyu%z*$~Lcy-4M{S*dC<71W>Rm~$#8vBCSFk6&2%Fh?le;G-M*kG*Nz0r%43b|~ zZ1)l>szE0S=zj7w)hXId+wXX*)wK14Gc4|OZ`|c!a}^zpd-#1-*R=TiZEotw+letr zkpzql4Z?{Hc_YDMUbJGUsqb#OCA#GzO8o3u1$Mvfg~i3h3p6ov0Ud@x8s-fk6dIra z6Pz?*4{LB}&#|qlS1zk@N;H|3d;X{|@a&KZB9n6LMnQIO)1Br-efRT-CvCyV6-P@? zYfeip{4!(u`5?s^z5)(#^^&CWX<_gC4YZ^o1Tor-t(Q_@JF(n9M8=v zo+4>4@&7sr>|Ia0--_Jb$2T<%_mkX`GzYP}QSOLfU-magn%K#Ox znfh(v?tSq0?(xlexVt|@AF#2c|3UAgzW10@TfiY*|CgsiQbFrfO%6 z@AN-VTn-l$ulGAtXD~|LGJ>D9He|?8epFMj9ylbkYgTA+xm1{G8 zlTN>Y+B_+ha%VY4&bFap*WCORGv8${I(2=*lk~m)b+ksK*J+i;y+ZE>-EeH#nyMA(1`W;masc^7#0e2mxQ} znsDT&ZS?<-%Z_68cLlHMr&G^(=fBOO;lXE8bcZGQQ(5g2fNnb0Ch~g6v-6$cq@p9_jNF2mm9yJkHnVMoUDNhT?Me zGOgm`WTRR5uPO!O=I2yB;Fi&jd-erAe)s`LL*q}KrU(jAZ(tjdBvcOyVL;9pMUl@@pmMB2+xy)!S@gU|hw&CftW zeEJWM%0&6LMuKk5?UKWTcIi56jo=2e$c$!9@rK3E*{3UDQE1FQdg`!jezcW*6LwK` zv?O=mzeQhu#)sZ1)cMfR@MRnR3h$=l@o(XTO+G)UxW|@PKh}iH1)rw@y7&xIwl2-k zvVRr_B^Ss>$$k`V5T_BX6_#KG+9b^Ty(BlDX$3C`swMM4_*PT>MrnA2-JeUR9*u52 z@IH_AKj7KZ$dZu1Q0&Mt_v}*yu`OF%ySbm;l1S6;M;5iDo!#B1Bq#8@wGO{g<#+#k zFd+UO<5y_AQ&Mu+7!2Z>v+1k*BIz1($`JJmAdtAjAQyyY9UrT)u)Vx%@Z=t?A(i3b z8!5&-$1nG9f6pR(a@e6HMT*rB8AuK;oPY_QAmq>x6} zysG)6eIDJr4+@X{Mhgo97h1IpKCraNz*a)bd@|(P9&*N%spj2KgycRq+Nak0WLhVw z9-DJITA8jVn{=52puLNA@VFHztslH4o1s~oec#>LFfafAFC1!ID8jG-*%=aZ5)rkzxs=g;EabV_WsjLc7n>S+2;?)zqY&%7H;<~8pTU`8_T1AYWCPgbc~ z1v^(Z8AEYjC7T5;K{BevNNNe@etDA;vdqbUW9sYnLc`SKVA>xyvXlZQldTTB66SuUp&xD!f4z})8vEkExH^>O z<31Q;V_jWB6WcC*FomP2Jy>jX`h)XRz)qj)ke8lb_q_Y}@89<)j$goq)!}6t7Psh^ zz=ft0e{y=X%PDKhlIH4V_`U0H8A1-x&543EoS(Pd)_1fhDq35 zHyf)o(!86@V;XdRFsqR+yc@`%_NItK?>?#8qJ)_B_APHn;>G+J61MxjvtaB@9R5zd zO#xZzNrHSjiz-W5s10(;q>r-geB4%Ej7Mk|2-32vw5Se0Va9jHRVBd(E$Xr)wro_m ziRSfl+hh5E?*z1N*y$8V9sbeTHcPdk7UOL6n^vScJu+x|M1VWyy;421)g^DSI-AHz zq48rNw#Bj-$2mPz0Jamdn}-K~vHD_awNo{X#LDEbt{a{403KEcyOYjx04VO$7sukE|rHVdTF}(5YWr1KsZiL|s!A4=BR``z;!jQO| z8kAZg2rO(|vkb+l`FSGGq9c_1|v;JQy& z)q(xX|ZU~hZ{J45SdTJ)?B#aNPXzyaQ;dI68|h*J{m{)(FcQ%@l~7PyXKe2K6) zC8oUJILN*#30|!sC)ooxz9{&mBn-`0$+EgcE6quW4yBX>bt0=f%CAl|x&5uybt0NU zaLS0D#^bXPRA;pebPZfW*}WNUpLrwu%q+-v?P5x={r9Mykt>5YdItUr3n3p#EZlt( z-TQ-)l9E#3>%0jMRa6-9O)2+o;h~F37fJ@?mFL`plzOQLHCwYoX8s@j_vx_}@WT1E z$%$G@os1K<*tE%|Ub)Rg`EXG}iA+>gDSu~I&-|T}n67cTKpJ8zAP;(dvMHD;e)x;S z5TqrV2OZ8UGYdHXMJ;$oyms#0K7po|Nt;M6uDHvNkw`8@v%qvHks@_Aq_~6oK?Mpj z`<%w~$-kEu-(#=v!VLcS@k4>h4aRWa5;oPDByMAfPg_K3wxj6^)Kh0_*I<3UpbE}C z>?dcPFMlXA%sASS9P)B$JY3kth`cZI8_>I@s-{*2nLoCD>FQXw>0@m^=W_)FA1bx* z(Y9L$d7x);pWX_4`}_C(JB!5E1?IjCnT^$Xf3gzbw$n%m(~>gs0_zQDxWvx8Y^*6jw1M?kR(NvTO(Mxi7(q=Qai`FyM? zSepACKe2$N+PnL=$DZ77H>uzjzoC=c$Btll)oGYSLCl#nAC>E~+wq}kbVsk3Uebly z8z&3`AScY~AM^JT;vDTS`Zg+dQ6fT2dIBu_a+hK?l#t^=?R0@hJ5ZjxeoFr=E-wD} z`-_8-5iBdGs^7UmBxokxHq0l{&-b>>l{J$LS5|KzR{HD!_M(dm3$F`2M#}wzOTPQD zK&;e}Z6q<>Xx2#5?n~X@0Vn&hO*_BU3PAJvPUTf}bo5|}>BUkDV;Jyj5?PC5UmTKE z(>Ts&*_%442eTGehim$UUUq$|uCMGtM(Pc1F{>1R5cJ84ZEwlKM-eN$y%ux&?OTck z3W0zG9|!w$zU{mMo1eBG))@Vd55nbgTu~=q@zo$z_sNhKz#x4M{@%#xS}%;$spkM( zNTwzxf4?}0m<|z=<)kfN;q3ApgJ?|nNlY~LollIg0BnQ}{s6z7J|(iK*5;?D0(N9Y zzqUbjdI{9t5vD4z;iG&z@hYUD z^72lW2A$%oN!#}4cekdusga`IYNYS2im!}U`JYA*E$&FMk?R+JXZ~t5p90|enPi36^}_Za9=8~*iJWNE5;vdCgj~xBA$db@~}8~ zFdB6VG!qjOK`{O`RGQm%tiv#f{89XfNw%A)6+u6}KGg`(gxm1!U6#4HL zK5-h;%oN)!yxwc`IqdDVG-KzQ`EJ-S*y_A)Wo5;x!RPXRZ1A3C*t8OJIt7EwMU^~J zcH8;Ug-%t^S@ea-lqQ?e3Klg=e|k#AuWlwP*j1BH#zhx1W^0nzqle2-%e?rnTd}S& z{W()1CqLgx-ZvBQ#O~62#k@9RlWhv=t1DDyYa<`nHu)?AN+rd5w`0x~l}mH3kX-yS zHsY>wVD5QrV0EZ=4IuX26OZ2Cqj4FSLu_mvXFC7R`Ib^Eix@oB!^^IuUyBbR@U}Z; zb0=b#W|$_v@#9vxUB9)Ww%^{$$eUH3XvjW$cjmwx&DiDM)zt+-DiCGKXVde7ww)0d zs_2X`%pEJk{5J+lF&NP?H(EVC?k;!JqKzz%_oW062Zq4x-lS(9#Y3+JDImET>31M1 zTw709QFNWRVdwX)SP22|wZCPSZ40~x2qfdeb9QcS;X*0_a$=SHXJMfXt&jK-A$Eod zTpC#tBfp6BbVhb>mK4`J4)ZB$(+3@Z840pKJiM^c8@r`iSNiRL~t;EB6HNB z&zzN!E3IQ1&umS41jUQaf+~B8D20U1xdrZY|J`LggmOVSbY{Jx=;L|bV9}Ew)1mC_ zmusgZXi91p(zuN)Pc~zNX(f+Fm4(u}M#~IAZK}~v51H5SS@Vx%7w)TP3i z6yt1K7yhj%XEFN{BQumHj@$f1uO8;wCor&gibl(#}o3 zGHuB5e>C)?e8yC^^!V<3_!wv^gFR)M1FJvuw9x&$}y8S_8=gffZxq%M`(qQA>;KxH8ER>Zb#6k-fhubkp+sjUGXg=^6Pu;x6rS!YNY+Nzbl8lsI}u>minK}KEt&L zL4gr1jANz76}F5jTU6P>eIUP}GWRQUNd8QRPl#PR+fz4bUOxA-sgZwq)PYXvwN*`T zP2iK&i89!FlUm&AeCN?;i1bIj+7xjbeGJCnZnLe>r`zwZjSs~(NS)(wzZduX_Y3e4 zcwy_+QLph@FCry5^sLnBPeI!DmvrKLu#l7=j$2{uoLc>8-Ud9X2qLYziaya;jG5iR zWK`r9Vre1^xcWl2W3BO;BRJ!AN6IwJHt@b0FoV#f|L)lilM${I*6^+2?IbR<1!IQe9i$=wPI*|6#;m=#nTJn7;8WPNXt z=975-*Td#7rW5$H?I$~Zlad^p!~2emq;lm*(QHFp6oQFfM!`hwTKvZ$GoQ^j6kTS0 zwFkU+k_0&eSnWlHpo z2j2+ADxCx|BLz>_AN2yzMD|r!n(pKmj*d>`K8|BY__bcubN`Qv=K=Q&1W>0tgHF8? zpDRn&UhqoUFqM&fgEnv4*DRSQlPn6GmHAsGt?nJ>e2g>eTQWI_ZpLSH?8^;bKQ6*1 zYPk~r&aA-?+BVZF->hwk-b$yZp+EYuLG&)+ozFF`t_&UuK;&;Pki zfUC`?{=35H_UzPHWw%>%StefagL_6sBOvdG{AWAx6?8Q9E{lZeX~OZ&G2jjTqeYYo zaf(G3^>4a`*G6nNp|!5$_}8= zr9f1?lYcng10I{8`mNdA+!-t!e1=_}U>JkusL; zD8mEIwP>!voLgzvU+Yv?S@&dhw8GcJB1hjK`K1D`-}<3QR`3vg;aNUfQU*OM=J{`9 z<5Tc>y`trcMf;|rHm$I|E;H#!wc9O(40%Wn4KzQIBoJs^sn}&*=JR}D4swLUFtq@3 z# zCS%=svN|??{G#qw{BtG`-Hl9$&hcMAwc1~kOi+>UpHpYW98VQ}CFbm9mzJ^z3 zuGgYhW2mJ<^9Gzxj~tM)tWKZ5RdP+VJi>{*dUG$ZN#zx5`A_KUnHf#4e*1C0;9i*@ z<>mDyX@0@I=T}a$Mg}S>YFL@n#Yq2yeu4MDUlT&zAC0Si*WF=JfvQ&SL@0N#(=RAi z;j->~$heN00QBii(E~-raQ5gDPz(B=4wp5qgLP&?n~$8=mqJ_h1%~|s=~I=2cZ5CC z+4jc=fxWBZuv6&DT>SoiqiMfb!@$z&*v)|w=kgfbAulcQRiTw>R|PWMdpembe+pj} z{m5SuR*wyBc6e}4XqS985_N4(sd|y1JVV`@MaA#Q!cR1v^+eE+=L1FD11U)B1z6YU z%d*FV%ZPqn?O;id>7mc7*nPSG4W}ei=7hw*+~<}0PEazq%nbAoD#IC?aT#8lH)i{8 zO_vRsyA-_2TC|Iv%S#v*e0LAICBS!O=Ap6%ohC?snZrt9%5!NFcG($DkEQKI5tnjn zkWQ5=%Xk)NK!G9|#XozlB zviF6s-O9WB42g5TQViAECL0o45-X@4t)BBv*d7D`Vzi*QCX7Tj#HC?ZU+4y%kS)nJ zF_|2R9OzZ_h+1TjDhcGj51_pz3@vq^ZzrUz7YjH*>glY&pjNKHH&X9QgJ7V15!m2l+*K}6t!|VAalO;1EcR$XAFqX#>V4=x+Qn$Sc+F9QVGJ`F z>KB5HEJ||n())Xp#~G>Al6uKK(s#Az`1dsW7w~Wy)eCkMn6!#diVAv0^S065>65ds#3* zpit?pwp<5v^zxiv#6lT#>7{5dhpVZn;mLN)a~UP^U&^-=b{h2z^z}70&9pcuGejkp zD{4P(y*9K9bKUn@YRe%nj~Wxq;77~NYP$tP&U-OIJk|B{M2la}}%iMLV9g}4RT zile*TGAvLzao=k3Z(;78GO20Ld5+qSzEc+AlY71}h=ly`@?uO-sIEe)Jr_hCAUUsWKy;8Tdp)!wW@QSX5;Hx0=6q(RgX}3kGyT zjXTfslFfq(DKF>g4DQj5^)Kz%6=V)J%8Pe-Zei3U9*vbS6Qp{g7k%|~_E)5eOj|B1 zk;}=zh;U5~dPloI@3rA<>}F7CwbI4j+kNtzFZ&?qtnJxoQUPUwGv!nxVeO!c{&2L+ zhup(PHAj(RCWM4zfy)hVr?DsF`4fU(EfJDwm)$Ql^Juot>HY}awP{w$e4fcw+JJM> zjL3up5^F(Ariu5`&F*h%>`etppyIX~%@$2r>DX(&dNF}NefB(Nn&pJEW92woXK zVq5~g&lZXJ&6`grtIR*e8q|w;Emt(1%jyi89M}Xqp(qib2;$?KSQuVU$GIKJ7apz}?o=W0e`vWl8mO=NJE_N-G0>eUM9$ zC0TW+3bLuweTKi_(@+j~9C~ zvlauP^`D=gkD={UJ}o}8*VCiml(I8^!bN$3h+a^B{vQ7xGJ+MfC44sC z%s$lX_o3(e>K)^T%1yf~?|7?qEnd>%_SNC{+PAdHz1N2A#tg}Q$DYVDIsx`U@(sYb zM<$F&#KgV~W%TXb>Ye8U6_2%^!TzXlYrvN< z5=A|Ut3y;QTY=i-jx%I(4=Np1kWjt_K_SWAQh@FY9C5ruOB^X@ffQAwh&nv&ZDLfZ z$hoQ`d+S`6fGM4oK>MIEyYt-==Ph-c+vKFiU+ljNsqnR#ml*YvtTDAxDJG#GCf>1c zbuRci@PYk4kIjH$lVaO zuK;=wNO)xK6e*W^} zSbnj@m?|#Kg<~1L0navTF+n6_f5raF$b})S)8Fc{`T$9Pqb1L@D1ovSlELKnQWOt$ zAF4{TB`|lEulVxJcKFkiD{jub;GKX;D1k+dr}iJyiiO@Q81sp=$$4w$NZU8b^?q7u z#NS=`(IrxBSe9E~A8>lqEVl1^3_GRo3Bx4(B zlOd~7fr9rocnIlx5y$(v{#DMfBkq5?t0-tK@x`tyXcE8hHOodwkr&>7PdNfBh-3=E z$8}XUgv9puyE*W4x$ZHptoo`f79)|%o^Njt*UDnIzm*u*v=agmf>2SA)%_Mmq$Mpm zo9Qk64VNJe6VW+Yq5R=$=7~SS{~l?*IF_%Krr05~p@O4AH-l%r$k;Uh2o69BG0FIT zAiC)3LlDF)=$fwwlXDRYF1x|Rsl~}T*5I$D_&uk*ydV1gM@ISunl>Mb+5$X&V!B(I z9LE4^+Q-^%91qy$mh{_65pkKQ^_p)wU0nwaT_*(DTmN`H_RWH*j+*fuX7?L;?sFto zg!2~mO|Lw33rEH^(^rj*A~WGXR-fe|gt`=bzhrqQM=9!3)~)Q%T7g znq{^6H5-o@n(S>%v&o>gZl9Mas|>H|A=oVLWL&%a0h!aDuT^ll@0D1^dh_=4r~N5ohc0f9geX;6 zh?1(IGs^EwnkU5u?75{5=APwHyoTPT@d7O^ZE`eiVcO}*iV2Un{SLd)-NM&qhHnTo z(yrW)01R0w^DKQ*yz0n{zT#z5j9ASvxijSUKJ+q^xkAb^z4o{`_>yOEooo;u|RU z$Dea^bAd0;dMr>_^nf{cG--&0Hi21%@b*XyTIjXva&(a`<26g#*!EPLzXBg;8*ax0 z_HNJc*DR;2g9ZaIiUyBGO|jnL3K^bH7#;)zIjJL!PnHr=UoYzSw40UHchJt)+x0Ja zib08v-JErCoTxtp*bAzmCO3IWJ}X5ZQ@fYs>Cs*T2>KeqrX$@fB*z$N{#2(iDl^xo z>&Fq!4vcxr9U|W6D>gJD&J|qn$eM{v7*4Kj%}-VuXKqP+s&Jb=@!g&@+-U=K{{Qc& zgWM&SM%n^CvvMxOBu-@gI*tmdyXdW7uF_vn;l%rIZw0+paTSst?>*-fTNbonX^J+J zoXL%E;r(g71Ew)eyFD|y9AE4QhT5ep|HpHYOie~NEDi1u~dr8x% znvdx3XU6rR#&wSUABonBko{GxGPj~hhfhq4vXz+xF6Y+27ad8_@*##L7Klmw3?%Za zltMi@-(M{r$1${mZr7Hs-fE!c`jDCN0i2pjN2AI|-!HejUGNZkda3EJua)&LRd99v z5q(kkr+lgwMEgCNbysc(O>OrYfZz2PA=TOZfB}`Rfr0c^$6E>f>yk;r$z6kll@G5j zmIuMTl}a_XkF{4C`3s6<2_S25T*TL^Eq{7hk`f4~3h}oABL?(ij0y*`Bm5T_G^Vgh zJv?)6Lt6iIpk(Z@+O<`mMQxm{EM9o(wq3XR>%Pscdz+>!+C(q$zA$)+ft-;W07dy@ z;#}t6MH>u8(EVN!Cw6`R55LVYY2H={$fB)u7Sq`86}lYh%B>WN7T*{u_FVQ1iDg+$ zh!`ul)b#LryY?-OF5UL?HZL~o7FrwoB7Ms!`6V0JL8a%5!d*Mj;BS=w@%{Z>5#db* zkSk|-FoYKUFrX`Z!A82WC6IchcY9inF|$$>Eia);PQEf+_RM!#@<;L<2^DdqgT1|i zb`0Ae&mS|Whd0p0bLO@jiI3h{=JWLf2LpR9n5w4JaXRn6BaNnYK+Da%jYTq&wQ2qP zo%aHRZImQvWT0?TQK-YVG9kDaPIaev@>fY;`hL0XJN<9h!;wIHO# z`AKoIdDe%Ht(~P$1^8k5O_IapzMTa|D!rr+1pEyAROhGwz-A{$w2oh<6oj#lGxvN zW83w)xqP?W{IeEove>txIr!Tuii*6xh;qM^?J!E}s_%_yhX4)T??U6iYF`kdjqB2T z!8i3S1Hrfen-tMU6m-`!ERMgJfnSRUt-*r_V$Of7 z8I!MQ3n%^vx0GDu_*b2dVsSOiwTs5gK)+Bhq1qc1v`1lJ@|0x9(SSUBX2zAn>T|%0 z$K`0E-|Ki}ixz%IpPJ5&8Uw*^%-3SLR81mQ54B-BWMzL4r4YI&@0d!-&C1nR_UgAB zqn^%Ie%LAk0Wjk?x4)p_xs9yP5_gWMnlJ=c5jvOkKaef#W!|e+tL!XJzUGbgARv7G&xILn5{J&(bkvZj1tLPeqnC zmD?5VXpCl$D#|x=-<+Z!_s=Y^>G zEBkL(*VoZCZ*lIvUM=~KJIS}AuAlJEHs_M#m?oV9;(7hnINLdzh3bEdEbf7~oW3t- zAkvk&=ynJjBAsdd{sQ5A<#F7lwkG4vK;;|X<}%Jr+i1dQH|l+qFSTwK-{@hJswA8t z5m(7t6b%cor^Z2$YvVF@R_*9K>HXic-F{ULIfMILqIVyaEsxwJ!718*>4UY&#DGiE zEK7fYtUhx)GR8g$W{3`Tvdm|j0OB5`O(mvPR$U7aDGlgrqg9tc48e;}f*Mb>$(_cm z{r*byQjxd9QbYLTFc~o?O3H}OMg>Z-SsH0VCGXz@8?$djCkAG{Ktcf)_YD8)V3nANx%1L9c~PmYDLBAv^0}D>3Zbh+J2@~FPOmk zm0v|qBd;fTs=rwVXJEW6Q(1S=>jc(S3;LJRoofO5*KAzhme?`r`K2LCuHHf`Wcw>k z*a5Vsf^!LseTKMlM)5U|f5)&1nR@Ht`DLZ4rjFNn2QQ>bJN`XLw7cP@qS>!&TE)mR z3c_AM*tE$%izY8NO7CkWLDC6bdyWxa7IRej6?+Gwn1=Jiflq)w!|=qWkztz(wurc| zpb7b8)^G$=JFSSbiRnkp#9x79<#yak+?kbndaqbQ%BE17Vww3pvNex&DppiPviFr0KWY3zulWI7 z7B_vSo&s6>J@>w%m8rHiQ?j6gxTp)-bJN zx;Ek#VwYBya{vB)ckFM2eww`OSL%3%%L02rehsvLFbK{xGX0XXJ{!@>4Uj*m(w{<2 zZ6JrGZ%mTQF(m$OkD=S1nEBqRyBsws&A+(H@DPIRVa2Y<%pSR{y89v zii%cD@zqnIg#C7}_B^sERGpCe+Vg(*?opRJi6?hi#Jq^MMCdVQw&9*e~ zxnLc~Y$}!N?3_5Jj6nuOVn$WaOf1J6G}TTGi1PjfU=8#!*9jT#DXLCZuEGGqLdtKH z#N8$4I)#vX5682x0*~>vRbTm*1{GkpxWtOr=oYKrex#m-N0gZ~iiqY;N`4g>M1iL1 zO>nUGTp=cVtj43D1OsqF{udTh7^>@J?8!htokZ^k4po*+lRE|B6}mqXD14!#G)og- zpZbc^xGs+7MPi3#EM6PC#P%kOe2RHSNglu7!M;LSA++9dj7hj-dt6yha{OdX{%MQk zfAR`kRGtjUkF-k)xQ^n<>P%se2`pl+1!vGmP3~WLWTKUoT#$4PYD$D6& zOAedFuhI$Q7bkrEzj|J!cy&6rihpz32<9S_v0ngpOye{ABTqwWGUi|xhNqv5JFO9?jh+>5_w%4-9 zPj_Mp7(<>|)EzW85A?rt+JVHGHjsQ+wZczB+JKR_<6Neo%K8kWOaJN?nw0dksS2_e3g@cWpgc8{ULov8!CdZ|9$(L{b+NKHBxbsNAXAR-tZJ!|g!6 z`O4c}(kHG%Yo=8iHLGX%YwP26k&B1K+^w~6lS8_7V{UHi$>BC3;frt6d~H6!ny5G4 zP9KYojF{z!9i^4gk?5~`^1C{mNh%)1fQQ>C<)=;fZX+0o%ed1hU#v{^_Ku1 zjK5PMQ%Qh(ExpTHe%kfU+jcTg=4<`TIu%*n!VZSmnXB{?2O#I2LN#;jLj&*x8pQEB zFB^*vSCq65p6o-x`yo2db=F*^(-wpNBz>1&HeV{Eq{QOKnp32}8e^cA@4#tUIA>Lo z{HOK*?0;d{qNoJ<--@;4Aoe5?&4Bhf7W#)LnFq#_y_p4^RmqcBzb1zS|6(}n4(e}Q z3SH0dr}lA2alOd;Xt15}b30%nhMC@t`)=y=E$j_+>>=s@+5}o6DOT)uX6wQIK!sha zkah7&ZdHNJ(yI*fdfRab1RhV*ecytkg!8w}l%8_WtwxYJ7a`%h%ff0&_2&2Q3&0&b z#G>ApUbDv#7M%-FtY(2c<4Ae^c-{U-lN$HqJ-Q3!neSqzfr&{B$s;Qo&{Ta}=r&f6 zmvj0UbXaNuyHk(AQh~z2OcZ2!o?;EY+t#uqYw!q8o-Bx{+utR5XzJt)Yz910$J^QG z0ifht1OElknL=9HrcXW8c#yeS_fg4CnYKF(6j2~97#DCa26`L!1wP+64bp<4PVHCVr5G-@Z^U(b+(8`7+ zrW+BPgT^j&QUN}LD~kX`Q4*_##ld9f^7~yUOy3O~-w5IED=1oqpt~}c$+JqQ-Z@l{ zTgE-*@9l93wtwDVW1Pc$szP5EPg1-Ey;^}z$Tn!Rf&gUO`!^v^g4c^9wdorlNpg0w zDBT2g2XYEN2b~@h-IS7`j~yBq2!zyc0PruY68!epZX>Vtwqz};B)nG5yF^4EFk+P~ z?EKdUh9ao6ng#uE7i9@pU1n5BV9EEEe%uiU)5AwlaCF2m!blFy4z#NyMmCwk@V@{> z($Tx#ymPnKYh8aEvnEK6aJrt4is+5TcMqQoqF&iDl$?;g8% zCoe*D9f%l8?}KOOk8%?@2#*dnzI}6LwPYjZ&sx-yX~`r>C>A8VY>Do#sIK;b4^$+E zW~kE1n4{y>%l6qm+lqJZ9{sLGgMXm;?9PWv) zbD3@hedsq%OiCK9Z1C=#fMN&(G-I-vU-7nK5=)mr8;UYf34O8zfh~egqrH(tbzJnZ z7pG(TThkR<)SHTmKSJH3qoc%hmjj^nx-aC)6tFvBU;I{P?hSe{Y}B= zZXz*n^CE;}VvjBS=&gHfRdBy}MdY%7YCLMcU8Om4v^xkoqzwHh4*;q#85#%e?N@nx zJlX1{d>Rna$aPG8eP;91=ctQ!NHc}+4AysGX#n(PH5n5h40^Y=c2m_a$^ENc+obG% z;)u_Lg0J90#Q)i*_m0LkK>m9Ya-vHzuUoe-T@3BLH(c$uz`d|o`- zovg%En+I%u2gV3^%zhwoK{H(mSM6k#%OpfZ-ZwE3m?^rJz&+ zw;yzC=LRHW5usDYPl9&u>pm}A_o+}n=^LSa>gxYL0MM1=>AfHa#^KM zZ)X3dVAuNY(`nt)d)m@kiM7R|he!W|MiTw1O843$;%nJw2fQ`e|3mmDu^#m?@W>(L zW@@F{XnS8I%;_XBPWR-VOd4T(&x4I=n=D`J*5}vCnMRUeefyPC z!K2N&77EUk$Uu17%DDuuXTZS%rUg@LW@Mx?|LzpNsgvE^p={XRgKr&Zi`;U0eL#|( z>o?m#dBYL?m^zDD1!l&8>F^Pq_0$JC#>Y@w$mE~_60>a6$;-=wI+(#>iNZb!GPwE{ zBqFvgN4p8kWT6nf(QDQKTTS&rfyb4$BMGx1v(=U1c-qa>k+$GV+yYe0`zTu_kR>8UT5#V}iT@q9&Lmb2vYQBd6seoos96lBp&aI5-A>(pR<<(-NfJl=|>o6#KN!9z;{uK6ei%f3) zEnaLiJpIm=9mJ#}Fk8SGoupruk@LCkt(pJf z)m{mYu8&ok)Y9~F5BuVS>Q7%0Y}S=bR;z4+g8m5XpDq~smI$IyhCdERI_~>$4$dIT zEh}(vgmiz_^-F3#*^6);I~qCa%Y|fY6ThHNCz}P>t)}scI1iUPEb@$1A`qE%o>1|B z9!ei}mi0W_n)+sOn7;h;LQ}4%x7(-0X&&f>#mF45eA0U^;dwZ3pq}cESsrkh+4K=} z+N6zPdqh18>aV)VQ)j@6FC3adzX&tYKLMCXvU zeEbG3u-*Z|2KVg>;av`EJ$PHgV^$(=CaB;Cn z5VUN;UnJI#9ZQ%eO>xPRHiKzzOJHg8pa-4?K46+?KTI{4Qf%ICe%9=THTL`$Z~Rfy zQqmFicEO@(#R=goS6svtGlsVP4lHp=?hV)#05~w{A5d79Xk$Pd3B+g}jEYlbS?%2dz zhYe!QL={wn28_J5l|)#vTgOk361fo zzZu=-=8H4huC)FMJyC&&F%2QZUvNh<_RioLT~cT*c|rbFM1$BUL?b5*k!#V5qadIA zuSJLvSN7?{D>e0}4#$kQNwF9d79DbOl-!B@D`&Ajck{O)bATu$m6O(^7dO5U7l}raO#@H#@V^ZtikZz0 zP2lPV7|B1_dlZH^a+r!vJvnzhGa^Vy8VqBLjItZsgBOR5Bv2y3+E4V07nt2p^3F=r zWy-M00G$SxNvRk&Sy`;vf?!?}nB;U+`eIBr6IkDj+JkND0O&Z$&eGWKEmgzSU|a zPy^!>;7ybi6tD}&eB)&~Q{2x1AAu~EH*$@?DkUkoVG5mS3T|>516~Lg&5kB|1)DrH zG`gW7#X@|1@P&hm%ab;ez@+p+A&FetkB1|b4FJY``}Xbk@83QWJ~9)YadIs3_wqC9U@)q!x>2F*wa35RnK zUF*+#Z{DhOeF2r9*iL_Ja)1~n3U3H~Lj{>X%i|*MI+wDmrghog!2xC+LsL__CJNz9SwXzLP7$31hpj$$O;+fARqQmfd+1c4ySTJIkL!0}5luUFk|0ekcp2gbQZgU;iw8_(j9R~lCpbYmtxic0_Y4fGKu7#o0CcdkAT2z9=2ywK z$RyeT-Jf6iQcB(Wk1ZP#ITKl?M&5haLa|N8>&VAR->-Jv#JoLu#6hM>{@u4}SzlNhReXfd1()8YjMy-r) z$jeumq+Q!wv{x3F?}?MRG@g9otZz0R`8n~VGp=Z*KAnDWzTJEkE9_Uu^;J#BLhQpli2pcbToG&CMf6s`YHdHLR{FGl%c#VF zvMpi}MRHirY{52J7mzX8e~Y~@?{c3n>o70g0bAT8`R1W?m}{e!-~-!C8SsTRoh;+J z7A=$Xv=^sK%=!35v=!M{8#Q5c#0>w`qlamXK)qQ7oLynz1 zVZ~B8_#sEQ!TtN+O!kBhWT{VcWrgx(=P$_(e=?Vrx++8WSjuDRPQbxAEF4iN+7A}P z$P)Iwi^imv)4(c-q~Xf=~9BJeO_uPm15YOS$BeQC==!6j8aV z#1y|d+YZvS+7ixFhtuqHcOiRST|>iv+L#1AN=4-9vwbuPbE~&$vOXupV+9V)%YeT*@xeeq%jMjIk?Fw~ zd&PAmDYkllj<62_o$x)9d)nGqAN1{wl9*LIUA9HyD_?o(5mBC=mzf$-nk4@!EG#W8 z{guyy_hgf8-VZy&B*Q+^Uven+wE5s0eY@<<1Kb1JIg~a67pqtBcSKRqJx&t-ODQ4s zc$cwwAKtxU^;hO@`gzhrkL5Lv=0b%)-M^7>&zjHC-py{bxelVFOKlwPKDqU{NTJ$Dv1WpDDg;qjb>n)(+PN_;&3F;C0UE||MFvHJ(CFG6GjbS$y zjoFi?55ujerX|ahLJZ5P6}h%@hF^4Qle-IZNT%eKXp(l7cJ*aSC-6{iuFuuz|Bs}z z42Y`xx;P~a(xpRpH_{z~fOK~aAq^tkEjVW_{uQn z-gD00d#&G6if)WB(K(#8k-+;FBRsJy{#}`NlyKJC!iqU#&(Z)}sf&6R_wHSY&UXIo zXGTcfuUN#w%yuRMQ&!r3uctIf>@eV+ivAi8k7qCYtg)X8cNzm`^U3r`5z1r9XwX4> zYwyBfAl3~esPc3Uv8A4w_`d0bV&E$Q+1WenOGm$;r5jSmWc)4PL^hvqHSfjoI%=Ld z+rR<_1I#aqSm@9Ksd)~l_C%jCRP1hQLA3G&A0B!?&gTGbRl5SlS+13dF=pg!Q-VQ0hT<(9f? zo^ivP^vvmcn>C2ZVA^8e$Mz|{&^?%AcGkLR>a(+$x$6p((nm|nLYA=QWfNhFX-`z7 zCHLIyY%~=jnOZQmz+)<1-)|yi%)YvFq$#+To($qh?+T7{ffb{ZH z9wxk66t=ooeBQi@-<}R3+BoM`k`2RWLeo$dTVbB{2E2&x{k}$ey+{w2-Kh9xPlsbT z_wFZ|-!*XHLiyBVkQ4d*k+HA@tZODI>o@RkHFZg38E+MP@HxG9t}j9@Ek6@8?_LPG=8*} zKZGdLNw%xs-lz*T+iAM6m>5$r`7nC8Wyx_|VGtp_2z){`HT=ZHjcUKXE`>03|67 zG@B%=DJL_<(oK0n>vR&&JwysDCYtLo3|=G!hxfDh#30jJ7;+I=T2S>A$GsgKutLFA zsr+MblYL}$qYlNKa$9x#UhI1c(A&6pa?qE4Up&9Nyg>A-&Cy_O`(`>LvPyqrtLzmr zl{~ffvi^1oBXWdOo8T2M39{_`IJuCz{RvRRA*L-$9>uxdHNDcdqH#E|xV+f5<%F)F z5e0oQ&qD^#);o56plmM$ihf{1kR6NvGWvFiy5Mep#Wk&oBgbt9O;)iIaErINcsvKf zrG$>!fT;(2>&teRx!Kv(pp}!FqgC;{Jp+zdUQJyCeY@f3gibh8OC{UPP`9qP$nHEb zN=yj0ZZ-{e5z8hEO)T9e%Qw*9zbs$BPJ9jP)+BhB+8g!~rHJLw++9aHY#J?%sz;h- zIs!cHBI4XK_XIOLTFsN&9K>98bL|T%ckXN@c(4pkkJ0&3og2 zGyM1OLf3s$Lf`}B+D~BAIyjgG4KUA30O_mV(b0CD7pkt-*hklIHf$-tkaW8|k}E~l zruesC{NtOcIp>YA3Ri{QDPZ`tZf$}AHve43Q) zU5Lx+D^s;{38b)*QAhZY4%jaIpZvyQ6O;S=LKvldY}#tL$gB=>QyP0ru>~y6bF8Qi4}mg)*k8}D2p2S=5lrws~q4el;0#>P>IC*?))`%^wS-LXeQ7u$+@^UncU^8l4p+eB*B5_^p-3iruyn zPH{Kjv?BmxEF*ExrP=WfO8dyAJ=H88MFW~nir*hslV{lGKR1x{Nl7<{cU(5E@g6%> zHnp7)2^vVQlbpE5m*htBR|&k+hNu{1jRJ$m#@pPQ$Bklk!=iz!aD2Uqus6~ysEQ3} z$hElR&Q(4fAQZLI)aejgNsPQ0hSDH=?`kYi#9Eo~hbXA$gXS4Wla#9LV<7D?pFDt$ z+|z;Qp+sTnv{{7dCMl#Z*vmSq1lxDe}hC)xB#XJ}!b3q_4J zOC=0K>E^DWY^wZ~e6K9^!nrxHM<yaZsNt;a?J zyK(_kqVnF{-xm-Qk_h%_s{28;cWx`}6P{6TY*mj(Etrp`ad;fNEmK|N(A#RnP07+B ztt)04QQ>)#cYpR0WneWJIRK3t8))=c*_Lpw+Gv#TEmx0ymGcN*b z*{?HzB|!#%SwBRJ0gX6@)S+p+BzG$xO+c%)It7TZiK$40cE%i|0r2H^7pJ+{Bbw@6Hv7mHYLCTROd!IJA>%eNy5R(gL zKVvy;AA~HIDrj^9!U}18GZxDm6O&(C`P)8_v3ZO z^SwSjJw1R?`_Kes5ArkXOj35Dn?y=`>5~hfuR1@rNZf1$KkIkjr*Q*QD|S@Wwl~Zx z%`f?da%I9*wELJie~+~oL^x-$Tn23P&nm{W1|ytX%sh8>gC77`n*VWKU0r-}Pr8I( z`74)K#t3dH2LV?AtZxFuj1d5G(Z7YRGN4j>GF>gK`0?~e^d zEbSLSMjgP;j;8YabfB$XO~v|tR>^?RUC845B$j<;3S0BoL+dzhKT;Dr_1PhC-2mKLVKmZ8VS(scWtxY&m1`|e1%Cj? z4nx=3e>t4N!M?xZgx1f`=@TR_24V-6o?&!N8QX0GSfR~3f5!zY=Z~(}yq2yd04OMY zy`$ybI|MrVWfGIoEEyWp=-XsB*c%O|VLLjet>)Gb4u?gM7rOJBzp!4NSVxd-?8~w& zaL*22;FQk7+x+%K_0Tr94d6lVXllTwF*W0UHoD^Z%dOeTDb-VuJzg*di zDDvRolcqeiasY)4zwW#O;BDfE!<@~Feeyga%;~s4*Ws3KKb9Rzb7z33Xif^T1VlcJ zlK|7MD{7ba7<{^j+j8gTJ78cM3^+AlIYO&WOlh&T4RIZ;ffUR&b_Pjg?tbMdo!u;f zeeAtHy}=R!t$6v=O?DJVe;; z3sw~?@)kw5zlizXMHap9jW0+}aW|+v6Ch&Y`u0~|JhEKJ>O0RGvUj6kt+rz%NMVGf zg12-L!uKmrftkX@?c&$#;3MOWuIHzro{)W7)~eb?guM-kJf;w~U)Y*fXwrF)xxU2x z+im#5hVSyhe%JMQt;97^d6(D5S_J;EmoAe6T-e^Px_yW1dF082G<(IME$lAbtlDd( z;0&>>-+j~FZIpRN{g>3U6pU&!JGd=)X;e=L!}^@4V2u1NZF@W6=>#r?*w}zy*Zr5u z{pQ(s%QnY(-u&o`DCL}eq@|*`%K4kQdp|vSDjk_4mfsQh`5Wp%+Nfy&;lQ#({{fH;FS%y-4z)-10rANJ5R$uV zL4)m1Y#%0b6Thi9cS|C2vQkzn^GyXQT6m&J!mU%h=KquRB>f6 z(@07lJM_aoeItWt75VTw&2~605)0VMCa}qI$rRL~E+TBF>?zP&osa2ZUBy8pe+(lq zqD=Y3$Laby1OoM?@^UzZJ0 zZ;5jZ88X-@ag9y@{Ra*n&NO)|U9;B38jn{v4ul~>WoA}D%14kX;+JV7rAf5q!gUMF zhfENiw-~0r?88k<_nMwsNFMvhHJcl?!Dm!;%f`+`H+OSU_dQXilJ_LSdO?MY4%#Mz zHe_Xj!)7!})7SUw>#>Ql;%wLwb@>Ce*Dt2g_P=;ZqEN1^d6l9(Kx?8M`B#dP@b5yM7vn3gK$^tLX)EA8VP0BB zR<*0@2HOZS+ESSe)*Em+8^@@CF5so3z*u%&YCYft;Fk?m*0kTXAGti8cT@UK|7M2e z>6+HU)N|r!KzecUc{c^+f$)wDM^xj^W1Ig{f4eY#x;#C+Fa|92Jv|}wqN>{E+RdT! zyM%8jmXY=!4$2G6+CLp^WJtE1A4URD={uW@$Fn*8r!yty;J@_L0cYo{ZT0kD%A|dg zEtBPwu8DsB&_X^3;H?4cE!?fQTjus*%>Ypi7(+f^Bf5hC`WZO|Mb(_coP$98_G^Te zko`Zx$i{i#=)WB`KQ;aMoO~1vM*x>Nb-RGmF)TpcGiv_y&+;9`l+j}g5B7rhN*f@K z23SJe9PM@@rEcfF(WmVzHLk$13m6EzC-%b050jP0kicTO(CJ8Q$8;vcbcx5=lrPWq zw}Vn9{@c_~Zc8@ZU56u;YN8kY%=tcaRRsmON)!mo+s#0D0sO?(Jm5EpY+p^saUmj=EZv_okXw5v1)Ec%iWPQ_^KzYsN?KuO`yAL zf4!UHDSkUacnhaM>~ZJBYn9i&dZhF8`WQ2CFQedDsEBf9X7;EgQ+-RSm+BV-o5&03!8J_YZ(h=c|I z`O!j^$enl6tWAerI+7im6~jTqHE}xI6Zw}(!ATLzm;m!YT%bHtJj*EZ%YyM?`zMz& zgkA5A>FAUP!60TEwk*gN+SbK!KS;$!_&Ns6*Q?F!GEwt6NhXKrH+jch zBHk4(WdnP0nK=aV#vCiZ=Uc0^azj%2unm)0n3ZQ%m@TZPX{%*Gyt>mBF0ySGCS%+Y ziD&-u&+8mBMC0Jx+%IeqF7t;H=Y4fu8%KP;E`?u-JLIc`X!WfWEVDbS>?RwW;^PUw zM{4x&+d}8JggYeHT&2>>kb1Sh(=e>nx!RvbsvAkAo68aPT7W(lX_kmY;r;0o1&vW3Pj^H+U z7eE3{qy@1s4}3b1g1w02fd^Fh5Zk<`3i_J~r-1LIuw&T4gYPOg#BRTa+m3&ak7KNf zQBAjTmu)0o^m(>|dHvIs>Y3zyNp1CK z$$x)}ixtARQZZy+33%w|-YIpQ)Zts^a;xNy@)tKb_*Vh7z2V}s)PC;O!J7#+LgCv< zq2N>K8?RM&^-`UtK2gfavMT+>)WY1))i~mZr;;Q*xGN|8{vCNY;t$^-)BKP33*BDx zhIWtLZ@TaHSEyTe{0k_lsM2L`EkSg@*HDWwAqpjTLHihdu`G~HU&DsqBJjTP*K2|B zcj^y2K-h>~V(=5ZD{w$8VSJ4oVM-=Lhn$GPPraOlVj`HuaUmP>5layj6@~bZZw`EX z<)HxA_b3V2pRfSH@E7i&rxYo~uO>K`*{7!$uaBFC*i(58h8E%SVzyKm#l7Gx8-5TH zeV$l0)Xyw^XWcco=QnfSL-?~REz$!bhB;{OO#u}}CgqTOsg@EUtEK9$#_I_Y?jetu z*CE0vA&0A+hjJdx%g;SR&qMZhCc$e>cEHM$*LIx#g$M2!@8{oj!715OUrqarfvb_z z7$1(pctk-cdD!7K32~m{-SbrDrqy~fh7KZb)dEaWJI2J;S#;~OW9F|JkJH4tf_9Z< zO?fIm|5d#ak^lPB2WI3z2#YU)zpNHN^YJcxtr;Qa4{O73WHsad+}~NNPV1MGUE?VU zi`jqBCHQJ`6r+N_zwn^seka&dZ5kNzK8fVM~Icu(Gd8)8BWpVAP-N-Fp zfQ$Hc6(?s|qeE-tD)Y;>huW4f7D_%tAKv%vZAuV=v^EBtr>usquw}A{zc)XQwA?Pi zNif;{)b{v4fzd>5U!gL_*@qp=mV%epDqD^%7LnD0Ho8*Suk>qpn@cPUfabiBNJQp zGoUix1sK&2;ncS@NCA5@1#J)IQts?AiiA{AnOX!xxgKvnCldyk%M$?arr7mT1JD=X z=I#cJFEBO2+W}@tKriw71W;&!Ascq;Qfv7|mm7rS)DigqYdkhdo;1$Xv~IuMP(!iHM6K>fn`? zbp_Djr;*DHLT)w?Er4X71;E(eE1pB$kIw%nqp9nFyl}1!9@P%74BpMzl+sVXcG_@k z;i{*I=j-REqjD)@MTwhm>gV%HbBPrNB|4zxQ*G^#Q=Z16NrPRNss&PIjW7DP1cd_< z&{6K^U%#ml8pH=FYN998?}LZISA9IAd^#`pp6KyqMJZRVldk@|M8p%pt|+#3l}~o` zav4x$dwhMR-aBc1(=jL~(pJtHPfwl3Yc;jJ^t}MKa3tC$ho(6fELwcoqe;qC=T@JHJOK(vY9Qktv8-)nsn&!gbLKe^g!B55;Ke2rNL z`~|GPR!>ejBWrN6YfYvk4^4+89~NUpjGda#?^pVN5JspdYk(SPX2H|5ED-{ODZ9&~ zQzA`0T^VHGaBzc9$Tvu21D=!53jWo>CW4pL{7*Uqr$6w-BWYF>?)h3I&Mz}KlYK+XJ12jtpk*Fuu_uGEqzNBcpg^WCI;Ns3x}wG73@xj-I@g@T0o z%ha+Ftf+0hhf{d|?Ie9=^qy1D_;ql$fU8S+F?Q&^1wOp&ru{`!Nn?icPm3Q`FfEDq z89GLeopVwrw>cZ@Zo@JNPP2!}!e~nWAxdwJE?HLjHrELLue^QIF?u7cAh3K%>ev}@ zY@P)?ie+e`WISKCAb$E$SF}!>Jxl944jYAGEQ%Xv58e~|?YYM&JH6FF$F`DHgw|2( z@eaNW@jJ#c7jK!G2852inm_}POu)X;!2Sn1ohL%Y4r%-YQNk24=(`FasnR`xL#*`y{eW-N#`DmQ4nd%K)+HK zqEenRmbcRFSMtVg!Xo+GFFZ-)#^xC=f8K6&eDiRAG9~*~%IJw&*1%Xh$l$mZEt-#6 zb{VKpTm5lhF>^hz#S|zTih7?8>uuOY%I&k`6Ut?f+;eN}ioXzYo0e3>LbNN+>*{9b z?s0H%9&hWZ-=G7w=kdAjP9vt6ecz%bH1^)C?_TDZmSx&05CU3n1g_G z%ff{wn`-cRKlQph!n~iy#OJ!Nw>PNzdeML*0u!YV91F1-z3>siHs@b_`3`Kj#();M zFSa51R~4h$DDM&RPLj}2CZS{o86pltE&W<<^ON2k0RIDoV(pjFlsAJ3!4Ht!D}Gcl z9cYm%X*k~Qa@PWlGvgEnf@T|XgS49mj0mWf z1@^T#5CM7lrQmsMPiox>YUndA(BI!5r@-bRJ)3np%6>tJTDn+&t~UW}!T|a7Zc^i9 z>Rm>@@80Vq-Q`Hk7a;*i9b^vdnuj~e)Bv^>C{4PNH`$*b(@=56dk$@iLqi=Z85Q2G ztZp;*b`GG+R+fe%Mbfh0E+{3-YoZ~&HgS6*#|>DU8GQ#FkHjmes2ORmo?i=-{&T)r zH*Q>!dOYBEmMD;-MSc-cHKom!s->K|$S>^=@*TIGUeG@~E02pZ1X{nr{+lv_-ZgUp zwV#fNnTO8Ld3d~lxS{ws&%vRUYf*dtE!I>6R?VW<_SuACEWh&i1;~KX+rC8`Mh}P% zxur+$uJ--&7n}t1vV%o%f!%dVV={6_6eW)uoj#oX`sve8%qY&M(eUp3ah#ApV*BO_ zzRq)pH>)BICxc_8*UOwBHnSU(_bCd?%}!himj)g@xFIHjL)%o2-a26HZrhD;$#Yj` z<&(gR(fzWpDP_|S3=PB>k~A?%{6_C-GdIre9@#U5VE30oC1J4qpk=|8(<*F?5dqo7 z;>G@%pwM03;&^^RC=&eJ$y~{0TF~Cb zVl4PeO2f6=y%w$=$$(Rj=f_KDFEuoay+}(7@(Jl|Hu53n+*)Hp*ZZ^IWuHLBR!O+y zH7$?|=<~I!83lSTa8DTMixjH2=%J}fLm!NoyYB(d#1@nhSp7_~?Y`aZD^G9YuL-&i z=GuIc&gG-Q*FnWmApNRRo|S!r#gTyHhhO|z({!}F9I2GBXwWAbLkC$lLi3dU?Yi4! z&W8JJ(lna+!}K%DHWsRQl{FtaLq$U9p^kOlH&o%0s%Af5M6L%H&t~w!sQHHNN*?ys z7@Vg#GgLNfP=msv8%(-j`SJct$=7M@U6*m8U7D69%CdBz+s(+-ha+ciNw~4pFau)Z zMYqAttafvM#am*K%pl7o8IK-eiEPwurvB6Z(T1b^IX6b<$s%4Zg{M}6zjE)bk1QrR zS(TQ8(Fta?amh&`6038IxD zJFj)s?8!5hy{h4!@sV>=BM1Y;=(&D}6`)N-h6#!mx)&+(=*%Y$H5vxMAnx8-d-ibE zP5$nD36?MEdgZGu6HHA`s#G+LhOGCpFEQ2Vf!h$8FbbG|m5L$(TR(Lge!0CLK)q&G zx&+{yHcn1sSK=AJlxB3SvrMf6pmh^`th<0w!@MiODuzCoWh&)LL(gG@qZ_-!HD(epRn7-zh||NovG6-ksM6|)Sb$^&yCpLv;)J# z!@w@y;1C(~uuWY#Ckz<_Pqwcl)GX;J(P5yEXniFlN1GQLUQ;p7&&)AUN`}VS$%L?z zxB?)k=h&| z7wusblmPuP5hOvA>72+JkJ|6=t8~4eE0y{_p3|~+vtnadmf8pQn4nbM7tyTH5nOS4 zLvxHF)w=V<_+B&*SN5A8J5dm$Ac(?#t+_b-=}B@zGd*VJ>53nJ3;0LHO8H0Y{i^p? z|9HN(OU;}J@y}SAV+3GU_(kriY;m>S7q&?My5PAQ)mjmY!-_e)vr-y{o!|3pn6M2Y zDzlYDxYl_6FXVz2-@f-v9U2I=ZlGk0$rWJV$sI^?@kX+YRl087e(p3Xl|tOrYS9)8 zU$BvsHu1jX0Yg|y?deMRfj$|QjN7D)D|x18-!Ggyf&HE+%uIIYT$AWQPIwVST07F$;$mT7Pq|U+B zHnx-Xt1Xs!Hu^=Lmr%NDKGw@%It`ew3Ib*AJ%{2;iEs_*Fa{lv0fD8fXObeBvjMa{)VQ64I!#}US?Xia(;s0Uu9qM$!0Gu+ZesV;rR%EJv}lcl4b((Ue?Li%~uif2LfmX6%{*BE`!aOX=i$#ya~Y~ewc zUo0mz*b`par{r2xI{=1W$NErUDOX`b3~<_&)$+D=(o z?#=)H04?8%`?tA3RMEo{3mx!%{*X3uR)n6I*kRt8CEBUkr{}-*X4hBBz+a-t-2c+tGvV}6KQS?l5@2|>l z2K$zk?U}$pU=O81GfelT>IaMFvBd9^mKV5>jtgCgdB*rS(Pj^IVRGrg;`Y%!gpBjD zrG(44x8K$@4PS=K87PP2qlG?aFZ?Wk0Hi(lPDLMhM%%N{4D{dSP_= z0g~B%;3}X(0O+=SL{BINRie#Tfw8P`GMsac#(B+D;X{oq%OG9?d4gVP&`n^1)@Qm! zAP35O-F{yB_WPdk!0<37C zjJ7vM>d;br(A7ma6RsSI=;AJQ5iG?os#SANB$(Er`Z!GMUUX_4U~X%8V%hY4diLmu zNY*+J*J6yrrS%CN$>+ooPLJkKRjU&w9N)k}B>N8zSBGDx`PkFON1k0JO;u@KL`Mf! z|Cy^@$G~qx3M&TzbS7{ir$eLkTC`LSO;vV07q^m^zcN)*=0QL7 zbZxJ#9Vo+8cF+FKkVN!o1m$g8iqd%6P>uuw<%0qL2|fxCi9lOUYG6g#1H!?fB24yi-!GEi zqh-aVoYETzgTt09^|bB1D@psDru+i()8gsD*SPcHfVn{YxH?+EdEwh@xP$Uf9hy=r zs7=-wpJ8>>e!xlYRaX#~hTrXIw^{RJ_6uYZBxtgEDPC1@xN_puc@}5Td*0R@mh-Bptwk zJS`kFCv}E{^gdmjSPzkGn**_q;3*5k{9{J+d9o-D(~Ngo0|}^q?+x6@!cZXATmGLh zk&qptvSK1ESgSt?(}H%7Y%in>t{%0NCq!gvB0imPug*PPGx z7--Tl1e73<0bHFvf^^&Ko;D{Y2G2(q}c9%6(rJ=TpiCI*~qe)WJoV0z3|pTcr~$` zw=COhe-3zr^5&LlvDz%m1(5#&zfmYLks^$aq7uO7zsTm4@cJ#%vZIi6)0Qzv#4zyXGNE<)LzstY+=?g#>|fW*z&LVFI`zakim@hGM}}1cn|>fLt1$=k(EIqZV0C ziaw@p6|v|I$7nhpKdV;oXj?fXTH#R1qO+n7_?0aq$s{6w_)ZjIm2%v=;u7q89{P?d z@IK8y0_o_`-#n}|lLhINzItf(#`w7Lmygc#QFY64=LW6R{Zs<(@)-$uJGQ)@;pQQo zpl<%E9Fh7yHIvx-@}!*yGxm{^mdA%diD=C3_uKWmNyvXu;_nJn4t}oT`CJVdzid26 zfNXva4MoySRM_ra{V3<&b!o7Hzq00RgDxl6A8fu3bT^*2%QNzUfCnN3h2Jm?=uia; zt!RRzhY7rCR0SZyRkB;(#9(}<#OtjmafI4f*(;?J$O2&Ic^2I^S4W0H;i&=mBVND` zkrZ&%l@4?bT$r+d4ADL1#?GWwid{!hHzO zlh>WkGeHGJTUwt=YKr#tH*ruef8qJ6pQWG1&%cD&DS>L~nEULa(HotEi1safS&wp`Ca%rSnG0B9f9d$bCFJ6dgRPDpCINMCux^U-ej@*+C6!vU