From b513120544cef2459c88c88657c9dddca2fe3058 Mon Sep 17 00:00:00 2001 From: yuluo-yx Date: Sat, 24 Oct 2026 17:58:15 +0800 Subject: [PATCH] refactor: remove legacy gates, deprecated APIs and duplicate utilities - Drop unused verification scripts, compatibility consumers, clear-room docs and their CI/make wiring; fix stale AgentScope references in documentation. - Remove the deprecated graph async generator package and other deprecated APIs; stream A2A responses directly with Flux and complete when the agent card URL is missing. - Make the ParallelNode default pool daemon and idle-releasing instead of registering a shutdown hook. - Keep one copy of duplicated CodeUtils/FileUtils/MessageUtils, move business outcome types to agent.outcome, and migrate JUnit 4 leftovers. - Resolve remaining FIXME/TODO items, including a configurable A2A user id metadata key and tool descriptions for ToolEmulatorInterceptor. --- .github/workflows/build-and-test.yml | 3 - .github/workflows/release.yml | 2 - AGENTS.md | 16 +- CONTRIBUTING.md | 4 +- README-zh.md | 9 +- README.md | 9 +- argi-agent/argi-agent-api/README.md | 8 - .../BusinessOutcomeAware.java | 2 +- .../BusinessOutcomeMapper.java | 2 +- .../BusinessOutcomeMapperRegistry.java | 2 +- .../agentic/ai/agent/react/ReActAgent.java | 2 +- .../BusinessOutcomeMapperRegistryTest.java | 2 +- argi-api/README.md | 16 +- .../ai/agent/orchestration/Builder.java | 2 +- .../a2a/A2aNodeActionWithConfig.java | 308 ++--------- .../orchestration/a2a/A2aRemoteAgent.java | 20 +- .../LargeResultEvictionInterceptor.java | 9 +- .../strategy/FlowGraphBuildingStrategy.java | 2 +- .../toolemulator/ToolEmulatorInterceptor.java | 27 +- .../toolretry/ToolRetryInterceptor.java | 16 +- .../orchestration/node/AgentLlmNode.java | 7 +- .../orchestration/node/AgentToolNode.java | 2 +- .../a2a/A2aNodeActionWithConfigTests.java | 216 ++++---- .../ToolEmulatorInterceptorTests.java | 14 + .../ToolRetryInterceptorUnitTest.java | 22 - .../interceptors/ToolRetryTest.java | 6 +- .../AgentToolNodeBusinessOutcomeTest.java | 4 +- .../AgentToolNodeContextPatchPolicyTest.java | 2 +- .../argi-workflow-graph-core/pom.xml | 5 - .../ai/workflow/graph/CompiledGraph.java | 15 - .../ai/workflow/graph/GraphRunnerContext.java | 37 +- .../ai/workflow/graph/HasMetadata.java | 11 - .../ai/workflow/graph/RunnableConfig.java | 8 +- .../agentic/ai/workflow/graph/StateGraph.java | 27 - .../workflow/graph/async/AsyncGenerator.java | 494 ------------------ .../graph/async/AsyncGeneratorOperators.java | 132 ----- .../graph/async/AsyncGeneratorQueue.java | 121 ----- .../workflow/graph/async/FlowGenerator.java | 69 --- .../async/internal/UnmodifiableDeque.java | 206 -------- .../internal/reactive/GeneratorPublisher.java | 81 --- .../reactive/GeneratorSubscriber.java | 124 ----- .../graph/internal/node/ParallelNode.java | 49 +- .../workflow/graph/serializer/Serializer.java | 12 +- .../ai/workflow/graph/utils/FileUtils.java | 132 ----- .../ai/workflow/graph/utils/MessageUtils.java | 2 +- .../ai/workflow/graph/utils/Messageutils.java | 168 ------ .../graph/DeepLoopStackDepthTest.java | 3 +- .../ai/workflow/graph/StateGraphTest.java | 27 +- .../ReasoningMessageSerializationTest.java | 2 +- ...Test.java => MessageUtilsNeutralTest.java} | 34 +- argi-runtime/argi-runtime-api/README.md | 9 +- argi-runtime/argi-runtime-core/README.md | 1 - argi-spring-boot-starter/README.md | 5 +- .../argi-starter-builtin-nodes/pom.xml | 6 - .../workflow/graph/node/code}/CodeUtils.java | 2 +- .../graph/{utils => node/code}/FileUtils.java | 2 +- .../code/LocalCommandlineCodeExecutor.java | 2 - .../ai/workflow/graph/utils/CodeUtils.java | 45 -- .../workflow/graph/node/AssignerNodeTest.java | 20 +- .../{utils => node/code}/FileUtilsTest.java | 2 +- .../agentic/ai/agentic/DeepResearchAgent.java | 2 +- docs/architecture/api-boundaries.md | 69 --- docs/architecture/checkpoint-compatibility.md | 104 ---- docs/architecture/clear-room-baseline.md | 94 ---- docs/architecture/persistence-ownership.md | 14 +- docs/architecture/target-architecture.md | 213 -------- tools/ci-config/labeler.yml | 47 -- .../compatibility/agent-api-consumer/pom.xml | 34 -- .../agentic/ai/contract/AgentApiConsumer.java | 201 ------- .../neutral-api-consumer/pom.xml | 61 --- .../agentic/ai/contract/AgentApiFixture.java | 163 ------ .../ai/contract/DefinitionApiFixture.java | 63 --- .../agentic/ai/contract/GraphApiFixture.java | 53 -- .../ai/contract/RunStoreApiFixture.java | 50 -- .../agentic/ai/contract/StudioApiFixture.java | 47 -- tools/compatibility/policy.yaml | 19 - .../react-agent-consumer/pom.xml | 34 -- .../ai/contract/ReActAgentConsumer.java | 116 ---- .../runtime-api-consumer/pom.xml | 34 -- .../ai/contract/RuntimeApiConsumer.java | 79 --- .../runtime-core-consumer/pom.xml | 34 -- .../ai/contract/RuntimeCoreConsumer.java | 112 ---- tools/compatibility/workflow-consumer/pom.xml | 34 -- .../agentic/ai/contract/WorkflowConsumer.java | 105 ---- tools/make/java.mk | 9 - tools/scripts/check-chinese-content.py | 313 ----------- tools/scripts/migrate-maven-coordinates.py | 59 --- tools/scripts/verify-api-contract.sh | 180 ------- tools/scripts/verify-clear-room.sh | 81 --- .../verify-extensions-compatibility.sh | 123 ----- tools/tests/release-tools-test.py | 263 ---------- 91 files changed, 370 insertions(+), 5027 deletions(-) rename argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/{interceptor => outcome}/BusinessOutcomeAware.java (95%) rename argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/{interceptor => outcome}/BusinessOutcomeMapper.java (97%) rename argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/{interceptor => outcome}/BusinessOutcomeMapperRegistry.java (98%) rename argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/{interceptor => outcome}/BusinessOutcomeMapperRegistryTest.java (99%) delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGenerator.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorOperators.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorQueue.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/FlowGenerator.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/UnmodifiableDeque.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorPublisher.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorSubscriber.java delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java rename argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java => argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/MessageUtils.java (99%) delete mode 100644 argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java rename argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/{MessageutilsNeutralTest.java => MessageUtilsNeutralTest.java} (75%) rename {argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils => argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code}/CodeUtils.java (96%) rename argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/{utils => node/code}/FileUtils.java (99%) delete mode 100644 argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java rename argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/{utils => node/code}/FileUtilsTest.java (98%) delete mode 100644 docs/architecture/api-boundaries.md delete mode 100644 docs/architecture/checkpoint-compatibility.md delete mode 100644 docs/architecture/clear-room-baseline.md delete mode 100644 docs/architecture/target-architecture.md delete mode 100644 tools/ci-config/labeler.yml delete mode 100644 tools/compatibility/agent-api-consumer/pom.xml delete mode 100644 tools/compatibility/agent-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiConsumer.java delete mode 100644 tools/compatibility/neutral-api-consumer/pom.xml delete mode 100644 tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiFixture.java delete mode 100644 tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/DefinitionApiFixture.java delete mode 100644 tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/GraphApiFixture.java delete mode 100644 tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/RunStoreApiFixture.java delete mode 100644 tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/StudioApiFixture.java delete mode 100644 tools/compatibility/policy.yaml delete mode 100644 tools/compatibility/react-agent-consumer/pom.xml delete mode 100644 tools/compatibility/react-agent-consumer/src/main/java/io/github/agentic/ai/contract/ReActAgentConsumer.java delete mode 100644 tools/compatibility/runtime-api-consumer/pom.xml delete mode 100644 tools/compatibility/runtime-api-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeApiConsumer.java delete mode 100644 tools/compatibility/runtime-core-consumer/pom.xml delete mode 100644 tools/compatibility/runtime-core-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeCoreConsumer.java delete mode 100644 tools/compatibility/workflow-consumer/pom.xml delete mode 100644 tools/compatibility/workflow-consumer/src/main/java/io/github/agentic/ai/contract/WorkflowConsumer.java delete mode 100644 tools/scripts/check-chinese-content.py delete mode 100644 tools/scripts/migrate-maven-coordinates.py delete mode 100755 tools/scripts/verify-api-contract.sh delete mode 100755 tools/scripts/verify-clear-room.sh delete mode 100755 tools/scripts/verify-extensions-compatibility.sh delete mode 100644 tools/tests/release-tools-test.py diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index bc2eb044f..67b351db9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -54,8 +54,6 @@ jobs: - uses: ./tools/github-actions/setup-deps - run: make tools - run: ./mvnw -B install - - run: tools/scripts/verify-api-contract.sh --skip-install - - run: tools/scripts/verify-clear-room.sh build: if: (github.repository == 'agentic-ai-java/argi') runs-on: ubuntu-22.04 @@ -72,7 +70,6 @@ jobs: - run: pnpm --dir argi-studio/agent-chat-ui exec tsc --noEmit - run: pnpm --dir argi-studio/agent-chat-ui run test - run: pnpm --dir argi-studio/agent-chat-ui run build:static - - run: python3 tools/tests/release-tools-test.py - run: make tools - name: Build release artifacts without signing or publishing run: ./mvnw -B -Prelease -Dgpg.skip=true -DskipTests=true package diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cd329d2e..5cd742cb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,8 +68,6 @@ jobs: ], text=True)) assert release['isDraft'] and release['targetCommitish'] == sha, 'Release draft target mismatch' PY - - name: Verify release tools - run: python3 tools/tests/release-tools-test.py - name: Check publishing credentials shell: bash env: diff --git a/AGENTS.md b/AGENTS.md index 0853d73b9..001dbb584 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ This file provides guidance for AI assistants working with the ARGI codebase. ## Project Overview -ARGI (Agent Runtime and Graph Intelligence, pronounced "AR-jee") is a production-ready framework for building agents, workflows, and multi-agent applications. It is forked from Spring AI Alibaba and focuses on stateful agent runtime capabilities: graph orchestration, persistence, context engineering, and human-in-the-loop support. Its runtime consumes framework-neutral `argi-api` contracts. Spring AI, LangChain4j, Google ADK Java, and AgentScope Java are isolated in optional adapters. +ARGI (Agent Runtime and Graph Intelligence, pronounced "AR-jee") is a production-ready framework for building agents, workflows, and multi-agent applications. It is forked from Spring AI Alibaba and focuses on stateful agent runtime capabilities: graph orchestration, persistence, context engineering, and human-in-the-loop support. Its runtime consumes framework-neutral `argi-api` contracts. Spring AI, LangChain4j, and Google ADK Java are isolated in optional adapters. > **Naming:** the product display name is *ARGI*. Published Maven coordinates use `io.github.agentic-ai-java:argi-*`, Java packages use `io.github.agentic.ai`, configuration prefixes use `argi.*`, and framework-specific public class names use the `Argi*` prefix. @@ -35,7 +35,7 @@ ARGI (Agent Runtime and Graph Intelligence, pronounced "AR-jee") is a production - Context Engineering with human-in-the-loop, context compaction, editing, model call limits - Graph-based workflow with conditional routing, nested graphs, parallel execution - A2A (Agent-to-Agent) client support (Nacos service discovery lives in the Extensions repository) -- Multi-model integration through four optional ChatClient adapters, plus MCP (Model Context Protocol) +- Multi-model integration through three optional ChatClient adapters, plus MCP (Model Context Protocol) - Embedded visual debugging studio ## Repository Structure @@ -46,8 +46,7 @@ argi/ │ ├── argi-api-base/ # Publishes argi-api; JDK-only public contracts │ ├── argi-adapter-spring-ai/ │ ├── argi-adapter-langchain4j/ -│ ├── argi-adapter-google-adk/ -│ └── argi-adapter-agentscope/ +│ └── argi-adapter-google-adk/ ├── argi-runtime/ # Runtime contracts and implementation │ ├── argi-runtime-api/ │ └── argi-runtime-core/ @@ -65,8 +64,7 @@ argi/ │ ├── argi-starter-graph-observation/ # Observability │ ├── argi-starter-adapter-spring-ai/ │ ├── argi-starter-adapter-langchain4j/ -│ ├── argi-starter-adapter-google-adk/ -│ └── argi-starter-adapter-agentscope/ +│ └── argi-starter-adapter-google-adk/ └── tools/ # Build and linting tools ``` @@ -116,14 +114,14 @@ make licenses-check ### Core Components - **Agent Framework**: Built-in agents like `SequentialAgent`, `ParallelAgent`, `RoutingAgent`, `LoopAgent`. -- **Graph Core**: Underlying engine for stateful agents, supporting persistence (PostgreSQL, MySQL, Oracle, MongoDB, Redis, File). -- **A2A (Agent-to-Agent)**: Enables agents to seek and communicate with each other using Nacos as a registry. +- **Graph Core**: Underlying engine for stateful agents with in-memory and file persistence; JDBC, MongoDB, and Redis savers live in the Extensions repository. +- **A2A (Agent-to-Agent)**: Enables agents to call remote agents through agent cards; Nacos discovery lives in the Extensions repository. - **Studio**: Provides embedded visual tools for debugging agent workflows. ### Technology Stack - **Framework**: Java 17; Spring Boot 4.1.x for optional starters and Studio -- **Model and Integration Layer**: `argi-api` contracts and four isolated provider adapters; MCP and A2A integrations +- **Model and Integration Layer**: `argi-api` contracts and three isolated provider adapters; MCP and A2A integrations - **Observability**: Lightweight Micrometer observation; no required OpenTelemetry deployment Core production sources, bytecode, and runtime dependency trees must remain free of provider SDKs. Native SDKs in core test fixtures must use test scope. This is a breaking replacement, not a deprecated compatibility bridge. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91b6e07bd..0cc45c39b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,9 +77,7 @@ maintainers should review untrusted workflow/script changes before approving run No welcome message or label is added automatically. Review requirements are managed by repository branch protection or rulesets. -Run local checks before submitting even when upstream CI is available. The CI -test job installs the tested artifacts once, then uses -`verify-api-contract.sh --skip-install` to compile the independent consumer. +Run local checks before submitting even when upstream CI is available. Commit format example: diff --git a/README-zh.md b/README-zh.md index bdb0735b8..38b2330a1 100644 --- a/README-zh.md +++ b/README-zh.md @@ -28,7 +28,7 @@ ARGI 是 **Agent Runtime and Graph Intelligence** 的缩写,读作 **“AR-jee - **上下文工程**:支持上下文压缩与编辑、调用限制、工具重试、规划和动态工具选择。 - **图工作流**:支持条件路由、并行执行、嵌套图、状态持久化和中断恢复。 - **智能体运行时基础**:提供模型无关的智能体编排、工作流状态和可嵌入调试支持。 -- **开放集成**:通过中立 ChatClient 接入 Spring AI、LangChain4j、Google ADK 和 AgentScope,并提供工具调用、MCP、A2A 和 Nacos。 +- **开放集成**:通过中立 ChatClient 接入 Spring AI、LangChain4j 和 Google ADK,并提供工具调用、MCP、A2A 和 Nacos。 - **可视化调试**:提供可嵌入 Spring Boot 应用的 Agent Chat UI。 ## 快速开始 @@ -57,7 +57,7 @@ mvn -f examples/chatbot/pom.xml spring-boot:run | 模块 | 说明 | | --- | --- | -| [API 与 Adapter](argi-api) | 中立 ChatClient 契约及 Spring AI、LangChain4j、Google ADK、AgentScope 接入 | +| [API 与 Adapter](argi-api) | 中立 ChatClient 契约及 Spring AI、LangChain4j、Google ADK 接入 | | [Runtime API](argi-runtime/argi-runtime-api) | JDK-only identity、definition 与 execution-control 契约 | | [Runtime Core](argi-runtime/argi-runtime-core) | 中立 Run/Task 数据、codec 与 reference storage | | [Agent API](argi-agent/argi-agent-api) | 引擎无关的 Agent invocation 与共享 interceptor 契约 | @@ -67,7 +67,7 @@ mvn -f examples/chatbot/pom.xml spring-boot:run | [Workflow Core](argi-orchestration/argi-workflow-core) | 版本化声明式工作流定义与独立执行 | | [Studio](argi-studio) | Agent 可视化调试界面 | | [Sandbox](https://github.com/agentic-ai-java/argi-extensions/tree/main/sandbox/argi-sandbox) | 工具调用的可选隔离执行环境,由 Extensions 维护 | -| [Spring Boot Starters](argi-spring-boot-starter) | 内置图节点、图可观测性与四个 Adapter 的自动配置 | +| [Spring Boot Starters](argi-spring-boot-starter) | 内置图节点、图可观测性与三个 Adapter 的自动配置 | | [Extensions](https://github.com/agentic-ai-java/argi-extensions) | 模型与文档契约、A2A、Nacos、AgentScope、存储等可选扩展 | | [Examples](https://github.com/agentic-ai-java/argi-examples/tree/main/examples) | Chatbot、多智能体、图工程和文档示例 | | [Benchmark](benchmark) | [ARGI 基准测试项目](https://github.com/agentic-ai-java/argi-benchmark) | @@ -78,9 +78,6 @@ mvn -f examples/chatbot/pom.xml spring-boot:run - [快速开始](https://agentic-ai-java.github.io/argi-website/docs/quick-start) - [Agent Framework 教程](https://agentic-ai-java.github.io/argi-website/docs/frameworks/agent-framework/tutorials/agents) - [Workflow Graph Core 快速开始](https://agentic-ai-java.github.io/argi-website/docs/frameworks/workflow-graph-core/quick-start) -- [Clear Room 基线与命名决策](docs/architecture/clear-room-baseline.md) -- [Checkpoint 兼容契约](docs/architecture/checkpoint-compatibility.md) -- [Agent、Studio 与 serializer 边界](docs/architecture/api-boundaries.md) - [示例项目](https://github.com/agentic-ai-java/argi-examples/tree/main/examples) - [模型厂商相关示例](https://github.com/agentic-ai-java/argi-extensions/tree/main/examples) diff --git a/README.md b/README.md index 6596dd493..25374c295 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ ARGI stands for **Agent Runtime and Graph Intelligence** and is pronounced **"AR - **Context engineering**: context compaction and editing, call limits, tool retries, planning, and dynamic tool selection. - **Graph workflows**: conditional routing, parallel execution, nested graphs, state persistence, and interruption recovery. - **Agent runtime foundation**: provider-neutral agent orchestration, workflow state, and embeddable debugging support. -- **Open integrations**: a neutral ChatClient with Spring AI, LangChain4j, Google ADK, and AgentScope adapters, plus tool calling, MCP, A2A, and Nacos. +- **Open integrations**: a neutral ChatClient with Spring AI, LangChain4j, and Google ADK adapters, plus tool calling, MCP, A2A, and Nacos. - **Visual debugging**: an embeddable Agent Chat UI for Spring Boot applications. ## Quick Start @@ -57,7 +57,7 @@ Open [http://localhost:8080/chatui/index.html](http://localhost:8080/chatui/inde | Module | Description | | --- | --- | -| [API and Adapters](argi-api) | Framework-neutral ChatClient contracts and Spring AI, LangChain4j, Google ADK, and AgentScope adapters | +| [API and Adapters](argi-api) | Framework-neutral ChatClient contracts and Spring AI, LangChain4j, and Google ADK adapters | | [Agent Framework](argi-orchestration/argi-agent-framework) | Agent development and multi-agent orchestration | | [Workflow Graph Core](argi-orchestration/argi-workflow-graph-core) | Stateful graph execution, persistence, and workflow runtime | | [Runtime API](argi-runtime/argi-runtime-api) | JDK-only identity, definition and execution-control contracts | @@ -67,7 +67,7 @@ Open [http://localhost:8080/chatui/index.html](http://localhost:8080/chatui/inde | [Runtime Core](argi-runtime/argi-runtime-core) | Engine-neutral Run/Task data, codecs and reference storage | | [Studio](argi-studio) | Visual debugging UI for agents | | [Sandbox](https://github.com/agentic-ai-java/argi-extensions/tree/main/sandbox/argi-sandbox) | Optional isolated execution environment for tool calls, maintained in Extensions | -| [Spring Boot Starters](argi-spring-boot-starter) | Built-in graph nodes, graph observability, and four adapter auto-configurations | +| [Spring Boot Starters](argi-spring-boot-starter) | Built-in graph nodes, graph observability, and three adapter auto-configurations | | [Extensions](https://github.com/agentic-ai-java/argi-extensions) | Model and document contracts, A2A, Nacos, AgentScope, storage, and other optional integrations | | [Examples](https://github.com/agentic-ai-java/argi-examples/tree/main/examples) | Chatbot, multi-agent, graph engineering, and documentation examples | | [Benchmark](benchmark) | [argi-benchmark](https://github.com/agentic-ai-java/argi-benchmark) | @@ -78,9 +78,6 @@ Open [http://localhost:8080/chatui/index.html](http://localhost:8080/chatui/inde - [Quick Start](https://agentic-ai-java.github.io/argi-website/en/docs/quick-start) - [Agent Framework tutorials](https://agentic-ai-java.github.io/argi-website/en/docs/frameworks/agent-framework/tutorials/agents) - [Workflow Graph Core Quick Start](https://agentic-ai-java.github.io/argi-website/en/docs/frameworks/workflow-graph-core/quick-start) -- [Clear Room baseline](docs/architecture/clear-room-baseline.md) -- [Checkpoint compatibility contract](docs/architecture/checkpoint-compatibility.md) -- [Agent, Studio, and serializer boundaries](docs/architecture/api-boundaries.md) - [Examples](https://github.com/agentic-ai-java/argi-examples/tree/main/examples) - [Provider-specific examples](https://github.com/agentic-ai-java/argi-extensions/tree/main/examples) diff --git a/argi-agent/argi-agent-api/README.md b/argi-agent/argi-agent-api/README.md index 28cf102fd..1a870ae83 100644 --- a/argi-agent/argi-agent-api/README.md +++ b/argi-agent/argi-agent-api/README.md @@ -108,16 +108,8 @@ API is explicit, not an automatic replacement for existing Graph calls. ```sh ./mvnw -B -pl :argi-agent-api -am test -tools/scripts/verify-api-contract.sh ``` -The independent consumer compiles and executes local model interception, a real -loopback HTTP call, and a two-step composition through the same Agent interface. -Its classpath contains exactly Agent API, Runtime API and API. The gate checks -public signatures, actual bytecode dependencies (java.base only), missing types, -and duplicate classes in Framework. These are executable contract consumers, -not production ReAct, declarative Workflow or remote-protocol engines. - Unit tests cover immutable boundaries, suspended outcomes, malformed streams, cancellation in both directions, late subscriptions, deadline and ownership loss, completion races, and 100,000 intermediate events without collector accumulation. diff --git a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeAware.java b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeAware.java similarity index 95% rename from argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeAware.java rename to argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeAware.java index 9f1c39a4f..d2979c9a3 100644 --- a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeAware.java +++ b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeAware.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.agentic.ai.agent.interceptor; +package io.github.agentic.ai.agent.outcome; /** * Implemented by tools that carry their own {@link BusinessOutcomeMapper}; lets diff --git a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapper.java b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapper.java similarity index 97% rename from argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapper.java rename to argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapper.java index 37483737a..ab666dd1f 100644 --- a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapper.java +++ b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.agentic.ai.agent.interceptor; +package io.github.agentic.ai.agent.outcome; import io.github.agentic.ai.api.chat.ToolCall; import io.github.agentic.ai.api.tool.ToolResult; diff --git a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapperRegistry.java b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapperRegistry.java similarity index 98% rename from argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapperRegistry.java rename to argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapperRegistry.java index 66ca6aa8c..5e403a055 100644 --- a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapperRegistry.java +++ b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapperRegistry.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.agentic.ai.agent.interceptor; +package io.github.agentic.ai.agent.outcome; import java.util.Collections; import java.util.HashMap; diff --git a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java index 831a21ef6..4187bf021 100644 --- a/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java +++ b/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java @@ -32,7 +32,7 @@ import io.github.agentic.ai.api.tool.ToolRegistry; import io.github.agentic.ai.api.tool.ToolResult; import io.github.agentic.ai.agent.AgentResult; -import io.github.agentic.ai.agent.interceptor.BusinessOutcomeMapperRegistry; +import io.github.agentic.ai.agent.outcome.BusinessOutcomeMapperRegistry; import io.github.agentic.ai.agent.interceptor.ModelCallHandler; import io.github.agentic.ai.agent.interceptor.ModelInterceptor; import io.github.agentic.ai.agent.interceptor.ModelRequest; diff --git a/argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapperRegistryTest.java b/argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapperRegistryTest.java similarity index 99% rename from argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapperRegistryTest.java rename to argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapperRegistryTest.java index 79aa2570d..bdd45d649 100644 --- a/argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/interceptor/BusinessOutcomeMapperRegistryTest.java +++ b/argi-agent/argi-agent-core/src/test/java/io/github/agentic/ai/agent/outcome/BusinessOutcomeMapperRegistryTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.agentic.ai.agent.interceptor; +package io.github.agentic.ai.agent.outcome; import io.github.agentic.ai.api.chat.ToolCall; import io.github.agentic.ai.api.chat.ToolDefinition; diff --git a/argi-api/README.md b/argi-api/README.md index 80fc0805b..55a4e94f0 100644 --- a/argi-api/README.md +++ b/argi-api/README.md @@ -10,7 +10,6 @@ runtime dependency. The root `pom.xml` aggregates each child module directly. | `argi-adapter-spring-ai` | `argi-adapter-spring-ai` | Adapt Spring AI ChatClient and ChatModel | | `argi-adapter-langchain4j` | `argi-adapter-langchain4j` | Adapt LangChain4j chat and streaming models | | `argi-adapter-google-adk` | `argi-adapter-google-adk` | Adapt Google ADK BaseLlm | -| `argi-adapter-agentscope` | `argi-adapter-agentscope` | Adapt AgentScope Model | All artifacts use the group ID `io.github.agentic-ai-java`. The `argi-api-base` directory intentionally publishes as **`argi-api`**: applications should not need @@ -27,7 +26,7 @@ From the repository root: ```shell ./mvnw -B -pl :argi-api -am test -./mvnw -B -pl :argi-adapter-spring-ai,:argi-adapter-langchain4j,:argi-adapter-google-adk,:argi-adapter-agentscope -am test +./mvnw -B -pl :argi-adapter-spring-ai,:argi-adapter-langchain4j,:argi-adapter-google-adk -am test ``` For path-based module selection, use the complete child path, for example @@ -39,7 +38,7 @@ Java packages, and adapter behavior are unchanged by the directory move. Import `io.github.agentic-ai-java:argi-bom` at the ARGI version used by the application. For a standalone ReAct application, add `argi-agent-framework` and -one of the four adapter artifacts. A graph-only application uses `argi-workflow-graph-core` +one of the three adapter artifacts. A graph-only application uses `argi-workflow-graph-core` and, for built-in model nodes, `argi-starter-builtin-nodes` instead. The directory name `argi-api-base` is not a dependency coordinate. @@ -51,9 +50,8 @@ wraps it explicitly. Adapter constructors accept these native types: | `SpringAiChatClientAdapter` | Spring AI `ChatModel` or `ChatClient` | | `LangChain4jChatClientAdapter` | LangChain4j `ChatModel`, optionally with `StreamingChatModel` | | `GoogleAdkChatClientAdapter` | Google ADK `BaseLlm` | -| `AgentScopeChatClientAdapter` | AgentScope `Model` | -After wrapping, the agent code is the same for all four frameworks: +After wrapping, the agent code is the same for all three frameworks: ```java import io.github.agentic.ai.api.chat.ChatClient; @@ -99,7 +97,7 @@ constants or Maven command-line secrets. -Dsurefire.failIfNoSpecifiedTests=false test ``` -The matrix covers four adapters, Graph and ReAct tool loops, and synchronous and +The matrix covers three adapters, Graph and ReAct tool loops, and synchronous and streaming calls. ADK uses its `ApigeeLlm` OpenAI-compatible transport in this matrix; it does not validate native Gemini/Vertex AI, native ADK Claude, or the upstream identity of a model served by a gateway. Studio's optional Jina MCP @@ -215,7 +213,7 @@ and `ToolRegistry`; existing Spring AI callbacks can be explicitly wrapped with The API, Graph, Agent/ReAct, Studio, and generic starters use neutral messages, tools, serializers, and model calls. Their production sources, compiled classes, and runtime dependency trees must not reference provider SDKs or adapters. Provider -SDKs are confined to the four adapter modules and their dedicated starters; +SDKs are confined to the three adapter modules and their dedicated starters; test-scoped adapter fixtures do not become application dependencies. Third-party Native Agent Mode is not implemented in this phase. @@ -305,7 +303,7 @@ compatible**. They fail explicitly; they must not become user messages or disapp Template content must be resolved with the previous runtime. There is no bundled one-command binary export tool: use the old runtime and an application-specific export that emits the supported historical JSON layout or neutral messages. -Do not pass legacy maps directly to `Messageutils`; deserialize them first. +Do not pass legacy maps directly to `MessageUtils`; deserialize them first. Before deployment: @@ -363,8 +361,6 @@ Studio attaches turn usage only to the final event when splitting a multi-messag output. Earlier split events carry unknown usage. Upgrade SSE consumers for `inputTokens`, `outputTokens`, `totalTokens` rather than the previous `promptTokens` / `completionTokens` fields; do not accumulate usage per message. -AgentScope uses the native total-token getter, without assuming a provider total -can always be reconstructed from input and output tokens. The agent framework uses JSON Schema Validator 3.x for its current neutral tool validation API. Studio pins 1.5.7 in test scope because its MCP fixture requires diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/Builder.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/Builder.java index 76c46be1b..1e0f3f5e8 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/Builder.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/Builder.java @@ -29,7 +29,7 @@ import io.github.agentic.ai.workflow.graph.KeyStrategy; import io.github.agentic.ai.agent.orchestration.hook.Hook; import io.github.agentic.ai.agent.interceptor.Interceptor; -import io.github.agentic.ai.agent.interceptor.BusinessOutcomeMapperRegistry; +import io.github.agentic.ai.agent.outcome.BusinessOutcomeMapperRegistry; import io.github.agentic.ai.agent.interceptor.ModelInterceptor; import io.github.agentic.ai.agent.orchestration.interceptor.StreamingModelInterceptor; import io.github.agentic.ai.agent.orchestration.interceptor.ToolInterceptor; diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfig.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfig.java index 518ca7c24..67f3d6225 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfig.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfig.java @@ -24,8 +24,6 @@ import io.github.agentic.ai.workflow.graph.OverAllState; import io.github.agentic.ai.workflow.graph.RunnableConfig; import io.github.agentic.ai.workflow.graph.action.NodeActionWithConfig; -import io.github.agentic.ai.workflow.graph.async.AsyncGenerator; -import io.github.agentic.ai.workflow.graph.async.AsyncGeneratorQueue; import io.github.agentic.ai.workflow.graph.streaming.OutputType; import io.github.agentic.ai.workflow.graph.streaming.StreamingOutput; @@ -51,12 +49,9 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.scheduler.Schedulers; @@ -65,6 +60,9 @@ public class A2aNodeActionWithConfig implements NodeActionWithConfig { + /** Default {@link RunnableConfig} metadata key holding the caller's user id. */ + public static final String DEFAULT_USER_ID_METADATA_KEY = "userId"; + private final String agentName; private final AgentCardWrapper agentCard; @@ -85,6 +83,8 @@ public class A2aNodeActionWithConfig implements NodeActionWithConfig { private AgentTracer agentTracer; + private String userIdMetadataKey = DEFAULT_USER_ID_METADATA_KEY; + public A2aNodeActionWithConfig(AgentCardWrapper agentCard, String agentName, boolean includeContents, String outputKeyToParent, String instruction, boolean streaming) { this.agentName = agentName; @@ -142,6 +142,21 @@ public A2aNodeActionWithConfig(AgentCardWrapper agentCard, String agentName, boo this.agentTracer = agentTracer; } + /** + * Select the {@link RunnableConfig} metadata key whose value is forwarded to the remote + * agent as the A2A {@code userId} metadata field. + * @param userIdMetadataKey the metadata key, defaults to + * {@link #DEFAULT_USER_ID_METADATA_KEY} + * @return this node action + */ + public A2aNodeActionWithConfig userIdMetadataKey(String userIdMetadataKey) { + if (!StringUtils.hasText(userIdMetadataKey)) { + throw new IllegalArgumentException("userIdMetadataKey must not be blank"); + } + this.userIdMetadataKey = userIdMetadataKey; + return this; + } + @Override public Map apply(OverAllState state, RunnableConfig config) throws Exception { RunnableConfig subGraphRunnableConfig = getSubGraphRunnableConfig(config); @@ -150,9 +165,7 @@ public Map apply(OverAllState state, RunnableConfig config) thro context = context.withDelegationId(UUID.randomUUID().toString()); } if (streaming) { - AsyncGenerator generator = createStreamingGenerator(state, subGraphRunnableConfig, context); - // Convert AsyncGenerator to Flux using the new toFlux() method - Flux> flux = toFlux(generator); + Flux> flux = createStreamingFlux(state, subGraphRunnableConfig, context); return Map.of(StringUtils.hasLength(this.outputKeyToParent) ? this.outputKeyToParent : "messages", flux); } else { @@ -217,105 +230,23 @@ public String subGraphId() { } /** - * Converts this AsyncGenerator to a Project Reactor Flux. This method provides - * forward compatibility for converting AsyncGenerator to reactive streams. - * @return a Flux that emits the elements from this AsyncGenerator + * Stream the remote agent response. The HTTP exchange starts on subscription and runs + * on a bounded elastic worker. Each non-empty text chunk is emitted as a + * {@link StreamingOutput}; transport and protocol failures are emitted as error text + * chunks. The stream always ends with a done response carrying the accumulated text + * for the parent state, unless the subscriber cancels first. */ - private Flux> toFlux(AsyncGenerator generator) { - return Flux.create(sink -> { - Disposable disposable = Schedulers.boundedElastic().schedule(() -> drainGenerator(generator, sink)); - sink.onCancel(disposable::dispose); - sink.onDispose(disposable::dispose); - }); - } - - private void drainGenerator(AsyncGenerator generator, FluxSink> sink) { - while (!sink.isCancelled()) { - final AsyncGenerator.Data data; - try { - data = generator.next(); - } - catch (Exception ex) { - sink.error(ex); - return; - } - - if (data.isDone()) { - data.resultValue().ifPresent(result -> { - if (!sink.isCancelled()) { - sink.next(GraphResponse.done(result)); - } - }); - if (!sink.isCancelled()) { - sink.complete(); - } - return; - } - - var future = data.getData(); - if (future == null) { - sink.error(new IllegalStateException("AsyncGenerator data is null without completion signal")); - return; - } - - if (future.isDone()) { - try { - E value = future.join(); - if (!sink.isCancelled()) { - sink.next(GraphResponse.of(value)); - } - } - catch (Exception ex) { - sink.error(unwrapCompletionException(ex)); - return; - } - continue; - } - - future.whenComplete((value, throwable) -> { - if (sink.isCancelled()) { - return; - } - - if (throwable != null) { - Throwable actual = unwrapCompletionException(throwable); - sink.error(actual); - return; - } - - if (!sink.isCancelled()) { - sink.next(GraphResponse.of(value)); - } - - drainGenerator(generator, sink); - }); - return; - } - } - - private Throwable unwrapCompletionException(Throwable throwable) { - if (throwable instanceof java.util.concurrent.CompletionException completionException - && completionException.getCause() != null) { - return completionException.getCause(); - } - return throwable; - } - - /** - * Create a streaming generator. - */ - private AsyncGenerator createStreamingGenerator(OverAllState state, RunnableConfig config, + private Flux> createStreamingFlux(OverAllState state, RunnableConfig config, AgentTraceContext context) throws Exception { final String requestPayload = buildSendStreamingMessageRequest(state, config, context); - final BlockingQueue> queue = new LinkedBlockingQueue<>(1000); final String outputKey = StringUtils.hasLength(this.outputKeyToParent) ? this.outputKeyToParent : "messages"; - final StringBuilder accumulated = new StringBuilder(); - return AsyncGeneratorQueue.of(queue, q -> { + return Flux.>create(sink -> { + final StringBuilder accumulated = new StringBuilder(); String baseUrl = resolveAgentBaseUrl(this.agentCard); if (baseUrl == null || baseUrl.isBlank()) { - StreamingOutput errorOutput = buildStreamingOutput("Error: AgentCard.url is empty", state); - queue.add(AsyncGenerator.Data.of(errorOutput)); + emit(sink, buildStreamingOutput("Error: AgentCard.url is empty", state)); + complete(sink, outputKey, accumulated); return; } @@ -335,14 +266,14 @@ private AsyncGenerator createStreamingGenerator(OverAllState state, if (statusCode != 200) { StreamingOutput errorOutput = buildStreamingOutput("HTTP request failed, status: " + statusCode, state); - queue.add(AsyncGenerator.Data.of(errorOutput)); + emit(sink, errorOutput); return null; } HttpEntity entity = response.getEntity(); if (entity == null) { StreamingOutput errorOutput = buildStreamingOutput("Empty HTTP entity", state); - queue.add(AsyncGenerator.Data.of(errorOutput)); + emit(sink, errorOutput); return null; } @@ -356,7 +287,7 @@ private AsyncGenerator createStreamingGenerator(OverAllState state, try (BufferedReader reader = new BufferedReader( new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8))) { String line; - while ((line = reader.readLine()) != null) { + while (!sink.isCancelled() && (line = reader.readLine()) != null) { String trimmed = line.trim(); if (!trimmed.startsWith("data:")) { continue; @@ -372,7 +303,7 @@ private AsyncGenerator createStreamingGenerator(OverAllState state, String text = extractResponseText(result); if (text != null && !text.isEmpty()) { accumulated.append(text); - queue.add(AsyncGenerator.Data.of(buildStreamingOutput(text, state))); + emit(sink, buildStreamingOutput(text, state)); } } } @@ -390,13 +321,12 @@ private AsyncGenerator createStreamingGenerator(OverAllState state, String text = extractResponseText(result); if (text != null && !text.isEmpty()) { accumulated.append(text); - queue.add(AsyncGenerator.Data.of(buildStreamingOutput(text, state))); + emit(sink, buildStreamingOutput(text, state)); } } catch (Exception ex) { receiveSpan.error(ex); - queue.add(AsyncGenerator.Data - .of(buildStreamingOutput("Error: " + ex.getMessage(), state))); + emit(sink, buildStreamingOutput("Error: " + ex.getMessage(), state)); } } receiveSpan.success(); @@ -421,116 +351,25 @@ private AsyncGenerator createStreamingGenerator(OverAllState state, } } catch (Exception e) { - StreamingOutput errorOutput = buildStreamingOutput("Error: " + e.getMessage(), state); - queue.add(AsyncGenerator.Data.of(errorOutput)); + emit(sink, buildStreamingOutput("Error: " + e.getMessage(), state)); } finally { - queue.add(AsyncGenerator.Data.done(buildFinalResult(outputKey, accumulated.toString()))); + complete(sink, outputKey, accumulated); } - }); + }).subscribeOn(Schedulers.boundedElastic()); } - /** - * Check whether the given text looks like an SSE response. - */ - private boolean isSSEResponse(String responseText) { - return responseText.contains("data: "); - } - - /** - * Create a streaming generator for SSE-formatted text. - */ - private AsyncGenerator createSseStreamingGenerator(String sseResponseText, OverAllState state) { - // Use the new real-time streaming method - return createRealTimeSseStreamingGenerator(sseResponseText, state); - } - - /** - * Create a real-time SSE streaming generator (recommended). This method starts - * processing SSE data immediately and pushes chunks as they arrive. - */ - private AsyncGenerator createRealTimeSseStreamingGenerator(String sseResponseText, OverAllState state) { - BlockingQueue> queue = new LinkedBlockingQueue<>(1000); - final String outputKey = StringUtils.hasLength(this.outputKeyToParent) ? this.outputKeyToParent : "messages"; - final StringBuilder accumulated = new StringBuilder(); - - // Start async processing immediately; do not wait for the entire content - return AsyncGeneratorQueue.of(queue, executor -> { - try { - // Process SSE response line by line to achieve true streaming - String[] lines = sseResponseText.split("\n"); - - for (String line : lines) { - line = line.trim(); - if (line.startsWith("data: ")) { - try { - String jsonContent = line.substring(6); // remove "data: " - // prefix - - // End marker - if ("[DONE]".equals(jsonContent)) { - break; - } - - Map parsed = parseJsonMap(jsonContent); - Map result = (Map) parsed.get("result"); - - if (result != null) { - StreamingOutput streamingOutput = createStreamingOutputFromResult(result, state); - if (streamingOutput != null) { - queue.add(AsyncGenerator.Data.of(streamingOutput)); - } - } - } - catch (Exception e) { - // Ignore parse errors and continue - continue; - } - } - } - - // Signal completion with final result value - queue.add(AsyncGenerator.Data.done(buildFinalResult(outputKey, accumulated.toString()))); - - } - catch (Exception e) { - // On error, emit an error message and signal completion - StreamingOutput errorOutput = buildStreamingOutput("Error: " + e.getMessage(), state); - queue.add(AsyncGenerator.Data.of(errorOutput)); - queue.add(AsyncGenerator.Data.done(buildFinalResult(outputKey, accumulated.toString()))); - } - }); + private static void emit(FluxSink> sink, NodeOutput output) { + if (!sink.isCancelled()) { + sink.next(GraphResponse.of(output)); + } } - /** - * Create a single-output streaming generator (for non-SSE responses). - */ - private AsyncGenerator createSingleStreamingGenerator(String responseText, OverAllState state) { - BlockingQueue> queue = new LinkedBlockingQueue<>(10); - final String outputKey = StringUtils.hasLength(this.outputKeyToParent) ? this.outputKeyToParent : "messages"; - final StringBuilder accumulated = new StringBuilder(); - - try { - Map resultMap = parseJsonMap(responseText); - Map result = (Map) resultMap.get("result"); - String responseText2 = extractResponseText(result); - - if (responseText2 != null && !responseText2.isEmpty()) { - accumulated.append(responseText2); - StreamingOutput streamingOutput = buildStreamingOutput(responseText2, state); - queue.add(AsyncGenerator.Data.of(streamingOutput)); - } - } - catch (Exception e) { - // On parse failure, emit an error message - StreamingOutput errorOutput = buildStreamingOutput("Error: " + e.getMessage(), state); - queue.add(AsyncGenerator.Data.of(errorOutput)); + private void complete(FluxSink> sink, String outputKey, StringBuilder accumulated) { + if (!sink.isCancelled()) { + sink.next(GraphResponse.done(buildFinalResult(outputKey, accumulated.toString()))); + sink.complete(); } - - // Signal completion with final result value - queue.add(AsyncGenerator.Data.done(buildFinalResult(outputKey, accumulated.toString()))); - - return new AsyncGeneratorQueue.Generator<>(queue); } /** @@ -558,51 +397,6 @@ private Map buildFinalResult(String outputKey, String text) { return Map.of(outputKey, value); } - /** - * Create a StreamingOutput from the parsed result map. - */ - private StreamingOutput createStreamingOutputFromResult(Map result, OverAllState state) { - String text = extractResponseText(result); - if (text != null && !text.isEmpty()) { - return buildStreamingOutput(text, state); - } - return null; - } - - // /** - // * Get the streaming generator (similar to LlmNode.stream). - // */ - // public Flux stream(OverAllState state) throws Exception { - // if (!this.streaming) { - // throw new IllegalStateException("Streaming is not enabled for this A2aNode"); - // } - // AsyncGenerator generator = createStreamingGenerator(state); - // Flux> graphResponseFlux = toFlux(generator); - // - // // Convert Flux> to Flux - // return graphResponseFlux.filter(graphResponse -> !graphResponse.isDone()) // Filter - // out completion signals - // .map(graphResponse -> { - // try { - // return graphResponse.getOutput().join(); - // } - // catch (Exception e) { - // throw new RuntimeException("Error extracting output from GraphResponse", e); - // } - // }); - // } - - // /** - // * Get the non-streaming result (similar to LlmNode.call). - // */ - // public String call(OverAllState state) throws Exception { - // String requestPayload = buildSendMessageRequest(state, this.inputKeyFromParent); - // String resultText = sendMessageToServer(this.agentCard, requestPayload); - // Map resultMap = autoDetectAndParseResponse(resultText); - // Map result = (Map) resultMap.get("result"); - // return extractResponseText(result); - // } - /** * Auto-detect response format and parse accordingly. * @param responseText The raw response text @@ -817,8 +611,7 @@ private String buildSendMessageRequest(OverAllState state, RunnableConfig config Map metadata = new HashMap<>(); config.threadId().ifPresent(threadId -> metadata.put("threadId", threadId)); - // FIXME, the key 'userId' should be configurable - config.metadata("userId").ifPresent(userId -> metadata.put("userId", userId)); + config.metadata(this.userIdMetadataKey).ifPresent(userId -> metadata.put("userId", userId)); putTraceMetadata(metadata, context); params.put("metadata", metadata); @@ -864,8 +657,7 @@ private String buildSendStreamingMessageRequest(OverAllState state, RunnableConf Map metadata = new HashMap<>(); config.threadId().ifPresent(threadId -> metadata.put("threadId", threadId)); - // FIXME, the key 'userId' should be configurable - config.metadata("userId").ifPresent(userId -> metadata.put("userId", userId)); + config.metadata(this.userIdMetadataKey).ifPresent(userId -> metadata.put("userId", userId)); putTraceMetadata(metadata, context); params.put("metadata", metadata); diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aRemoteAgent.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aRemoteAgent.java index dd5127fa9..cfddfe161 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aRemoteAgent.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/a2a/A2aRemoteAgent.java @@ -53,6 +53,8 @@ public class A2aRemoteAgent extends BaseAgent { private AgentTracer agentTracer; + private String userIdMetadataKey; + // Private constructor for Builder pattern private A2aRemoteAgent(Builder builder) { super(builder.name, builder.description, builder.includeContents, builder.returnReasoningContents, builder.outputKey, builder.outputKeyStrategy); @@ -64,6 +66,7 @@ private A2aRemoteAgent(Builder builder) { this.instruction = builder.instruction; this.shareState = builder.shareState; this.agentTracer = builder.agentTracer; + this.userIdMetadataKey = builder.userIdMetadataKey; } @Override @@ -79,7 +82,7 @@ protected StateGraph initGraph() throws GraphStateException { StateGraph graph = new StateGraph(name, this.keyStrategyFactory); graph.addNode("A2aNode", AsyncNodeActionWithConfig.node_async( new A2aNodeActionWithConfig(agentCard, name, includeContents, outputKey, instruction, streaming, - this.shareState, this.compileConfig, this.agentTracer))); + this.shareState, this.compileConfig, this.agentTracer).userIdMetadataKey(this.userIdMetadataKey))); graph.addEdge(StateGraph.START, "A2aNode"); graph.addEdge("A2aNode", StateGraph.END); return graph; @@ -110,7 +113,7 @@ private class A2aRemoteAgentNode extends Node implements SubGraphNode { public A2aRemoteAgentNode(String id, boolean includeContents, boolean returnReasoningContents, String instruction, AgentCardWrapper agentCard, boolean streaming, boolean shareState, CompiledGraph subGraph) { super(Objects.requireNonNull(id, "id cannot be null"), - (config) -> AsyncNodeActionWithConfig.node_async(new A2aNodeActionWithConfig(agentCard, subGraph.stateGraph.getName(), includeContents, A2aRemoteAgent.this.outputKey, instruction, streaming, shareState, config, A2aRemoteAgent.this.agentTracer))); + (config) -> AsyncNodeActionWithConfig.node_async(new A2aNodeActionWithConfig(agentCard, subGraph.stateGraph.getName(), includeContents, A2aRemoteAgent.this.outputKey, instruction, streaming, shareState, config, A2aRemoteAgent.this.agentTracer).userIdMetadataKey(A2aRemoteAgent.this.userIdMetadataKey))); this.subGraph = subGraph; } @@ -152,6 +155,8 @@ public static class Builder { private AgentTracer agentTracer; + private String userIdMetadataKey = A2aNodeActionWithConfig.DEFAULT_USER_ID_METADATA_KEY; + public Builder name(String name) { this.name = name; return this; @@ -229,6 +234,17 @@ public Builder shareState(boolean shareState) { return this; } + /** + * Select the run metadata key whose value is sent to the remote agent as the A2A + * {@code userId} metadata field. Defaults to {@code userId}. + * @param userIdMetadataKey the metadata key + * @return this builder + */ + public Builder userIdMetadataKey(String userIdMetadataKey) { + this.userIdMetadataKey = userIdMetadataKey; + return this; + } + public A2aRemoteAgent build() { // Validation if (name == null || name.trim().isEmpty()) { diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/extension/interceptor/LargeResultEvictionInterceptor.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/extension/interceptor/LargeResultEvictionInterceptor.java index 4b4e7f783..9bbc79610 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/extension/interceptor/LargeResultEvictionInterceptor.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/extension/interceptor/LargeResultEvictionInterceptor.java @@ -28,6 +28,9 @@ import java.util.List; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Tool interceptor that automatically evicts large tool results to the filesystem. * @@ -52,6 +55,8 @@ */ public class LargeResultEvictionInterceptor extends ToolInterceptor { + private static final Logger log = LoggerFactory.getLogger(LargeResultEvictionInterceptor.class); + // Constants from Python implementation private static final int MAX_LINE_LENGTH = 2000; private static final int LINE_NUMBER_WIDTH = 6; @@ -151,10 +156,10 @@ private ToolCallResponse processLargeResult(ToolCallResponse response, String to WriteResult writeResult = backend.write(filePath, content); if (writeResult.getError() != null) { // Log warning but continue with eviction message - System.err.println("Warning: Failed to write large result to filesystem: " + writeResult.getError()); + log.warn("Failed to write large result to filesystem: {}", writeResult.getError()); } } else { - System.err.println("Warning: Backend not configured, large result not persisted to filesystem"); + log.warn("Backend not configured, large result not persisted to filesystem"); } // Extract first N lines as sample diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/flow/strategy/FlowGraphBuildingStrategy.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/flow/strategy/FlowGraphBuildingStrategy.java index 1f2e83584..82ba38c8e 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/flow/strategy/FlowGraphBuildingStrategy.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/flow/strategy/FlowGraphBuildingStrategy.java @@ -114,7 +114,7 @@ default void processAgentKeyStrategies(Agent agent, Map key // ReactAgent: only handle outputKey processOutputKey(reactAgent.getOutputKey(), reactAgent.getOutputKeyStrategy(), keyStrategyMap, defaultStrategy); } else if (agent instanceof BaseAgent baseAgent) { - // BaseAgent (e.g. AgentScopeAgent): handle outputKey + // Other BaseAgent implementations: handle outputKey processOutputKey(baseAgent.getOutputKey(), baseAgent.getOutputKeyStrategy(), keyStrategyMap, defaultStrategy); } else if (agent instanceof FlowAgent flowAgent) { // FlowAgent: recursively process sub-agents diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolemulator/ToolEmulatorInterceptor.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolemulator/ToolEmulatorInterceptor.java index 0550b30e9..0da9039d7 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolemulator/ToolEmulatorInterceptor.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolemulator/ToolEmulatorInterceptor.java @@ -25,8 +25,10 @@ import io.github.agentic.ai.api.chat.ChatRequest; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.slf4j.Logger; @@ -62,11 +64,14 @@ public class ToolEmulatorInterceptor extends ToolInterceptor { private final String promptTemplate; + private final Map toolDescriptions; + private ToolEmulatorInterceptor(Builder builder) { this.chatClient = builder.chatClient; this.emulateAll = builder.emulateAll; this.toolsToEmulate = new HashSet<>(builder.toolsToEmulate); this.promptTemplate = builder.promptTemplate; + this.toolDescriptions = Map.copyOf(builder.toolDescriptions); } public static Builder builder() { @@ -94,14 +99,9 @@ public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandl try { // Build prompt for emulator LLM - String prompt = String.format(promptTemplate, toolName, "No description available", // TODO: - // Tool - // descriptions - // not - // available - // in - // ToolCallRequest - request.getArguments()); + // Tool call requests carry no definitions, so descriptions are configured here. + String description = toolDescriptions.getOrDefault(toolName, "No description available"); + String prompt = String.format(promptTemplate, toolName, description, request.getArguments()); // Get emulated response from LLM String emulatedResult = chatClient.callBlocking(ChatRequest.of(List.of(ChatMessage.user(prompt)))).text(); @@ -129,6 +129,8 @@ public static class Builder { private final Set toolsToEmulate = new HashSet<>(); + private final Map toolDescriptions = new HashMap<>(); + private ChatClient chatClient; private boolean emulateAll = true; // Default: emulate all tools @@ -169,6 +171,15 @@ public Builder addTools(Collection toolNames) { return this; } + /** + * Describe a tool to the emulator model. Without a description the prompt states + * that none is available, which usually yields less realistic output. + */ + public Builder toolDescription(String toolName, String description) { + this.toolDescriptions.put(toolName, description); + return this; + } + /** * Set whether to emulate all tools or only specified ones. Default is true * (emulate all tools). diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolretry/ToolRetryInterceptor.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolretry/ToolRetryInterceptor.java index 317423e67..66dfe08d8 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolretry/ToolRetryInterceptor.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/interceptor/toolretry/ToolRetryInterceptor.java @@ -38,7 +38,7 @@ * * Example: * ToolRetryInterceptor interceptor = ToolRetryInterceptor.builder() - * .maxRetries(3) + * .maxAttempts(4) * .backoffFactor(2.0) * .initialDelay(1000) * .build(); @@ -208,20 +208,6 @@ public Builder maxAttempts(int maxAttempts) { return this; } - /** - * Set the maximum number of retries (excluding the first call). - * @param maxRetries number of retries; total attempts will be {@code maxRetries + 1} - * @deprecated use {@link #maxAttempts(int)} instead, which counts the initial call - */ - @Deprecated - public Builder maxRetries(int maxRetries) { - if (maxRetries < 0) { - throw new IllegalArgumentException("maxRetries must be >= 0"); - } - this.maxAttempts = maxRetries + 1; - return this; - } - public Builder toolNames(Set toolNames) { this.toolNames = toolNames; return this; diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentLlmNode.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentLlmNode.java index 1b30a082a..50d877ed4 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentLlmNode.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentLlmNode.java @@ -332,11 +332,8 @@ private List appendSystemPromptIfNeeded(ModelRequest modelRequest) // Create a new list and copy messages from modelRequest List messages = new ArrayList<>(modelRequest.getMessages()); - // FIXME, there should have only one ChatMessage. - // Users may have added SystemMessages in hooks or somewhere else, simply remove - // will cause unexpected agent behaviour. - // messages.removeIf(message -> message instanceof ChatMessage && ((ChatMessage) - // message).role() == ChatRole.SYSTEM); + // System messages added by hooks or callers are kept on purpose: dropping them + // would silently change agent behaviour. Duplicates are reported below instead. // Add the ChatMessage from modelRequest at the beginning if present if (modelRequest.getSystemMessage() != null) { diff --git a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNode.java b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNode.java index 34891bfbe..7cbc20667 100644 --- a/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNode.java +++ b/argi-orchestration/argi-agent-framework/src/main/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNode.java @@ -28,7 +28,7 @@ import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallExecutionContext; import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallHandler; import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallRequest; -import io.github.agentic.ai.agent.interceptor.BusinessOutcomeMapperRegistry; +import io.github.agentic.ai.agent.outcome.BusinessOutcomeMapperRegistry; import io.github.agentic.ai.agent.orchestration.interceptor.ToolOutcome; import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallResponse; import io.github.agentic.ai.agent.orchestration.interceptor.ToolInterceptor; diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfigTests.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfigTests.java index abc058fbf..412438e37 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfigTests.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/a2a/A2aNodeActionWithConfigTests.java @@ -19,14 +19,12 @@ import io.github.agentic.ai.workflow.graph.NodeOutput; import io.github.agentic.ai.workflow.graph.OverAllState; import io.github.agentic.ai.workflow.graph.RunnableConfig; -import io.github.agentic.ai.workflow.graph.async.AsyncGenerator; import io.github.agentic.ai.workflow.graph.streaming.OutputType; import io.github.agentic.ai.workflow.graph.streaming.StreamingOutput; import io.github.agentic.ai.api.chat.ChatMessage; import io.github.agentic.ai.api.chat.ChatSourceKind; import io.github.agentic.ai.api.chat.ChatTrustLevel; -import io.github.agentic.ai.api.chat.Usage; import io.a2a.spec.AgentCard; import org.junit.jupiter.api.Test; @@ -43,7 +41,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -62,100 +59,149 @@ class A2aNodeActionWithConfigTests { - private static final Method TO_FLUX = initToFluxMethod(); - private final A2aNodeActionWithConfig action = new A2aNodeActionWithConfig(createAgentCardWrapper(), "", false, "messages", "instruction", true); @Test - void toFluxEmitsAsyncData() throws Exception { - AsyncGenerator generator = new AsyncGenerator<>() { - private final AtomicInteger index = new AtomicInteger(); - - @Override - public Data next() { - int step = index.getAndIncrement(); - if (step == 0) { - // Use completedFuture to avoid timing issues with async execution - return Data.of(CompletableFuture - .completedFuture(NodeOutput.of("node-1", "", new OverAllState(), Usage.unknown()))); - } - if (step == 1) { - return Data.done(Map.of("result", "ok")); - } - return Data.done(); - } - }; + void streamingEmitsEventStreamChunksAndAccumulatedResult() throws Exception { + HttpServer server = startServer(200, "text/event-stream", """ + data: {"jsonrpc":"2.0","id":"1","result":{"kind":"artifact-update","artifact":{"parts":[{"text":"hello "}]}}} - Flux> flux = invokeToFlux(generator); + data: {"jsonrpc":"2.0","id":"1","result":{"kind":"artifact-update","artifact":{"parts":[{"text":"world"}]}}} - List> responses = flux.collectList().block(Duration.ofSeconds(5)); + data: [DONE] + """); + try { + List> responses = stream(baseUrl(server)); + + assertEquals(3, responses.size()); + assertEquals("hello ", streamingText(responses.get(0))); + assertEquals("world", streamingText(responses.get(1))); + GraphResponse terminal = responses.get(2); + assertTrue(terminal.isDone()); + assertEquals("hello world", ((Map) terminal.resultValue().orElseThrow()).get("reply")); + } + finally { + server.stop(0); + } + } - assertNotNull(responses); - assertEquals(2, responses.size()); + @Test + void streamingReportsHttpFailureAndStillCompletes() throws Exception { + HttpServer server = startServer(500, "application/json", "{}"); + try { + List> responses = stream(baseUrl(server)); - GraphResponse first = responses.get(0); - assertFalse(first.isDone()); - NodeOutput output = first.getOutput().getNow(null); - assertNotNull(output); - assertEquals("node-1", output.node()); + assertEquals(2, responses.size()); + assertEquals("HTTP request failed, status: 500", streamingText(responses.get(0))); + assertTrue(responses.get(1).isDone()); + assertEquals("", ((Map) responses.get(1).resultValue().orElseThrow()).get("reply")); + } + finally { + server.stop(0); + } + } - GraphResponse second = responses.get(1); - assertTrue(second.isDone()); - Map resultValue = (Map) second.resultValue().orElseThrow(); - assertEquals("ok", resultValue.get("result")); + @Test + void streamingWithoutAgentUrlCompletesInsteadOfWaitingForever() throws Exception { + List> responses = stream(null); + + assertEquals(2, responses.size()); + assertEquals("Error: AgentCard.url is empty", streamingText(responses.get(0))); + assertTrue(responses.get(1).isDone()); } @Test - void toFluxDrainsCompletedFuturesWithoutRecursiveStackGrowth() throws Exception { - int count = 20_000; - AsyncGenerator generator = new AsyncGenerator<>() { - private final AtomicInteger index = new AtomicInteger(); - - @Override - public Data next() { - int step = index.getAndIncrement(); - if (step < count) { - return Data.of(NodeOutput.of("node-" + step, "", new OverAllState(), Usage.unknown())); - } - return Data.done(Map.of("result", "ok")); - } - }; + void streamingDoesNotContactServerBeforeSubscription() throws Exception { + AtomicInteger requests = new AtomicInteger(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + requests.incrementAndGet(); + exchange.sendResponseHeaders(500, -1); + exchange.close(); + }); + server.start(); + try { + A2aNodeActionWithConfig node = new A2aNodeActionWithConfig(createAgentCardWrapper(baseUrl(server)), + "remote-agent", false, "reply", "instruction", true); + node.apply(new OverAllState(), RunnableConfig.builder().threadId("lazy").build()); - Flux> flux = invokeToFlux(generator); + assertEquals(0, requests.get()); + } + finally { + server.stop(0); + } + } - List> responses = flux.collectList().block(Duration.ofSeconds(5)); + @Test + void forwardsUserIdFromConfiguredMetadataKey() throws Exception { + ObjectMapper json = new ObjectMapper(); + AtomicReference captured = new AtomicReference<>(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + captured.set(json.readTree(exchange.getRequestBody())); + byte[] bytes = """ + {"jsonrpc":"2.0","id":"1","result":{"kind":"artifact-update","artifact":{"parts":[{"text":"ok"}]}}} + """.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + }); + server.start(); + try { + A2aNodeActionWithConfig node = new A2aNodeActionWithConfig(createAgentCardWrapper(baseUrl(server)), + "remote-agent", false, "reply", "instruction", false) + .userIdMetadataKey("tenantUser"); + RunnableConfig config = RunnableConfig.builder() + .threadId("user-key") + .addMetadata("tenantUser", "u-42") + .addMetadata("userId", "ignored") + .build(); + + node.apply(new OverAllState(), config); + + assertEquals("u-42", captured.get().at("/params/metadata/userId").asText()); + assertThrows(IllegalArgumentException.class, () -> node.userIdMetadataKey(" ")); + } + finally { + server.stop(0); + } + } + @SuppressWarnings("unchecked") + private static List> stream(String baseUrl) throws Exception { + A2aNodeActionWithConfig node = new A2aNodeActionWithConfig(createAgentCardWrapper(baseUrl), "remote-agent", + false, "reply", "instruction", true); + Map result = node.apply(new OverAllState(), + RunnableConfig.builder().threadId("stream-test").build()); + Flux> flux = (Flux>) result.get("reply"); + List> responses = flux.collectList().block(Duration.ofSeconds(5)); assertNotNull(responses); - assertEquals(count + 1, responses.size()); - assertEquals("node-0", responses.get(0).getOutput().getNow(null).node()); - assertEquals("node-" + (count - 1), responses.get(count - 1).getOutput().getNow(null).node()); - assertTrue(responses.get(count).isDone()); + return responses; } - @Test - void toFluxPropagatesErrors() throws Exception { - AsyncGenerator generator = new AsyncGenerator<>() { - private final AtomicInteger index = new AtomicInteger(); - - @Override - public Data next() { - int step = index.getAndIncrement(); - if (step == 0) { - // Use a completed exceptionally future to avoid timing issues - CompletableFuture failedFuture = new CompletableFuture<>(); - failedFuture.completeExceptionally(new IllegalStateException("boom")); - return Data.of(failedFuture); - } - return Data.done(); - } - }; + private static String streamingText(GraphResponse response) { + assertFalse(response.isDone()); + StreamingOutput output = assertInstanceOf(StreamingOutput.class, response.getOutput().getNow(null)); + return output.text(); + } - Flux> flux = invokeToFlux(generator); + private static HttpServer startServer(int status, String contentType, String body) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", contentType); + exchange.sendResponseHeaders(status, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + }); + server.start(); + return server; + } - IllegalStateException exception = assertThrows(IllegalStateException.class, - () -> flux.collectList().block(Duration.ofSeconds(5))); - assertEquals("boom", exception.getMessage()); + private static String baseUrl(HttpServer server) { + return "http://127.0.0.1:" + server.getAddress().getPort() + "/"; } @Test @@ -251,22 +297,6 @@ void rendersInstructionBeforeSendingAndRejectsMissingVariablesBeforeTransport(bo } } - @SuppressWarnings("unchecked") - private Flux> invokeToFlux(AsyncGenerator generator) throws Exception { - return (Flux>) TO_FLUX.invoke(this.action, generator); - } - - private static Method initToFluxMethod() { - try { - Method method = A2aNodeActionWithConfig.class.getDeclaredMethod("toFlux", AsyncGenerator.class); - method.setAccessible(true); - return method; - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException(ex); - } - } - private static AgentCardWrapper createAgentCardWrapper() { return createAgentCardWrapper(null); } diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolEmulatorInterceptorTests.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolEmulatorInterceptorTests.java index 2c5a9a984..de5d661ef 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolEmulatorInterceptorTests.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolEmulatorInterceptorTests.java @@ -95,6 +95,20 @@ void selectiveModeEmulatesNamedToolsAndUsesCustomPrompt() { verifyNoInteractions(handler); } + @Test + void configuredToolDescriptionIsIncludedInPrompt() { + ToolEmulatorInterceptor interceptor = ToolEmulatorInterceptor.builder() + .chatClient(client) + .toolDescription("weather", "Current weather for a city") + .promptTemplate("Emulate %s (%s) with %s") + .build(); + interceptor.interceptToolCall(request, handler); + ArgumentCaptor captured = ArgumentCaptor.forClass(ChatRequest.class); + verify(client).call(captured.capture()); + assertThat(captured.getValue().messages().get(0).text()) + .isEqualTo("Emulate weather (Current weather for a city) with " + request.getArguments()); + } + @Test void unlistedToolsExecuteNormally() { ToolCallResponse actual = ToolCallResponse.of("call-17", "weather", "Rainy"); diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryInterceptorUnitTest.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryInterceptorUnitTest.java index 4182ec1a3..a543ba3f5 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryInterceptorUnitTest.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryInterceptorUnitTest.java @@ -149,26 +149,4 @@ void nonRetryableExceptionIsRethrownImmediately() { })); assertEquals(1, calls.get()); } - - @Test - @SuppressWarnings("deprecation") - void deprecatedMaxRetriesAliasMapsToAttempts() { - // maxRetries(n) == maxAttempts(n + 1) - AtomicInteger calls = new AtomicInteger(); - ToolRetryInterceptor.builder().maxRetries(2).initialDelay(1).jitter(false).build() - .interceptToolCall(request(), r -> { - calls.incrementAndGet(); - throw new RuntimeException("boom"); - }); - assertEquals(3, calls.get()); - - // The previously buggy edge case: maxRetries(0) must still run the tool once, not zero times. - AtomicInteger zeroCalls = new AtomicInteger(); - ToolRetryInterceptor.builder().maxRetries(0).initialDelay(1).jitter(false).build() - .interceptToolCall(request(), r -> { - zeroCalls.incrementAndGet(); - throw new RuntimeException("boom"); - }); - assertEquals(1, zeroCalls.get()); - } } diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryTest.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryTest.java index 593d64f77..6b9d61753 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryTest.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/interceptors/ToolRetryTest.java @@ -70,7 +70,7 @@ public void testToolRetryOnFailure() throws Exception { // Create tool retry interceptor ToolRetryInterceptor toolRetryInterceptor = ToolRetryInterceptor.builder() - .maxRetries(3) + .maxAttempts(4) .initialDelay(100) .backoffFactor(2.0) .build(); @@ -116,7 +116,7 @@ public void testToolRetryMaxRetriesExceeded() throws Exception { // Create tool retry interceptor with limited retries ToolRetryInterceptor toolRetryInterceptor = ToolRetryInterceptor.builder() - .maxRetries(2) + .maxAttempts(3) .initialDelay(50) .onFailure(ToolRetryInterceptor.OnFailureBehavior.RAISE) .build(); @@ -166,7 +166,7 @@ public void testToolRetryWithSpecificTools() throws Exception { // Create tool retry interceptor for specific tool only ToolRetryInterceptor toolRetryInterceptor = ToolRetryInterceptor.builder() - .maxRetries(2) + .maxAttempts(3) .initialDelay(50) .toolName("specific_tool") .build(); diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeBusinessOutcomeTest.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeBusinessOutcomeTest.java index df4b651a6..8c4200d77 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeBusinessOutcomeTest.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeBusinessOutcomeTest.java @@ -22,8 +22,8 @@ import io.github.agentic.ai.api.tool.ToolContext; import io.github.agentic.ai.api.tool.ToolRegistry; import io.github.agentic.ai.api.tool.ToolResult; -import io.github.agentic.ai.agent.interceptor.BusinessOutcomeMapper; -import io.github.agentic.ai.agent.interceptor.BusinessOutcomeMapperRegistry; +import io.github.agentic.ai.agent.outcome.BusinessOutcomeMapper; +import io.github.agentic.ai.agent.outcome.BusinessOutcomeMapperRegistry; import io.github.agentic.ai.workflow.graph.OverAllState; import io.github.agentic.ai.workflow.graph.RunnableConfig; import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallHandler; diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeContextPatchPolicyTest.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeContextPatchPolicyTest.java index ccfab89d6..1abc9702f 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeContextPatchPolicyTest.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeContextPatchPolicyTest.java @@ -21,7 +21,7 @@ import io.github.agentic.ai.api.tool.ToolContext; import io.github.agentic.ai.api.tool.ToolRegistry; import io.github.agentic.ai.api.tool.ToolResult; -import io.github.agentic.ai.agent.interceptor.BusinessOutcomeMapperRegistry; +import io.github.agentic.ai.agent.outcome.BusinessOutcomeMapperRegistry; import io.github.agentic.ai.workflow.graph.OverAllState; import io.github.agentic.ai.workflow.graph.RunnableConfig; import io.github.agentic.ai.agent.tool.patch.ToolContextPatchPolicy; diff --git a/argi-orchestration/argi-workflow-graph-core/pom.xml b/argi-orchestration/argi-workflow-graph-core/pom.xml index d893a7787..cac9039ee 100644 --- a/argi-orchestration/argi-workflow-graph-core/pom.xml +++ b/argi-orchestration/argi-workflow-graph-core/pom.xml @@ -114,11 +114,6 @@ - - junit - junit - test - org.junit.platform diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/CompiledGraph.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/CompiledGraph.java index 945662eaa..99a4e16c7 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/CompiledGraph.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/CompiledGraph.java @@ -614,21 +614,6 @@ public int getMaxIterations() { return maxIterations; } - /** - * Sets the maximum number of iterations for the graph execution. - * - * @param maxIterations the maximum number of iterations - * @throws IllegalArgumentException if maxIterations is less than or equal to 0 - * @deprecated use CompileConfig.recursionLimit() instead - */ - @Deprecated(forRemoval = true) - public void setMaxIterations(int maxIterations) { - if (maxIterations <= 0) { - throw new IllegalArgumentException("maxIterations must be > 0!"); - } - this.maxIterations = maxIterations; - } - public GraphResponse invokeAndGetResponse(Map inputs, RunnableConfig config) { return graphResponseStream(inputs, config).last().block(); } diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/GraphRunnerContext.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/GraphRunnerContext.java index 6e5a3dd5f..09d2fa2a4 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/GraphRunnerContext.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/GraphRunnerContext.java @@ -208,10 +208,13 @@ private void initializeFromStart(OverAllState initialState, RunnableConfig confi this.nextNodeId = null; } - // FIXME, duplicated method with CompiledGraph.stateCreate, need to have a - // unified way of when and how to do OverallState creation. - // This temporary fix is to make sure the message provided by user is always the - // last element in the messages list. + /** + * Rebuild the run state from merged inputs. Unlike {@code CompiledGraph.stateCreate}, + * which starts a new run from the graph's key strategies and compile-time store, this + * keeps the key strategies and store of the state handed to the runner, so resumed and + * subgraph runs observe the same merge rules. Rebuilding after the merge also keeps + * the message provided by the user as the last element of the messages list. + */ private OverAllState stateCreate(Map inputs, OverAllState initialState) { // Creates a new OverAllState instance using key strategies from the graph and // provided input data. @@ -390,11 +393,20 @@ public BaseCheckpointSaver.Tag releaseCheckpoint() throws Exception { // ================================================================================================================ public NodeOutput buildOutput(String nodeId, Optional checkpoint) throws Exception { + Optional snapshot = snapshotOutput(checkpoint); + return snapshot.isPresent() ? snapshot.get() : buildNodeOutput(nodeId); + } + + /** + * In {@link CompiledGraph.StreamMode#SNAPSHOTS} mode a node that produced a checkpoint + * is reported as the persisted snapshot instead of an incremental output. + */ + private Optional snapshotOutput(Optional checkpoint) { if (checkpoint.isPresent() && config.streamMode() == CompiledGraph.StreamMode.SNAPSHOTS) { - return StateSnapshot.of(getKeyStrategyMap(), checkpoint.get(), config, - compiledGraph.stateGraph.getStateSerializer().stateFactory()); + return Optional.of(StateSnapshot.of(getKeyStrategyMap(), checkpoint.get(), config, + compiledGraph.stateGraph.getStateSerializer().stateFactory())); } - return buildNodeOutput(nodeId); + return Optional.empty(); } public StreamingOutput buildStreamingOutput(ChatMessage message, Object originData, String nodeId, @@ -637,8 +649,8 @@ public Optional value(TypeRef ref) { } /** - * FIXME Below are duplicated methods. Need to have a unified way of streaming output - * to end user. + * Record a checkpoint for the current transition and build the output reported for the + * node, carrying the latest message turn from {@code updateStates}. */ public NodeOutput buildNodeOutputAndAddCheckpoint(Map updateStates) throws Exception { Optional cp = addCheckpoint(currentNodeId, nextNodeId); @@ -647,11 +659,8 @@ public NodeOutput buildNodeOutputAndAddCheckpoint(Map updateStat public NodeOutput buildOutput(String nodeId, Map updateStates, Optional checkpoint, boolean streaming) throws Exception { - if (checkpoint.isPresent() && config.streamMode() == CompiledGraph.StreamMode.SNAPSHOTS) { - return StateSnapshot.of(getKeyStrategyMap(), checkpoint.get(), config, - compiledGraph.stateGraph.getStateSerializer().stateFactory()); - } - return buildNodeOutput(nodeId, updateStates, streaming); + Optional snapshot = snapshotOutput(checkpoint); + return snapshot.isPresent() ? snapshot.get() : buildNodeOutput(nodeId, updateStates, streaming); } public NodeOutput buildNodeOutput(String nodeId, Map updateStates, boolean streaming) diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/HasMetadata.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/HasMetadata.java index 58b599c28..96c6239d0 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/HasMetadata.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/HasMetadata.java @@ -57,17 +57,6 @@ default Optional metadata(String key, TypeRef typeRef) { return metadata(key).flatMap(typeRef::cast); } - /** - * return metadata value for key - * @param key given metadata key - * @return metadata value for key if any - * @deprecated use {@link #metadata(String)} instead - */ - @Deprecated(forRemoval = true) - default Optional getMetadata(String key) { - return metadata(key); - }; - default Optional getMetadataAndRemove(String key, TypeRef typeRef) { Optional value = metadata(key, typeRef); if (value.isPresent()) { diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/RunnableConfig.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/RunnableConfig.java index 583ab3dbf..91d90d69a 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/RunnableConfig.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/RunnableConfig.java @@ -300,7 +300,13 @@ public Optional interruptData(String key) { return ofNullable(interruptedNodes).map(m -> m.get(key)); } - // FIXME, allow modification or not? + /** + * Return the live metadata map of this config. The map is intentionally mutable: + * {@code HasMetadata.getMetadataAndRemove(String, TypeRef)} consumes one-shot entries + * such as resume feedback in place. Callers that only read metadata should use + * {@link #metadata(String)}. + * @return the mutable metadata map + */ @Override public Optional> metadata() { return Optional.of(metadata); diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/StateGraph.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/StateGraph.java index 63da88cf1..dbc81e7eb 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/StateGraph.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/StateGraph.java @@ -32,7 +32,6 @@ import io.github.agentic.ai.workflow.graph.internal.node.SubCompiledGraphNode; import io.github.agentic.ai.workflow.graph.internal.node.SubStateGraphNode; import io.github.agentic.ai.workflow.graph.serializer.StateSerializer; -import io.github.agentic.ai.workflow.graph.serializer.plain_text.PlainTextStateSerializer; import io.github.agentic.ai.workflow.graph.serializer.plain_text.jackson.ArgiJacksonStateSerializer; import io.github.agentic.ai.workflow.graph.state.AgentStateFactory; import com.fasterxml.jackson.databind.ObjectMapper; @@ -111,32 +110,6 @@ public class StateGraph { public static final StateSerializer DEFAULT_JACKSON_SERIALIZER = new ArgiJacksonStateSerializer(OverAllState::new, new ObjectMapper()); - /** - * Constructs a StateGraph with the specified name, key strategy factory, and state - * serializer. - * @param name the name of the graph - * @param keyStrategyFactory the factory for providing key strategies - * @param stateSerializer the plain text state serializer to use - * @deprecated Use {@link #StateGraph(String, KeyStrategyFactory, StateSerializer)} - * instead - */ - @Deprecated - public StateGraph(String name, KeyStrategyFactory keyStrategyFactory, PlainTextStateSerializer stateSerializer) { - this(name, keyStrategyFactory, (StateSerializer) stateSerializer); - } - - /** - * Constructs a StateGraph with the specified key strategy factory and plain text - * state serializer. - * @param keyStrategyFactory the factory for providing key strategies - * @param stateSerializer the plain text state serializer to use - * @deprecated Use {@link #StateGraph(KeyStrategyFactory, StateSerializer)} instead - */ - @Deprecated - public StateGraph(KeyStrategyFactory keyStrategyFactory, PlainTextStateSerializer stateSerializer) { - this(keyStrategyFactory, (StateSerializer) stateSerializer); - } - public StateGraph(String name, KeyStrategyFactory keyStrategyFactory) { this(name, keyStrategyFactory, DEFAULT_JACKSON_SERIALIZER); } diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGenerator.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGenerator.java deleted file mode 100644 index 45388388d..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGenerator.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async; - -import io.github.agentic.ai.workflow.graph.async.internal.UnmodifiableDeque; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import static java.util.Optional.ofNullable; -import static java.util.concurrent.CompletableFuture.completedFuture; - -/** - * An asynchronous generator interface that allows generating asynchronous elements. - * - * @param the type of elements. The generator will emit {@link CompletableFuture - * CompletableFutures<E>} elements - */ -@Deprecated -public interface AsyncGenerator extends Iterable, AsyncGeneratorOperators { - - interface HasResultValue { - - Optional resultValue(); - - } - - static Optional resultValue(AsyncGenerator generator) { - if (generator instanceof HasResultValue withResult) { - return withResult.resultValue(); - } - return Optional.empty(); - } - - static Optional resultValue(Iterator iterator) { - if (iterator instanceof HasResultValue withResult) { - return withResult.resultValue(); - } - return Optional.empty(); - } - - /** - * An asynchronous generator decorator that allows retrieving the result value of the - * asynchronous operation, if any. - * - * @param the type of elements in the generator - */ - class WithResult implements AsyncGenerator, HasResultValue { - - protected final AsyncGenerator delegate; - - private Object resultValue; - - public WithResult(AsyncGenerator delegate) { - this.delegate = delegate; - } - - public AsyncGenerator delegate() { - return delegate; - } - - /** - * Retrieves the result value of the generator, if any. - * @return an {@link Optional} containing the result value if present, or an empty - * Optional if not - */ - public Optional resultValue() { - return ofNullable(resultValue); - }; - - @Override - public final Data next() { - final Data result = delegate.next(); - if (result.isDone()) { - resultValue = result.resultValue; - } - return result; - } - - } - - /** - * An asynchronous generator decorator that allows to generators composition embedding - * other generators. - * - * @param the type of elements in the generator - */ - class WithEmbed implements AsyncGenerator, HasResultValue { - - protected final Deque> generatorsStack = new ArrayDeque<>(2); - - private final Deque> returnValueStack = new ArrayDeque<>(2); - - public WithEmbed(AsyncGenerator delegate, EmbedCompletionHandler onGeneratorDoneWithResult) { - generatorsStack.push(new Embed<>(delegate, onGeneratorDoneWithResult)); - } - - public WithEmbed(AsyncGenerator delegate) { - this(delegate, null); - } - - public Deque> resultValues() { - return new UnmodifiableDeque<>(returnValueStack); - } - - public Optional resultValue() { - return ofNullable(returnValueStack.peek()).map(r -> r.resultValue); - } - - private void clearPreviousReturnsValuesIfAny() { - // Check if the return values are which ones from previous run - if (returnValueStack.size() > 1 && returnValueStack.size() == generatorsStack.size()) { - returnValueStack.clear(); - } - } - - // private AsyncGenerator.WithResult toGeneratorWithResult( AsyncGenerator - // generator ) { - // return ( generator instanceof WithResult ) ? - // (AsyncGenerator.WithResult) generator : - // new WithResult<>(generator); - // } - - protected boolean isLastGenerator() { - return generatorsStack.size() == 1; - } - - @Override - public Data next() { - if (generatorsStack.isEmpty()) { // GUARD - throw new IllegalStateException("no generator found!"); - } - - final Embed embed = generatorsStack.peek(); - final Data result = embed.generator.next(); - - if (result.isDone()) { - clearPreviousReturnsValuesIfAny(); - returnValueStack.push(result); - if (embed.onCompletion != null /* && result.resultValue != null */ ) { - try { - embed.onCompletion.accept(result.resultValue); - } - catch (Exception e) { - return Data.error(e); - } - } - if (isLastGenerator()) { - return result; - } - generatorsStack.pop(); - return next(); - } - if (result.embed != null) { - if (generatorsStack.size() >= 2) { - return Data.error(new UnsupportedOperationException( - "Currently recursive nested generators are not supported!")); - } - generatorsStack.push(result.embed); - return next(); - } - - return result; - } - - } - - @FunctionalInterface - interface EmbedCompletionHandler { - - void accept(Object t) throws Exception; - - } - - class Embed { - - final AsyncGenerator generator; - - final EmbedCompletionHandler onCompletion; - - public Embed(AsyncGenerator generator, EmbedCompletionHandler onCompletion) { - Objects.requireNonNull(generator, "generator cannot be null"); - this.generator = generator; - this.onCompletion = onCompletion; - } - - public AsyncGenerator getGenerator() { - return generator; - } - - } - - /** - * Represents a data element in the AsyncGenerator. - * - * @param the type of the data element - */ - class Data { - - final CompletableFuture data; - - final Embed embed; - - final Object resultValue; - - public Data(CompletableFuture data, Embed embed, Object resultValue) { - this.data = data; - this.embed = embed; - this.resultValue = resultValue; - } - - public CompletableFuture getData() { - return data; - } - - public Embed getEmbed() { - return embed; - } - - public Optional resultValue() { - return resultValue == null ? Optional.empty() : Optional.of(resultValue); - } - - public boolean isDone() { - return data == null && embed == null; - } - - public boolean isError() { - return data != null && data.isCompletedExceptionally(); - } - - public static Data of(CompletableFuture data) { - return new Data<>(data, null, null); - } - - public static Data of(E data) { - return new Data<>(completedFuture(data), null, null); - } - - public static Data composeWith(AsyncGenerator generator, EmbedCompletionHandler onCompletion) { - return new Data<>(null, new Embed<>(generator, onCompletion), null); - } - - public static Data done() { - return new Data<>(null, null, null); - } - - public static Data done(Object resultValue) { - return new Data<>(null, null, resultValue); - } - - public static Data error(Throwable exception) { - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(exception); - return Data.of(future); - } - - } - - default AsyncGeneratorOperators async(Executor executor) { - return new AsyncGeneratorOperators() { - @Override - public Data next() { - return AsyncGenerator.this.next(); - } - - @Override - public Executor executor() { - return executor; - } - }; - } - - /** - * Retrieves the next asynchronous element. - * @return the next element from the generator - */ - Data next(); - - /** - * Converts the AsyncGenerator to a CompletableFuture. - * @return a CompletableFuture representing the completion of the AsyncGenerator - */ - default CompletableFuture toCompletableFuture() { - final Data next = next(); - if (next.isDone()) { - return completedFuture(next.resultValue); - } - return next.data.thenCompose(v -> toCompletableFuture()); - } - - /** - * Returns a sequential Stream with the elements of this AsyncGenerator. Each - * CompletableFuture is resolved and then make available to the stream. - * @return a Stream of elements from the AsyncGenerator - */ - default Stream stream() { - return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED), false); - } - - /** - * Returns an iterator over the elements of this AsyncGenerator. Each call to `next` - * retrieves the next "resolved" asynchronous element from the generator. - * @return an iterator over the elements of this AsyncGenerator - */ - default Iterator iterator() { - return new InternalIterator(this); - } - - /** - * Returns an empty AsyncGenerator. - * @param the type of elements - * @return an empty AsyncGenerator - */ - static AsyncGenerator empty() { - return Data::done; - } - - /** - * create a generator, mapping each element to an asynchronous counterpart. - * @param the type of elements in the collection - * @param the type of elements in the CompletableFuture - * @param iterator the elements iterator - * @param mapFunction the function to map elements to {@link CompletableFuture} - * @return an AsyncGenerator instance with mapped elements - */ - static AsyncGenerator map(Iterator iterator, Function> mapFunction) { - return () -> { - if (!iterator.hasNext()) { - return Data.done(); - } - return Data.of(mapFunction.apply(iterator.next())); - }; - } - - /** - * Collects asynchronous elements from an iterator. - * @param the type of elements in the iterator - * @param the type of elements in the CompletableFuture - * @param iterator the iterator containing elements to collect - * @param consumer the function to consume elements and add them to the accumulator - * @return an AsyncGenerator instance with collected elements - */ - static AsyncGenerator collect(Iterator iterator, - BiConsumer>> consumer) { - final List> accumulator = new ArrayList<>(); - - final Consumer> addElement = accumulator::add; - while (iterator.hasNext()) { - consumer.accept(iterator.next(), addElement); - } - - final Iterator> it = accumulator.iterator(); - return () -> { - if (!it.hasNext()) { - return Data.done(); - } - return Data.of(it.next()); - }; - } - - /** - * create a generator, mapping each element to an asynchronous counterpart. - * @param the type of elements in the collection - * @param the type of elements in the CompletableFuture - * @param collection the collection of elements to map - * @param mapFunction the function to map elements to CompletableFuture - * @return an AsyncGenerator instance with mapped elements - */ - static AsyncGenerator map(Collection collection, Function> mapFunction) { - if (collection == null || collection.isEmpty()) { - return empty(); - } - return map(collection.iterator(), mapFunction); - } - - /** - * Collects asynchronous elements from a collection. - * @param the type of elements in the iterator - * @param the type of elements in the CompletableFuture - * @param collection the iterator containing elements to collect - * @param consumer the function to consume elements and add them to the accumulator - * @return an AsyncGenerator instance with collected elements - */ - static AsyncGenerator collect(Collection collection, - BiConsumer>> consumer) { - if (collection == null || collection.isEmpty()) { - return empty(); - } - return collect(collection.iterator(), consumer); - } - - /** - * Creates an AsyncGenerator from a Project Reactor Flux. This method provides - * backward compatibility for converting reactive streams to the AsyncGenerator - * interface. - * @param the type of elements in the Flux - * @param flux the Flux to convert - * @return an AsyncGenerator that wraps the Flux - */ - static AsyncGenerator fromFlux(reactor.core.publisher.Flux flux) { - Objects.requireNonNull(flux, "flux cannot be null"); - - // Convert Flux to Iterator using blocking approach for simplicity - // This maintains compatibility with the existing AsyncGenerator pattern - final Iterator iterator = flux.toIterable().iterator(); - - return () -> { - if (!iterator.hasNext()) { - return Data.done(); - } - try { - E element = iterator.next(); - return Data.of(element); - } - catch (Exception e) { - return Data.error(e); - } - }; - } - -} - -@Deprecated -class InternalIterator implements Iterator, AsyncGenerator.HasResultValue { - - private final AsyncGenerator delegate; - - final AtomicReference> currentFetchedData; - - public InternalIterator(AsyncGenerator delegate) { - this.delegate = delegate; - currentFetchedData = new AtomicReference<>(delegate.next()); - } - - @Override - public boolean hasNext() { - final var value = currentFetchedData.get(); - return value != null && !value.isDone(); - } - - @Override - public E next() { - var next = currentFetchedData.get(); - - if (next == null || next.isDone()) { - throw new IllegalStateException("no more elements into iterator"); - } - - if (!next.isError()) { - currentFetchedData.set(delegate.next()); - } - - return next.data.join(); - } - - @Override - public Optional resultValue() { - if (delegate instanceof AsyncGenerator.HasResultValue withResult) { - return withResult.resultValue(); - } - return Optional.empty(); - } - -}; diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorOperators.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorOperators.java deleted file mode 100644 index fbc27faba..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorOperators.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -@Deprecated -public interface AsyncGeneratorOperators { - - AsyncGenerator.Data next(); - - default Executor executor() { - return Runnable::run; - } - - /** - * Maps the elements of this generator to a new asynchronous generator. - * @param mapFunction the function to map elements to a new asynchronous counterpart - * @param the type of elements in the new generator - * @return a generator with mapped elements - */ - default AsyncGenerator map(Function mapFunction) { - return () -> { - final AsyncGenerator.Data next = next(); - if (next.isDone()) { - return AsyncGenerator.Data.done(next.resultValue); - } - return AsyncGenerator.Data.of(next.data.thenApplyAsync(mapFunction, executor())); - }; - } - - /** - * Maps the elements of this generator to a new asynchronous generator, and flattens - * the resulting nested generators. - * @param mapFunction the function to map elements to a new asynchronous counterpart - * @param the type of elements in the new generator - * @return a generator with mapped and flattened elements - */ - default AsyncGenerator flatMap(Function> mapFunction) { - return () -> { - final AsyncGenerator.Data next = next(); - if (next.isDone()) { - return AsyncGenerator.Data.done(next.resultValue); - } - return AsyncGenerator.Data.of(next.data.thenComposeAsync(mapFunction, executor())); - }; - } - - /** - * Filters the elements of this generator based on the given predicate. Only elements - * that satisfy the predicate will be included in the resulting generator. - * @param predicate the predicate to test elements against - * @return a generator with elements that satisfy the predicate - */ - default AsyncGenerator filter(Predicate predicate) { - return () -> { - AsyncGenerator.Data next = next(); - while (!next.isDone()) { - - final E value = next.data.join(); - - if (predicate.test(value)) { - return next; - } - next = next(); - } - return AsyncGenerator.Data.done(next.resultValue); - }; - } - - /** - * Asynchronously iterates over the elements of the AsyncGenerator and applies the - * given consumer to each element. - * @param consumer the consumer function to be applied to each element - * @return a CompletableFuture representing the completion of the iteration process. - */ - default CompletableFuture forEachAsync(Consumer consumer) { - CompletableFuture future = completedFuture(null); - for (AsyncGenerator.Data next = next(); !next.isDone(); next = next()) { - final AsyncGenerator.Data finalNext = next; - if (finalNext.embed != null) { - future = future.thenCompose(v -> finalNext.embed.generator.async(executor()).forEachAsync(consumer)); - } - else { - future = future - .thenCompose(v -> finalNext.data.thenAcceptAsync(consumer, executor()).thenApply(x -> null)); - } - } - return future; - } - - /** - * Collects elements from the AsyncGenerator asynchronously into a list. - * @param the type of the result list - * @param result the result list to collect elements into - * @param consumer the consumer function for processing elements - * @return a CompletableFuture representing the completion of the collection process - */ - default > CompletableFuture collectAsync(R result, BiConsumer consumer) { - CompletableFuture future = completedFuture(result); - for (AsyncGenerator.Data next = next(); !next.isDone(); next = next()) { - final AsyncGenerator.Data finalNext = next; - future = future.thenCompose(res -> finalNext.data.thenApplyAsync(v -> { - consumer.accept(res, v); - return res; - }, executor())); - } - return future; - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorQueue.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorQueue.java deleted file mode 100644 index 5abea5659..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/AsyncGeneratorQueue.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async; - -import java.util.Objects; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -import static java.util.concurrent.ForkJoinPool.commonPool; - -/** - * Represents a queue-based asynchronous generator. - */ -@Deprecated -public class AsyncGeneratorQueue { - - /** - * Inner class to generate asynchronous elements from the queue. - * - * @param the type of elements in the queue - */ - public static class Generator implements AsyncGenerator { - - Data isEnd = null; - - final BlockingQueue> queue; - - /** - * Constructs a Generator with the specified queue. - * @param queue the blocking queue to generate elements from - */ - public Generator(BlockingQueue> queue) { - this.queue = queue; - } - - public BlockingQueue> queue() { - return queue; - } - - /** - * Retrieves the next element from the queue asynchronously. - * @return the next element from the queue - */ - @Override - public Data next() { - while (isEnd == null) { - Data value = queue.poll(); - if (value != null) { - if (value.isDone()) { - isEnd = value; - } - return value; - } - } - return isEnd; - } - - } - - /** - * Creates an AsyncGenerator from the provided blocking queue and consumer. - * @param the type of elements in the queue - * @param the type of blocking queue - * @param queue the blocking queue to generate elements from - * @param consumer the consumer for processing elements from the queue - * @return an AsyncGenerator instance - */ - public static >> AsyncGenerator of(Q queue, - Consumer consumer) { - return of(queue, consumer, commonPool()); - } - - /** - * Creates an AsyncGenerator from the provided queue, executor, and consumer. - * @param the type of elements in the queue - * @param the type of blocking queue - * @param queue the blocking queue to generate elements from - * @param consumer the consumer for processing elements from the queue - * @param executor the executor for asynchronous processing - * @return an AsyncGenerator instance - */ - public static >> AsyncGenerator of(Q queue, - Consumer consumer, Executor executor) { - Objects.requireNonNull(queue); - Objects.requireNonNull(executor); - Objects.requireNonNull(consumer); - - executor.execute(() -> { - try { - consumer.accept(queue); - } - catch (Throwable ex) { - CompletableFuture error = new CompletableFuture<>(); - error.completeExceptionally(ex); - queue.add(AsyncGenerator.Data.of(error)); - } - finally { - queue.add(AsyncGenerator.Data.done()); - } - - }); - - return new Generator<>(queue); - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/FlowGenerator.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/FlowGenerator.java deleted file mode 100644 index 6ac1e6775..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/FlowGenerator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async; - -import io.github.agentic.ai.workflow.graph.async.internal.reactive.GeneratorPublisher; -import io.github.agentic.ai.workflow.graph.async.internal.reactive.GeneratorSubscriber; - -import java.util.concurrent.Flow; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.function.Supplier; - -/** - * Provides methods for converting between {@link FlowGenerator} and various - * {@link Flow.Publisher} types. - * - * @since 3.0.0 - */ -@Deprecated -public interface FlowGenerator { - - /** - * Creates an {@code AsyncGenerator} from a {@code Flow.Publisher}. - * @param the type of item emitted by the publisher - * @param

the type of the publisher - * @param publisher the publisher to subscribe to for retrieving items asynchronously - * @param mapResult function that will set generator's result - * @return an {@code AsyncGenerator} that emits items from the publisher - */ - @SuppressWarnings("unchecked") - static , R> AsyncGenerator fromPublisher(P publisher, Supplier mapResult) { - var queue = new LinkedBlockingQueue>(); - return new GeneratorSubscriber<>(publisher, (Supplier) mapResult, queue); - } - - /** - * Creates an {@code AsyncGenerator} from a {@code Flow.Publisher}. - * @param the type of item emitted by the publisher - * @param

the type of the publisher - * @param publisher the publisher to subscribe to for retrieving items asynchronously - * @return an {@code AsyncGenerator} that emits items from the publisher - */ - static > AsyncGenerator fromPublisher(P publisher) { - return fromPublisher(publisher, null); - } - - /** - * Converts an {@code AsyncGenerator} into a {@code Flow.Publisher}. - * @param the type of elements emitted by the publisher - * @param generator the async generator to convert - * @return a flow publisher - */ - static Flow.Publisher toPublisher(AsyncGenerator generator) { - return new GeneratorPublisher<>(generator); - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/UnmodifiableDeque.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/UnmodifiableDeque.java deleted file mode 100644 index 1e43ada02..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/UnmodifiableDeque.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async.internal; - -import java.util.Collection; -import java.util.Deque; -import java.util.Iterator; - -@Deprecated -public class UnmodifiableDeque implements Deque { - - private final Deque deque; - - public UnmodifiableDeque(Deque deque) { - this.deque = deque; - } - - @Override - public boolean add(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean offer(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public T remove() { - throw new UnsupportedOperationException(); - } - - @Override - public T poll() { - throw new UnsupportedOperationException(); - } - - @Override - public T element() { - return deque.element(); - } - - @Override - public T peek() { - return deque.peek(); - } - - @Override - public void addFirst(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public void addLast(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean offerFirst(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean offerLast(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public T removeFirst() { - throw new UnsupportedOperationException(); - } - - @Override - public T removeLast() { - throw new UnsupportedOperationException(); - } - - @Override - public T pollFirst() { - throw new UnsupportedOperationException(); - } - - @Override - public T pollLast() { - throw new UnsupportedOperationException(); - } - - @Override - public T getFirst() { - return deque.getFirst(); - } - - @Override - public T getLast() { - return deque.getLast(); - } - - @Override - public T peekFirst() { - return deque.peekFirst(); - } - - @Override - public T peekLast() { - return deque.peekLast(); - } - - @Override - public boolean removeFirstOccurrence(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeLastOccurrence(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean addAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsAll(Collection c) { - return deque.containsAll(c); - } - - @Override - public boolean contains(Object o) { - return deque.contains(o); - } - - @Override - public int size() { - return deque.size(); - } - - @Override - public boolean isEmpty() { - return deque.isEmpty(); - } - - @Override - public Iterator iterator() { - return deque.iterator(); - } - - @Override - public Object[] toArray() { - return deque.toArray(); - } - - @Override - public T1[] toArray(T1[] a) { - return deque.toArray(a); - } - - @Override - public Iterator descendingIterator() { - return deque.descendingIterator(); - } - - @Override - public void push(T t) { - throw new UnsupportedOperationException(); - } - - @Override - public T pop() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorPublisher.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorPublisher.java deleted file mode 100644 index 5a2ecd172..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorPublisher.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async.internal.reactive; - -import io.github.agentic.ai.workflow.graph.async.AsyncGenerator; - -import java.util.concurrent.Flow; - -/** - * A {@code GeneratorPublisher} is a {@link Flow.Publisher} that generates items from an - * asynchronous generator. - * - * @param the type of items to be published - */ -@Deprecated -public class GeneratorPublisher implements Flow.Publisher { - - private final AsyncGenerator delegate; - - /** - * Constructs a new GeneratorPublisher with the specified async - * generator. - * @param delegate The async generator to be used by this publisher. - */ - public GeneratorPublisher(AsyncGenerator delegate) { - this.delegate = delegate; - } - - /** - * Subscribes the provided {@code Flow.Subscriber} to this signal. The subscriber - * receives initial subscription, handles asynchronous data flow, and manages any - * errors or completion signals. - * @param subscriber The subscriber to which the signal will be delivered. - */ - @Override - public void subscribe(Flow.Subscriber subscriber) { - subscriber.onSubscribe(new Flow.Subscription() { - /** - * Requests more elements from the upstream Publisher. - * - *

- * The Publisher calls this method to indicate that it wants more items. The - * parameter {@code n} specifies the number of additional items requested. - * @param n the number of items to request, a count greater than zero - */ - @Override - public void request(long n) { - } - - /** - * Cancels the operation. - * @throws UnsupportedOperationException if the method is not yet implemented. - */ - @Override - public void cancel() { - throw new UnsupportedOperationException("cancel is not implemented yet!"); - } - }); - - delegate.forEachAsync(subscriber::onNext).thenAccept(value -> { - subscriber.onComplete(); - }).exceptionally(ex -> { - subscriber.onError(ex); - return null; - }).join(); - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorSubscriber.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorSubscriber.java deleted file mode 100644 index 678f765a4..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/async/internal/reactive/GeneratorSubscriber.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.async.internal.reactive; - -import io.github.agentic.ai.workflow.graph.async.AsyncGenerator; -import io.github.agentic.ai.workflow.graph.async.AsyncGeneratorQueue; - -import java.util.Optional; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Flow; -import java.util.function.Supplier; - -/** - * Represents a subscriber for generating asynchronous data streams. - * - *

- * This class implements the {@link Flow.Subscriber} and {@link AsyncGenerator} interfaces - * to handle data flow and produce asynchronous data. It is designed to subscribe to a - * publisher, process incoming items, and manage error and completion signals. - *

- * - * @param The type of elements produced by this generator. - */ -@Deprecated -public class GeneratorSubscriber implements Flow.Subscriber, AsyncGenerator { - - private final AsyncGeneratorQueue.Generator delegate; - - private final Supplier mapResult; - - public Optional> mapResult() { - return Optional.ofNullable(mapResult); - } - - /** - * Constructs a new instance of {@code GeneratorSubscriber}. - * @param

the type of the publisher, which must extend {@link Flow.Publisher} - * @param mapResult function that will set generator's result - * @param publisher the source publisher that will push data to this subscriber - * @param queue the blocking queue used for storing asynchronous generator data - */ - public

> GeneratorSubscriber(P publisher, Supplier mapResult, - BlockingQueue> queue) { - this.delegate = new AsyncGeneratorQueue.Generator<>(queue); - this.mapResult = mapResult; - publisher.subscribe(this); - } - - /** - * Constructs a new instance of {@code GeneratorSubscriber}. - * @param

the type of the publisher, which must extend {@link Flow.Publisher} - * @param publisher the source publisher that will push data to this subscriber - * @param queue the blocking queue used for storing asynchronous generator data - */ - public

> GeneratorSubscriber(P publisher, BlockingQueue> queue) { - this(publisher, null, queue); - } - - /** - * Handles the subscription event from a Flux. - *

- * This method is called when a subscription to the source {@link Flow} has been - * established. The provided {@code Flow.Subscription} can be used to manage and - * control the flow of data emissions. - * @param subscription The subscription object representing this resource owner - * lifecycle. Used to signal that resources being subscribed to should not be released - * until this subscription is disposed. - */ - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } - - /** - * Passes the received item to the delegated queue as an {@link Data} object. - * @param item The item to be processed and queued. - */ - @Override - public void onNext(T item) { - delegate.queue().add(Data.of(item)); - } - - /** - * Handles an error by queuing it in the delegate's queue with an errored data. - * @param error The Throwable that represents the error to be handled. - */ - @Override - public void onError(Throwable error) { - delegate.queue().add(Data.error(error)); - } - - /** - * This method is called when the asynchronous operation is completed successfully. It - * notifies the delegate that no more data will be provided by adding a done marker to - * the queue. - */ - @Override - public void onComplete() { - delegate.queue().add(Data.done(mapResult().map(Supplier::get).orElse(null))); - } - - /** - * Returns the next {@code Data} object from this iteration. - * @return the next element in the iteration, or null if there is no such element - */ - @Override - public Data next() { - return delegate.next(); - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/internal/node/ParallelNode.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/internal/node/ParallelNode.java index ff9b952eb..9376b111c 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/internal/node/ParallelNode.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/internal/node/ParallelNode.java @@ -36,7 +36,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; @@ -61,12 +60,6 @@ public class ParallelNode extends Node { public static final String MAX_CONCURRENCY_KEY = "__MAX_CONCURRENCY__"; - static { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - shutdownDefaultExecutor(); - }, "default-executor-shutdown-hook")); - } - private static final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() { private final AtomicInteger threadNum = new AtomicInteger(1); @@ -76,7 +69,8 @@ public class ParallelNode extends Node { public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName(prefix + threadNum.getAndIncrement()); - thread.setDaemon(false); // Non-daemon thread to ensure task completion + // The shared pool is never shut down, so its workers must not keep the JVM alive. + thread.setDaemon(true); thread.setUncaughtExceptionHandler((t, e) -> logger .error("Thread {} encountered an exception during task execution: {}", t.getName(), e.getMessage(), e)); return thread; @@ -117,10 +111,12 @@ public static NodeAggregationStrategy getAggregationStrategy(RunnableConfig conf * Optimized default thread pool executor based on industry best practices. Features: * 1. Dynamic core pool size based on system resources 2. Higher maximum pool size for * handling burst workloads 3. Larger work queue to buffer tasks during peak loads 4. - * Caller runs policy to prevent task rejection under extreme load 5. Configurable - * keep-alive time for better resource management + * Caller runs policy to prevent task rejection under extreme load 5. Idle core and + * burst threads time out, so the process-wide pool releases all threads when idle and + * never needs an explicit shutdown. Applications that need lifecycle control should + * provide their own executor through {@link RunnableConfig}. */ - private static final ExecutorService DEFAULT_EXECUTOR = new ThreadPoolExecutor( + private static final ThreadPoolExecutor DEFAULT_EXECUTOR = new ThreadPoolExecutor( // Core thread count: CPU cores * 2 (optimized for mixed IO/CPU workloads) calculateCorePoolSize(), // Maximum thread count: CPU cores * 4 (handles burst workloads) @@ -162,13 +158,12 @@ protected void afterExecute(Runnable r, Throwable t) { } } - @Override - protected void terminated() { - super.terminated(); - logger.info("ParallelNode default thread pool terminated"); - } }; + static { + DEFAULT_EXECUTOR.allowCoreThreadTimeOut(true); + } + /** * Calculate optimal core pool size based on system resources and workload * characteristics. For mixed IO/CPU workloads, 2x CPU cores is typically optimal. @@ -212,28 +207,6 @@ private static int calculateQueueCapacity() { return queueCapacity; } - public static void shutdownDefaultExecutor() { - if (!DEFAULT_EXECUTOR.isShutdown()) { - logger.info("Shutting down ParallelNode default executor"); - DEFAULT_EXECUTOR.shutdown(); - try { - // Wait for 60 seconds, force shutdown if tasks remain unfinished - if (!DEFAULT_EXECUTOR.awaitTermination(60, TimeUnit.SECONDS)) { - logger.warn("ParallelNode default executor did not terminate gracefully, forcing shutdown"); - DEFAULT_EXECUTOR.shutdownNow(); - } - else { - logger.info("ParallelNode default executor shut down successfully"); - } - } - catch (InterruptedException e) { - logger.warn("Interrupted while waiting for ParallelNode default executor shutdown", e); - DEFAULT_EXECUTOR.shutdownNow(); - Thread.currentThread().interrupt(); // Preserve interrupt status - } - } - } - public static String formatNodeId(String nodeId) { return format("%s(%s)", PARALLEL_PREFIX, requireNonNull(nodeId, "nodeId cannot be null!")); } diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/serializer/Serializer.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/serializer/Serializer.java index f93611736..a14b8a96d 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/serializer/Serializer.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/serializer/Serializer.java @@ -56,19 +56,9 @@ default T bytesToObject(byte[] bytes) throws IOException, ClassNotFoundException } } - @Deprecated(forRemoval = true) - default byte[] writeObject(T object) throws IOException { - return objectToBytes(object); - } - - @Deprecated(forRemoval = true) - default T readObject(byte[] bytes) throws IOException, ClassNotFoundException { - return bytesToObject(bytes); - } - default T cloneObject(T object) throws IOException, ClassNotFoundException { Objects.requireNonNull(object, "object cannot be null"); - return readObject(writeObject(object)); + return bytesToObject(objectToBytes(object)); } // Fix issue for string greater than 65K diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java deleted file mode 100644 index 2f1075dd0..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.utils; - -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * @author HeYQ - * @since 2024-11-28 11:47 - */ -public class FileUtils { - - private FileUtils() { - throw new IllegalStateException("Utility class"); - } - - public static void writeCodeToFile(String workDir, String filename, String code) { - try { - if (code == null) { - throw new IllegalArgumentException("Code must not be null"); - } - Path filepath = Path.of(workDir, filename); - // ensure the parent directory exists - Path fileDir = filepath.getParent(); - if (fileDir != null && !Files.exists(fileDir)) { - Files.createDirectories(fileDir); - } - // write the code to the file - Files.writeString(filepath, code); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Deletes the file specified by the filename from the provided working directory. - * @param workDir The working directory where the file to be deleted is located. - * @param filename The name of the file to be deleted. - */ - public static void deleteFile(String workDir, String filename) { - try { - Path filepath = Path.of(workDir, filename); - Files.deleteIfExists(filepath); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Copies all JAR files from the resources/lib directory to the specified working - * directory. - * @param workDir The target working directory where the JAR files will be copied. - */ - public static void copyResourceJarToWorkDir(String workDir) { - try { - // Get the JAR files from resources/lib directory - ClassLoader classLoader = FileUtils.class.getClassLoader(); - URL libUrl = classLoader.getResource("lib"); - if (libUrl == null) { - throw new RuntimeException("Could not find lib directory in resources"); - } - - // Create target directory if it doesn't exist - Path targetDir = Path.of(workDir); - if (!Files.exists(targetDir)) { - Files.createDirectories(targetDir); - } - - // Get all JAR files from lib directory - Path libPath = Path.of(libUrl.toURI()); - try (var stream = Files.walk(libPath)) { - stream.filter(path -> path.toString().endsWith(".jar")).forEach(jarPath -> { - try { - Path targetPath = targetDir.resolve(jarPath.getFileName()); - Files.copy(jarPath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } - catch (IOException e) { - throw new RuntimeException("Failed to copy JAR file: " + jarPath, e); - } - }); - } - } - catch (Exception e) { - throw new RuntimeException("Failed to copy JAR files to working directory", e); - } - } - - /** - * Deletes all JAR files from the specified working directory. - * @param workDir The working directory from which the JAR files will be deleted. - */ - public static void deleteResourceJarFromWorkDir(String workDir) { - try { - Path workDirPath = Path.of(workDir); - if (Files.exists(workDirPath)) { - try (var stream = Files.walk(workDirPath)) { - stream.filter(path -> path.toString().endsWith(".jar")).forEach(jarPath -> { - try { - Files.deleteIfExists(jarPath); - } - catch (IOException e) { - throw new RuntimeException("Failed to delete JAR file: " + jarPath, e); - } - }); - } - } - } - catch (IOException e) { - throw new RuntimeException("Failed to delete JAR files from working directory", e); - } - } - -} diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/MessageUtils.java similarity index 99% rename from argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java rename to argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/MessageUtils.java index 9050ced96..dbb2c5d11 100644 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java +++ b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/MessageUtils.java @@ -31,7 +31,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -public class Messageutils { +public class MessageUtils { private static final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java b/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java deleted file mode 100644 index 9050ced96..000000000 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/Messageutils.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.utils; - -import io.github.agentic.ai.api.chat.ChatMessage; -import io.github.agentic.ai.api.chat.ChatRole; -import io.github.agentic.ai.api.chat.ContentPart; -import io.github.agentic.ai.api.chat.ToolCall; -import io.github.agentic.ai.workflow.graph.serializer.plain_text.jackson.LegacyChatRecordSupport; -import java.util.Locale; - -import org.springframework.util.StringUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class Messageutils { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Generic method to convert various types of messages to ChatMessage list Supported - * types: 1. String - converted to ChatMessage 2. Map (e.g. {"role": "user", "text": - * "test"}) - create corresponding ChatMessage based on role 3. Neutral ChatMessage - - * use directly 4. List - recursively process each element in the list - */ - public static List convertToMessages(Object value) { - List result = new ArrayList<>(); - - if (value == null) { - return result; - } - - if (value instanceof List) { - List list = (List) value; - if (list.isEmpty()) { - return result; - } - - for (Object item : list) { - result.addAll(convertToMessages(item)); - } - } - else if (value instanceof ChatMessage) { - // Preserve an existing neutral message without rebuilding its content. - result.add((ChatMessage) value); - } - else if (value instanceof String) { - // If it's a string, convert to ChatMessage - String text = (String) value; - if (StringUtils.hasLength(text)) { - result.add(ChatMessage.user(text)); - } - } - else if (value instanceof Map) { - // If it's a Map, attempt to parse as ChatMessage - Map map = (Map) value; - ChatMessage message = convertMapToMessage(map); - if (message != null) { - result.add(message); - } - } - else { - // For other types, attempt conversion through JSON - // serialization/deserialization - JsonNode jsonNode; - try { - String json = objectMapper.writeValueAsString(value); - jsonNode = objectMapper.readTree(json); - } - catch (com.fasterxml.jackson.core.JsonProcessingException e) { - // If conversion fails, treat it as a string - String text = value.toString(); - if (StringUtils.hasLength(text)) { - result.add(ChatMessage.user(text)); - } - return result; - } - // Invalid roles and legacy markers must not silently become user text. - if (jsonNode.isObject()) { - ChatMessage message = convertJsonNodeToMessage(jsonNode); - if (message != null) { - result.add(message); - } - } - } - - return result; - } - - /** - * Convert Map to ChatMessage - */ - public static ChatMessage convertMapToMessage(Map map) { - if (map == null || map.isEmpty()) { - return null; - } - - for (String marker : List.of("@class", "@type", "@typeHint")) { - Object type = map.get(marker); - if (type instanceof String name && LegacyChatRecordSupport.isLegacyType(name)) { - throw new IllegalArgumentException( - "Read legacy messages with ArgiJacksonStateSerializer before conversion"); - } - } - ChatRole role = ChatRole - .valueOf(map.get("role") == null ? "USER" : map.get("role").toString().toUpperCase(Locale.ROOT)); - Object content = map.get("content") != null ? map.get("content") : map.get("text"); - if (content == null && role != ChatRole.TOOL && !map.containsKey("toolCalls")) { - return null; - } - var builder = ChatMessage.builder(role); - if (content instanceof String text) { - builder.text(text); - } - else if (content instanceof List parts) { - builder.content(parts.stream().map(part -> objectMapper.convertValue(part, ContentPart.class)).toList()); - } - else if (content != null) { - throw new IllegalArgumentException("Message content must be text or a list of content parts"); - } - if (map.get("toolCalls") != null) { - builder.toolCalls(List.of(objectMapper.convertValue(map.get("toolCalls"), ToolCall[].class))); - } - if (map.get("metadata") != null) { - if (!(map.get("metadata") instanceof Map properties) - || properties.keySet().stream().anyMatch(key -> !(key instanceof String))) { - throw new IllegalArgumentException("Message metadata must be an object with string keys"); - } - @SuppressWarnings("unchecked") - Map metadata = (Map) map.get("metadata"); - builder.metadata(metadata); - } - if (role == ChatRole.TOOL) { - builder.toolResult((String) map.get("toolCallId"), (String) map.get("toolName")); - } - return builder.build(); - } - - /** - * Convert JsonNode to ChatMessage - */ - public static ChatMessage convertJsonNodeToMessage(JsonNode jsonNode) { - if (jsonNode == null || !jsonNode.isObject()) { - return null; - } - - return convertMapToMessage(objectMapper.convertValue(jsonNode, Map.class)); - } - -} diff --git a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/DeepLoopStackDepthTest.java b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/DeepLoopStackDepthTest.java index 3f41500ad..f1ce7d7be 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/DeepLoopStackDepthTest.java +++ b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/DeepLoopStackDepthTest.java @@ -55,8 +55,7 @@ public void longSelfLoopShouldCompleteWithoutStackOverflow() throws Exception { edge_async(state -> (int) state.value("count").orElse(0) < ITERATIONS ? "loop" : "done"), Map.of("loop", "worker", "done", END)); - CompiledGraph app = graph.compile(); - app.setMaxIterations(ITERATIONS + 10); + CompiledGraph app = graph.compile(CompileConfig.builder().recursionLimit(ITERATIONS + 10).build()); // Before the fix the StackOverflowError was swallowed by Reactor (onErrorDropped) // and the stream never terminated, so guard with a timeout instead of blocking forever. diff --git a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/StateGraphTest.java b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/StateGraphTest.java index f0f7672cb..a509ebf8d 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/StateGraphTest.java +++ b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/StateGraphTest.java @@ -23,8 +23,6 @@ import io.github.agentic.ai.workflow.graph.action.Command; import io.github.agentic.ai.workflow.graph.action.CommandAction; import io.github.agentic.ai.workflow.graph.action.MultiCommand; -import io.github.agentic.ai.workflow.graph.async.AsyncGenerator; -import io.github.agentic.ai.workflow.graph.async.AsyncGeneratorQueue; import io.github.agentic.ai.workflow.graph.exception.GraphStateException; import io.github.agentic.ai.workflow.graph.serializer.plain_text.PlainTextStateSerializer; import io.github.agentic.ai.workflow.graph.serializer.plain_text.jackson.ArgiJacksonStateSerializer; @@ -45,7 +43,6 @@ import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; @@ -61,7 +58,7 @@ import static io.github.agentic.ai.workflow.graph.action.AsyncEdgeAction.edge_async; import static io.github.agentic.ai.workflow.graph.action.AsyncNodeAction.node_async; import static java.util.concurrent.CompletableFuture.completedFuture; -import static org.junit.Assert.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; @@ -428,11 +425,9 @@ private AsyncNodeAction makeNode(String id) { private AsyncNodeAction makeNodeForStream(String id) { return node_async(state -> { log.info("call node {}", id); - final AsyncGenerator it = AsyncGeneratorQueue.of(new LinkedBlockingQueue<>(), queue -> { - for (int i = 0; i < 10; ++i) { - queue.add(AsyncGenerator.Data.of(completedFuture(new StreamingOutput(id + i, id, state)))); - } - }); + Flux> it = Flux.range(0, 10) + .map(i -> GraphResponse.of(new StreamingOutput<>(id + i, id, state))) + .concatWith(Flux.just(GraphResponse.done(Map.of("messages", id)))); return Map.of("messages", it); }); @@ -555,9 +550,17 @@ void testWithParallelBranchWithStream() throws GraphStateException { .addEdge("C", END); var app = workflow.compile(); - app.stream(Map.of()).subscribe(output -> { - System.out.println("Node output: " + output); - }); + List outputs = app.stream(Map.of()).collectList().block(java.time.Duration.ofSeconds(10)); + + assertNotNull(outputs); + // Each streaming branch emits ten business chunks; completion snapshots are excluded. + long chunks = outputs.stream() + .filter(StreamingOutput.class::isInstance) + .map(output -> ((StreamingOutput) output).getOriginData()) + .filter(data -> data instanceof String text && text.matches("A[12]\\d")) + .count(); + assertEquals(20, chunks); + assertEquals(StateGraph.END, outputs.get(outputs.size() - 1).node()); } @Test diff --git a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/plain_text/ReasoningMessageSerializationTest.java b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/plain_text/ReasoningMessageSerializationTest.java index 4db2184f3..3b8a690e3 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/plain_text/ReasoningMessageSerializationTest.java +++ b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/plain_text/ReasoningMessageSerializationTest.java @@ -33,9 +33,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** diff --git a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/MessageutilsNeutralTest.java b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/MessageUtilsNeutralTest.java similarity index 75% rename from argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/MessageutilsNeutralTest.java rename to argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/MessageUtilsNeutralTest.java index a33fc834f..fd910c3e6 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/MessageutilsNeutralTest.java +++ b/argi-orchestration/argi-workflow-graph-core/src/test/java/io/github/agentic/ai/workflow/graph/utils/MessageUtilsNeutralTest.java @@ -27,16 +27,16 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -class MessageutilsNeutralTest { +class MessageUtilsNeutralTest { @Test void preservesToolResultsAndMetadataFromMapAndJson() throws Exception { var source = Map.of("role", "tool", "text", "result", "toolCallId", "id", "toolName", "search", "metadata", Map.of("source", "checkpoint")); var expected = ChatMessage.tool("id", "search", "result").withMetadata(Map.of("source", "checkpoint")); - assertEquals(expected, Messageutils.convertMapToMessage(source)); - assertEquals(expected, Messageutils.convertJsonNodeToMessage(new ObjectMapper().valueToTree(source))); - assertEquals(List.of(expected), Messageutils.convertToMessages(List.of(source))); + assertEquals(expected, MessageUtils.convertMapToMessage(source)); + assertEquals(expected, MessageUtils.convertJsonNodeToMessage(new ObjectMapper().valueToTree(source))); + assertEquals(List.of(expected), MessageUtils.convertToMessages(List.of(source))); } @Test @@ -44,17 +44,17 @@ void preservesStructuredPartsAndAssistantCalls() { var part = ContentPart.json("{\"ok\":true}"); var tool = new ToolCall("id", "search", "{}"); assertEquals(ChatMessage.builder(ChatRole.ASSISTANT).content(List.of(part)).toolCalls(List.of(tool)).build(), - Messageutils.convertMapToMessage( + MessageUtils.convertMapToMessage( Map.of("role", "assistant", "content", List.of(part), "toolCalls", List.of(tool)))); - assertEquals(ChatMessage.user("plain"), Messageutils.convertMapToMessage(Map.of("text", "plain"))); - assertNull(Messageutils.convertMapToMessage(Map.of())); + assertEquals(ChatMessage.user("plain"), MessageUtils.convertMapToMessage(Map.of("text", "plain"))); + assertNull(MessageUtils.convertMapToMessage(Map.of())); } @Test void convertsValidPojoMessagesAndPreservesSerializationFallback() { assertEquals(List.of(ChatMessage.assistant("answer")), - Messageutils.convertToMessages(new MessagePayload("assistant", "answer"))); - assertEquals(List.of(), Messageutils.convertToMessages(new MessagePayload("user", null))); + MessageUtils.convertToMessages(new MessagePayload("assistant", "answer"))); + assertEquals(List.of(), MessageUtils.convertToMessages(new MessagePayload("user", null))); for (String text : List.of("fallback", "")) { Object payload = new Object() { @Override @@ -63,7 +63,7 @@ public String toString() { } }; assertEquals(text.isEmpty() ? List.of() : List.of(ChatMessage.user(text)), - Messageutils.convertToMessages(payload)); + MessageUtils.convertToMessages(payload)); } } @@ -71,20 +71,20 @@ public String toString() { void rejectsLegacyMapsAndInvalidNeutralMessages() throws Exception { for (String marker : List.of("@class", "@type", "@typeHint")) { assertThrows(IllegalArgumentException.class, - () -> Messageutils.convertToMessages(Map.of(marker, "ASSISTANT", "text", "old"))); + () -> MessageUtils.convertToMessages(Map.of(marker, "ASSISTANT", "text", "old"))); } assertThrows(IllegalArgumentException.class, - () -> Messageutils.convertMapToMessage(Map.of("role", "other", "text", "x"))); + () -> MessageUtils.convertMapToMessage(Map.of("role", "other", "text", "x"))); assertThrows(IllegalArgumentException.class, - () -> Messageutils.convertMapToMessage(Map.of("content", Map.of("text", "x")))); + () -> MessageUtils.convertMapToMessage(Map.of("content", Map.of("text", "x")))); assertThrows(IllegalArgumentException.class, - () -> Messageutils.convertMapToMessage(Map.of("text", "x", "metadata", List.of()))); + () -> MessageUtils.convertMapToMessage(Map.of("text", "x", "metadata", List.of()))); assertThrows(IllegalArgumentException.class, - () -> Messageutils.convertMapToMessage(Map.of("role", "tool", "text", "x"))); + () -> MessageUtils.convertMapToMessage(Map.of("role", "tool", "text", "x"))); var legacy = new ObjectMapper().readTree("{\"@type\":\"ASSISTANT\",\"text\":\"old\"}"); - assertThrows(IllegalArgumentException.class, () -> Messageutils.convertToMessages(legacy)); + assertThrows(IllegalArgumentException.class, () -> MessageUtils.convertToMessages(legacy)); assertThrows(IllegalArgumentException.class, - () -> Messageutils.convertToMessages(new MessagePayload("other", "bad"))); + () -> MessageUtils.convertToMessages(new MessagePayload("other", "bad"))); } record MessagePayload(String role, String text) { diff --git a/argi-runtime/argi-runtime-api/README.md b/argi-runtime/argi-runtime-api/README.md index c8765a197..d7895a558 100644 --- a/argi-runtime/argi-runtime-api/README.md +++ b/argi-runtime/argi-runtime-api/README.md @@ -104,12 +104,7 @@ Import the matching `argi-bom` to manage the version. The first target is ```sh ./mvnw -B -pl :argi-runtime-api -am test -tools/scripts/verify-api-contract.sh ``` -The API consumer gate compiles and executes a separate runtime-only consumer -with only the runtime API jar and JDK on its execution classpath. It checks -delegation, immutable constraints, reference resolution and the absence of -Graph, Agent, model, Spring and Reactor classes. JDK `jdeps` checks the runtime -jar's actual bytecode dependency set. Existing Graph and Agent tests continue -to validate propagation, authorization and versioned storage behavior. +Existing Graph and Agent tests validate propagation, authorization and versioned +storage behavior. diff --git a/argi-runtime/argi-runtime-core/README.md b/argi-runtime/argi-runtime-core/README.md index 14933e0c4..48c2b7d72 100644 --- a/argi-runtime/argi-runtime-core/README.md +++ b/argi-runtime/argi-runtime-core/README.md @@ -91,7 +91,6 @@ the Loop/data migration work, rather than silently breaking stored Java names. ## Verification and Remaining Engine Work ./mvnw -B -pl :argi-runtime-core -am test - tools/scripts/verify-api-contract.sh Fixtures under src/test/resources/execution were generated with the pre-extraction Graph jar from the #218 baseline (8b16e46ce), before rebuilding the new modules. diff --git a/argi-spring-boot-starter/README.md b/argi-spring-boot-starter/README.md index a5277f4df..130b22635 100644 --- a/argi-spring-boot-starter/README.md +++ b/argi-spring-boot-starter/README.md @@ -11,9 +11,8 @@ aggregates each child module directly. | `argi-starter-adapter-spring-ai` | Spring AI ChatClient adapter auto-configuration | | `argi-starter-adapter-langchain4j` | LangChain4j ChatClient adapter auto-configuration | | `argi-starter-adapter-google-adk` | Google ADK ChatClient adapter auto-configuration | -| `argi-starter-adapter-agentscope` | AgentScope ChatClient adapter auto-configuration | -All artifacts use the group ID `io.github.agentic-ai-java`. The four adapter +All artifacts use the group ID `io.github.agentic-ai-java`. The three adapter starters depend on the corresponding implementations in [`argi-api`](../argi-api). They wrap application-provided native beans; they do not manage provider credentials. A custom ARGI ChatClient bean overrides auto-configuration. When @@ -22,7 +21,7 @@ multiple adapter starters are present, select one using `argi.chat.adapter`: ```yaml argi: chat: - adapter: spring-ai # spring-ai | langchain4j | google-adk | agentscope + adapter: spring-ai # spring-ai | langchain4j | google-adk ``` From the repository root: diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/pom.xml b/argi-spring-boot-starter/argi-starter-builtin-nodes/pom.xml index 0b3c27761..3d766520c 100644 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/pom.xml +++ b/argi-spring-boot-starter/argi-starter-builtin-nodes/pom.xml @@ -149,12 +149,6 @@ mcp-core - - junit - junit - test - - diff --git a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/CodeUtils.java similarity index 96% rename from argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java rename to argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/CodeUtils.java index ecb43bda0..d9bcb1760 100644 --- a/argi-orchestration/argi-workflow-graph-core/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java +++ b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/CodeUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.agentic.ai.workflow.graph.utils; +package io.github.agentic.ai.workflow.graph.node.code; /** * @author HeYQ diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/FileUtils.java similarity index 99% rename from argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java rename to argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/FileUtils.java index e16a65033..9c7d3aed6 100644 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/FileUtils.java +++ b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/FileUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.agentic.ai.workflow.graph.utils; +package io.github.agentic.ai.workflow.graph.node.code; import java.io.IOException; import java.net.URI; diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/LocalCommandlineCodeExecutor.java b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/LocalCommandlineCodeExecutor.java index a97501f18..e4eef8c13 100644 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/LocalCommandlineCodeExecutor.java +++ b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/node/code/LocalCommandlineCodeExecutor.java @@ -18,8 +18,6 @@ import io.github.agentic.ai.workflow.graph.node.code.entity.CodeBlock; import io.github.agentic.ai.workflow.graph.node.code.entity.CodeExecutionConfig; import io.github.agentic.ai.workflow.graph.node.code.entity.CodeExecutionResult; -import io.github.agentic.ai.workflow.graph.utils.CodeUtils; -import io.github.agentic.ai.workflow.graph.utils.FileUtils; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java deleted file mode 100644 index ecb43bda0..000000000 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/main/java/io/github/agentic/ai/workflow/graph/utils/CodeUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.workflow.graph.utils; - -/** - * @author HeYQ - * @since 2025-06-01 20:35 - */ - -public class CodeUtils { - - public static String getExecutableForLanguage(String language) throws Exception { - return switch (language) { - case "python3", "python" -> language; - case "shell", "bash", "sh", "powershell" -> "sh"; - case "nodejs" -> "node"; - case "java" -> "java"; - default -> throw new Exception("Language not recognized in code execution:" + language); - }; - } - - public static String getFileExtForLanguage(String language) throws Exception { - return switch (language) { - case "python3", "python" -> "py"; - case "shell", "bash", "sh", "powershell" -> "sh"; - case "nodejs" -> "js"; - case "java" -> "java"; - default -> throw new Exception("Language not recognized in code execution:" + language); - }; - } - -} diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/AssignerNodeTest.java b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/AssignerNodeTest.java index d6f3753a5..0badebdda 100644 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/AssignerNodeTest.java +++ b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/AssignerNodeTest.java @@ -22,20 +22,20 @@ import java.util.List; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author yHong * @version 1.0 * @since 2025/7/23 10:52 */ -public class AssignerNodeTest { +class AssignerNodeTest { @Test - public void testBatchOverWrite() { + void testBatchOverWrite() { OverAllState state = new OverAllState(); state.registerKeyAndStrategy("a", new ReplaceStrategy()); state.registerKeyAndStrategy("b", new ReplaceStrategy()); @@ -55,7 +55,7 @@ public void testBatchOverWrite() { } @Test - public void testBatchAppend() { + void testBatchAppend() { OverAllState state = new OverAllState(); state.registerKeyAndStrategy("a", new ReplaceStrategy()); state.registerKeyAndStrategy("b", new ReplaceStrategy()); @@ -78,7 +78,7 @@ public void testBatchAppend() { } @Test - public void testBatchClear() { + void testBatchClear() { OverAllState state = new OverAllState(); state.registerKeyAndStrategy("x", new ReplaceStrategy()); state.registerKeyAndStrategy("y", new ReplaceStrategy()); @@ -98,7 +98,7 @@ public void testBatchClear() { } @Test - public void testBatchInputConstant() { + void testBatchInputConstant() { OverAllState state = new OverAllState(); state.registerKeyAndStrategy("x", new ReplaceStrategy()); state.registerKeyAndStrategy("y", new ReplaceStrategy()); @@ -114,7 +114,7 @@ public void testBatchInputConstant() { } @Test - public void testMixBatch() { + void testMixBatch() { OverAllState state = new OverAllState(); state.registerKeyAndStrategy("a", new ReplaceStrategy()); state.registerKeyAndStrategy("b", new ReplaceStrategy()); @@ -141,7 +141,7 @@ public void testMixBatch() { } @Test - public void testCompatibleSingleVariable() { + void testCompatibleSingleVariable() { OverAllState state = new OverAllState(); state.registerKeyAndStrategy("input", new ReplaceStrategy()); state.updateState(Map.of("input", "single")); diff --git a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/utils/FileUtilsTest.java b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/code/FileUtilsTest.java similarity index 98% rename from argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/utils/FileUtilsTest.java rename to argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/code/FileUtilsTest.java index 68e939f12..b37142b47 100644 --- a/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/utils/FileUtilsTest.java +++ b/argi-spring-boot-starter/argi-starter-builtin-nodes/src/test/java/io/github/agentic/ai/workflow/graph/node/code/FileUtilsTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.agentic.ai.workflow.graph.utils; +package io.github.agentic.ai.workflow.graph.node.code; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; diff --git a/argi-studio/src/test/java/io/github/agentic/ai/agentic/DeepResearchAgent.java b/argi-studio/src/test/java/io/github/agentic/ai/agentic/DeepResearchAgent.java index f9af8dd96..dc6c507bf 100644 --- a/argi-studio/src/test/java/io/github/agentic/ai/agentic/DeepResearchAgent.java +++ b/argi-studio/src/test/java/io/github/agentic/ai/agentic/DeepResearchAgent.java @@ -97,7 +97,7 @@ public DeepResearchAgent() { this.patchToolCallsInterceptor = PatchToolCallsInterceptor.builder().build(); this.toolRetryInterceptor = ToolRetryInterceptor.builder() - .maxRetries(1) + .maxAttempts(2) .onFailure(ToolRetryInterceptor.OnFailureBehavior.RETURN_MESSAGE) .build(); diff --git a/docs/architecture/api-boundaries.md b/docs/architecture/api-boundaries.md deleted file mode 100644 index 6fc3d10db..000000000 --- a/docs/architecture/api-boundaries.md +++ /dev/null @@ -1,69 +0,0 @@ -# Agent, Studio, and Serializer Boundaries - -Status: adopted for the current Loop migration. Reviewed 2026-10-24. - -This is the implementation decision for [#184](https://github.com/agentic-ai-java/argi/issues/184). -The boundary is defined by ownership and dependencies, not by mechanically -renaming every package containing `graph`. - -## Ownership map - -| Area | Public entry points | Owner | Dependency rule | -| --- | --- | --- | --- | -| Neutral Agent invocation | `io.github.agentic.ai.agent.*` in `argi-agent-api` | Agent API | Runtime API and neutral API only; no Loop, Spring, Reactor, or provider SDK. | -| Independent ReAct execution | `io.github.agentic.ai.agent.react.*` in `argi-agent-core` | Agent Core | Uses Agent API and neutral model/tool contracts. It does not construct `StateGraph` or require `argi-loop-core`. | -| Independent Workflow execution | `io.github.agentic.ai.workflow.*` in `argi-workflow-core` | Workflow Core | Uses Agent API for optional model/agent calls. It does not construct `StateGraph` or require `argi-loop-core`. | -| Graph-backed flow agents and legacy interceptors | `io.github.agentic.ai.graph.agent.*` | Agent Framework / transitional Agent API contracts | Retained because the current flow-agent implementation and public interceptor contracts use these names. This is an integration boundary, not the independent Agent API. | -| Graph execution | `io.github.agentic.ai.graph.*` | Loop Core | Owns topology, compilation, state, checkpointing, serialization, and graph recovery. | -| Graph serializer | `io.github.agentic.ai.graph.serializer.*` | Loop Core | Owns neutral state encoding and historical graph checkpoint reads. It may use neutral API chat records but not provider SDK classes in production bytecode. | -| Studio management and HTTP surface | `io.github.agentic.ai.agent.studio.*` | Studio | Owns controllers, DTOs, loaders, web resources, and graph/agent management adapters. Studio may depend on Loop for graph views and on Agent Framework for legacy agent views. | -| Neutral chat records | `io.github.agentic.ai.api.chat.*` | API | Owns framework-neutral message and tool contracts. It must not depend on Loop or Studio. | - -## Decisions - -### Keep graph serializer packages - -The serializer reads historical `@class`, `@type`, and `@typeHint` values and -emits stable `ARGI_*` markers. Moving it to a new package would change Java -object-stream and type-marker compatibility without improving ownership: it is -still a Loop checkpoint concern. Keep the package and use the compatibility -contract in `checkpoint-compatibility.md`. - -### Keep transitional graph-agent packages - -`argi-agent-api` contains model interception and authorization contracts under -`io.github.agentic.ai.graph.agent.*`, and `argi-agent-core` consumes them. A -package rename would affect public FQCNs without removing the actual graph-backed -flow implementation. Keep these contracts until an independent interception -contract replaces them; do not claim that the package name alone makes ReAct -graph-dependent. - -### Keep Studio's graph/agent split - -Studio has separate loaders and controllers for graph and agent execution. Graph -DTOs and graph loaders remain Studio-facing management objects; they are not moved -into Loop. A future Studio API can add a neutral execution management contract, -but it must not duplicate checkpoint or serializer implementations. - -## Public API migration table - -| Existing API | Decision | Migration guidance | -| --- | --- | --- | -| `argi-agent-api` neutral invocation contracts | Keep | Use `Agent`, `AgentRequest`, `AgentOutcome`, and `AgentExecutionContext`. | -| `argi-agent-core` ReAct implementation | Keep | Use `ReActAgent`; do not add `asNode` or `CompiledGraph` adapters to make it appear independent. | -| `argi-workflow-core` definition and execution | Keep | Use `WorkflowDefinition`, `WorkflowRegistry`, and `WorkflowAgent`; loop steps stay Workflow-owned. | -| `argi-loop-core` graph and serializer classes | Keep FQCNs | Change only the Maven artifact from `argi-graph-core` to `argi-loop-core` in this phase. | -| `argi-studio` graph and agent endpoints | Keep | Existing HTTP paths and DTO names are management API compatibility, not Loop public execution contracts. | - -## Release sequencing - -1. Publish Runtime API/Core and neutral Agent API contracts. -2. Publish independent Agent Core and Workflow Core artifacts. -3. Publish Loop Core under the `argi-loop-core` coordinate. -4. Publish Agent Framework and Studio integrations that consume Loop. -5. Publish matching BOM and starter coordinates, then update Extensions and - examples. - -The sequence prevents an independent Agent or Workflow consumer from acquiring -Loop transitively while retaining a working graph-backed integration for existing -applications. diff --git a/docs/architecture/checkpoint-compatibility.md b/docs/architecture/checkpoint-compatibility.md deleted file mode 100644 index 863b25e6e..000000000 --- a/docs/architecture/checkpoint-compatibility.md +++ /dev/null @@ -1,104 +0,0 @@ -# Checkpoint and Serializer Compatibility Contract - -Status: adopted for the current Loop migration. Reviewed 2026-10-24. - -This document records the decisions required by [#182](https://github.com/agentic-ai-java/argi/issues/182). -The protocol is intentionally split into historical formats and new neutral data -boundaries. A module or package rename must not silently change these formats. - -## Format inventory - -| Format | Producer and markers | Compatibility rule | -| --- | --- | --- | -| Java object stream state | `Serializer.writeUTF` inside an `ObjectOutputStream`; may contain `@class`, `@type`, or `@typeHint` values from the old runtime | Read only when the old Java classes and serializer semantics are available. There is no generic safe converter for arbitrary object graphs. Unsupported binary data fails with an explicit export/migration instruction. | -| Jackson plain-text state | Untyped JSON object with stable `ARGI_*` markers for neutral records and map envelopes | Current format. Preserve field names, null behavior, arrays, map keys, metadata, and marker values. New code must not enable default typing implicitly. | -| Historical provider chat state | Spring AI class names or role markers such as `SYSTEM`, `USER`, `ASSISTANT`, `TOOL`, `DEEPSEEK_ASSISTANT`, and `ZHI_PU_AI_ASSISTANT` | Read by `LegacyChatRecordSupport`, normalize into neutral `ChatMessage`/`ToolCall` values, and write only neutral markers. The reader does not load provider SDK classes. | -| Runtime Run/Task payload | Strict JSON `RunPayload` with schema, type, version, canonical JSON, and SHA-256 input hash | Separate from graph state serialization. Envelope and application payload versions evolve independently. Do not reuse Java object serialization. | -| Checkpoint and Store JSON fixtures | `checkpoint.json` and `store-item.json` under `src/test/resources/compatibility/2.1.0` | Continue reading and writing the existing field names and numeric values. These fixtures are regression evidence, not a new interchange specification. | - -## Marker contract - -The following stable type IDs are data identifiers and must not be changed by a -Java package move: - -| Marker | Neutral value | -| --- | --- | -| `ARGI_CHAT_MESSAGE` | `ChatMessage` | -| `ARGI_CHAT_EVENT` | `ChatEvent` | -| `ARGI_CHAT_RESPONSE` | neutral `ChatResponse` | -| `ARGI_CONTENT_PART` | `ContentPart` | -| `ARGI_TOOL_CALL` | `ToolCall` | -| `ARGI_USAGE` | `Usage` | -| `ARGI_DURATION` | `Duration` | -| `ARGI_STREAMING_OUTPUT` | runtime streaming snapshot | - -Historical `@class`, `@type`, and `@typeHint` values are interpreted only when -they match a known legacy chat shape or a registered application type. Unknown -business fields named `@class` or `@type` remain data when no supported legacy -shape is recognized; they are not silently converted into a different value. - -`graphId`, `graphVersion`, `DefinitionKind.GRAPH.wireName()` (`graph`), storage -namespace encoding, and `argi.graph.*` observation names remain stable. They are -wire and operational identifiers, not product display names. - -## Compatibility matrix - -| Input | Action | Supported version | Failure and rollback behavior | -| --- | --- | --- | --- | -| 2.1.0 checkpoint JSON | Read, validate, and write equivalent JSON semantics | 2.1.0 and later | Fail before publishing a replacement if required fields cannot be decoded. Restore the previous checkpoint on write failure. | -| 2.1.0 Store item JSON | Read and write unchanged namespace, key, value, and epoch fields | 2.1.0 and later | Reject malformed namespace/key/value shapes; do not delete the original record. | -| Neutral `ARGI_*` chat records | Read, normalize, and write neutral records | Current neutral marker version | Unknown marker fields remain data; malformed known records fail with an `IOException`. | -| Historical provider chat records | Read and normalize to neutral records | Historical formats represented by `LegacyChatRecordSupport` | Provider classes are never loaded. Unsupported media, templates, malformed tool calls, and invalid metadata fail explicitly and require export with the old runtime or an offline converter. | -| Historical `ChatResponse`, streaming snapshots, or provider binary payloads | Do not guess a conversion | Not supported by the neutral reader | Fail with a migration instruction. The original bytes remain untouched. | -| Java object-stream state with unavailable classes | Do not degrade to an empty state or map | No generic version | Fail closed with the class/migration error. Restore from a backup or run an offline conversion in the old runtime. | -| Type ID renamed in application code | Resolve through an explicit `TypeMapper.Reference` alias or migrate offline | Application-defined | If no alias exists, fail or preserve the marker as business data; never silently instantiate a different type. | -| `RunPayload` envelope | Validate exact version, schema, type, canonical JSON, and hash | Envelope v1 | Reject unknown fields, duplicate keys, trailing documents, scalar coercion, and forged hashes before admission. | - -## Implementation boundaries - -- `JacksonStateSerializer` owns new neutral JSON writing and recursive state - normalization. -- `JacksonDeserializer` owns marker dispatch and safe map fallback. It does not - enable Jackson default typing for untrusted data. -- `LegacyChatRecordSupport` owns historical provider chat conversion and has no - provider SDK dependency. -- `TypeMapper` owns explicit stable type registrations. An application that needs - a custom type must register a stable ID and test both the old fixture and the - new output. -- `RunPayload` and its codecs own recoverable Run/Task input data. They are not a - replacement for graph state serializers. -- Extensions persistence adapters must consume these contracts without changing - graph wire literals. A backend adapter additionally proves restart persistence, - transaction rollback, and cross-JVM behavior using its real storage service. - -## Evidence and tests - -The current source and fixtures provide evidence for: - -- legacy checkpoint and Store JSON round trips in - `LegacySerializationCompatibilityTest`; -- historical provider chat conversion and explicit refusal in - `LegacyChatCheckpointMigrationTest`; -- pre-runtime-extraction trusted context data in - `execution/trusted-context-v1-before-runtime-extraction.json`; -- strict Run/Task payload validation in `argi-runtime-core` contract tests. - -The fixture `compatibility/2.1.0/legacy-chat-checkpoint.json` is intentionally -written in the historical provider format. It proves that the migration reader -consumes data produced before the neutral type IDs were introduced, rather than -only testing a new serializer against itself. - -## Migration and rollback - -1. Back up the original checkpoint or Store record. -2. Upgrade the artifact coordinate without changing Java package names or wire - literals. -3. Read the old record with the compatibility reader and write the neutral form - only after successful normalization. -4. Keep the original record until a complete recovery execution succeeds. -5. If conversion fails, restore the original bytes and run the old-runtime export - path for unsupported binary, media, template, or provider-specific values. - -No automatic in-place rewrite is performed by the Loop artifact migration. This -keeps rollback possible and avoids treating a successful JSON parse as proof that -all execution semantics were preserved. diff --git a/docs/architecture/clear-room-baseline.md b/docs/architecture/clear-room-baseline.md deleted file mode 100644 index 286bc0c43..000000000 --- a/docs/architecture/clear-room-baseline.md +++ /dev/null @@ -1,94 +0,0 @@ -# Clear Room Baseline and Naming Decisions - -Status: adopted for the first Loop migration slice. Reviewed 2026-10-24. - -This document records the evidence and decisions required by [#181](https://github.com/agentic-ai-java/argi/issues/181). -It is a migration input, not a promise to rename every existing type. - -## Source baselines - -| Repository | Evidence | Decision | -| --- | --- | --- | -| Core | `alibaba/spring-ai-alibaba` tag `v1.0.0-M2`, commit `4db66e296356f981ecca9885c9b07d3a2f153fc6`, dated 2024-09-22. The same commit is present in the imported ARGI history. | Use this as the last directly verifiable upstream Core baseline. Later ARGI commits are treated as ARGI-owned changes unless a commit is explicitly linked to an upstream object. | -| Extensions | `spring-ai-alibaba/spring-ai-extensions` tags are available, including `v1.1.0.0-M3` (`270b96f200da`) and `v2.0.0-M1.1` (`9f4cfe33697f`). No matching upstream object was found for the ARGI Extensions baseline `495414b76a37aca4d7c1022d973a8e5b9a7e92e6`. | Keep the Extensions import base marked unknown. Do not substitute upstream `main` or a tag for the missing ancestry evidence. Reconstruct ownership from the ARGI commit history when an Extensions capability is changed. | -| ARGI Core | Current migration baseline is `53635bd1f` (`main`) after PR #257. | New work must build on the Loop artifact and preserve the explicit data compatibility rules below. | - -The source repositories, tag names, commit IDs, and dates above were checked from -the public GitHub API on 2026-10-24. An unknown ancestry is an evidence gap, not -permission to infer a migration point. - -## Responsibility map - -| Legacy path or concept | Current owner | Target or retention decision | -| --- | --- | --- | -| `argi-graph-core` / `argi-graph-core` Maven artifact | `argi-loop-core` / `argi-loop-core` | Adopted breaking artifact replacement in #198. No compatibility artifact is published. | -| `io.github.agentic.ai.graph.StateGraph`, `CompiledGraph`, node, edge, and executor types | `argi-loop-core` | Retain FQCNs. These names describe graph topology and execution accurately; package renaming is deferred until #182 and #183 establish a safe migration. | -| Graph state, checkpoint savers, graph serializers, and graph recovery | `argi-loop-core` | Retain graph semantics and historical wire literals. Persistence adapters remain optional integrations. | -| Identity, deadline, budget, delegation, Run/Task data, and definition admission | `argi-runtime-api` and `argi-runtime-core` | Keep extracted neutral contracts outside Loop. Loop consumes them but does not own their definitions. | -| ReAct execution | `argi-agent-core` | Independent execution loop; no Loop or Agent Framework dependency. | -| Declarative Workflow execution | `argi-workflow-core` | Independent execution engine; no Loop or Agent Framework dependency. | -| Existing graph-backed Agent Framework flow agents | `argi-agent-framework` plus optional Loop dependency | Retain until a replacement flow implementation is proven. The dependency is an integration choice, not a requirement of the independent Agent or Workflow engines. | -| `MemorySaver` / graph checkpoint memory | Loop | Graph checkpoint semantics and namespace rules remain Loop-owned. | -| `MemoryRunStore` / Run data memory | Runtime Core | Run reservation, fencing, and revision semantics remain outside Loop. | -| `Store` and database-backed store implementations | Core plus Extensions under the #190 ownership decision | Do not duplicate a second store contract during naming work. | -| Document, rerank, provider SDK, and vendor model types | Extensions or API adapters | Keep vendor names where they identify an external protocol or SDK. Do not move provider dependencies into Loop. | - -## Terminology - -- **Graph** means the node/edge topology and the graph execution model exposed by - Loop. `StateGraph` and `CompiledGraph` remain precise public type names. -- **Loop** means the independently consumable product boundary that owns - resumable, stateful graph execution. It is not a synonym for `LoopAgent`, a - Workflow loop step, or an internal ReAct turn loop. -- **Workflow** means a declarative definition and execution model. A Workflow may - contain a loop step without constructing a `StateGraph`. -- **Run** is a recoverable execution instance. **Task** is a child unit of a Run. - Neither term implies graph execution. -- **Registry** stores versioned definitions or runtime components. **Registrar** - performs admission or registration; **Registration** is the immutable result. -- **State** is execution data owned by an engine. **Checkpoint** is a persisted - resumable state boundary. They are not interchangeable names. -- **Memory** is qualified by its owner (`MemorySaver`, `MemoryRunStore`, or - `MemoryStore`) because the unqualified term has three different semantics. - -## Naming rules - -1. Use the `Argi` prefix for ARGI-specific public facades, configuration objects, - and integrations where it prevents collision with a provider or framework. -2. Do not prefix neutral industry concepts such as `StateGraph`, `Checkpoint`, - `RunStore`, `Workflow`, or `ToolCall` merely to distinguish ARGI. -3. Preserve SDK and vendor names in adapters and historical wire identifiers. - They are compatibility data, not ARGI branding. -4. New Maven artifacts use the `argi-*` namespace. The Loop product artifact is - `io.github.agentic-ai-java:argi-loop-core`. -5. Configuration keys remain under `argi.*` unless a separate issue proves that - a key is owned by another engine. Existing `argi.graph.*` observation names - remain stable data and metric identifiers. - -## Breaking-change register - -| Change | Problem evidence | Alternatives considered | Decision and migration | -| --- | --- | --- | --- | -| `argi-graph-core` artifact to `argi-loop-core` | The old artifact name coupled graph topology, persistence, and the new Loop product boundary, and prevented a clear independent consumer contract. | Keep the artifact; publish a second alias; replace the artifact. A second alias leaves two dependency identities and does not clarify ownership. | Replace directly in #198. Update BOM, starters, examples, benchmarks, and consumers. Roll back by pinning the previous ARGI release; no persisted data conversion is required. | -| Graph Java package and type names | Existing source references and serialized class names use `io.github.agentic.ai.graph.*`. | Rename all packages; add wrappers; retain names. A mechanical rename adds source, binary, and wire migration cost without a demonstrated behavior benefit. | Retain for now. Revisit only with a measured compatibility plan under #182/#183. | -| `graphId`, `graphVersion`, `DefinitionKind.GRAPH`, and `argi.graph.*` wire names | Run definitions, observability, and persisted data use these literals. | Rename immediately; add aliases; retain literals. | Retain literals. Product naming and data naming are separate decisions. | - -## Consumer migration - -1. Replace the Maven coordinate `io.github.agentic-ai-java:argi-graph-core` with - `io.github.agentic-ai-java:argi-loop-core` and import the matching BOM. -2. Replace module selectors and local paths from `argi-graph-core` to - `argi-loop-core`. -3. Do not change `io.github.agentic.ai.graph.*` imports, checkpoint keys, graph - IDs, graph versions, or graph observation names in this slice. -4. Rebuild graph-backed framework integrations after the coordinate change. -5. For durable data, follow the compatibility matrix in - `checkpoint-compatibility.md`; an artifact rename alone does not require a - checkpoint rewrite. - -## Review conclusion - -Adopt the artifact boundary, neutral runtime extraction, and independent Agent -and Workflow engines already merged. Retain mature graph terminology, Java -packages, and persistence literals. Defer package and configuration renames until -there is a concrete ownership problem, a migration plan, and fixture evidence. diff --git a/docs/architecture/persistence-ownership.md b/docs/architecture/persistence-ownership.md index a033791a3..1b9e379e2 100644 --- a/docs/architecture/persistence-ownership.md +++ b/docs/architecture/persistence-ownership.md @@ -11,12 +11,12 @@ rename. | Capability | Contract owner | Implementation owner | Core status | Compatibility evidence | | --- | --- | --- | --- | --- | -| Checkpoint read/write/release | `argi-loop-core` `BaseCheckpointSaver` | Loop: `MemorySaver`, `FileSystemSaver`; Extensions: JDBC, MongoDB, Redis | External drivers removed from Loop | `checkpoint-compatibility.md`; backend fixtures in Extensions | -| Versioned checkpoint CAS | `argi-loop-core` `VersionedCheckpointSaver` | Extensions Redis and future durable adapters | Contract remains in Loop | `VersionedCheckpointScope` and Redis CAS tests | -| Execution leases | `argi-loop-core` `LeasedCheckpointSaver` | Extensions Redis | Contract remains in Loop | lease scope and Redis fencing tests | -| General Store API | `argi-loop-core` `Store` / `VersionedStore` | Loop: `MemoryStore`, `FileSystemStore`; Extensions: durable stores | External stores removed from Loop | Store JSON fixtures and contract tests | +| Checkpoint read/write/release | `argi-workflow-graph-core` `BaseCheckpointSaver` | Loop: `MemorySaver`, `FileSystemSaver`; Extensions: JDBC, MongoDB, Redis | External drivers removed from Loop | Checkpoint round-trip tests; backend fixtures in Extensions | +| Versioned checkpoint CAS | `argi-workflow-graph-core` `VersionedCheckpointSaver` | Extensions Redis and future durable adapters | Contract remains in Loop | `VersionedCheckpointScope` and Redis CAS tests | +| Execution leases | `argi-workflow-graph-core` `LeasedCheckpointSaver` | Extensions Redis | Contract remains in Loop | lease scope and Redis fencing tests | +| General Store API | `argi-workflow-graph-core` `Store` / `VersionedStore` | Loop: `MemoryStore`, `FileSystemStore`; Extensions: durable stores | External stores removed from Loop | Store JSON fixtures and contract tests | | Run/Task persistence | `argi-runtime-core` `RunStore` | Extensions durable adapters | Never owned by Loop backend classes | Runtime contract and Redis RunStore tests | -| State serialization | `argi-loop-core` serializer contracts | Loop serializer and backend adapters | Wire literals unchanged | `checkpoint-compatibility.md` | +| State serialization | `argi-workflow-graph-core` serializer contracts | Loop serializer and backend adapters | Wire literals unchanged | Serializer fixture tests | | Database drivers and auto-configuration | No Core contract | Extensions persistence modules and starters | No provider driver in Loop runtime | POM dependency and module boundaries | The contract modules own interfaces, records, compatibility rules and in-memory @@ -75,7 +75,7 @@ backend classes and drivers are delivered. 5. For a rollback, pin the previous Core/Extensions pair. No checkpoint or Store data conversion is required. -The Extensions persistence modules now compile against `argi-loop-core` at the +The Extensions persistence modules now compile against `argi-workflow-graph-core` at the matching RC2 snapshot. Other Extensions modules that still consume the older graph coordinate remain outside this focused migration and are tracked by their own consumer tasks; they must not be silently changed as part of #190. @@ -99,6 +99,6 @@ services must not be reported as a real-backend validation. ## Removal rule -Adding a database client or durable backend implementation to `argi-loop-core` +Adding a database client or durable backend implementation to `argi-workflow-graph-core` is a boundary violation. New persistence work belongs in an Extensions `graph-persistence` module and must add or reuse a contract test from Core. diff --git a/docs/architecture/target-architecture.md b/docs/architecture/target-architecture.md deleted file mode 100644 index 5c48dc29a..000000000 --- a/docs/architecture/target-architecture.md +++ /dev/null @@ -1,213 +0,0 @@ -# ARGI Target Architecture - -Status: adopted for the first independent execution plan. Reviewed 2026-10-24. - -This document is the architecture deliverable for -[#186](https://github.com/agentic-ai-java/argi/issues/186). It defines the -product boundaries and the replacement sequence; it does not claim that all -future execution services or integrations already exist. - -## Product scope - -ARGI provides independently consumable Java building blocks for four related -but distinct concerns: - -1. **Agent execution**: a model/tool turn with explicit identity, budget, - cancellation and outcome semantics. -2. **Workflow execution**: a versioned declarative definition that composes - agents, tools, conditions, bounded loops and bounded parallel branches. -3. **Loop execution**: a resumable graph engine with topology, state merge, - checkpoints and graph recovery. -4. **Runtime management**: definition references, Run/Task identity, durable - commit contracts, events and capability admission. - -Persistence adapters, provider SDKs, Studio and remote protocols are optional -integrations. They consume the contracts above; they do not define them. - -### Non-goals - -- A universal state map shared by Agent, Workflow and Loop. -- A hidden Graph delegate behind the independent Agent or Workflow APIs. -- An arbitrary Java/script execution language in declarative definitions. -- A mandatory plugin marketplace, hot JAR loader or distributed scheduler in - the first independent execution release. -- A claim of source-origin independence. Existing source and copyright history - remain recorded; this plan addresses dependency and contract independence. - -## Behavioral boundaries - -| Boundary | Owns | Must not own | -| --- | --- | --- | -| `argi-api` | Neutral messages, model, tool and usage contracts | Execution state, provider SDKs, persistence | -| `argi-runtime-api` | Trusted identity, budget, delegation, cancellation and definition references | JSON codecs, engines, storage drivers | -| `argi-runtime-core` | Run/Task records, versioned payloads, admission, resolver and RunStore SPI | Graph topology, model calls, provider adapters | -| `argi-agent-api` | One invocation contract, request, event and outcome | Graph state, checkpoints, scheduler | -| `argi-agent-core` | Independent ReAct turn loop and tool/model coordination | Loop classes, Graph persistence, Spring context | -| `argi-workflow-core` | Declarative definition, validation, registry and direct execution | Loop as a required backend | -| `argi-loop-core` | Graph topology, compile/execute, state, checkpoint and graph recovery | Agent definition registry, vendor drivers, Studio controllers | -| `argi-agent-framework` | Transitional Graph-backed agents and framework integrations | The independent Agent/Workflow implementation contracts | -| `argi-studio` | Definition/run management UI and optional Loop graph view | Direct mutation of `CompiledGraph` as its management API | -| Extensions | Durable backend adapters, provider integrations and optional starters | A second copy of Core contracts or neutral execution engines | - -The stable distinction is **engine state** versus **business data**. Agent -requests and outcomes are not implicit checkpoints. A Loop checkpoint is a -resumable graph boundary. A Run payload is versioned application data. A -Workflow state is owned by its executor and maps to a result; it is not -silently serialized as Loop state. - -## Dependency direction - -```mermaid -flowchart TD - API[argi-api\nneutral model/tool contracts] - RUNTIME_API[argi-runtime-api\nidentity, budget, control] - RUNTIME_CORE[argi-runtime-core\nRun/Task, codecs, RunStore SPI] - AGENT_API[argi-agent-api\nAgent request/event/outcome] - AGENT_CORE[argi-agent-core\nindependent ReAct] - WORKFLOW[argi-workflow-core\ndeclarative Workflow] - LOOP[argi-loop-core\nGraph/Loop engine] - FRAMEWORK[argi-agent-framework\nlegacy Graph-backed integration] - STUDIO[argi-studio\nmanagement and optional graph view] - EXT[Extensions\nbackend/provider adapters] - - API --> AGENT_API - RUNTIME_API --> AGENT_API - RUNTIME_API --> RUNTIME_CORE - API --> AGENT_CORE - AGENT_API --> AGENT_CORE - RUNTIME_API --> AGENT_CORE - AGENT_API --> WORKFLOW - RUNTIME_API --> WORKFLOW - RUNTIME_CORE --> LOOP - RUNTIME_API --> LOOP - API --> LOOP - LOOP --> FRAMEWORK - AGENT_API --> FRAMEWORK - LOOP --> STUDIO - FRAMEWORK --> STUDIO - RUNTIME_CORE --> STUDIO - LOOP --> EXT - RUNTIME_CORE --> EXT -``` - -The graph shows allowed compile-time direction, not an obligation to include -every dependency in every application. In particular, an Agent-only consumer -can use `argi-agent-api` and `argi-agent-core` without Loop, and a Workflow -consumer can use `argi-workflow-core` without Loop. - -## Public API shape - -The independent invocation boundary is intentionally small: - -```java -Agent agent = ReActAgent.builder(chatClient) - .tools(new ToolRegistry(List.of(searchTool))) - .build(); - -try (AgentExecutionContext context = AgentExecutionContext.builder(trustedContext).build()) { - AgentOutcome outcome = agent.invoke( - AgentRequest.messages(List.of(ChatMessage.user("Find the latest order status"))), - context) - .get(); -} -``` - -The public concepts have one owner: - -| Concept | Contract | Invariant | -| --- | --- | --- | -| Agent definition | `DefinitionReference` plus a published definition record | A published version is immutable and addressable by kind, id and version | -| Invocation | `Agent`, `AgentRequest`, `AgentEvent`, `AgentOutcome` | One invocation has one terminal outcome or one explicit suspension | -| Runtime identity | `TrustedExecutionContext` | Ordinary input maps and model metadata cannot create trusted identity | -| Limits | `ExecutionBudget`, `ExecutionControl` | Executors check control before and after side effects | -| Run | `RunSpec`, `RunState`, `RunStore` | Commit validates namespace, revision and fencing ownership | -| Loop state | `OverAllState`, `Checkpoint`, `BaseCheckpointSaver` | State merge and checkpoint history remain Loop semantics | -| Tool | `ToolCallback`, `ToolRegistry` and authorization policy | Authorization happens at the execution boundary, not in model text | -| Model | `ChatClient` adapter contract | Provider SDKs stay in adapters and Extensions | -| Studio | Management service and event DTOs | UI operations address a definition and run version, not a mutable graph object | - -Definition publication, validation and instantiation are shared management -responsibilities. The concrete Workflow DSL remains Workflow-owned; Loop graph -topology remains Loop-owned. A future definition factory must resolve a -published definition into the neutral `Agent` contract rather than registering -arbitrary Java objects in a user-facing registry. - -## Minimal definition-to-run example - -The following is the target application shape. It is a contract example, not a -claim that the complete definition factory has already shipped in #199. - -```java -DefinitionReference reference = definitions.publish( - AgentDefinition.builder("order-support", "1.2.0") - .model(ModelReference.of("support-model", "2026-10-01")) - .tools(List.of(ToolReference.of("order.read", "1"))) - .inputSchema("{\"type\":\"object\"}") - .build()); - -Agent agent = definitions.createAgent(reference, authenticatedContext); -RunSpec run = runs.submit(RunRequest.forAgent(reference, input), authenticatedContext); -AgentOutcome outcome = agent.invoke(request, authenticatedContext).get(); -runs.commit(run, outcome); -``` - -The actual factory must reject unknown model/tool versions and unsupported -parameters before a model or tool side effect. In-flight runs keep the exact -definition and capability versions selected at admission. Updating the latest -version affects new runs only. - -## Decisions and alternatives - -| Decision | Evidence | Alternatives | Result | -| --- | --- | --- | --- | -| Keep `StateGraph`/`CompiledGraph` names inside Loop | The types describe topology and are present in checkpoint and observation contracts | Rename all packages; add wrappers; retain names | Retain FQCNs until a data-safe migration proves a behavior benefit | -| Make Agent and Workflow independent | Existing implementations now compile without Loop and have independent behavior tests | Keep Graph delegate; put a renamed facade over Graph | Rejected because wrappers preserve the coupling without a new contract | -| Keep Runtime API JDK-only | Identity and control are consumed by independent engines | Put runtime types in API or Loop | Rejected because it either couples model adapters or makes Loop a shared monolith | -| Version definitions and capabilities | Recovery already requires exact version selection | Mutable latest definition; class-only registration | Rejected because in-flight and recovered runs become ambiguous | -| Separate Run payload from Loop checkpoint | They have different ownership and compatibility rules | Serialize every Run as graph state | Rejected because it leaks graph semantics into Agent and Workflow consumers | -| Keep provider integrations optional | Core production bytecode must be provider-neutral | Add provider SDKs to Core | Rejected because it prevents independent adapters and increases transitive surface | - -Every breaking decision is limited to a concrete boundary. Artifact replacement -from `argi-graph-core` to `argi-loop-core` was adopted in #198 because the old -coordinate prevented an independent Loop product boundary. Package names and -wire literals remain unchanged where renaming would add migration risk without -removing a dependency. - -## Replacement sequence - -| Phase | Deliverable | Entry condition | Exit/removal condition | -| --- | --- | --- | --- | -| 0 | Neutral API and compatibility baseline | #181/#182/#183/#184/#185 | Fixtures and dependency gates are reproducible | -| 1 | Agent API, independent ReAct and Workflow | #195/#196/#197 | Standalone consumers execute without Loop; behavior tests cover cancellation, tools and outcomes | -| 2 | Loop artifact and runtime extraction | #198/#200 | Loop consumers use `argi-loop-core`; Runtime API/Core owns neutral Run contracts | -| 3 | Definition publication and factory | #199 | Publish/query/version conflict and unsupported capability tests pass; no Java rebuild for a new definition | -| 4 | Capability packages and policy diagnostics | #201/#202/#203 | Effective capability snapshot, scope and rollback tests pass | -| 5 | Studio management boundary | #204 | Studio creates and queries definition-bound runs without direct graph mutation | -| 6 | Durable execution and backend ownership | #124/#190/#235/#236 | Backend contract tests pass with restart, fencing and real storage evidence | - -The old Graph-backed Agent Framework remains during the transition. Its removal -requires all supported consumers to use the neutral Agent contract, a published -replacement for each supported hook, and a release with an explicit migration -note. No compatibility layer is added merely to make an old engine look -independent. Rollback is by reverting the matching Core/BOM/Extensions release; -durable data follows the matrix in -[`checkpoint-compatibility.md`](checkpoint-compatibility.md). - -## Technical independence acceptance - -Each independent artifact must satisfy all of the following before it is called -complete: - -- public signatures contain no `StateGraph`, `CompiledGraph`, `RunnableConfig`, - provider SDK or Studio type unless the artifact is explicitly an integration; -- bytecode dependency checks confirm the intended graph/provider boundary; -- a clean consumer can compile and run using only the intended artifact set; -- behavior tests cover normal completion, tool failure, cancellation, deadline, - suspension/resume and version pinning where the capability supports them; -- durable implementations prove restart persistence, fencing and rollback using - their actual backend, not only an in-memory double; -- historical checkpoint and Run payload fixtures continue to round-trip under - the compatibility contract. - -This is technical architecture independence. It intentionally does not assert -that the repository has a clean-room source history. diff --git a/tools/ci-config/labeler.yml b/tools/ci-config/labeler.yml deleted file mode 100644 index fcd24cc93..000000000 --- a/tools/ci-config/labeler.yml +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. -# -"area/infra": - - changed-files: - - any-glob-to-any-file: - - ".github/**/*" - - "tools/**/*" - - ".gitignore" - - "CONTRIBUTING.md" - - "CONTRIBUTING-zh.md" -"area/docs": - - changed-files: - - any-glob-to-any-file: - - "**/README.md" - - "**/README-zh.md" - - "**/README-ja.md" -"area/graph": - - changed-files: - - any-glob-to-any-file: - - "argi-orchestration/argi-workflow-graph-core/**/*" -"area/core": - - changed-files: - - any-glob-to-any-file: - - "argi-orchestration/argi-agent-framework/**/*" - - "argi-runtime/**/*" - - "argi-agent/**/*" - - "argi-orchestration/argi-workflow-core/**/*" - - "argi-bom/**/*" - - "argi-api/**/*" - - "argi-spring-boot-starter/**/*" -"area/studio": - - changed-files: - - any-glob-to-any-file: - - "argi-studio/**/*" diff --git a/tools/compatibility/agent-api-consumer/pom.xml b/tools/compatibility/agent-api-consumer/pom.xml deleted file mode 100644 index 894c4aa27..000000000 --- a/tools/compatibility/agent-api-consumer/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 4.0.0 - io.github.agentic.ai - agent-api-consumer - 1.0.0 - - UTF-8 - 17 - 2.1.0-RC2-SNAPSHOT - - - - io.github.agentic-ai-java - argi-agent-api - ${argi.version} - - - diff --git a/tools/compatibility/agent-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiConsumer.java b/tools/compatibility/agent-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiConsumer.java deleted file mode 100644 index d45fa8983..000000000 --- a/tools/compatibility/agent-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiConsumer.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import com.sun.net.httpserver.HttpServer; -import io.github.agentic.ai.agent.Agent; -import io.github.agentic.ai.agent.AgentEvent; -import io.github.agentic.ai.agent.AgentExecutionContext; -import io.github.agentic.ai.agent.AgentOutcome; -import io.github.agentic.ai.agent.AgentRequest; -import io.github.agentic.ai.api.chat.ChatMessage; -import io.github.agentic.ai.api.chat.Usage; -import io.github.agentic.ai.agent.interceptor.ModelCallHandler; -import io.github.agentic.ai.agent.interceptor.ModelInterceptor; -import io.github.agentic.ai.agent.interceptor.ModelRequest; -import io.github.agentic.ai.agent.interceptor.ModelResponse; -import io.github.agentic.ai.agent.interceptor.authorization.ToolAuthorizationPolicy; -import io.github.agentic.ai.runtime.execution.PrincipalReference; -import io.github.agentic.ai.runtime.execution.TenantReference; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContext; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Clock; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -/** Real calls through one contract, with no Graph, framework, provider or Reactor jar. */ -public final class AgentApiConsumer { - - private AgentApiConsumer() { - } - - public static void main(String[] args) throws Exception { - for (String unavailable : Set.of("io.github.agentic.ai.workflow.graph.StateGraph", - "io.github.agentic.ai.workflow.graph.OverAllState", "io.github.agentic.ai.workflow.graph.RunnableConfig", - "io.github.agentic.ai.agent.orchestration.Agent", "reactor.core.publisher.Flux", - "org.springframework.context.ApplicationContext", "com.fasterxml.jackson.databind.ObjectMapper")) { - try { - Class.forName(unavailable, false, AgentApiConsumer.class.getClassLoader()); - throw new IllegalStateException("Unexpected dependency: " + unavailable); - } - catch (ClassNotFoundException expected) { - // A genuine standalone consumer cannot load any of these classes. - } - } - AtomicInteger remoteCalls = new AtomicInteger(); - HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); - server.createContext("/agent", exchange -> { - remoteCalls.incrementAndGet(); - String input = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); - byte[] output = ("remote:" + input).getBytes(StandardCharsets.UTF_8); - exchange.sendResponseHeaders(200, output.length); - try (exchange) { - exchange.getResponseBody().write(output); - } - }); - server.start(); - try { - Agent local = (request, context) -> outcomePublisher(() -> { - context.control().assertActive(); - if (request.resumeToken() != null) { - throw new IllegalArgumentException("This consumer does not support resume"); - } - if (ToolAuthorizationPolicy.requireMatchingTenant().authorize("echo", context.trusted()).isDenied()) { - throw new SecurityException("Missing tenant"); - } - ModelInterceptor interceptor = new ModelInterceptor() { - public String getName() { - return "fixture-prefix"; - } - - public ModelResponse interceptModel(ModelRequest modelRequest, ModelCallHandler next) { - return next.call(modelRequest); - } - }; - ModelResponse response = interceptor.interceptModel( - ModelRequest.builder() - .messages(request.messages()) - .trustedExecutionContext(context.trusted()) - .build(), - modelRequest -> ModelResponse - .of(ChatMessage.assistant("local:" + modelRequest.getMessages().get(0).text()))); - return AgentOutcome.completed(List.of(response.getMessage()), Map.of(), Usage.unknown()); - }); - HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); - URI endpoint = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/agent"); - Agent remote = (request, context) -> outcomePublisher(() -> { - context.control().assertActive(); - if (request.resumeToken() != null) { - throw new IllegalArgumentException("This transport does not support resume"); - } - HttpRequest http = HttpRequest.newBuilder(endpoint) - .timeout(Duration.ofSeconds(5)) - .POST(HttpRequest.BodyPublishers.ofString(request.messages().get(0).text())) - .build(); - var response = client.sendAsync(http, HttpResponse.BodyHandlers.ofString()); - try (AutoCloseable cancellation = context.control().onLoss(() -> response.cancel(true))) { - HttpResponse received = response.join(); - context.control().assertActive(); - if (received.statusCode() != 200) { - throw new IllegalStateException("Unexpected remote status: " + received.statusCode()); - } - return AgentOutcome.completed(List.of(ChatMessage.assistant(received.body())), Map.of(), - Usage.unknown()); - } - }); - Agent workflow = (request, context) -> outcomePublisher(() -> { - AgentOutcome first = local.invoke(request, context).join(); - AgentOutcome second = remote.invoke(AgentRequest.messages(first.messages()), context).join(); - return AgentOutcome.completed(second.messages(), Map.of("steps", 2), second.usage()); - }); - List agents = List.of(local, remote, workflow); - List expected = List.of("local:hello", "remote:hello", "remote:local:hello"); - for (int index = 0; index < agents.size(); index++) { - TrustedExecutionContext trusted = TrustedExecutionContext.builder() - .tenant(new TenantReference("fixture", "tenant")) - .principal(new PrincipalReference("fixture", "caller")) - .runId("run-" + index) - .correlationId("fixture") - .build(); - try (AgentExecutionContext context = new AgentExecutionContext(trusted, Clock.systemUTC())) { - AgentOutcome result = agents.get(index) - .invoke(AgentRequest.messages(List.of(ChatMessage.user("hello"))), context) - .join(); - if (!expected.get(index).equals(result.messages().get(0).text())) { - throw new IllegalStateException("Invocation did not execute the selected implementation"); - } - if (index == 2 && !Integer.valueOf(2).equals(result.outputs().get("steps"))) { - throw new IllegalStateException("Workflow lost its business output"); - } - } - } - if (remoteCalls.get() != 2) { - throw new IllegalStateException("Remote and workflow invocations must perform real transport calls"); - } - } - finally { - server.stop(0); - } - } - - private static Flow.Publisher outcomePublisher(Callable work) { - return subscriber -> subscriber.onSubscribe(new Flow.Subscription() { - private final AtomicBoolean started = new AtomicBoolean(); - - private volatile boolean cancelled; - - public void request(long demand) { - if (cancelled || !started.compareAndSet(false, true)) { - return; - } - if (demand <= 0) { - subscriber.onError(new IllegalArgumentException("Demand must be positive")); - return; - } - try { - AgentOutcome outcome = work.call(); - if (!cancelled) { - subscriber.onNext(new AgentEvent.Finished(outcome)); - if (!cancelled) { - subscriber.onComplete(); - } - } - } - catch (Exception exception) { - if (!cancelled) { - subscriber.onError(exception); - } - } - } - - public void cancel() { - cancelled = true; - } - }); - } - -} diff --git a/tools/compatibility/neutral-api-consumer/pom.xml b/tools/compatibility/neutral-api-consumer/pom.xml deleted file mode 100644 index 9848d6345..000000000 --- a/tools/compatibility/neutral-api-consumer/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - 4.0.0 - - io.github.agentic.ai - neutral-api-consumer - 1.0.0 - jar - - - UTF-8 - UTF-8 - 17 - 2.1.0-RC2-SNAPSHOT - - - - - io.github.agentic-ai-java - argi-workflow-graph-core - ${argi.version} - - - io.github.agentic-ai-java - argi-agent-framework - ${argi.version} - - - io.github.agentic-ai-java - argi-studio - ${argi.version} - - - io.github.agentic-ai-java - argi-starter-graph-observation - ${argi.version} - - - io.github.agentic-ai-java - argi-starter-builtin-nodes - ${argi.version} - - - diff --git a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiFixture.java b/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiFixture.java deleted file mode 100644 index 8b11cfc05..000000000 --- a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/AgentApiFixture.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; - -import io.github.agentic.ai.workflow.graph.OverAllState; -import io.github.agentic.ai.workflow.graph.RunnableConfig; -import io.github.agentic.ai.agent.orchestration.ReactAgent; -import io.github.agentic.ai.agent.orchestration.flow.agent.LlmRoutingAgent; -import io.github.agentic.ai.agent.orchestration.flow.agent.LoopAgent; -import io.github.agentic.ai.agent.orchestration.flow.agent.ParallelAgent; -import io.github.agentic.ai.agent.orchestration.flow.agent.SequentialAgent; -import io.github.agentic.ai.agent.orchestration.flow.agent.loop.LoopMode; -import io.github.agentic.ai.agent.orchestration.hook.AgentHook; -import io.github.agentic.ai.agent.interceptor.ModelCallHandler; -import io.github.agentic.ai.agent.interceptor.ModelInterceptor; -import io.github.agentic.ai.agent.interceptor.ModelRequest; -import io.github.agentic.ai.agent.interceptor.ModelResponse; -import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallHandler; -import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallRequest; -import io.github.agentic.ai.agent.orchestration.interceptor.ToolCallResponse; -import io.github.agentic.ai.agent.orchestration.interceptor.ToolInterceptor; -import io.github.agentic.ai.agent.orchestration.tools.task.BoundedTaskRepository; -import io.github.agentic.ai.agent.orchestration.tools.task.TaskToolsBuilder; - -import io.github.agentic.ai.api.chat.ChatClient; - -final class AgentApiFixture { - - private AgentApiFixture() { - } - - static void compileOnly(ChatClient chatClient) throws Exception { - SampleAgentHook hook = new SampleAgentHook(); - SampleModelInterceptor modelInterceptor = new SampleModelInterceptor(); - SampleToolInterceptor toolInterceptor = new SampleToolInterceptor(); - RunnableConfig config = RunnableConfig.builder().threadId("agent-fixture").build(); - - ReactAgent reactAgent = ReactAgent.builder() - .name("react") - .description("compile fixture") - .instruction("respond") - .chatClient(chatClient) - .hooks(hook) - .interceptors(modelInterceptor, toolInterceptor) - .build(); - - SequentialAgent sequentialAgent = SequentialAgent.builder() - .name("sequential") - .description("compile fixture") - .subAgents(List.of(reactAgent)) - .hooks(hook) - .build(); - ParallelAgent parallelAgent = ParallelAgent.builder() - .name("parallel") - .description("compile fixture") - .subAgents(List.of(reactAgent)) - .hooks(hook) - .build(); - LoopAgent loopAgent = LoopAgent.builder() - .name("loop") - .description("compile fixture") - .subAgent(reactAgent) - .loopStrategy(LoopMode.count(1)) - .hooks(hook) - .build(); - LlmRoutingAgent routingAgent = LlmRoutingAgent.builder() - .name("routing") - .description("compile fixture") - .chatClient(chatClient) - .subAgents(List.of(reactAgent)) - .fallbackAgent("react") - .systemPrompt("route") - .instruction("choose") - .hooks(hook) - .build(); - - reactAgent.call("hello", config); - reactAgent.invoke(Map.of("messages", "hello"), config); - reactAgent.stream("hello", config); - reactAgent.asNode(); - sequentialAgent.invoke(Map.of("messages", "hello"), config); - parallelAgent.stream(Map.of("messages", "hello"), config); - loopAgent.invoke(Map.of("messages", "hello"), config); - routingAgent.invoke(Map.of("messages", "hello"), config); - } - - static void boundedTasks(ReactAgent agent, ExecutorService callerExecutor) { - try (BoundedTaskRepository owned = new BoundedTaskRepository(2, 4, 8, Duration.ofMinutes(5)); - BoundedTaskRepository borrowed = new BoundedTaskRepository(callerExecutor, 2, 4, 8, - Duration.ofMinutes(5))) { - TaskToolsBuilder.builder().taskRepository(owned).subAgent("worker", agent).build(); - borrowed.putTask("task", () -> "result"); - borrowed.getTask("task"); - borrowed.removeTask("task"); - borrowed.clear(); - } - } - - static final class SampleAgentHook extends AgentHook { - - @Override - public String getName() { - return "sample-agent-hook"; - } - - @Override - public Map getKeyStrategys() { - return Map.of("messages", io.github.agentic.ai.workflow.graph.KeyStrategy.APPEND); - } - - } - - static final class SampleModelInterceptor extends ModelInterceptor { - - @Override - public String getName() { - return "sample-model-interceptor"; - } - - @Override - public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { - return handler.call(request); - } - - } - - static final class SampleToolInterceptor extends ToolInterceptor { - - @Override - public String getName() { - return "sample-tool-interceptor"; - } - - @Override - public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) { - return handler.call(request); - } - - } - - static OverAllState stateReference(ReactAgent agent, RunnableConfig config) throws Exception { - return agent.invoke("state", config).orElseThrow(); - } - -} diff --git a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/DefinitionApiFixture.java b/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/DefinitionApiFixture.java deleted file mode 100644 index a3a8bc691..000000000 --- a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/DefinitionApiFixture.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2025-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import io.github.agentic.ai.runtime.execution.definition.DefinitionKind; -import io.github.agentic.ai.runtime.execution.definition.DefinitionReference; -import io.github.agentic.ai.runtime.execution.definition.DefinitionRegistry; -import io.github.agentic.ai.runtime.execution.definition.ResolvedRunDefinitions; -import io.github.agentic.ai.runtime.execution.definition.RunDefinitionAdmission; -import io.github.agentic.ai.runtime.execution.definition.RunDefinitionRecord; -import io.github.agentic.ai.runtime.execution.definition.RunDefinitionRecordCodec; -import io.github.agentic.ai.runtime.execution.definition.RuntimeComponentDeclaration; -import io.github.agentic.ai.runtime.execution.definition.RuntimeComponentRole; -import io.github.agentic.ai.runtime.execution.definition.WorkerCapabilities; -import io.github.agentic.ai.runtime.execution.run.MemoryRunStore; -import io.github.agentic.ai.runtime.execution.run.RunSpec; -import io.github.agentic.ai.runtime.execution.run.RunStore; - -import java.util.List; -import java.util.Set; - -/** Compile the opt-in definition contract as a consumer without a model SDK or Extensions. */ -final class DefinitionApiFixture { - - private DefinitionApiFixture() { - } - - static ResolvedRunDefinitions compileOnly(RunSpec admitted) { - RunStore store = new MemoryRunStore(); - RunDefinitionRecordCodec codec = new RunDefinitionRecordCodec(); - RunDefinitionRecord record = RunDefinitionRecord.forRun(admitted, - DefinitionReference.of(DefinitionKind.TOOL_CATALOG, "tools", "3"), - DefinitionReference.of(DefinitionKind.CODEC, "argi-json", "2"), - DefinitionReference.of(DefinitionKind.APPLICATION, "application", "1")); - DefinitionRegistry registry = DefinitionRegistry.builder() - .register(record.graph(), "graph") - .register(record.toolCatalog(), "tools") - .register(record.codec(), "codec") - .register(record.application(), "application") - .build(); - WorkerCapabilities worker = new WorkerCapabilities(1, Set.of(DefinitionKind.values()), Set.of(record.codec())); - RunDefinitionAdmission.requireDurableMode(store.capabilities(), - List.of(RuntimeComponentDeclaration.notApplicable(RuntimeComponentRole.CUSTOM_NODE), - RuntimeComponentDeclaration.notApplicable(RuntimeComponentRole.CHAT_MEMORY), - RuntimeComponentDeclaration.dataOnly(RuntimeComponentRole.STORE, "store"), - RuntimeComponentDeclaration.notApplicable(RuntimeComponentRole.BACKGROUND_TOOL))); - return RunDefinitionAdmission.resolve(admitted, codec.decode(codec.encode(record)), registry, worker); - } - -} diff --git a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/GraphApiFixture.java b/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/GraphApiFixture.java deleted file mode 100644 index 0db3453d5..000000000 --- a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/GraphApiFixture.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import java.util.Map; - -import io.github.agentic.ai.workflow.graph.CompileConfig; -import io.github.agentic.ai.workflow.graph.CompiledGraph; -import io.github.agentic.ai.workflow.graph.KeyStrategy; -import io.github.agentic.ai.workflow.graph.RunnableConfig; -import io.github.agentic.ai.workflow.graph.StateGraph; -import io.github.agentic.ai.workflow.graph.checkpoint.config.SaverConfig; -import io.github.agentic.ai.workflow.graph.checkpoint.savers.MemorySaver; - -import static io.github.agentic.ai.workflow.graph.action.AsyncNodeAction.node_async; - -final class GraphApiFixture { - - private GraphApiFixture() { - } - - static void compileOnly() throws Exception { - MemorySaver saver = MemorySaver.builder().build(); - StateGraph graph = new StateGraph(() -> Map.of("messages", KeyStrategy.APPEND)) - .addNode("node", node_async(state -> Map.of("messages", "done"))) - .addEdge(StateGraph.START, "node") - .addEdge("node", StateGraph.END); - CompileConfig compileConfig = CompileConfig.builder() - .saverConfig(SaverConfig.builder().register(saver).build()) - .build(); - RunnableConfig runnableConfig = RunnableConfig.builder().threadId("fixture").build(); - CompiledGraph compiled = graph.compile(compileConfig); - - compiled.stream(Map.of(), RunnableConfig.builder().threadId("fixture").build()); - compiled.invoke(Map.of(), runnableConfig); - compiled.getStateHistory(runnableConfig); - compiled.updateState(runnableConfig, Map.of("messages", "updated")); - } - -} diff --git a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/RunStoreApiFixture.java b/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/RunStoreApiFixture.java deleted file mode 100644 index ad3325aac..000000000 --- a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/RunStoreApiFixture.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2025-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import io.github.agentic.ai.runtime.execution.TrustedExecutionContext; -import io.github.agentic.ai.runtime.execution.run.MemoryRunStore; -import io.github.agentic.ai.runtime.execution.run.RunCommit; -import io.github.agentic.ai.runtime.execution.run.RunLease; -import io.github.agentic.ai.runtime.execution.run.RunPayload; -import io.github.agentic.ai.runtime.execution.run.RunSnapshot; -import io.github.agentic.ai.runtime.execution.run.RunSpec; -import io.github.agentic.ai.runtime.execution.run.RunSpecCodec; -import io.github.agentic.ai.runtime.execution.run.RunState; -import io.github.agentic.ai.runtime.execution.run.RunStore; - -import java.time.Duration; -import java.util.List; -import java.util.UUID; - -/** Compile the opt-in contract as a consumer without a model SDK or Extensions. */ -final class RunStoreApiFixture { - - private RunStoreApiFixture() { - } - - static RunSnapshot compileOnly(TrustedExecutionContext authenticated) { - RunStore store = new MemoryRunStore(); - RunSpec spec = RunSpec.from(authenticated, "application", "session", "request", "graph", "1", - new RunPayload(1, "application.input", "{}")); - RunSpecCodec codec = new RunSpecCodec(); - RunSnapshot admitted = store.submit(codec.decodeRun(codec.encodeRun(spec)), authenticated); - RunLease lease = store.acquire(spec.namespace(), spec.runId(), UUID.randomUUID(), Duration.ofSeconds(30)); - return store.commit(spec.namespace(), spec.runId(), lease, - new RunCommit(admitted.revision(), RunState.RUNNING, null, List.of(), List.of(), List.of())); - } - -} diff --git a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/StudioApiFixture.java b/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/StudioApiFixture.java deleted file mode 100644 index 653569867..000000000 --- a/tools/compatibility/neutral-api-consumer/src/main/java/io/github/agentic/ai/contract/StudioApiFixture.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import java.util.Map; - -import io.github.agentic.ai.agent.studio.dto.AgentResumeRequest; -import io.github.agentic.ai.agent.studio.dto.AgentRunRequest; -import io.github.agentic.ai.agent.studio.dto.GraphRunRequest; -import io.github.agentic.ai.agent.studio.dto.Thread; -import io.github.agentic.ai.agent.studio.loader.AgentLoader; -import io.github.agentic.ai.agent.studio.loader.GraphLoader; - -final class StudioApiFixture { - - private StudioApiFixture() { - } - - static void compileOnly(AgentLoader agentLoader, GraphLoader graphLoader) { - AgentRunRequest agentRunRequest = new AgentRunRequest(); - AgentResumeRequest agentResumeRequest = new AgentResumeRequest(); - GraphRunRequest graphRunRequest = new GraphRunRequest(); - Thread thread = Thread.builder("fixture").appName("agent").userId("user").values(Map.of()).build(); - - agentLoader.listAgents(); - graphLoader.listGraphs(); - agentRunRequest.appName = "agent"; - agentRunRequest.threadId = thread.threadId(); - agentResumeRequest.threadId = thread.threadId(); - graphRunRequest.graphName = "graph"; - graphRunRequest.getGraphName(); - } - -} diff --git a/tools/compatibility/policy.yaml b/tools/compatibility/policy.yaml deleted file mode 100644 index 1f950a729..000000000 --- a/tools/compatibility/policy.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. - -# The neutral API directly replaces the Spring AI API; no legacy compatibility gate. -replacement_policy: breaking -api_consumer: tools/compatibility/neutral-api-consumer -validation_gates: - - api-contract-check diff --git a/tools/compatibility/react-agent-consumer/pom.xml b/tools/compatibility/react-agent-consumer/pom.xml deleted file mode 100644 index af9c554bf..000000000 --- a/tools/compatibility/react-agent-consumer/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 4.0.0 - io.github.agentic.ai - react-agent-consumer - 1.0.0 - - UTF-8 - 17 - 2.1.0-RC2-SNAPSHOT - - - - io.github.agentic-ai-java - argi-agent-core - ${argi.version} - - - diff --git a/tools/compatibility/react-agent-consumer/src/main/java/io/github/agentic/ai/contract/ReActAgentConsumer.java b/tools/compatibility/react-agent-consumer/src/main/java/io/github/agentic/ai/contract/ReActAgentConsumer.java deleted file mode 100644 index f4d0ba27d..000000000 --- a/tools/compatibility/react-agent-consumer/src/main/java/io/github/agentic/ai/contract/ReActAgentConsumer.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import io.github.agentic.ai.agent.AgentExecutionContext; -import io.github.agentic.ai.agent.AgentOutcome; -import io.github.agentic.ai.agent.AgentRequest; -import io.github.agentic.ai.agent.react.ReActAgent; -import io.github.agentic.ai.api.chat.ChatClient; -import io.github.agentic.ai.api.chat.ChatClientCapabilities; -import io.github.agentic.ai.api.chat.ChatEvent; -import io.github.agentic.ai.api.chat.ChatMessage; -import io.github.agentic.ai.api.chat.ChatRequest; -import io.github.agentic.ai.api.chat.ChatResponse; -import io.github.agentic.ai.api.chat.FinishReason; -import io.github.agentic.ai.api.chat.ToolCall; -import io.github.agentic.ai.api.chat.ToolDefinition; -import io.github.agentic.ai.api.chat.Usage; -import io.github.agentic.ai.api.tool.Tool; -import io.github.agentic.ai.api.tool.ToolContext; -import io.github.agentic.ai.api.tool.ToolRegistry; -import io.github.agentic.ai.api.tool.ToolResult; -import io.github.agentic.ai.runtime.execution.DelegationScope; -import io.github.agentic.ai.runtime.execution.ExecutionBudget; -import io.github.agentic.ai.runtime.execution.PrincipalReference; -import io.github.agentic.ai.runtime.execution.TenantReference; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContext; - -import java.time.Clock; -import java.util.ArrayDeque; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Flow; - -/** Executes a real two-turn ReAct invocation with only Agent Core and its neutral dependencies. */ -public final class ReActAgentConsumer { - - private ReActAgentConsumer() { - } - - public static void main(String[] args) throws Exception { - for (String unavailable : List.of("io.github.agentic.ai.workflow.graph.StateGraph", - "io.github.agentic.ai.workflow.graph.CompiledGraph", "io.github.agentic.ai.workflow.graph.RunnableConfig", - "reactor.core.publisher.Flux", "org.springframework.context.ApplicationContext")) { - try { - Class.forName(unavailable, false, ReActAgentConsumer.class.getClassLoader()); - throw new IllegalStateException("Unexpected Graph or framework dependency: " + unavailable); - } - catch (ClassNotFoundException expected) { - // The independent ReAct consumer must work without these artifacts. - } - } - ToolCall call = new ToolCall("call-1", "lookup", "{}"); - Queue responses = new ArrayDeque<>(List.of( - response(ChatMessage.assistant("tool", List.of(call)), List.of(call)), - response(ChatMessage.assistant("answer"), List.of()))); - ChatClient client = new ChatClient() { - public CompletionStage call(ChatRequest request) { - return CompletableFuture.completedFuture(responses.remove()); - } - - public Flow.Publisher stream(ChatRequest request) { - throw new UnsupportedOperationException(); - } - - public ChatClientCapabilities capabilities() { - return ChatClientCapabilities.basic(); - } - }; - Tool tool = new Tool() { - public ToolDefinition definition() { - return new ToolDefinition("lookup", "lookup", "{}"); - } - - public CompletionStage execute(ToolCall ignored, ToolContext context) { - return CompletableFuture.completedFuture(ToolResult.text("tool result")); - } - }; - ReActAgent agent = ReActAgent.builder(client).tools(new ToolRegistry(List.of(tool))).build(); - TrustedExecutionContext trusted = TrustedExecutionContext.builder() - .tenant(new TenantReference("consumer", "tenant")) - .principal(new PrincipalReference("consumer", "principal")) - .runId("run").correlationId("correlation").budget(ExecutionBudget.unlimited()) - .delegationScope(DelegationScope.none()).build(); - try (AgentExecutionContext context = new AgentExecutionContext(trusted, Clock.systemUTC())) { - AgentOutcome outcome = agent.invoke(AgentRequest.messages(List.of(ChatMessage.user("hello"))), context) - .get(); - if (!"answer".equals(outcome.messages().get(outcome.messages().size() - 1).text()) - || outcome.messages().stream().noneMatch(message -> "tool result".equals(message.text()))) { - throw new IllegalStateException("ReAct loop did not execute the independent tool turn"); - } - } - } - - private static ChatResponse response(ChatMessage message, List calls) { - return new ChatResponse(null, "consumer", List.of(message), calls, FinishReason.STOP, - new Usage(1L, 1L, 1L), Map.of()); - } - -} diff --git a/tools/compatibility/runtime-api-consumer/pom.xml b/tools/compatibility/runtime-api-consumer/pom.xml deleted file mode 100644 index 514bb7f40..000000000 --- a/tools/compatibility/runtime-api-consumer/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 4.0.0 - io.github.agentic.ai - runtime-api-consumer - 1.0.0 - - UTF-8 - 17 - 2.1.0-RC2-SNAPSHOT - - - - io.github.agentic-ai-java - argi-runtime-api - ${argi.version} - - - diff --git a/tools/compatibility/runtime-api-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeApiConsumer.java b/tools/compatibility/runtime-api-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeApiConsumer.java deleted file mode 100644 index 12828de26..000000000 --- a/tools/compatibility/runtime-api-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeApiConsumer.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import java.time.Instant; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import io.github.agentic.ai.runtime.execution.DelegationScope; -import io.github.agentic.ai.runtime.execution.ExecutionBudget; -import io.github.agentic.ai.runtime.execution.PrincipalReference; -import io.github.agentic.ai.runtime.execution.TenantReference; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContext; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContextReference; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContextResolver; - -/** Executes the shared identity contract with only its jar and the JDK available. */ -public final class RuntimeApiConsumer { - - private RuntimeApiConsumer() { - } - - public static void main(String[] args) throws Exception { - for (String unavailable : Set.of("io.github.agentic.ai.workflow.graph.StateGraph", - "io.github.agentic.ai.workflow.graph.RunnableConfig", "io.github.agentic.ai.agent.orchestration.Agent", - "io.github.agentic.ai.api.chat.ChatClient", "org.springframework.context.ApplicationContext", - "reactor.core.publisher.Flux", "com.fasterxml.jackson.databind.ObjectMapper")) { - try { - Class.forName(unavailable, false, RuntimeApiConsumer.class.getClassLoader()); - throw new IllegalStateException("Unexpected runtime dependency: " + unavailable); - } - catch (ClassNotFoundException expected) { - // A standalone runtime contract must work with these engines and libraries absent. - } - } - - TrustedExecutionContext parent = TrustedExecutionContext.builder() - .tenant(new TenantReference("application-iam", "tenant")) - .principal(new PrincipalReference("application-iam", "principal")) - .runId("parent").correlationId("correlation").deadline(Instant.parse("2030-01-01T00:00:00Z")) - .budget(ExecutionBudget.of(Map.of("model_calls", 3L))) - .delegationScope(DelegationScope.of(Set.of("agents:delegate", "tools:read"))).build(); - TrustedExecutionContext child = parent.delegate("child", Set.of("tools:read"), null, - ExecutionBudget.of(Map.of("model_calls", 1L))); - if (!parent.tenant().equals(child.tenant()) || !parent.deadline().equals(child.deadline()) - || child.budget().limits().get("model_calls") != 1L - || child.delegationScope().allows("agents:delegate")) { - throw new IllegalStateException("Child execution did not preserve and narrow its parent constraints"); - } - TrustedExecutionContextReference reference = child.toReference(); - TrustedExecutionContextResolver resolver = candidate -> candidate.equals(reference) - ? Optional.of(child) : Optional.empty(); - if (resolver.resolveRequired(reference) != child || reference.schemaVersion() != 1) { - throw new IllegalStateException("Reference resolution changed the execution identity"); - } - try { - child.budget().limits().put("model_calls", 100L); - throw new IllegalStateException("Execution constraints must be immutable"); - } - catch (UnsupportedOperationException expected) { - // A consumer cannot mutate authenticated constraints through collection accessors. - } - } - -} diff --git a/tools/compatibility/runtime-core-consumer/pom.xml b/tools/compatibility/runtime-core-consumer/pom.xml deleted file mode 100644 index d088282f3..000000000 --- a/tools/compatibility/runtime-core-consumer/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 4.0.0 - io.github.agentic.ai - runtime-core-consumer - 1.0.0 - - UTF-8 - 17 - 2.1.0-RC2-SNAPSHOT - - - - io.github.agentic-ai-java - argi-runtime-core - ${argi.version} - - - diff --git a/tools/compatibility/runtime-core-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeCoreConsumer.java b/tools/compatibility/runtime-core-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeCoreConsumer.java deleted file mode 100644 index 33a024a4d..000000000 --- a/tools/compatibility/runtime-core-consumer/src/main/java/io/github/agentic/ai/contract/RuntimeCoreConsumer.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2025-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import io.github.agentic.ai.runtime.execution.PrincipalReference; -import io.github.agentic.ai.runtime.execution.TenantReference; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContext; -import io.github.agentic.ai.runtime.execution.definition.DefinitionKind; -import io.github.agentic.ai.runtime.execution.definition.DefinitionReference; -import io.github.agentic.ai.runtime.execution.definition.DefinitionRegistry; -import io.github.agentic.ai.runtime.execution.definition.RunDefinitionAdmission; -import io.github.agentic.ai.runtime.execution.definition.RunDefinitionRecord; -import io.github.agentic.ai.runtime.execution.definition.RunDefinitionRecordCodec; -import io.github.agentic.ai.runtime.execution.definition.WorkerCapabilities; -import io.github.agentic.ai.runtime.execution.run.MemoryRunStore; -import io.github.agentic.ai.runtime.execution.run.RunCommit; -import io.github.agentic.ai.runtime.execution.run.RunEntry; -import io.github.agentic.ai.runtime.execution.run.RunPayload; -import io.github.agentic.ai.runtime.execution.run.RunSpec; -import io.github.agentic.ai.runtime.execution.run.RunSpecCodec; -import io.github.agentic.ai.runtime.execution.run.RunState; -import io.github.agentic.ai.runtime.execution.ExecutionControl; -import java.time.Clock; -import java.time.Duration; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -/** A standalone runtime contract consumer, not an engine implementation. */ -public final class RuntimeCoreConsumer { - - private RuntimeCoreConsumer() { - } - - @FunctionalInterface - private interface LocalAction { - - RunPayload execute(RunPayload input); - - } - - public static void main(String[] args) throws Exception { - DefinitionKind kind = DefinitionKind.fromWireName(args[0]); - for (String absent : Set.of("io.github.agentic.ai.workflow.graph.StateGraph", "io.github.agentic.ai.agent.orchestration.Agent", - "reactor.core.publisher.Flux", "org.springframework.context.ApplicationContext", - "io.micrometer.observation.ObservationRegistry")) { - try { - Class.forName(absent, false, RuntimeCoreConsumer.class.getClassLoader()); - throw new IllegalStateException("Unexpected engine dependency: " + absent); - } - catch (ClassNotFoundException expected) { - // Engines and their integrations must not be available. - } - } - var context = TrustedExecutionContext.builder() - .tenant(new TenantReference("iam", "tenant")) - .principal(new PrincipalReference("iam", "user")) - .runId("run") - .correlationId("trace") - .build(); - var target = DefinitionReference.of(kind, "local", "1"); - var tools = DefinitionReference.of(DefinitionKind.TOOL_CATALOG, "tools", "1"); - var codec = DefinitionReference.of(DefinitionKind.CODEC, "json", "1"); - var application = DefinitionReference.of(DefinitionKind.APPLICATION, "app", "1"); - var original = RunSpec.from(context, "app", "session", "key", target, new RunPayload(1, "input", "{}")); - var run = new RunSpecCodec().decodeRun(new RunSpecCodec().encodeRun(original)); - var record = RunDefinitionRecord.forRun(run, tools, codec, application); - var restored = new RunDefinitionRecordCodec().decode(new RunDefinitionRecordCodec().encode(record)); - LocalAction action = input -> new RunPayload(1, "result", "{}"); - var registry = DefinitionRegistry.builder() - .register(target, action) - .register(tools, "tools") - .register(codec, "json") - .register(application, "app") - .build(); - var definitions = RunDefinitionAdmission.resolve(run, restored, registry, - new WorkerCapabilities(2, - Set.of(kind, DefinitionKind.TOOL_CATALOG, DefinitionKind.CODEC, DefinitionKind.APPLICATION), - Set.of(codec))); - var store = new MemoryRunStore(); - store.submit(run, context); - var lease = store.acquire(run.namespace(), run.runId(), UUID.randomUUID(), Duration.ofSeconds(30)); - store.commit(run.namespace(), run.runId(), lease, - new RunCommit(0, RunState.RUNNING, null, List.of(), List.of(), List.of())); - try (var control = new ExecutionControl(context, Clock.systemUTC())) { - control.assertActive(); - RunPayload result = ((LocalAction) definitions.execution()).execute(run.input()); - control.assertActive(); - var event = new RunEntry(1, "completed", RunEntry.Kind.EVENT, result); - var finished = store.commit(run.namespace(), run.runId(), lease, - new RunCommit(1, RunState.SUCCEEDED, null, List.of(), List.of(event), List.of(event))); - if (finished.state() != RunState.SUCCEEDED) { - throw new IllegalStateException(); - } - } - } - -} diff --git a/tools/compatibility/workflow-consumer/pom.xml b/tools/compatibility/workflow-consumer/pom.xml deleted file mode 100644 index 6269f6e01..000000000 --- a/tools/compatibility/workflow-consumer/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 4.0.0 - io.github.agentic.ai - workflow-consumer - 1.0.0 - - UTF-8 - 17 - 2.1.0-RC2-SNAPSHOT - - - - io.github.agentic-ai-java - argi-workflow-core - ${argi.version} - - - diff --git a/tools/compatibility/workflow-consumer/src/main/java/io/github/agentic/ai/contract/WorkflowConsumer.java b/tools/compatibility/workflow-consumer/src/main/java/io/github/agentic/ai/contract/WorkflowConsumer.java deleted file mode 100644 index b635e2189..000000000 --- a/tools/compatibility/workflow-consumer/src/main/java/io/github/agentic/ai/contract/WorkflowConsumer.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.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 io.github.agentic.ai.contract; - -import io.github.agentic.ai.agent.Agent; -import io.github.agentic.ai.agent.AgentEvent; -import io.github.agentic.ai.agent.AgentExecutionContext; -import io.github.agentic.ai.agent.AgentOutcome; -import io.github.agentic.ai.agent.AgentRequest; -import io.github.agentic.ai.api.chat.ChatMessage; -import io.github.agentic.ai.api.chat.Usage; -import io.github.agentic.ai.api.tool.Tool; -import io.github.agentic.ai.api.tool.ToolContext; -import io.github.agentic.ai.api.tool.ToolRegistry; -import io.github.agentic.ai.api.tool.ToolResult; -import io.github.agentic.ai.runtime.execution.DelegationScope; -import io.github.agentic.ai.runtime.execution.ExecutionBudget; -import io.github.agentic.ai.runtime.execution.PrincipalReference; -import io.github.agentic.ai.runtime.execution.TenantReference; -import io.github.agentic.ai.runtime.execution.TrustedExecutionContext; -import io.github.agentic.ai.workflow.WorkflowAgent; -import io.github.agentic.ai.workflow.WorkflowDefinition; -import io.github.agentic.ai.workflow.WorkflowDefinitionParser; -import java.time.Clock; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Flow; - -/** Runs a parsed two-step workflow without Graph, Loop or framework artifacts. */ -public final class WorkflowConsumer { - - private WorkflowConsumer() { - } - - public static void main(String[] args) throws Exception { - for (String unavailable : List.of("io.github.agentic.ai.workflow.graph.StateGraph", - "io.github.agentic.ai.workflow.graph.CompiledGraph", "io.github.agentic.ai.workflow.graph.RunnableConfig", - "reactor.core.publisher.Flux", "org.springframework.context.ApplicationContext")) { - try { - Class.forName(unavailable, false, WorkflowConsumer.class.getClassLoader()); - throw new IllegalStateException("Unexpected Graph or framework dependency: " + unavailable); - } - catch (ClassNotFoundException expected) { - } - } - WorkflowDefinition definition = new WorkflowDefinitionParser().parse(""" - {"id":"consumer","version":"1.0.0","steps":[ - {"type":"agent","id":"answer","agentRef":"echo"}, - {"type":"tool","id":"record","toolName":"record","arguments":{"value":{"type":"state","path":"answer"}}} - ]}"""); - Agent echo = (request, context) -> subscriber -> { - subscriber.onSubscribe(new Flow.Subscription() { - public void request(long demand) { - if (demand > 0) { - subscriber.onNext(new AgentEvent.Finished(AgentOutcome.completed( - List.of(ChatMessage.assistant("echo")), Map.of("answer", "echo"), new Usage(0L, 1L, 1L)))); - subscriber.onComplete(); - } - } - - public void cancel() { - } - }); - }; - Tool record = new Tool() { - public io.github.agentic.ai.api.chat.ToolDefinition definition() { - return new io.github.agentic.ai.api.chat.ToolDefinition("record", "record", "{}"); - } - - public CompletionStage execute(io.github.agentic.ai.api.chat.ToolCall call, ToolContext context) { - return CompletableFuture.completedFuture(ToolResult.text("recorded")); - } - }; - WorkflowAgent workflow = WorkflowAgent.builder(definition).agents(Map.of("echo", echo)) - .tools(new ToolRegistry(List.of(record))).build(); - TrustedExecutionContext trusted = TrustedExecutionContext.builder() - .tenant(new TenantReference("consumer", "tenant")) - .principal(new PrincipalReference("consumer", "principal")) - .runId("run").correlationId("correlation").budget(ExecutionBudget.unlimited()) - .delegationScope(DelegationScope.none()).build(); - try (AgentExecutionContext context = new AgentExecutionContext(trusted, Clock.systemUTC())) { - AgentOutcome outcome = workflow.invoke(AgentRequest.messages(List.of(ChatMessage.user("hello"))), context) - .get(); - if (!"recorded".equals(outcome.outputs().get("record"))) { - throw new IllegalStateException("Independent workflow did not execute its tool step"); - } - } - } - -} diff --git a/tools/make/java.mk b/tools/make/java.mk index 19ef72cff..be2289983 100644 --- a/tools/make/java.mk +++ b/tools/make/java.mk @@ -45,12 +45,3 @@ checkstyle-check: ## Checkstyle Check the code and output to target/checkstyle-r @$(LOG_TARGET) ./mvnw -Dcheckstyle.skip=false -Dcheckstyle.output.file=checkstyle-report.xml checkstyle:check -.PHONY: api-contract-check -api-contract-check: ## Compile current public API consumers without provider SDKs - @$(LOG_TARGET) - tools/scripts/verify-api-contract.sh - -.PHONY: clear-room-check -clear-room-check: ## Verify Clear Room ownership, compatibility, and dependency boundaries - @$(LOG_TARGET) - tools/scripts/verify-clear-room.sh diff --git a/tools/scripts/check-chinese-content.py b/tools/scripts/check-chinese-content.py deleted file mode 100644 index b79b67334..000000000 --- a/tools/scripts/check-chinese-content.py +++ /dev/null @@ -1,313 +0,0 @@ -# -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. -# - -""" -Agentic for Spring AI Chinese Content Checker -Tool for checking Chinese content in Java and frontend code for GitHub Actions -""" - -import os -import re -import sys -import json -import argparse -from pathlib import Path -from typing import List, Dict, Set -from collections import defaultdict - -class ChineseContentChecker: - def __init__(self, target_dirs: List[str], file_patterns: List[str] = None): - self.target_dirs = [Path(d) for d in target_dirs] - self.file_patterns = file_patterns or ['**/*.java', '**/*.vue', '**/*.ts', '**/*.js', '**/*.jsx', '**/*.tsx'] - - # Detect Chinese characters, excluding Chinese punctuation to avoid false positives - self.chinese_pattern = re.compile(r'[\u4e00-\u9fff]') - # Chinese punctuation detection - self.chinese_punctuation = re.compile(r',。!?;:“”()【】《》') - - # Exclude common English phrases to avoid false positives - self.exclude_patterns = [ - r'\bAS IS\b', # "AS IS" in Apache License - r'\bIS NULL\b', # "IS NULL" in SQL - r'\bIS NOT\b', # "IS NOT" in SQL - r'@author\s+\w+', # Author information - r'@time\s+\d{4}/\d{1,2}/\d{1,2}', # Time information - ] - - # Exclude directories and files - self.exclude_dirs = { - 'target', 'node_modules', '.git', '.idea', 'dist', 'build', - 'logs', 'h2-data', '.mvn', 'playwright', 'i18n', 'locales' - } - - self.exclude_files = { - 'package-lock.json', 'yarn.lock', 'pom.xml' - } - - # Exclude i18n related file patterns - self.exclude_file_patterns = [ - r'.*i18n.*\.json$', - r'.*locale.*\.json$', - r'.*lang.*\.json$', - r'.*messages.*\.properties$', - r'.*_zh.*\.json$', - r'.*_cn.*\.json$' - ] - - self.issues = [] - - def should_exclude_file(self, file_path: Path) -> bool: - """Check if file should be excluded from checking""" - # Check if any parent directory is in exclude list - for part in file_path.parts: - if part in self.exclude_dirs: - return True - - # Check if filename is in exclude list - if file_path.name in self.exclude_files: - return True - - # Check if file matches i18n patterns - file_str = str(file_path) - for pattern in self.exclude_file_patterns: - if re.match(pattern, file_str, re.IGNORECASE): - return True - - return False - - def has_real_chinese_content(self, text: str) -> bool: - """Check if text contains real Chinese content (excluding false positives)""" - # First check if there are Chinese characters or Chinese punctuation - if not (self.chinese_pattern.search(text) or self.chinese_punctuation.search(text)): - return False - - # Exclude common English phrases - for pattern in self.exclude_patterns: - if re.search(pattern, text, re.IGNORECASE): - # If matched exclude pattern, further check if it really contains Chinese - temp_text = re.sub(pattern, '', text, flags=re.IGNORECASE) - if not (self.chinese_pattern.search(temp_text) or self.chinese_punctuation.search(temp_text)): - return False - - return True - - def check_file(self, file_path: Path) -> List[Dict]: - """Check single file for Chinese content, return list of issues""" - issues = [] - - try: - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: - lines = f.readlines() - - in_multiline_comment = False - in_template_section = False - - for line_num, line in enumerate(lines, 1): - original_line = line.rstrip() - line_stripped = line.strip() - - if not line_stripped: - continue - - # Check if contains real Chinese content - if not self.has_real_chinese_content(line_stripped): - continue - - # Analyze the type of location where Chinese content appears - content_type = self._analyze_content_type(line_stripped, in_multiline_comment, in_template_section, file_path) - - # Update multiline comment status - if '/*' in line_stripped: - in_multiline_comment = True - if '*/' in line_stripped: - in_multiline_comment = False - - # Update template section status for Vue files - if file_path.suffix == '.vue': - if '' in line_stripped: - in_template_section = False - - issues.append({ - 'file': str(file_path), - 'line': line_num, - 'content': original_line, - 'type': content_type, - 'message': f"Found Chinese content in {content_type}" - }) - - except Exception as e: - print(f"::warning::Error reading file {file_path}: {e}") - - return issues - - def _analyze_content_type(self, line: str, in_multiline_comment: bool, in_template_section: bool, file_path: Path) -> str: - """Analyze the type of Chinese content location""" - if in_multiline_comment or line.startswith('/*'): - return "multiline comment" - - if line.startswith('//'): - return "single line comment" - - if '//' in line: - comment_part = line[line.find('//'):] - if self.has_real_chinese_content(comment_part): - return "inline comment" - - # Check Vue template content - if in_template_section and file_path.suffix == '.vue': - return "Vue template" - - # Check string literals - string_matches = re.finditer(r'"([^"]*)"', line) - for match in string_matches: - if self.has_real_chinese_content(match.group(1)): - return "string literal" - - # Check template literals (for JS/TS files) - if file_path.suffix in ['.js', '.ts', '.jsx', '.tsx', '.vue']: - template_matches = re.finditer(r'`([^`]*)`', line) - for match in template_matches: - if self.has_real_chinese_content(match.group(1)): - return "template literal" - - # Check character literals - char_matches = re.finditer(r"'([^']*)'", line) - for match in char_matches: - if self.has_real_chinese_content(match.group(1)): - return "character literal" - - # Check identifiers - temp_line = re.sub(r'"[^"]*"', '', line) # Remove strings - temp_line = re.sub(r"'[^']*'", '', temp_line) # Remove characters - temp_line = re.sub(r'`[^`]*`', '', temp_line) # Remove template literals - temp_line = re.sub(r'//.*$', '', temp_line) # Remove single line comments - - if self.has_real_chinese_content(temp_line): - return "identifier or code" - - return "unknown location" - - def check_directories(self) -> bool: - """Check all target directories, return whether there are issues""" - all_files = [] - - for target_dir in self.target_dirs: - if not target_dir.exists(): - print(f"::warning::Directory does not exist: {target_dir}") - continue - - for pattern in self.file_patterns: - files = list(target_dir.rglob(pattern)) - # Filter out excluded files - files = [f for f in files if not self.should_exclude_file(f)] - all_files.extend(files) - - if not all_files: - print(f"::notice::No files found matching patterns {self.file_patterns} in directories {[str(d) for d in self.target_dirs]}") - return True - - print(f"::notice::Found {len(all_files)} files, starting check...") - - for file_path in all_files: - file_issues = self.check_file(file_path) - self.issues.extend(file_issues) - - return len(self.issues) == 0 - - def report_issues(self) -> None: - """Report discovered issues""" - if not self.issues: - print("::notice::✅ No files with Chinese content found") - return - - print(f"::error::❌ Found {len(self.issues)} Chinese content issues") - - # Group issues by file - files_with_issues = defaultdict(list) - for issue in self.issues: - files_with_issues[issue['file']].append(issue) - - for file_path, file_issues in files_with_issues.items(): - print(f"::error file={file_path}::File contains {len(file_issues)} Chinese content issues") - - for issue in file_issues: - print(f"::error file={file_path},line={issue['line']}::{issue['message']}: {issue['content'][:100]}") - - # Output modification suggestions - print("\n::notice::Modification suggestions:") - print("::notice::1. Change Chinese comments to English comments") - print("::notice::2. Extract Chinese strings to resource files or configuration files") - print("::notice::3. Change Chinese identifiers to English identifiers") - print("::notice::4. For test data, consider using English or placeholders") - -def main(): - parser = argparse.ArgumentParser(description='Check Chinese content in Java and frontend code') - parser.add_argument('--dirs', '-d', - nargs='+', - default=['argi-orchestration/argi-agent-framework/src/main/java', - 'argi-orchestration/argi-workflow-graph-core/src/main/java', - 'argi-studio/src/main/java', - 'argi-api', - 'argi-spring-boot-starter', - 'argi-studio/agent-chat-ui/src'], - help='Directory paths to check (relative to current directory)') - parser.add_argument('--patterns', '-p', - nargs='+', - default=['**/*.java', '**/*.vue', '**/*.ts', '**/*.js', '**/*.jsx', '**/*.tsx'], - help='File patterns to check') - parser.add_argument('--fail-on-found', '-f', - action='store_true', - help='Return non-zero exit code when Chinese content is found') - - args = parser.parse_args() - - try: - # Expand glob patterns in directory paths - expanded_dirs = [] - for dir_pattern in args.dirs: - if '*' in dir_pattern: - # Use glob to expand patterns - from glob import glob - matches = glob(dir_pattern) - expanded_dirs.extend(matches) - else: - expanded_dirs.append(dir_pattern) - - # Filter out non-existent directories - existing_dirs = [d for d in expanded_dirs if Path(d).exists()] - - if not existing_dirs: - print("::warning::No existing directories found to check") - return 0 - - checker = ChineseContentChecker(existing_dirs, args.patterns) - is_clean = checker.check_directories() - checker.report_issues() - - if args.fail_on_found and not is_clean: - print(f"::error::Check failed: Found {len(checker.issues)} Chinese content issues") - return 1 - - return 0 - - except Exception as e: - print(f"::error::Error occurred during check: {e}") - return 1 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/scripts/migrate-maven-coordinates.py b/tools/scripts/migrate-maven-coordinates.py deleted file mode 100644 index d1c70d94d..000000000 --- a/tools/scripts/migrate-maven-coordinates.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. - -import argparse -import re -import xml.etree.ElementTree as ET -from pathlib import Path - - -def main() -> None: - parser = argparse.ArgumentParser(description="Migrate Core Maven coordinates, preserving POM formatting") - parser.add_argument("poms", nargs="+", type=Path) - parser.add_argument("--write", action="store_true", help="Apply changes with exclusive .before-argi-rc1 backups") - args = parser.parse_args() - core_artifacts = { - "argi", "argi-bom", "argi-workflow-graph-core", "argi-agent-framework", "argi-studio", - "argi-starter-builtin-nodes", "argi-starter-graph-observation", - } - # Restrict migration to Core; Extensions artifacts have a separate release lifecycle. - block = re.compile(r"<(dependency|parent)>.*?", re.DOTALL) - coordinate = re.compile(r"(\s*)io\.github\.agentic-ai(\s*)") - artifact = re.compile(r"\s*([^<]+?)\s*") - plans = [] - for path in dict.fromkeys(args.poms): - ET.parse(path) - text = path.read_text(encoding="utf-8") - def migrate(match): - value = match.group(0) - name = artifact.search(value) - return coordinate.sub(r"\g<1>io.github.agentic-ai-java\2", value) if name and name[1] in core_artifacts else value - updated = block.sub(migrate, text) - if updated != text: - plans.append((path, text, updated)) - if args.write: - for path, text, updated in plans: - backup = path.with_name(path.name + ".before-argi-rc1") - with backup.open("x", encoding="utf-8", newline="") as stream: - stream.write(text) - path.write_text(updated, encoding="utf-8", newline="") - print(f"Migrated {path}; backup: {backup}") - else: - for path, _, _ in plans: - print(f"Would migrate {path}") - - -if __name__ == "__main__": - main() diff --git a/tools/scripts/verify-api-contract.sh b/tools/scripts/verify-api-contract.sh deleted file mode 100755 index d153c104a..000000000 --- a/tools/scripts/verify-api-contract.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. -# - -set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel)" -readonly REPO_ROOT -readonly CONTRACT_MODULES=':argi-agent-framework,:argi-studio,:argi-starter-graph-observation,:argi-starter-builtin-nodes' -readonly FIXTURE_POM="${REPO_ROOT}/tools/compatibility/neutral-api-consumer/pom.xml" -readonly RUNTIME_FIXTURE="${REPO_ROOT}/tools/compatibility/runtime-api-consumer" - -# Install current artifacts, then compile a separate consumer with no provider SDK. -if [[ $# -gt 1 ]]; then - echo "Usage: $0 [--skip-install]" >&2 - exit 2 -fi -case "${1:-}" in - '') "${REPO_ROOT}/mvnw" -B -Dmaven.test.skip=true -pl "${CONTRACT_MODULES}" -am install ;; - --skip-install) ;; # CI has already installed and tested this checkout's artifacts. - *) echo "Usage: $0 [--skip-install]" >&2; exit 2 ;; -esac -"${REPO_ROOT}/mvnw" -B -f "${FIXTURE_POM}" clean compile - -echo "Neutral public API consumer gate passed" - -# A separate JVM verifies that shared contracts do not load the graph or provider stack. -"${REPO_ROOT}/mvnw" -B -f "${RUNTIME_FIXTURE}/pom.xml" clean compile \ - org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath \ - -Dmdep.outputFile=target/runtime-classpath.txt -runtime_classpath="$(cat "${RUNTIME_FIXTURE}/target/runtime-classpath.txt")" -if [[ -z "${runtime_classpath}" || "${runtime_classpath}" == *:* ]]; then - echo "Runtime API must resolve to exactly one jar without transitive dependencies" >&2 - exit 1 -fi -java -cp "${RUNTIME_FIXTURE}/target/classes:${runtime_classpath}" \ - io.github.agentic.ai.contract.RuntimeApiConsumer -if [[ "$(jdeps --multi-release 17 --print-module-deps "${runtime_classpath}")" != 'java.base' ]]; then - echo "Runtime API bytecode must depend only on java.base" >&2 - exit 1 -fi -echo "Standalone runtime API consumer and bytecode gates passed" - -readonly RUNTIME_CORE_FIXTURE="${REPO_ROOT}/tools/compatibility/runtime-core-consumer" -"${REPO_ROOT}/mvnw" -B -f "${RUNTIME_CORE_FIXTURE}/pom.xml" clean compile \ - org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath \ - -Dmdep.outputFile=target/runtime-classpath.txt -runtime_core_classpath="$(cat "${RUNTIME_CORE_FIXTURE}/target/runtime-classpath.txt")" -IFS=: read -r -a runtime_jars <<< "${runtime_core_classpath}" -for dependency in "${runtime_jars[@]}"; do - case "$(basename "${dependency}")" in - argi-runtime-api-*.jar|argi-runtime-core-*.jar|jackson-annotations-*.jar|jackson-core-*.jar|jackson-databind-*.jar) ;; - *) echo "Unexpected runtime core dependency: ${dependency}" >&2; exit 1 ;; - esac -done - -# Agent callers get only three neutral jars, never an execution engine or provider SDK. -readonly AGENT_FIXTURE="${REPO_ROOT}/tools/compatibility/agent-api-consumer" -"${REPO_ROOT}/mvnw" -B -f "${AGENT_FIXTURE}/pom.xml" clean compile \ - org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath \ - -Dmdep.outputFile=target/agent-classpath.txt -agent_classpath="$(cat "${AGENT_FIXTURE}/target/agent-classpath.txt")" -IFS=: read -r -a agent_jars <<< "${agent_classpath}" -if [[ ${#agent_jars[@]} -ne 3 ]]; then - echo "Agent API must resolve to exactly API, Runtime API and Agent API" >&2 - exit 1 -fi -agent_api_jar="" -for dependency in "${agent_jars[@]}"; do - case "$(basename "${dependency}")" in - argi-agent-api-*.jar) agent_api_jar="${dependency}" ;; - argi-runtime-api-*.jar|argi-api-*.jar) ;; - *) echo "Unexpected Agent API dependency: ${dependency}" >&2; exit 1 ;; - esac - if [[ "$(jdeps --multi-release 17 --class-path "${agent_classpath}" --print-module-deps "${dependency}")" != 'java.base' ]]; then - echo "Agent API and its dependencies must use only java.base" >&2 - exit 1 - fi -done -test -n "${agent_api_jar}" -java -cp "${AGENT_FIXTURE}/target/classes:${agent_classpath}" \ - io.github.agentic.ai.contract.AgentApiConsumer -jdeps --multi-release 17 -verbose:class --class-path "${agent_classpath}" \ - "${agent_api_jar}" > "${AGENT_FIXTURE}/target/bytecode-dependencies.txt" -if grep -q 'not found' "${AGENT_FIXTURE}/target/bytecode-dependencies.txt"; then - echo "Agent API has unresolved bytecode dependencies" >&2 - exit 1 -fi -javap -classpath "${agent_classpath}" -public \ - io.github.agentic.ai.agent.Agent io.github.agentic.ai.agent.AgentRequest \ - io.github.agentic.ai.agent.AgentOutcome io.github.agentic.ai.agent.AgentEvent \ - io.github.agentic.ai.agent.AgentExecutionContext > "${AGENT_FIXTURE}/target/public-signatures.txt" -if grep -E 'OverAllState|RunnableConfig|CompiledGraph|StateGraph|initGraph|getAndCompileGraph|asNode' \ - "${AGENT_FIXTURE}/target/public-signatures.txt"; then - echo "Agent invocation signatures expose graph execution" >&2 - exit 1 -fi -framework_jar="${REPO_ROOT}/argi-orchestration/argi-agent-framework/target/$(basename "${agent_api_jar}" | sed s/argi-agent-api/argi-agent-framework/)" -jar tf "${framework_jar}" | grep '.class$' | sort > "${AGENT_FIXTURE}/target/framework-classes.txt" -jar tf "${agent_api_jar}" | grep '.class$' | sort > "${AGENT_FIXTURE}/target/api-classes.txt" -if [[ -n "$(comm -12 "${AGENT_FIXTURE}/target/framework-classes.txt" "${AGENT_FIXTURE}/target/api-classes.txt")" ]]; then - echo "Agent Framework duplicates extracted API classes" >&2 - exit 1 -fi -echo "Standalone local, workflow and HTTP agent calls, signatures and bytecode gates passed" - -readonly REACT_FIXTURE="${REPO_ROOT}/tools/compatibility/react-agent-consumer" -"${REPO_ROOT}/mvnw" -B -f "${REACT_FIXTURE}/pom.xml" clean compile \ - org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath \ - -Dmdep.outputFile=target/react-classpath.txt -react_classpath="$(cat "${REACT_FIXTURE}/target/react-classpath.txt")" -if [[ "${react_classpath}" == *argi-workflow-graph-core* || "${react_classpath}" == *argi-agent-framework* \ - || "${react_classpath}" == *spring* || "${react_classpath}" == *reactor* ]]; then - echo "Independent ReAct consumer resolved a Graph, framework, Spring or Reactor dependency" >&2 - exit 1 -fi -java -cp "${REACT_FIXTURE}/target/classes:${react_classpath}" \ - io.github.agentic.ai.contract.ReActAgentConsumer -echo "Standalone ReAct consumer executed without Graph or framework artifacts" - -readonly WORKFLOW_FIXTURE="${REPO_ROOT}/tools/compatibility/workflow-consumer" -"${REPO_ROOT}/mvnw" -B -f "${WORKFLOW_FIXTURE}/pom.xml" clean compile \ - org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath \ - -Dmdep.outputFile=target/workflow-classpath.txt -workflow_classpath="$(cat "${WORKFLOW_FIXTURE}/target/workflow-classpath.txt")" -if [[ "${workflow_classpath}" == *argi-workflow-graph-core* || "${workflow_classpath}" == *argi-agent-framework* \ - || "${workflow_classpath}" == *spring* || "${workflow_classpath}" == *reactor* ]]; then - echo "Independent Workflow consumer resolved a Graph, framework, Spring or Reactor dependency" >&2 - exit 1 -fi -java -cp "${WORKFLOW_FIXTURE}/target/classes:${workflow_classpath}" \ - io.github.agentic.ai.contract.WorkflowConsumer -echo "Standalone Workflow consumer executed without Graph or framework artifacts" - -for kind in agent workflow graph; do - java -cp "${RUNTIME_CORE_FIXTURE}/target/classes:${runtime_core_classpath}" \ - io.github.agentic.ai.contract.RuntimeCoreConsumer "${kind}" -done -echo "Standalone agent, workflow and graph runtime contract consumers passed" - -runtime_core_jar="" -for dependency in "${runtime_jars[@]}"; do - case "$(basename "${dependency}")" in - argi-runtime-core-*.jar) runtime_core_jar="${dependency}" ;; - esac -done -test -n "${runtime_core_jar}" -jdeps --multi-release 17 -verbose:class --class-path "${runtime_core_classpath}" \ - "${runtime_core_jar}" > "${RUNTIME_CORE_FIXTURE}/target/bytecode-dependencies.txt" -if grep -q "not found" "${RUNTIME_CORE_FIXTURE}/target/bytecode-dependencies.txt"; then - echo "Runtime Core has unresolved bytecode dependencies" >&2 - exit 1 -fi - -loop_jar="${REPO_ROOT}/argi-orchestration/argi-workflow-graph-core/target/$(basename "${runtime_core_jar}" | sed s/argi-runtime-core/argi-workflow-graph-core/)" -jar tf "${loop_jar}" | grep '.class$' | sort > "${RUNTIME_CORE_FIXTURE}/target/graph-classes.txt" -for dependency in "${runtime_jars[@]}"; do - case "$(basename "${dependency}")" in - argi-runtime-*.jar) - jar tf "${dependency}" | grep '.class$' | sort > "${RUNTIME_CORE_FIXTURE}/target/runtime-classes.txt" - if [[ -n "$(comm -12 "${RUNTIME_CORE_FIXTURE}/target/graph-classes.txt" "${RUNTIME_CORE_FIXTURE}/target/runtime-classes.txt")" ]]; then - echo "Graph duplicates extracted runtime classes" >&2 - exit 1 - fi - ;; - esac -done diff --git a/tools/scripts/verify-clear-room.sh b/tools/scripts/verify-clear-room.sh deleted file mode 100755 index d18bf570c..000000000 --- a/tools/scripts/verify-clear-room.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. -# - -set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel)" -readonly REPO_ROOT - -fail() { - echo "Clear Room gate failed: $*" >&2 - exit 1 -} - -[[ -d "${REPO_ROOT}/argi-orchestration/argi-workflow-graph-core" ]] || fail "argi-workflow-graph-core is missing" -[[ ! -e "${REPO_ROOT}/argi-graph-core" ]] || fail "legacy argi-graph-core directory still exists" - -grep -q 'argi-orchestration/argi-workflow-graph-core' "${REPO_ROOT}/pom.xml" \ - || fail "root reactor does not declare argi-workflow-graph-core" -grep -q 'argi-workflow-graph-core' "${REPO_ROOT}/argi-orchestration/argi-workflow-graph-core/pom.xml" \ - || fail "Workflow Graph Core artifact id is missing" - -for module in argi-agent/argi-agent-api argi-agent/argi-agent-core \ - argi-orchestration/argi-workflow-core argi-runtime/argi-runtime-api \ - argi-runtime/argi-runtime-core; do - if grep -n 'argi-workflow-graph-core' "${REPO_ROOT}/${module}/pom.xml"; then - fail "${module} must not depend on Workflow Graph Core" - fi -done - -legacy_production_refs="$(git -C "${REPO_ROOT}" grep -l 'argi-graph-core' -- \ - . \ - ':(exclude)AGENTS.md' \ - ':(exclude)docs/**' \ - ':(exclude)argi-orchestration/argi-workflow-graph-core/README.md' \ - ':(exclude)argi-orchestration/argi-workflow-graph-core/src/test/**' \ - ':(exclude)benchmark/**' \ - ':(exclude)example/**' \ - ':(exclude)tools/scripts/verify-clear-room.sh' \ - ':(exclude)**/target/**' \ - ':(exclude)**/.flattened-pom.xml' || true)" -[[ -z "${legacy_production_refs}" ]] || fail "legacy artifact references remain:${legacy_production_refs}" - -git -C "${REPO_ROOT}" grep -q 'GRAPH("graph")' -- argi-runtime/argi-runtime-api/src/main/java \ - || fail "DefinitionKind.GRAPH wire name is not preserved" -git -C "${REPO_ROOT}" grep -q -E 'graphId|graphVersion' -- argi-runtime/argi-runtime-core/src/main/java \ - || fail "graph definition identifiers are not documented in runtime code" -git -C "${REPO_ROOT}" grep -q -E 'ARGI_CHAT_MESSAGE|LegacyChatRecordSupport' \ - -- argi-orchestration/argi-workflow-graph-core/src/main \ - || fail "Loop serializer compatibility boundary is missing" - -for document in clear-room-baseline.md checkpoint-compatibility.md api-boundaries.md; do - [[ -f "${REPO_ROOT}/docs/architecture/${document}" ]] \ - || fail "architecture document is missing: ${document}" -done - -if [[ -d "${REPO_ROOT}/benchmark" ]]; then - if grep -R -n --include='*.xml' --exclude-dir=target 'argi-graph-core' "${REPO_ROOT}/benchmark"; then - fail "benchmark submodule still references argi-graph-core" - fi -fi -if [[ -d "${REPO_ROOT}/example" ]]; then - if grep -R -n --include='*.xml' --exclude-dir=target 'argi-graph-core' "${REPO_ROOT}/example"; then - fail "example submodule still references argi-graph-core" - fi -fi - -echo "Clear Room ownership, artifact, wire-name, and consumer-boundary gates passed" diff --git a/tools/scripts/verify-extensions-compatibility.sh b/tools/scripts/verify-extensions-compatibility.sh deleted file mode 100755 index fe3e97026..000000000 --- a/tools/scripts/verify-extensions-compatibility.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. -# - -set -euo pipefail - -INVOCATION_DIR="$(pwd -P)" -readonly INVOCATION_DIR -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -readonly SCRIPT_DIR -CORE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd -P)" -readonly CORE_DIR -extensions_dir="${1:-}" - -if [[ -z "${extensions_dir}" || ! -d "${extensions_dir}" || ! -f "${extensions_dir}/pom.xml" ]]; then - echo "Usage: $0 /path/to/argi-extensions" >&2 - exit 2 -fi - -EXTENSIONS_DIR="$(cd "${extensions_dir}" && pwd -P)" -readonly EXTENSIONS_DIR - -path_is_inside_target_tree() { - local root_dir="$1" - local candidate_path="$2" - local relative_path - - case "${candidate_path}" in - "${root_dir}") - return 1 - ;; - "${root_dir}/"*) - relative_path="${candidate_path#"${root_dir}/"}" - ;; - *) - return 1 - ;; - esac - - [[ "${relative_path}" == "target" || "${relative_path}" == target/* || "${relative_path}" == */target || "${relative_path}" == */target/* ]] -} - -canonicalize_maven_repo() { - local repo_path="$1" - local absolute_repo_path - - if [[ "${repo_path}" == /* ]]; then - absolute_repo_path="${repo_path}" - else - absolute_repo_path="${INVOCATION_DIR}/${repo_path}" - fi - if ! mkdir -p "${absolute_repo_path}"; then - return 1 - fi - ( cd "${absolute_repo_path}" && pwd -P ) -} - -if path_is_inside_target_tree "${CORE_DIR}" "${EXTENSIONS_DIR}"; then - echo "Extensions checkout must not live under a Core target/ directory: ${EXTENSIONS_DIR}" >&2 - exit 2 -fi - -readonly TMP_PARENT="${TMPDIR:-/tmp}" -owned_maven_repo=0 -if [[ -n "${EXTENSIONS_COMPAT_MAVEN_REPO:-}" ]]; then - if ! MAVEN_REPO="$(canonicalize_maven_repo "${EXTENSIONS_COMPAT_MAVEN_REPO}")"; then - echo "Cannot prepare caller-owned Maven repository: ${EXTENSIONS_COMPAT_MAVEN_REPO}" >&2 - exit 2 - fi -else - if ! MAVEN_REPO="$(mktemp -d "${TMP_PARENT%/}/argi-extensions-compat-m2.XXXXXX")"; then - echo "Cannot create task-owned Maven repository under ${TMP_PARENT}" >&2 - exit 2 - fi - owned_maven_repo=1 -fi -readonly MAVEN_REPO - -if path_is_inside_target_tree "${CORE_DIR}" "${MAVEN_REPO}"; then - echo "Caller-owned Maven repository must not live under a Core target/ directory: ${MAVEN_REPO}" >&2 - exit 2 -fi -if path_is_inside_target_tree "${EXTENSIONS_DIR}" "${MAVEN_REPO}"; then - echo "Caller-owned Maven repository must not live under an Extensions target/ directory: ${MAVEN_REPO}" >&2 - exit 2 -fi - -cleanup() { - if [[ "${owned_maven_repo}" == "1" && "${MAVEN_REPO}" == "${TMP_PARENT%/}"/argi-extensions-compat-m2.* && -d "${MAVEN_REPO}" ]]; then - rm -rf "${MAVEN_REPO}" - fi -} -trap cleanup EXIT - -echo "Testing and installing Core into ${MAVEN_REPO}" -( - cd "${CORE_DIR}" - ./mvnw -B -Dmaven.repo.local="${MAVEN_REPO}" clean install -) - -extensions_maven="${EXTENSIONS_DIR}/mvnw" -if [[ ! -x "${extensions_maven}" ]]; then - extensions_maven="${MAVEN_CMD:-mvn}" -fi - -echo "Testing Extensions from ${EXTENSIONS_DIR} against candidate Core artifacts" -( - cd "${EXTENSIONS_DIR}" - "${extensions_maven}" -B -f pom.xml -Dmaven.repo.local="${MAVEN_REPO}" clean test -) diff --git a/tools/tests/release-tools-test.py b/tools/tests/release-tools-test.py deleted file mode 100644 index 46446d3e5..000000000 --- a/tools/tests/release-tools-test.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2024-2026 the original author or authors. -# -# Licensed under the Apache License, Version 2.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. - -import hashlib -import importlib.util -import json -import subprocess -import sys -import tempfile -import unittest -import xml.etree.ElementTree as ET -from unittest.mock import patch -from pathlib import Path -from zipfile import ZipFile - - -MIGRATOR = Path(__file__).resolve().parents[1] / "scripts/migrate-maven-coordinates.py" -VERIFIER_PATH = Path(__file__).resolve().parents[1] / "scripts/verify-release-artifacts.py" -SPEC = importlib.util.spec_from_file_location("release_verifier", VERIFIER_PATH) -VERIFIER = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(VERIFIER) -PUBLISHER_PATH = Path(__file__).resolve().parents[1] / "scripts/publish-central-bundle.py" -PUBLISHER_SPEC = importlib.util.spec_from_file_location("central_publisher", PUBLISHER_PATH) -PUBLISHER = importlib.util.module_from_spec(PUBLISHER_SPEC) -PUBLISHER_SPEC.loader.exec_module(PUBLISHER) -POM = """ - - com.exampleapp - io.github.agentic-aiargi - - io.github.agentic-aiargi-bom - 2.1.0-dev - - - io.github.agentic-aiargi-studio - io.github.agentic-aiargi-graph-persistence-jdbc - - -""" - - -class ModuleLayoutTest(unittest.TestCase): - ROOT = Path(__file__).resolve().parents[2] - NS = {"m": "http://maven.apache.org/POM/4.0.0"} - GROUPED_MODULES = { - "argi-api/argi-api-base": "argi-api", - "argi-api/argi-adapter-spring-ai": "argi-adapter-spring-ai", - "argi-api/argi-adapter-langchain4j": "argi-adapter-langchain4j", - "argi-api/argi-adapter-google-adk": "argi-adapter-google-adk", - "argi-spring-boot-starter/argi-starter-builtin-nodes": "argi-starter-builtin-nodes", - "argi-spring-boot-starter/argi-starter-graph-observation": "argi-starter-graph-observation", - "argi-spring-boot-starter/argi-starter-adapter-spring-ai": "argi-starter-adapter-spring-ai", - "argi-spring-boot-starter/argi-starter-adapter-langchain4j": "argi-starter-adapter-langchain4j", - "argi-spring-boot-starter/argi-starter-adapter-google-adk": "argi-starter-adapter-google-adk", - } - - def test_root_aggregates_grouped_children_without_extra_artifacts(self): - root = ET.parse(self.ROOT / "pom.xml").getroot() - modules = [node.text for node in root.findall("m:modules/m:module", self.NS)] - grouped = {module for module in modules - if module.startswith(("argi-api/", "argi-spring-boot-starter/"))} - self.assertEqual(set(self.GROUPED_MODULES), grouped) - self.assertEqual(len(modules), len(set(modules))) - for group in ("argi-api", "argi-spring-boot-starter"): - self.assertNotIn(group, modules) - self.assertFalse((self.ROOT / group / "pom.xml").exists()) - self.assertFalse((self.ROOT / "spring-boot-starters").exists()) - for artifact in self.GROUPED_MODULES.values(): - if artifact.startswith("argi-adapter-"): - self.assertFalse((self.ROOT / artifact).exists()) - - def test_grouped_children_preserve_coordinates_and_resolve_root_parent(self): - for module, artifact in self.GROUPED_MODULES.items(): - with self.subTest(module=module): - directory = self.ROOT / module - pom = ET.parse(directory / "pom.xml").getroot() - self.assertEqual(artifact, pom.findtext("m:artifactId", namespaces=self.NS)) - parent = pom.find("m:parent", self.NS) - self.assertEqual("io.github.agentic-ai-java", - parent.findtext("m:groupId", namespaces=self.NS)) - self.assertEqual("argi", parent.findtext("m:artifactId", namespaces=self.NS)) - relative_path = parent.findtext("m:relativePath", namespaces=self.NS) - self.assertEqual(self.ROOT / "pom.xml", (directory / relative_path).resolve()) - self.assertTrue((directory / "src/main").is_dir()) - self.assertTrue((directory / "src/test").is_dir()) - - def test_bom_manages_all_grouped_artifacts(self): - bom = ET.parse(self.ROOT / "argi-bom/pom.xml").getroot() - managed = {dependency.findtext("m:artifactId", namespaces=self.NS) - for dependency in bom.findall("m:dependencyManagement/m:dependencies/m:dependency", self.NS) - if dependency.findtext("m:groupId", namespaces=self.NS) == "io.github.agentic-ai-java"} - self.assertTrue(set(self.GROUPED_MODULES.values()).issubset(managed)) - - def test_security_patch_management_matches_published_bom(self): - root = ET.parse(self.ROOT / "pom.xml").getroot() - bom = ET.parse(self.ROOT / "argi-bom/pom.xml").getroot() - properties = ("jackson-2-bom.version", "jackson-bom.version", - "bouncycastle.version", "jsoup.version", "tomcat.version") - for name in properties: - with self.subTest(property=name): - expected = root.findtext(f"m:properties/m:{name}", namespaces=self.NS) - self.assertIsNotNone(expected) - self.assertEqual(expected, bom.findtext(f"m:properties/m:{name}", - namespaces=self.NS)) - for document in (root, bom): - managed = {} - for dependency in document.findall("m:dependencyManagement/m:dependencies/m:dependency", - self.NS): - version = dependency.findtext("m:version", namespaces=self.NS) - if version in {"${" + name + "}" for name in properties}: - key = (dependency.findtext("m:groupId", namespaces=self.NS), - dependency.findtext("m:artifactId", namespaces=self.NS)) - managed[key] = (version, dependency.findtext("m:scope", namespaces=self.NS)) - if document is root: - expected_management = managed - self.assertEqual(10, len(managed)) - else: - self.assertEqual(expected_management, managed) - - -class CentralBundleTest(unittest.TestCase): - def write_bundle(self, directory, extra=None, omit_pom=False, corrupt=False): - source = Path(directory) / "payload.pom" - source.write_bytes(b"") - name = "io/github/agentic-ai-java/argi/2.1.0-RC1/argi-2.1.0-RC1.pom" - bundle = Path(directory) / "central-bundle.zip" - with ZipFile(bundle, "w") as archive: - payload = b"corrupt" if corrupt else source.read_bytes() - if not omit_pom: - archive.writestr(name, payload) - for algorithm in ("md5", "sha1"): - archive.writestr(name + "." + algorithm, hashlib.new(algorithm, payload).hexdigest()) - if extra: - archive.writestr(extra, b"") - return bundle, {name: source} - - def test_valid_bundle(self): - with tempfile.TemporaryDirectory() as directory: - VERIFIER.verify_bundle(*self.write_bundle(directory)) - - def test_generated_signed_bundle_round_trip(self): - with tempfile.TemporaryDirectory() as directory: - bundle, payloads = self.write_bundle(directory) - signature = Path(directory) / "payload.pom.asc" - signature.write_bytes(b"signed payload") - payloads[next(iter(payloads)) + ".asc"] = signature - VERIFIER.create_bundle(bundle, payloads) - VERIFIER.verify_bundle(bundle, payloads) - with ZipFile(bundle) as archive: - self.assertEqual(6, len(archive.namelist())) - - def test_corrupt_checksum_is_rejected(self): - with tempfile.TemporaryDirectory() as directory: - bundle, payloads = self.write_bundle(directory) - with ZipFile(bundle) as archive: - contents = {name: archive.read(name) for name in archive.namelist()} - contents[next(iter(payloads)) + ".sha1"] = b"0" * 40 - with ZipFile(bundle, "w") as archive: - for name, data in contents.items(): - archive.writestr(name, data) - with self.assertRaisesRegex(ValueError, "Invalid Central bundle checksum"): - VERIFIER.verify_bundle(bundle, payloads) - - def test_repository_metadata_is_rejected(self): - with tempfile.TemporaryDirectory() as directory: - bundle, payloads = self.write_bundle( - directory, extra="io/github/agentic-ai-java/argi/maven-metadata.xml" - ) - with self.assertRaisesRegex(ValueError, "unexpected files"): - VERIFIER.verify_bundle(bundle, payloads) - - def test_missing_pom_is_rejected(self): - with tempfile.TemporaryDirectory() as directory: - with self.assertRaisesRegex(ValueError, "Missing or mismatched"): - VERIFIER.verify_bundle(*self.write_bundle(directory, omit_pom=True)) - - def test_changed_payload_is_rejected(self): - with tempfile.TemporaryDirectory() as directory: - with self.assertRaisesRegex(ValueError, "Missing or mismatched"): - VERIFIER.verify_bundle(*self.write_bundle(directory, corrupt=True)) - - -class CentralPublicationTest(unittest.TestCase): - def test_waits_for_published_and_archives_public_status(self): - with tempfile.TemporaryDirectory() as directory: - record = Path(directory) / "deployment.json" - replies = [json.dumps({"deploymentId": "test-id", "deploymentState": state}).encode() - for state in ("VALIDATING", "PUBLISHED")] - with patch.object(PUBLISHER, "request", side_effect=replies) as request_mock: - with patch.object(PUBLISHER.time, "sleep"): - PUBLISHER.wait_until_published("test-id", "secret-token", record) - self.assertEqual(2, request_mock.call_count) - self.assertEqual("PUBLISHED", json.loads(record.read_text())["deploymentState"]) - self.assertNotIn("secret-token", record.read_text()) - - def test_failed_deployment_stops_publication(self): - with tempfile.TemporaryDirectory() as directory: - record = Path(directory) / "deployment.json" - reply = json.dumps({"deploymentId": "test-id", "deploymentState": "FAILED", - "errors": {"common": ["invalid bundle"]}}).encode() - with patch.object(PUBLISHER, "request", return_value=reply): - with self.assertRaisesRegex(RuntimeError, "Central validation failed"): - PUBLISHER.wait_until_published("test-id", "secret-token", record) - self.assertEqual("FAILED", json.loads(record.read_text())["deploymentState"]) - - -class MigrationTest(unittest.TestCase): - def test_preview_then_write_preserves_external_coordinates_and_backup(self): - with tempfile.TemporaryDirectory() as directory: - pom = Path(directory) / "pom.xml" - pom.write_text(POM) - subprocess.run([sys.executable, str(MIGRATOR), str(pom)], check=True) - self.assertEqual(POM, pom.read_text()) - self.assertEqual([pom], list(Path(directory).iterdir())) - subprocess.run([sys.executable, str(MIGRATOR), "--write", str(pom)], check=True) - backup = pom.with_name("pom.xml.before-argi-rc1") - self.assertEqual(POM, backup.read_text()) - updated = pom.read_text() - self.assertEqual(3, updated.count("io.github.agentic-ai-java")) - self.assertIn("io.github.agentic-aiargi-graph-persistence-jdbc", updated) - self.assertIn("2.1.0-dev", updated) - self.assertIn("", updated) - subprocess.run([sys.executable, str(MIGRATOR), "--write", str(pom)], check=True) - self.assertEqual(updated, pom.read_text()) - self.assertEqual(POM, backup.read_text()) - - def test_existing_backup_stops_write(self): - with tempfile.TemporaryDirectory() as directory: - pom = Path(directory) / "pom.xml" - pom.write_text(POM) - backup = pom.with_name("pom.xml.before-argi-rc1") - backup.write_text("Earlier backup\n") - result = subprocess.run([sys.executable, str(MIGRATOR), "--write", str(pom)], capture_output=True) - self.assertNotEqual(0, result.returncode) - self.assertEqual(POM, pom.read_text()) - self.assertEqual("Earlier backup\n", backup.read_text()) - - def test_invalid_pom_stops_before_any_write(self): - with tempfile.TemporaryDirectory() as directory: - pom = Path(directory) / "pom.xml" - broken = Path(directory) / "broken.xml" - pom.write_text(POM) - broken.write_text("") - result = subprocess.run([sys.executable, str(MIGRATOR), "--write", str(pom), str(broken)], capture_output=True) - self.assertNotEqual(0, result.returncode) - self.assertEqual(POM, pom.read_text()) - self.assertFalse(pom.with_name("pom.xml.before-argi-rc1").exists()) - - -if __name__ == "__main__": - unittest.main()