Overview

Agentic AI is the use of large language (and other multi-modal) models not just to generate text, but to act as reasoning, goal-driven agents that can plan, call tools, and adapt their actions to deliver outcomes.

The JVM is a compelling platform for this because its strong type safety provides guardrails for integrating LLM-driven behaviors with real systems. Because so many production applications already run on the JVM it is the natural place to embed AI.

While Agentic AI has been hyped, much of it has lived in academic demos with little practical value; by integrating directly into legacy and enterprise JVM applications, we can unlock AI capabilities without rewriting core systems or tearing down a factory to install new machinery.

Glossary

Before we begin, in this glossary we’ll explain some terms that may be new if you’re taking your first steps as an applied AI software developer. It is assumed that you already know what a large language model (LLM) is from an end-user’s point of view.

  • Agent
    An Agent in the Embabel framework is a self-contained component that bundles together domain logic, AI capabilities, and tool usage to achieve a specific goal on behalf of the user.

    Inside, it exposes multiple @Action methods, each representing discrete steps the agent can take. Actions depend on typically structured (sometimes natural language) input. The input is used to perform tasks on behalf of the user - executing domain code, calling AI models or even calling other agents as a sub-process.

    When an AI model is called it may be given access to tools that expand its capabilities in order to achieve a goal. The output is a new type, representing a transformation of the input, however during execution one or more side-effects can occur. An example of side effects might be new records stored in a database, orders placed on an e-commerce site and so on.

  • Tools
    Tools extend the raw capabilities of an LLM by letting it interact with the outside world. On its own, a language model can only generate responses from its training data and context window, which risks producing inaccurate or “hallucinated” answers.

    While tool usage is inspired by an technique known as ReAct (Reason + Act), which itself builds on Chain of Thought reasoning, most recent LLMs allow specifying tools specifically instead of relying on prompt engineering techniques.

    When tools are present, the LLM interprets the user request, plans steps, and then delegates certain tasks to tools in a loop. This lets the model alternate between reasoning (“what needs to be done?”) and acting (“which tool can do it?”).

    Benefits of tools include:

    • The ability to answer questions or perform tasks beyond what the LLM was trained on, by delegating to domain-specific or external systems.

    • Producing useful side effects, such as creating database records, generating visualizations, booking flights, or invoking any process the system designer provides.

      In short, tools are one way to bridge the gap between text prediction and real-world action, turning an LLM into a practical agent capable of both reasoning and execution. In Embabel many tools are bound domain objects.

  • MCP
    Model Context Protocol (MCP) is a standardized way of hosting and sharing tools. Unlike plain tools, which are usually wired directly into one agent or app, an MCP Server makes tools discoverable and reusable across models and runtimes they can be registered system-wide or at runtime, and invoked through a common protocol. Embabel can both consume and publish such tools for systems integration.

  • Domain Integrated Context Engineering (DICE)
    Enhances context engineering by grounding both LLM inputs and outputs in typed domain objects. Instead of untyped prompts, context is structured with business-aware models that provide precision, testability, and seamless integration with existing systems. DICE transforms context into a re-usable, inspectable, and reliably manipulable artifact.

Why do we need an Agent Framework?

Aren’t LLMs smart enough to solve our problems directly? Aren’t MCP tools all we need to allow them to solve complex problems? LLMs seem to get more capable by the day and MCPs can give LLMs access to a lot of empowering tools, making them even more capable.

But there are still many reasons that a higher level orchestration technology is needed, especially for business applications. Here are some of the most important:

  • Explainability: Why were choices made in solving a problem?
  • Discoverability: How do we find the right tools at each point, and ensure that models aren’t confused in choosing between them?
  • Ability to mix models, so that we are not reliant only on the largest models but can use local, cheaper, private models for many tasks
  • Ability to inject guardrails at any point in a flow
  • Ability to manage flow execution and introduce greater resilience
  • Composability of flows at scale. We’ll soon be seeing not just agents running on one system, but federations of agents.
  • Safer integration with sensitive existing systems such as databases, where it is dangerous to allow even the best LLM write access.

Agent frameworks break complex tasks into smaller, manageable components, offering greater control and predictability.

Agent frameworks offer "code agency" as well as "LLM agency." This division is well described in this paper from NVIDIA Research.

Further reading:

Embabel Differentiators

So how does Embabel differ from other agent frameworks? We like to believe the Embabel agent framework is to be the best fit for developing agentic AI in the enterprise.

Sophisticated Planning

Goes beyond a finite state machine or sequential execution with nesting by introducing a true planning step, using a non-LLM AI algorithm. This enables the system to perform tasks it wasn’t programmed to do by combining known steps in a novel order, as well as make decisions about parallelization and other runtime behavior.

Superior Extensibility and Reuse

Because of dynamic planning, adding more domain objects, actions, goals and conditions can extend the capability of the system, without editing FSM definitions or existing code.

Strong Typing and Object Orientation

Actions, goals and conditions are informed by a domain model, which can include behavior. Everything is strongly typed and prompts and manually authored code interact cleanly. No more magic maps. Enjoy full refactoring support.

Platform Abstraction

Clean separation between programming model and platform internals allows running locally while potentially offering higher QoS in production without changing application code.

LLM Mixing

It is easy to build applications that mix LLMs, ensuring the most cost-effective yet capable solution. This enables the system to leverage the strengths of different models for different tasks. In particular, it facilitates the use of local models for point tasks. This can be important for cost and privacy.

Spring and JVM Integration

Built on Spring and the JVM, making it easy to access existing enterprise functionality and capabilities. For example:

  • Spring can inject and manage agents, including using Spring AOP to decorate functions.
  • Robust persistence and transaction management solutions are available.

Designed for Testability

Both unit testing and agent end-to-end testing are easy from the ground up.

Core Concepts

Agent frameworks break up tasks into separate smaller interactions, making LLM use more predictable and focused.

Embabel models agentic flows in terms of:

  • Actions: Steps an agent takes. These are the building blocks of agent behavior.
  • Goals: What an agent is trying to achieve.
  • *** Conditions***: Conditions to do evaluations while planning. Conditions are reassessed after each action is executed.
  • Domain Model: Objects underpinning the flow and informing Actions, Goals and Conditions.

This enables Embabel to create a plan: A sequence of actions to achieve a goal. Plans are dynamically formulated by the system, not the programmer. The system replans after the completion of each action, allowing it to adapt to new information as well as observe the effects of the previous action. This is effectively an OODA loop.

Complete Example

Let’s look at a complete example that demonstrates how Embabel infers conditions from input/output types and manages data flow between actions. This example comes from the Embabel Agent Examples repository:

@Agent(description = "Find news based on a person's star sign")  // ①
public class StarNewsFinder {

    private final HoroscopeService horoscopeService;  // ②
    private final int storyCount;

    public StarNewsFinder(
            HoroscopeService horoscopeService,  // ③
            @Value("$\{star-news-finder.story.count:5}") int storyCount) {
        this.horoscopeService = horoscopeService;
        this.storyCount = storyCount;
    }

    @Action  // ④
    public StarPerson extractStarPerson(UserInput userInput, OperationContext context) {  // ⑤
        return context.ai()
            .withLlm(OpenAiModels.GPT_41)
            .createObject("""
                Create a person from this user input, extracting their name and star sign:
                %s""".formatted(userInput.getContent()), StarPerson.class);  // ⑥
    }

    @Action  // ⑦
    public Horoscope retrieveHoroscope(StarPerson starPerson) {  // ⑧
        // Uses regular injected Spring service - not LLM
        return new Horoscope(horoscopeService.dailyHoroscope(starPerson.sign()));  // ⑨
    }

    @Action  // ⑩
    public RelevantNewsStories findNewsStories(
            StarPerson person, Horoscope horoscope, OperationContext context) {  // ⑪
        var prompt = """
            %s is an astrology believer with the sign %s.
            Their horoscope for today is: %s
            Given this, use web tools to find %d relevant news stories.
            """.formatted(person.name(), person.sign(), horoscope.summary(), storyCount);

        return context.ai().withDefaultLlm()
            .withToolGroup(CoreToolGroups.WEB)  // ⑫
            .createObject(prompt, RelevantNewsStories.class);
    }

    @AchievesGoal(description = "Write an amusing writeup based on horoscope and news")  // ⑬
    @Action
    public Writeup writeup(
            StarPerson person, RelevantNewsStories stories, Horoscope horoscope,
            OperationContext context) {  // ⑭
        var llm = LlmOptions.fromCriteria(ModelSelectionCriteria.getAuto())
            .withTemperature(0.9);  // ⑮

        var storiesFormatted = stories.items().stream()
            .map(s -> "- " + s.url() + ": " + s.summary())
            .collect(Collectors.joining("\n"));

        var prompt = """
            Write something amusing for %s based on their horoscope and news stories.
            Format as Markdown with links.
            <horoscope>%s</horoscope>
            <news_stories>
            %s
            </news_stories>
            """.formatted(person.name(), horoscope.summary(), storiesFormatted);  // ⑯

        return context.ai().withLlm(llm).createObject(prompt, Writeup.class);  // ⑰
    }
}
  1. Agent Declaration: The @Agent annotation defines this as an agent capable of a multi-step flow.
  2. Spring Integration: Regular Spring dependency injection - the agent uses both LLM services and traditional business services.
  3. Service Injection: HoroscopeService is injected like any Spring bean - agents can mix AI and non-AI operations seamlessly.
  4. Action Definition: @Action marks methods as steps the agent can take. Each action represents a capability.
  5. Input Condition Inference: The method signature extractStarPerson(UserInput userInput, ...) tells Embabel:
    • Precondition: "A UserInput object must be available"
    • Required Data: The agent needs user input to proceed
    • Capability: This action can extract structured data from unstructured input
  6. Output Condition Creation: Returning StarPerson creates:
    • Postcondition: "A StarPerson object is now available in the world state"
    • Data Availability: This output becomes input for subsequent actions
    • Type Safety: The domain model enforces structure
  7. Non-LLM Action: Not all actions use LLMs - this demonstrates hybrid AI/traditional programming.
  8. Data Flow Chain: The method signature retrieveHoroscope(StarPerson starPerson) creates:
    • Precondition: "A StarPerson object must exist" (from previous action)
    • Dependency: This action can only execute after extractStarPerson completes
    • Service Integration: Uses the injected horoscopeService rather than an LLM
  9. Regular Service Call: This action calls a traditional Spring service - demonstrating how agents blend AI and conventional operations.
  10. Another Action: This action uses tools specified at the PromptRunner level.
  11. Multi-Input Dependencies: This method requires both StarPerson and Horoscope - showing complex data flow orchestration.
  12. Tool-Enabled LLM: withToolGroup(CoreToolGroups.WEB) adds web search tools to this LLM call, allowing it to search for current news stories.
  13. Goal Achievement: @AchievesGoal marks this as a terminal action that completes the agent’s objective.
  14. Complex Input Requirements: The final action requires three different data types, showing sophisticated orchestration.
  15. Creative Configuration: High temperature (0.9) optimizes for creative, entertaining output - appropriate for amusing writeups.
  16. Structured Prompt with Data: The prompt includes both the horoscope summary and formatted news stories using XML-style tags. This ensures the LLM has all the context it needs from earlier actions.
  17. Final Output: Returns Writeup, completing the agent’s goal with personalized content.

State is managed by the framework, through the process blackboard.

The Inferred Execution Plan for the Example

Based on the type signatures alone, Embabel automatically infers this execution plan for the example agent above:

Goal: Produce a Writeup (final return type of @AchievesGoal action)

The initial plan:

  • To emit Writeup → need writeup() action
  • writeup() requires StarPerson, RelevantNewsStories, and Horoscope
  • To get StarPerson → need extractStarPerson() action
  • To get Horoscope → need retrieveHoroscope() action (requires StarPerson)
  • To get RelevantNewsStories → need findNewsStories() action (requires StarPerson and Horoscope)
  • extractStarPerson() requires UserInput → must be provided by user

Execution sequence:

UserInputextractStarPerson()StarPersonretrieveHoroscope()HoroscopefindNewsStories()RelevantNewsStorieswriteup()Writeup and achieves goal.

Key Benefits of Type-Driven Flow

Automatic Orchestration: No manual workflow definition needed - the agent figures out the sequence from type dependencies. This is particularly beneficial if things go wrong, as the planner can re-evaluate the situation and may be able to find an alternative path to the goal.

Dynamic Replanning: After each action, the agent reassesses what’s possible based on available data objects.

Type Safety: Compile-time guarantees that data flows correctly between actions. No magic string keys.

Flexible Execution: If multiple actions could produce the required input type, the agent chooses based on context and efficiency. (Actions can have cost and value.)

This demonstrates how Embabel transforms simple method signatures into sophisticated multi-step agent behavior, with the complex orchestration handled automatically by the framework.

Was this page helpful?

Share