Programming languages allow us to communicate with computers, and they operate like sets of instructions. There are numerous types of languages, including procedural, functional, object-oriented, and more. Whether you’re looking to learn a new language or trying to find some tips or tricks, the resources in the Languages Zone will give you all the information you need and more.
dbt Meets Apache Flink: One Workflow for Data Engineers
Porting GPU Drivers to Rust on ARM64: The Hardest Trial for Kernel-Level Computing
If you ask Java developers about the concept of ‘Sneaky Throws,’ I am almost sure there will be a couple of opinions that are quite differently expressed, but similar in their meaning. Some will sum it up as being able to throw checked exceptions without declaring them explicitly; others will amend that it means writing functional-style code (lambdas) and being allowed to call methods that throw checked exceptions. Most probably, it will be surely mentioned that there’s a Lombok annotation called exactly @SneakyThrows that solves the problem immediately when put on a method. Last but not least, to outline it in a more pragmatic manner, the concept allows tricking the Java compiler into treating checked exceptions as runtime exceptions. All of these are valid points of view, and to clarify the concept, this article aims to provide a straightforward yet useful approach to handling methods that throw checked exceptions. Let’s jump right in and imagine the following situation. The team is requested to enhance the currently delivered application and implement new functionalities. This obviously happens on a ‘sprint-ly’ basis. Nevertheless, the project has been successfully developed for quite a while now; it also deals with legacy code, and moreover, developers are interacting with other parts of code that were written, let’s say, in a less fortunate manner. Such an example is the class below. Java public class TwoDigitsInteger { private final Integer value; public TwoDigitsInteger(Integer value) { this.value = value; } public boolean isValid() throws NotSetException { if (value == null) { throw new NotSetException("Number value not set."); } return value >= 10 && value <= 99; } public Integer getValue() throws NotSetException { if (value == null) { throw new NotSetException("Number value not set."); } return value; } } Just as its name suggests, it models a two-digit integer number. Instances of this class are immutable; the value is set upon construction, and it declares two methods, one for reading the value — getValue() — and another one for validating it — isValid(). We’re not going to further elaborate on the quality of the code, as it helps in the experiment done. The main issue here, the plot of this article, is the fact that both methods declare a NotSetException as they might throw it under certain circumstances, and even that might be fine unless this Exception hadn’t been a checked one. Java public class NotSetException extends Exception { public NotSetException(String message) { super(message); } } One option (and definitely the one worth taking into account) is to profit and consider the moment a good opportunity to refactor this ‘legacy’ code and at least make the Exception a runtime one. A few unit tests can be written (in case these are missing), then the implementation improved, and focus can be moved on the newly requested features. Nevertheless, for the sake of the experiment in this article, it’s assumed the TwoDigitsInteger class is kept as it currently is and the Exception remains checked. Exception Function Let’s consider a very simple scenario: there is a collection of TwoDigitsIntegers and the intent is to create a string expression that outlines the sum of the numbers. Java List<TwoDigitsInteger> numbers = List.of(new TwoDigitsInteger(10), new TwoDigitsInteger(25), new TwoDigitsInteger(37)); If writing the code as in the test below, Java @Test void sumExpression() { String result = numbers.stream() .map(TwoDigitsInteger::getValue) .map(String::valueOf) .collect(Collectors.joining("+")); Assertions.assertEquals("10+25+37", result); } the Java compiler will complain, saying — Unhandled exception: com.hcd.utilities.NotSetException – as the getValue() method declares a checked Exception and obviously it cannot be used inside a stream. To solve the issue, a try-catch is needed, which makes the code quite difficult to read (and ugly). Not to mention that we’re modifying the state of the joiner as we loop the collection. Java @Test void sumExpression1() { StringJoiner joiner = new StringJoiner("+"); for (TwoDigitsInteger number : numbers) { try { joiner.add(String.valueOf(number.getValue())); } catch (NotSetException e) { throw new RuntimeException(e); } } String result = joiner.toString(); Assertions.assertEquals("10+25+37", result); } In order to overcome this and allow having a fluent API even in situations where checked Exceptions are present, the following ExceptionFunction interface is created. Java @FunctionalInterface public interface ExceptionFunction<T, R, E extends Exception> { R apply(T t) throws E; } It is general enough; it represents a function that accepts one argument (of type T), produces a result (of type R) and when applied, an Exception subclass (of type E) might be thrown. Implementers shall define a single method, which effectively applies the function. Additionally, the following class is defined. Java public final class ExceptionWrapper { public static <T, R, E extends Exception> Function<T, R> apply(ExceptionFunction<T, R, E> function) { return t -> { try { return function.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } ExceptionWrapper() { throw new UnsupportedOperationException("No need to be called."); } } When the ExceptionWrapper#apply() method is called, in case an Exception is thrown, it is wrapped into a RuntimeException one and thrown further irrespective of the type of the initial one (the checked Exception case is obviously covered as well, so we’re good). The ExceptionFunction passed as a parameter represents the initial call that is wrapped to overcome the problem. The previously discussed test is modified to use the ExceptionWrapper#apply() method. Not only does it now compile and run successfully, but the code readability is definitely improved. Java @Test void sumExpression() { String result = numbers.stream() .map(ExceptionWrapper.apply(TwoDigitsInteger::getValue)) .map(String::valueOf) .collect(Collectors.joining("+")); Assertions.assertEquals("10+25+37", result); } Exception Predicate Let’s now consider another straightforward scenario, one in which we want to count only the valid two-digit integers that are found in a designated range. Also, for the sake of this experiment, it’s assumed the previous TwoDigitsInteger class is used. As in the previous case, the following piece of code that would do the job doesn’t compile because of the same reason – Unhandled exception: com.hcd.utilities.NotSetException — as the isValid() method declares a checked exception, and it cannot be used inside a stream. Java long count = IntStream.range(0, 150) .mapToObj(TwoDigitsInteger::new) .filter(TwoDigitsInteger::isValid) .count(); Again, assuming the TwoDigitsInteger is needed, one would have to loop through the numbers, check them in a try-catch for checked NotSetExceptions as isValid() declares it, then pack the Exception as a RuntimeException one and throw it further, finally count the valid number. This is already way too complicated even when only enumerating the steps in natural language. To be able to keep the API fluid and use streams when performing checks that declare checked Exception, the next interface is declared. Java @FunctionalInterface public interface ExceptionPredicate<T, E extends Exception> { boolean test(T t) throws E; } It represents a predicate (a boolean-valued function) of one argument that might throw an Exception subclass. The method evaluates the predicate on the given argument and returns true if the input argument matches, or false otherwise. In addition, the following method is added to the ExceptionWrapper class, very similar to the apply() one. Java public static <T, E extends Exception> Predicate<T> test(ExceptionPredicate<T, E> predicate) { return t -> { try { return predicate.test(t); } catch (Exception e) { throw new RuntimeException(e); } }; } When called, it effectively applies the provided predicate. In case an Exception is thrown, it is wrapped into a RuntimeException one and thrown further. The initial code can now be rewritten as below and successfully compiled and executed. Java @Test void count() { long count = IntStream.range(0, 150) .mapToObj(TwoDigitsInteger::new) .filter(ExceptionWrapper.test(TwoDigitsInteger::isValid)) .count(); Assertions.assertEquals(90, count); } Takeaways Although simple and to-the-point, the presented solution comes in very handy, especially when dealing with functions that declare checked Exceptions and are further used in the code that we produce. For sure, other ready-to-use alternatives already exist, an example being the Lombok @SneakyThrows annotation. Personally, I have very rarely included the Lombok library in any of my projects and as Java introduced the records, this becomes even more unlikely to happen in the future. That being said, the structures described in this article are very helpful, lightweight, and easy to understand and use when needed. ExceptionWrapper, ExceptionFunction and ExceptionPredicate source code is part of the asentinel-orm open-source project. To use it, one may either declare the Maven dependency in their pom.xml file (version 1.72.2 is the latest at the moment of this writing) XML <dependency> <groupId>com.asentinel.common</groupId> <artifactId>asentinel-common</artifactId> <version>1.72.2</version> </dependency> or use it directly if considering there’s too much overhead to include the whole library. Resources [1] – asentinel-orm open-source ORM project is here [2] – the picture was taken at ‘Harry Potter Warner Bros. Studios’, near London
A chatbot can explain data, summarize a screen, or answer questions, yet the application still behaves largely as before: business state lives elsewhere, actions remain disconnected from model output, and the interface is reduced to a transcript. Agentic UI takes a different approach. The model becomes a planner over explicit application capabilities, while Angular remains responsible for state, rendering, validation, authorization boundaries, and interaction. Angular’s current AI guidance already distinguishes basic chat experiences from agentic workflows and dynamic server-driven interfaces, while protocols such as AG-UI formalize streaming state and tool events between agent backends and frontends. Chat Is an Output Channel, Not the Application Model The key design shift is to model an agent run as a workflow rather than a sequence of messages. A purchasing screen, for example, can expose inventory lookup, draft modification, approval, and submission as capabilities. Natural language may start the flow, but the resulting interface should remain a normal application UI: editable fields, status indicators, review cards, validation messages, and explicit confirmation controls. AG-UI follows this direction by defining lifecycle, text, tool-call, and state events instead of treating every interaction as plain assistant text. Tool calls are represented through structured events, allowing a frontend to represent work in progress without attempting to parse model prose into application behavior. A small TypeScript event contract is enough to establish that separation. Discriminated unions fit especially well because TypeScript narrows union members through control flow, making event handling explicit and allowing every event variant to carry only the fields relevant to that state transition. TypeScript type AgentEvent = | { type: 'run.started'; runId: string } | { type: 'draft.updated'; patch: Partial<OrderDraft> } | { type: 'action.requested'; action: PendingAction } | { type: 'action.finished'; actionId: string; result: ActionResult } | { type: 'run.failed'; message: string }; function applyAgentEvent(event: AgentEvent) { switch (event.type) { case 'run.started': phase.set('running'); break; case 'draft.updated': draft.update(value => ({ ...value, ...event.patch })); break; case 'action.requested': pendingAction.set(event.action); phase.set('approval'); break; case 'action.finished': pendingAction.set(null); phase.set('ready'); break; case 'run.failed': error.set(event.message); phase.set('failed'); } } This reducer keeps model output away from direct DOM mutation. The agent proposes state transitions; Angular applies validated events to application state. Network payloads still require runtime validation because TypeScript annotations disappear during compilation and do not perform runtime checks. Casting arbitrary JSON to AgentEvent therefore establishes a compiler assumption rather than a runtime trust boundary. Let Angular Render State Instead of Model Prose Signals provide a natural projection layer for agent-driven state because Angular tracks signal reads and updates dependent consumers when signal values change. Angular also provides asynchronous resource APIs for integrating async data with signal-based code, although workflow event streams often benefit from an explicit reducer because event ordering, approvals, resumable execution, and intermediate actions are domain state rather than ordinary resource loading TypeScript const phase = signal<'idle' | 'running' | 'approval' | 'ready' | 'failed'>('idle'); const draft = signal<OrderDraft>(emptyDraft); const pendingAction = signal<PendingAction | null>(null); const error = signal<string | null>(null); const busy = computed(() => phase() === 'running'); const approvalRequired = computed(() => pendingAction() !== null); The template can render that workflow through established Angular components instead of constructing another interaction model inside a chat transcript. Signal reads naturally connect the workflow state to Angular rendering. HTML @if (pendingAction(); as action) { <app-action-review [action]="action" (approve)="approve(action.id)" (reject)="reject(action.id)" /> } <app-order-editor [draft]="draft()" [disabled]="busy()" /> This boundary also preserves the application’s existing component system. The model determines intent and proposes changes, while known Angular components determine presentation and interaction semantics. That division becomes increasingly important as model-produced output becomes more dynamic, since a trusted component vocabulary provides substantially more control than arbitrary generated markup. A2UI applies the same general principle by allowing agents to describe interface intent while host applications render native components from an approved catalog. Capabilities Need Stronger Boundaries Than Prompts An agent should not receive an unrestricted instruction to invoke arbitrary frontend behavior. Capabilities should be explicit, typed, narrow, and policy-aware. AG-UI distinguishes backend-defined and client-provided tools, including tools that request human input or confirmation. Angular 22 also introduced experimental WebMCP support for exposing structured application tools to agents running in browser environments, with the explicit goal of reducing dependence on brittle DOM-level interaction. A capability registry keeps execution deterministic while still allowing an agent to choose among operations deliberately exposed by the application. TypeScript type CapabilityName = 'lookupInventory' | 'applyDiscount' | 'submitOrder'; const capabilities = { lookupInventory: { mutates: false, validate: validateInventoryArgs, execute: lookupInventory }, applyDiscount: { mutates: true, validate: validateDiscountArgs, execute: applyDiscount }, submitOrder: { mutates: true, requiresApproval: true, validate: validateSubmitArgs, execute: submitOrder } } satisfies Record<CapabilityName, Capability>; async function dispatch(action: PendingAction) { const capability = capabilities[action.name]; const args = capability.validate(action.args); return capability.execute(args); } The satisfies operator verifies that the registry conforms to the required shape while retaining the more specific inferred type of each value, making capability registries practical without unnecessarily widening their entries. The runtime validate operation solves a different problem: tool arguments originated outside the TypeScript compiler and therefore cannot become trustworthy merely through static type declarations. Human approval should interrupt a run rather than merely decorate a destructive operation with a confirmation sentence. AG-UI formalizes this concept through interrupts: an agent run can pause for approval or structured input and later resume with an explicit response. That model maps naturally to Angular workflow state because an approval card can remain visible until a correlated decision is submitted. TypeScript async function approve(actionId: string) { const action = pendingAction(); if (!action || action.id !== actionId) { return; } await agent.resume({ actionId, decision: 'approved' }); } Server-side authorization still remains authoritative; frontend approval represents an interaction decision rather than permission to bypass backend policy. The same rule applies to generated content. Angular’s security guidance treats untrusted values as a security concern and specifically warns that bypassing sanitization with untrusted content can expose applications to cross-site scripting vulnerabilities. Model output therefore belongs in the same untrusted-input category as any other external payload. Dynamic UI Should Come From a Catalog, Not Arbitrary Markup Some workflows need more than predefined page states. An agent may need to choose whether a result is best represented as a form, comparison view, approval card, or status panel. A2UI addresses that requirement with a declarative format in which an agent describes UI intent and the host renders the result using native components from a trusted catalog. The project supports Angular among its rendering targets and is explicitly designed around declarative UI descriptions rather than transferring arbitrary executable frontend code across the agent boundary. That distinction matters. Generating raw HTML and injecting it into Angular creates unnecessary sanitization pressure, weakens design-system consistency, and expands the amount of generated material that must be treated as untrusted. A constrained component vocabulary limits what an agent can request while retaining enough flexibility for adaptive layouts. Google’s A2UI documentation describes the same model as declarative JSON rendered through components controlled by the host application rather than raw HTML, CSS, or JavaScript supplied by the remote agent. AG-UI and A2UI consequently address different parts of the same frontend problem. AG-UI provides the interaction stream for runs, state changes, tool calls, and human-in-the-loop control, while A2UI provides a declarative mechanism for richer agent-selected views. Neither protocol is mandatory for an Angular implementation; an application-specific event protocol and component registry can implement the same core ideas. Standardization becomes more valuable when several agent runtimes or frontend surfaces must share the same interaction contract. Angular’s experimental WebMCP support introduces another useful direction: capabilities already present in an application can be exposed as structured tools rather than rediscovered through DOM manipulation. Because Angular currently marks the relevant WebMCP APIs as experimental, isolating them behind the same capability layer prevents an emerging transport mechanism from leaking into business logic. Conclusion Agentic Angular interfaces become useful when AI stops being a chat-shaped feature and starts participating in typed application workflows. The durable boundary is not a prompt; it is a contract consisting of validated events, explicit capabilities, observable state transitions, controlled rendering, and deliberate approval points. Angular Signals provide a reactive surface for projecting agent state, TypeScript discriminated unions make workflow events tractable, and emerging protocols such as AG-UI, A2UI, and WebMCP demonstrate a broader shift toward structured agent-to-application interaction. The strongest implementation keeps business authority and UI integrity inside the application while allowing the model to plan, propose, and coordinate. That boundary produces software that remains testable, accessible, secure, and understandable even as agent behavior becomes substantially more capable.
Playwright is a widely used open-source test automation framework developed by Microsoft. It allows developers and test automation engineers to reliably automate web applications across multiple browsers and platforms. Playwright supports several popular programming languages, such as JavaScript, TypeScript, Java, C#, and Python. One of its standout features is built-in API automation testing, which gives it a strong advantage over many traditional web automation frameworks. In this tutorial, we’ll explore how to use Playwright with TypeScript and learn how to automate GET API requests. Installing Playwright With TypeScript The first step is to install and set up Playwright with TypeScript. Let’s create a new folder and run the following command by navigating to the newly created folder: Plain Text npm init playwright@latest After running the above command, make sure you select “TypeScript” as the programming language. Next, select the appropriate options for the other questions asked by the Playwright setup and install Playwright and its dependencies. Application Under Test We’ll be using free, publicly available RESTful e-commerce APIs from a demo e-commerce application hosted on GitHub. The project can be run locally using either Node.js or Docker and provides several order management APIs, including creating, updating, retrieving, and deleting orders. How to Test GET API Requests With Playwright TypeScript Playwright provides a request API that lets us create and manage HTTP request contexts. Let’s learn about sending GET requests step-by-step with different options: Send a GET API Request and Verify the Status Code Let’s perform a simple test by sending a GET API request and verifying that a 200 status code is returned in the response. TypeScript import { test, expect } from "@playwright/test"; test("Get Order details API test with status code check", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { user_id: "1", }, }); expect(response.status()).toBe(200); }); Code Walkthrough This test sends a GET request to the /getOrder API with a user_id parameter using Playwright’s request context. It verifies that the API responds successfully by checking that the status code returned is 200. The following are additional details about this test: test(…): The test(…) defines a Playwright test case. The string “Get Order details API test with status code check” is the name of the test and will be shown in the Playwright report.async ({ request }): It uses Playwright’s built-in request fixture, which injects an APIRequestContext and allows us to make HTTP calls.Sending a GET request: The following line sends an HTTP GET request to the /getOrder/ endpoint. TypeScript const response = await request.get("http://localhost:3004/getOrder/", { The await keyword pauses execution until the API responds. Finally, the result is stored in the response variable, which is an APIResponse object. Params: The following line adds a query parameter “user_id” to the GET request. TypeScript params: { user_id: "1", }, expect statement: The response.status() retrieves the HTTP status code returned by the API, and expect(…).toBe(200) asserts that the API responded successfully with HTTP 200 OK. Similarly, we can perform the assertions for a status code other than 200. In the code below, the value for the “id” parameter is updated to “2”, for which no records exist in the system. TypeScript test("Get Order details API test with status code 404", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { id: 2, }, }); expect(response.status()).toBe(404); }); The expectation is that it should return status code 404. The expect(...) statement performs the required status code check. Send a GET API Request With Multiple Parameters There are situations where we need to provide multiple parameters in the GET request to filter and fetch the required records. Using Playwright TypeScript, multiple parameters can be supplied while sending a GET request, as shown below: TypeScript test("Get Order details API test with multiple params", async ({ request }) => { const params = { id: 1, user_id: "1", product_id: "79", }; const response = await request.get("http://localhost:3004/getOrder/", { params, }); expect(response.status()).toBe(200); }); This test defines multiple query parameters (id, user_id, and product_id) in a single params object and sends them with a GET API request. Playwright automatically appends these parameters to the request URL. Send a GET API Request With Headers Headers play an important role in retrieving data from the server. They can be supplied in the GET request as shown below: TypeScript test("Get Order details API test with headers", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { id: 1, user_id: "1", }, headers: { ContentType: "application/json", }, }); expect(response.status()).toBe(200); }); This test sends a GET request with custom HTTP headers along with query parameters, where the headers option is used to specify that the request content type is JSON. Similarly, other headers such as “Authorization”, “Accept”, “User-Agent”, etc. can also be supplied. Send a GET API Request With a Timeout Option Playwright provides the timeout option that can be passed to the request.get() method for setting a timeout to limit how long to wait for the response. TypeScript test("Get order details API test with timeout", async ({ request }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { user_id: 1, }, headers: { ContentType: "application/json", }, timeout: 300, }); expect(response.status()).toBe(200); }); If the API does not respond within the given timeout, Playwright fails the request and throws a timeout error. It helps prevent tests from hanging and makes failures faster and more predictable, especially for slow or unstable APIs. Send a GET API Request With the failOnStatusCode Option The failOnStatusCode option tells Playwright to automatically fail the request if the API responds with a non-2xx status code (such as 400, 404, 500, etc). TypeScript test("Get order details API test with fail on status code", async ({ request, }) => { const response = await request.get("http://localhost:3004/getOrder/", { params: { user_id: "1", }, headers: { ContentType: "application/json", }, failOnStatusCode: true, }); }); Using this option, we can get rid of performing the checks using response.status() as Playwright throws an error immediately if the API does not respond with a 2xx status code. The failOnStatusCode option is useful when a request must succeed for the test to continue. For example, if we need to validate the response data, we must use this option to ensure that the API responds with a 2xx status code before proceeding with deeper response validation. Test Execution Let's execute all the tests that we discussed and also check the built-in report provided by Playwright. To run the tests, execute the following command from the terminal: Plain Text npx playwright test After the test execution is complete, the built-in Playwright report can be generated using the following command: Plain Text npx playwright show-report The report shows details of the test run, including test names, time taken, the browser agent used, and the number of tests executed, along with their pass/fail status. Watch the step-by-step YouTube tutorial on how to test GET API requests with Playwright TypeScript. Summary Testing GET API requests with Playwright using TypeScript allows you to easily send requests with query parameters and custom headers while keeping your tests clean and readable. Playwright also provides options such as timeout to control request duration and failOnStatusCode to automatically fail tests on non-successful responses. Together, these features help test the GET API requests efficiently.
Nuxt.js is a popular framework for Vue.js, and it is widely used for websites that require server-side rendering. It is similar to the Next.js framework for React.js. In this article, I’m going to share how you can fetch values randomly from a static JSON file with a Node and Express server. To make this example more realistic, we will store some words with their meanings in the words.json file in a static folder at the root. The necessary frameworks and libraries need to be installed on your machine, and basic knowledge is required: Node.js/ expressVue.js/Nuxt.js CLIJSONnpm (Source) #static/words.json JSON [ { "word": "lysis", "type": "noun", "meaning": "The resolution or favorable termination of a disease, coming on gradually and not marked by abrupt change." }, { "word": "outwit", "type": "verb", "meaning": "To surpass in wisdom, esp. in cunning; to defeat or overreach by superior craft." }, { "word": "completive", "type": "adjective", "meaning": "Making complete." } ] The words.json file above contains a few words, each with its type and meaning. Next, we need an Express server that listens for API calls from the front Nuxt/Vue page. #server.js JavaScript const path = require('path'); const express = require('express'); const cors = require('cors'); const fs = require('fs'); const app = express(); const PORT = 3001; app.use(cors()); let words = JSON.parse(fs.readFileSync('static/words.json', 'utf-8')); words = words.map(w => ({ ...w, type: w.type ? w.type.trim().toLowerCase() : '' })); app.get('/api/types', (req, res) => { const uniqueTypes = [...new Set(words.map(w => w.type))].sort(); res.json(uniqueTypes); }); //Random word generator with filters app.get('/api/random', (req, res) => { let filtered = [...words]; const { type, start, end, op, len, count } = req.query; const requestedType = type ? type.trim().toLowerCase() : ''; const startLetter = start ? start.trim().toLowerCase() : ''; const endLetter = end ? end.trim().toLowerCase() : ''; const wordLength = len ? parseInt(len) : null; const limit = parseInt(count) || 5; if (startLetter) { filtered = filtered.filter(w => w.word?.toLowerCase().startsWith(startLetter)); } if (endLetter) { filtered = filtered.filter(w => w.word?.toLowerCase().endsWith(endLetter)); } if (requestedType && requestedType !== 'all') { filtered = filtered.filter(w => w.type === requestedType); } if (op && wordLength) { if (op === '=') filtered = filtered.filter(w => w.word.length === wordLength); else if (op === '<') filtered = filtered.filter(w => w.word.length < wordLength); else if (op === '>') filtered = filtered.filter(w => w.word.length > wordLength); } const result = []; const available = [...filtered]; while (result.length < limit && available.length > 0) { const index = Math.floor(Math.random() * available.length); result.push(available.splice(index, 1)[0]); } res.json(result); }); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); }); As the Nuxt.js server runs on port 3000 by default, we have specified port number 3001. Next up is the Vue/nuxt.js code. With an input selection form and a “generate words” button. #pages/index.vue Vue.js Component <section class="card"> <div class="filters"> <div class="field"> <label>Number of Words</label> <input type="number" min="1" max="100" v-model.number="wordCount" /> </div> <div class="field"> <label>Word Type</label> <select v-model="wordType"> <option value="All">All</option> <option value="Noun">Noun</option> <option value="Verb">Verb</option> <option value="Adjective">Adjective</option> <option value="past participle">Past Participle</option> <option value="plural">Plural</option> <option value="preposition">Preposition</option> </select> </div> <div class="field"> <label>Starts With</label> <input type="text" maxlength="1" v-model="startLetter" /> </div> <div class="field"> <label>Ends With</label> <input type="text" maxlength="1" v-model="endLetter" /> </div> <div class="field"> <label>Word Length</label> <div class="length-filter"> <select v-model="lengthOperator"> <option value="">--</option> <option value="=">=</option> <option value="<"><</option> <option value=">">></option> </select> <input type="number" min="1" v-model.number="wordLength" /> </div> </div> <div class="action"> <button @click="getFilteredWords">Generate Words</button> </div> </div> </section> <section class="results"> <h2>Random Words List</h2> <div class="results-list"> <div v-show="!results.length" class="placeholder"> <p>Your generated words will appear here.</p> </div> <ul v-show="results.length"> <li v-for="(word, index) in results" :key="index" class="result-item"> <div class="word-card"> <strong class="word-title">{{ word.word }</strong> <small v-if="word.type" class="word-type">({{ word.type })</small> <p class="word-meaning">{{ word.meaning }</p> </div> </li> </ul> </div> </section> This is the normal HTML form that will be placed inside <template></temple>. This is the Vue.js variables section: data() { return { menuOpen: false, results: [], wordCount: 3, wordType: 'All', startLetter: '', endLetter: '', lengthOperator: '', wordLength: null }; }, Below is the code to send request to express server: async getFilteredWords() { const params = new URLSearchParams({ count: this.wordCount, type: this.wordType, start: this.startLetter, end: this.endLetter, op: this.lengthOperator, len: this.wordLength }); const res = await fetch(`http://localhost:3001/api/random?${params.toString()}`); this.results = await res.json(); this.$nextTick(() => { const resultsSection = document.querySelector('.results'); if (resultsSection) { resultsSection.classList.add('show'); resultsSection.classList.add('highlight'); setTimeout(() => { resultsSection.classList.remove('highlight'); }, 1500); } }); } And done. We have successfully set up the words.json file inside the static folder (static/words.json). Vue.js code inside pages/index.vue file. Express server code is inside the/server.js file. Run the project: To run the Nuxt server: “npm run dev.” To run the Express server: “node server.js.” Once these two commands are running in cmd, open a web browser and go to: http://localhost:3000/. Project Explanation Step by Step In this code, we have developed a random word finder from the words.json file, and we have shown randomly generated words to the users. In this code, we have used Vue.js/Nuxt.js for the front end and node/express server for the backend. Vue/Nuxt server is running on localhost:3000, and the Express server is running on localhost:3001. Step 1: Front-End With Vue.js Vue.js gathers the selected word options and sends an API request to the backend Express server running on port 3000. First, Vue.js binds all the user input options to params: JavaScript async getFilteredWords() { const params = new URLSearchParams({ count: this.wordCount, type: this.wordType, start: this.startLetter, end: this.endLetter, op: this.lengthOperator, len: this.wordLength }); Once bound, the information is sent to the backend with the following code. Step 2: The Backend Server With Express.js Server The backend API in the Express server is triggered with app.get(). First, the Express server fetches word information from the static words.json file and stores words in a filtered constant. Then processes the incoming information from the front-end and, as per the user's requirements, filters out words fetched from the words.json file. Once filtered, it sends words to the front end with res.json(). Step 3: Show Words to the Users In the Vue.js front end, we have used the async/await syntax. So, the following code line makes Vue.js wait until it gets a response from the Express.js server. Conclusion So, this is a simple full-stack code to pick information from the static JSON file randomly. In this article, Node.js is used for the back end to retrieve data randomly, and Vue.js is used for the front-end user interface. This looks like a few simple lines of code, but this code can be used in several educational and fun applications that process information randomly.
In Part 1 of this series, we built a Quarkus-based MCP tool server and connected it to the Goose AI agent over Streamable HTTP. The tools worked, the demo was clean, and everything ran on localhost. But the moment you imagine 50 developers running Goose on their laptops, all hitting the same set of backend MCP servers, the architecture starts to crack. Who authenticated that tool call? Which role authorized the getAuditTrail invocation? What stops a poisoned tool name from injecting payloads into your backend? This article answers those questions by placing agentgateway — the Linux Foundation's open-source proxy for agentic AI traffic — between Goose clients and the Quarkus MCP microservices we built in Part 1. The Problem: Direct Agent-to-Backend Connections Don't Scale When Goose (or any MCP client) connects directly to a backend MCP server, every tool call is a point-to-point trust relationship: This works for demos. It breaks in production for three reasons: No authentication. The MCP Streamable HTTP endpoint accepts any JSON-RPC call. There is no token verification, no session binding, and no identity propagation.No authorization. Every caller can invoke every tool. An intern running Goose has the same access as an SRE — getAuditTrail, getOrderStatus, everything.No guardrails. A compromised or misconfigured agent can send tool names containing prototype-pollution payloads (__proto__), path-traversal sequences (../), or CRLF-injected headers. The backend has to defend itself alone. The Solution: agentgateway as a Unified Control Plane agentgateway is a Rust-based proxy purpose-built for AI agent traffic. It understands the MCP protocol natively — it doesn't just forward HTTP; it parses JSON-RPC envelopes, manages MCP sessions, and applies policies at the tool-call level. Here is the architecture we're building: Goose connects to agentgateway on port 3000. agentgateway validates the JWT, checks the caller's roles against tool-level RBAC rules, passes the call through an ExtMCP guardrail server that sanitizes headers and blocks poisoning attempts, and only then forwards the clean request to the Quarkus backend on port 8080. Prerequisites You'll need everything from Part 1, plus: agentgateway binary (v1.4+): Shell curl -sL https://agentgateway.dev/install | bash Verify your Part 1 Quarkus MCP server still works: Shell cd part1-quarkus-mcp mvn quarkus:dev Then confirm the MCP endpoint responds: Shell curl -s http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}' | jq . Step 1: Deploy agentgateway Alongside the Quarkus MCP Server Create the agentgateway configuration at part2-agentgateway/agentgateway/config-dev.yaml. This development config proxies MCP traffic without requiring JWT, so you can validate the plumbing first: YAML # yaml-language-server: $schema=https://agentgateway.dev/schema/config mcp: port: 3000 policies: cors: allowOrigins: - "*" allowHeaders: - mcp-protocol-version - content-type - mcp-session-id exposeHeaders: - Mcp-Session-Id targets: - name: customer-tools mcp: host: http://localhost:8080/mcp Start agentgateway: YAML agentgateway -f part2-agentgateway/agentgateway/config-dev.yaml Now test the proxied MCP endpoint. Note that agentgateway returns SSE format (event: message\ndata: {...}), so we extract the JSON from the data: line: Shell curl -s http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}' \ | grep '^data: ' | sed 's/^data: //' | jq . You should see the same customer-tools server info as Part 1, but the traffic now flows through agentgateway. Open http://localhost:15000/ui to see the agentgateway admin UI with your MCP target listed. Step 2: Add JWT Authentication With the proxy working, let's lock it down. The mcpAuthentication policy implements the MCP Authorization specification — it validates JWT bearer tokens on every MCP request and supports OAuth 2.1 with PKCE for browser-based flows. Update the config to part2-agentgateway/agentgateway/config.yaml: YAML # yaml-language-server: $schema=https://agentgateway.dev/schema/config mcp: port: 3000 policies: cors: allowOrigins: - "*" allowHeaders: - mcp-protocol-version - content-type - mcp-session-id - authorization exposeHeaders: - Mcp-Session-Id mcpAuthentication: issuer: http://localhost:9000 audiences: - "http://localhost:3000/mcp" jwks: url: http://localhost:9000/.well-known/jwks.json resourceMetadata: resource: http://localhost:3000/mcp scopesSupported: - "mcp:tools:read" - "mcp:tools:execute" bearerMethodsSupported: - header targets: - name: customer-tools mcp: host: http://localhost:8080/mcp How It Works When a Goose client (or any MCP client) connects to http://localhost:3000/mcp: Discovery. The client fetches /.well-known/oauth-protected-resource from agentgateway and discovers it needs a bearer token with the mcp:tools:execute scope.Token acquisition. The client runs the OAuth 2.1 Authorization Code flow with PKCE against the issuer (http://localhost:9000), obtains an access token, and includes it as Authorization: Bearer <token> on subsequent MCP requests.Validation. agentgateway downloads the JWKS from the issuer, verifies the token signature, checks exp, iss, and aud claims, and extracts the sub and role claims for downstream authorization.Forwarding. Only after validation does agentgateway forward the JSON-RPC call to the Quarkus backend. Connecting to a Real OIDC Provider For production, replace the issuer and JWKS URL with your OIDC provider. Here is an example using Keycloak: YAML mcpAuthentication: issuer: https://keycloak.example.com/realms/mcp audiences: - "https://gateway.example.com/mcp" jwks: url: https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs provider: keycloak: {} agentgateway has built-in support for Keycloak, Auth0, Okta, Microsoft Entra ID, and other OIDC providers. Step 3: Configure Tool-Level RBAC With CEL Expressions JWT authentication tells you who is calling. MCP authorization tells you what they're allowed to do. agentgateway uses CEL (Common Expression Language) to define fine-grained, tool-level RBAC rules. Add the mcpAuthorization policy to your config: YAML mcpAuthorization: rules: # Operators can call any tool - 'has(jwt.roles) && "operator" in jwt.roles' # Viewers can only read status and health - > has(jwt.roles) && "viewer" in jwt.roles && mcp.tool.name in ["getCustomerStatus", "getZoneHealthLogs", "getSLACompliance"] # Auditors can access audit trail and SLA compliance - > has(jwt.roles) && "auditor" in jwt.roles && mcp.tool.name in ["getAuditTrail", "getSLACompliance"] How the Rules Work Each rule is a CEL expression that evaluates to true (allow) or false (deny). agentgateway evaluates them in order — the first match wins. RoleAllowed ToolsDenied ToolsoperatorAll five toolsNoneviewergetCustomerStatus, getZoneHealthLogs, getSLACompliancegetOrderStatus, getAuditTrailauditorgetAuditTrail, getSLACompliancegetCustomerStatus, getZoneHealthLogs, getOrderStatusNo roleNoneAll These aren't abstract labels — at Acme FinServ they map to real people and a real segregation-of-duties story: PersonaRoleWhy this scopeSofia — SRE, on-call for the platformoperatorNeeds to drive operational tools during incidents; full access is justified and logged.Acme Status Dashboard — an internal read-only serviceviewerShows customers and health at a glance; must never read getOrderStatus or getAuditTrail (PII/financial).Priya — external SOC 2 auditorauditorReviews the audit trail and SLA posture only. Giving her getCustomerStatus would violate least privilege — an auditor reading live customer data is itself a finding. The auditor scope is the one a SOC 2 assessor will scrutinize: it proves the audit function is separated from the operational function, and that access is granted by need, not convenience. agentgateway also auto-filters tools/list responses — if a viewer calls tools/list, they only see the three tools they're authorized to invoke. The agent never even learns that getAuditTrail exists. Available CEL Variables VariableDescriptionmcp.tool.nameThe tool being invoked (e.g., getCustomerStatus)mcp.tool.targetThe backend target name (e.g., customer-tools)jwt.subThe subject claim from the JWTjwt.rolesRole claims extracted from the JWThas(jwt.<claim>)Check whether a JWT claim exists Step 4: Prevent Tool Poisoning With ExtMCP Guardrails JWT and RBAC protect the identity layer. Guardrails protect the content layer. A valid, authenticated operator can still send a tool call with a poisoned name like getCustomerStatus/../../../etc/passwd or arguments containing <script> tags. The Quarkus backend's @Pattern annotations from Part 1 catch some of this, but defense in depth means filtering at the proxy too. agentgateway's ExtMCP guardrails intercept MCP method calls before they reach the backend, passing them through an external gRPC policy server that can inspect, mutate, or deny each call. Building the Guardrail Server With Quarkus gRPC Instead of relying on a third-party Docker image, we'll build our own ExtMCP guardrail server using Quarkus gRPC — keeping the entire stack in Java. The guardrail server lives in part2-agentgateway/extmcp-guardrail/ and implements the agentgateway ExtMCP protocol. First, the protobuf service definition (src/main/proto/extmcp.proto): ProtoBuf syntax = "proto3"; package agentgateway.dev.ext_mcp; option java_package = "com.example.guardrail.grpc"; import "google/protobuf/struct.proto"; service ExtMcp { rpc CheckRequest (McpRequest) returns (McpRequestResult); rpc CheckResponse (McpResponse) returns (McpResponseResult); } message McpRequest { repeated string service_names = 1; string method = 2; google.protobuf.Struct metadata_context = 3; optional bytes mcp_request = 4; repeated McpHeader headers = 5; } message McpRequestResult { oneof result { Pass pass = 1; bytes mutated = 2; AuthorizationError error = 3; } HeaderMutation header_mutation = 4; } message AuthorizationError { enum Code { UNKNOWN = 0; PERMISSION_DENIED = 1; RESOURCE_EXHAUSTED = 2; INVALID = 3; } Code code = 1; string reason = 2; optional bytes mcp_error = 3; } The Quarkus service implementation performs header sanitization and tool-poisoning detection: Java @GrpcService public class ExtMcpGuardrailService implements ExtMcp { private static final Pattern DANGEROUS_HEADER = Pattern.compile( "(?i)^(x-mcp-|x-forwarded-|x-real-ip)"); private static final List<String> BLOCKED_PATTERNS = List.of( "__proto__", "constructor", "../", "eval(", "exec(", "<script"); @Override public Uni<McpRequestResult> checkRequest(McpRequest request) { if (!"tools/call".equals(request.getMethod())) { return passRequest(); } // 1. Sanitize x-mcp-* headers for CRLF injection String headerError = sanitizeHeaders(request.getHeadersList()); if (headerError != null) { return denyRequest("header sanitization failed: " + headerError); } // 2. Check tool name and arguments for poisoning patterns if (request.hasMcpRequest()) { String poisonError = checkToolPoisoning( request.getMcpRequest().toStringUtf8()); if (poisonError != null) { return denyRequest("tool poisoning detected: " + poisonError); } } return passRequest(); } @Override public Uni<McpResponseResult> checkResponse(McpResponse response) { if (!"tools/list".equals(response.getMethod())) { return passResponse(); } // Append [guardrail-verified] marker to every tool description String original = response.getMcpResponse().toStringUtf8(); String mutated = original.replace("\"description\":\"", "\"description\":\"[guardrail-verified] "); return Uni.createFrom().item(McpResponseResult.newBuilder() .setMutated(ByteString.copyFrom(mutated, StandardCharsets.UTF_8)) .build()); } } Start the guardrail server on port 9001: Shell cd part2-agentgateway/extmcp-guardrail mvn quarkus:dev Configuring the Guardrail Policy Add the mcpGuardrails section to the agentgateway config: YAML mcpGuardrails: processors: - kind: remote host: "localhost:9001" failureMode: failClosed methods: tools/call: request tools/list: response The key settings: SettingValueWhyfailureModefailClosedIf the guardrail server is down, deny all tool calls rather than allowing unfiltered traffictools/call: requestPre-forwardInspect and sanitize before the call reaches the Quarkus backendtools/list: responsePost-forwardAnnotate or filter the tool list after the backend responds How Tool Poisoning Prevention Works When a tools/call request arrives, the guardrail flow is: Plain Text Goose → agentgateway → [JWT verified] → [RBAC checked] → → ExtMCP CheckRequest() → guardrail server inspects: 1. Scan x-mcp-* headers for CRLF injection 2. Validate header value lengths (≤ 256 bytes) 3. Check tool name for blocked patterns (__proto__, ../, eval()...) 4. Check tool arguments for injection payloads → Pass / Mutate / Deny → [if passed] → Quarkus MCP backend Sanitizing x-mcp-header Values The x-mcp-* headers carry protocol metadata between MCP clients and servers. A malicious client can inject CRLF sequences (\r\n) into these headers to smuggle additional HTTP headers or split responses. The guardrail server strips these by: Matching any header whose name starts with x-mcp-, x-forwarded-, or x-real-ipRejecting values that contain \r or \n charactersEnforcing a 256-byte maximum length on these header values Building a Custom Guardrail Server For production, implement the ExtMCP gRPC protocol with two methods: CheckRequest – Called before the tool call reaches the backend. Inspect the tool name, arguments, and headers. Return Pass, Mutate (rewrite params), or Deny with an AuthorizationError.CheckResponse – Called after the backend responds. Inspect the result. Return Pass, Mutate (redact sensitive data), or Deny. The part2-agentgateway/extmcp-guardrail/ directory contains the complete Quarkus gRPC implementation with the proto definition, the guardrail service, and the Maven build. Verifying the Guardrail With all three services running, test that the guardrail is active: Shell # Initialize session export MCP_SESSION_ID=$(curl -s -D - http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}' \ | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r') # Complete handshake curl -s http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' # List tools — descriptions should show the guardrail marker curl -s http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-03-26" \ -H "mcp-session-id: $MCP_SESSION_ID" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}' \ | grep '^data: ' | sed 's/^data: //' | jq '.result.tools[].description' Each tool description should start with the [guardrail-verified] marker, confirming that every tools/list response passes through the ExtMCP guardrail before reaching the client. Step 5: Configure Goose to Use agentgateway The final step is the simplest. In Part 1, Goose connected directly to the Quarkus backend: YAML # Part 1 — direct connection extensions: customer-tools: enabled: true type: http uri: http://localhost:8080/mcp headers: Content-Type: "application/json" For Part 2, change the URI to point to agentgateway: YAML # Part 2 — through agentgateway extensions: customer-tools: enabled: true type: http uri: http://localhost:3000/mcp headers: Content-Type: "application/json" Copy the updated config: YAML cp part2-agentgateway/goose-extension-config.yaml ~/.config/goose/config.yaml Now launch Goose and test the same prompts from Part 1: Plain Text Check customer status for CUST-4091 and verify health logs for their region The response is identical to Part 1, but the traffic now flows through agentgateway with JWT validation, RBAC enforcement, and guardrail inspection. You can verify this by checking the agentgateway UI at http://localhost:15000/ui — every tool call appears in the request log with its authentication status and policy decisions. The Complete Configuration Here is the full config.yaml combining all four security layers: YAML # yaml-language-server: $schema=https://agentgateway.dev/schema/config config: tracing: endpoint: http://localhost:4317 protocol: grpc sampling: parent: true default: 1.0 mcp: port: 3000 policies: cors: allowOrigins: - "*" allowHeaders: - mcp-protocol-version - content-type - mcp-session-id - authorization exposeHeaders: - Mcp-Session-Id mcpAuthentication: issuer: http://localhost:9000 audiences: - "http://localhost:3000/mcp" jwks: url: http://localhost:9000/.well-known/jwks.json resourceMetadata: resource: http://localhost:3000/mcp scopesSupported: - "mcp:tools:read" - "mcp:tools:execute" bearerMethodsSupported: - header mcpAuthorization: rules: - 'has(jwt.roles) && "operator" in jwt.roles' - > has(jwt.roles) && "viewer" in jwt.roles && mcp.tool.name in ["getCustomerStatus", "getZoneHealthLogs", "getSLACompliance"] - > has(jwt.roles) && "auditor" in jwt.roles && mcp.tool.name in ["getAuditTrail", "getSLACompliance"] mcpGuardrails: processors: - kind: remote host: "localhost:9001" failureMode: failClosed methods: tools/call: request tools/list: response targets: - name: customer-tools mcp: host: http://localhost:8080/mcp Bonus: Interactive Security Console The demo includes a browser-based SPA (index.html) that lets you visualize the entire security flow without touching the command line. The start-all.sh script serves it automatically on port 8888. Open http://localhost:8888/index.html and you'll see an enterprise-style console with: Config selector – switch between three tiered configs to see how each maps to a real deployment stage:Live stat tiles – session status, request count, tools discovered, and security checks passed/deniedAnimated architecture diagram – watch MCP requests flow from Goose through agentgateway's security layers to the Quarkus backend in real timeConfig-aware security layers – JWT, RBAC, and ExtMCP layers animate as "checking → passed" when enabled in the selected config, or appear as "skipped" with a badge when not configured ConfigUse CaseSecurity Layersconfig-dev.yamlLocal development — pure proxy pass-through for rapid iteration without security overheadNoneconfig-guardrails.yamlStaging / shared environments — blocks tool poisoning and header injection before requests reach the backendExtMCPconfig.yamlProduction deployment — full security stack with JWT identity verification, role-based tool access via CEL, and input sanitizationJWT + RBAC + ExtMCP The four demo steps — Initialize, List Tools, Call Tool, and Poison Test — make real MCP requests through agentgateway and display the JSON-RPC responses. Switching configs lets you demonstrate the difference: with config-dev.yaml, the poison test passes through unblocked; with config-guardrails.yaml, the ExtMCP guardrail catches and denies it; with config.yaml, every request also passes through JWT authentication and RBAC authorization before reaching the guardrail layer. What We Achieved Starting from the unprotected Quarkus MCP server in Part 1, we added four security layers without changing a single line of the backend Java code: LayerWhat It Doesagentgateway FeatureAuthenticationVerifies caller identity via JWT/OAuth 2.1 with PKCEmcpAuthenticationAuthorizationEnforces tool-level RBAC per rolemcpAuthorization with CELInput sanitizationBlocks tool poisoning and header injectionmcpGuardrails (ExtMCP)ObservabilityTraces every tool call through the proxyOpenTelemetry integration The Quarkus MCP backend remains a clean, focused tool server. All governance concerns live in the agentgateway configuration and the Quarkus gRPC guardrail service — keeping the entire stack in Java, exactly where platform engineers expect to find them. What's Next: Part 3 In Part 3: End-to-End Tracing and Observability Across Goose, agentgateway, and Quarkus, we'll wire up distributed tracing across the full agent traffic path. You'll see how a single Goose prompt generates a trace that spans the agent, the gateway, and the Quarkus backend — with tool-call latency, RBAC decisions, and guardrail verdicts all visible in a single Jaeger or Grafana Tempo timeline. We'll configure OpenTelemetry exporters in all three components and build a Grafana dashboard that gives platform teams real-time visibility into their agentic infrastructure. Stay tuned.
In a previous article, Working with Spreadsheets in Java: A Practical Overview, we walked through the common scenarios where Java applications need to interact with spreadsheets and the categories of tools available for the job. One of the factors mentioned there was support for modern Excel formulas — a topic that deserves more space than a single bullet point. Java applications interact with Excel more often than most teams plan for: file uploads from finance, calculation logic authored in a workbook, reporting exports back to business users. The files these users produce today are not the same as the files they produced five years ago. Excel 365 and Excel 2021 introduced a new formula model, and workbooks authored in those versions routinely use it. Depending on which library you use, those formulas may evaluate correctly, fail silently with stale cached values, or throw exceptions at recalculation time. This article goes deeper on that topic: what dynamic arrays and spill behavior are, what the new function set looks like, why supporting them is technically difficult, and what Java developers should look for when evaluating whether a library handles them correctly. Why You're Seeing Them in Real-World Workbooks Dynamic arrays spread rapidly because they eliminate many of the helper columns, copied formulas, and Ctrl+Shift+Enter array formulas that older Excel workbooks depended on. Workbooks become shorter, easier to audit, and easier to maintain. As organizations migrate to Microsoft 365, these newer formulas increasingly appear in spreadsheets exchanged with Java applications, even when the application itself hasn't changed. What Changed: One Formula, Many Values Before dynamic arrays, formulas that returned multiple values generally required a pre-sized array range and legacy array-formula syntax. Dynamic arrays changed this by allowing a single formula to return a variable-sized array and automatically spill into neighboring cells. For example: =UNIQUE(A1:A6) Entered in one cell, this returns the full list of distinct values from A1:A6. The result fills as many cells as there are distinct values. If the source data changes and the number of unique values changes, the spill range automatically grows or shrinks. This is not just a new function. It is a change in the evaluation model itself. The Mechanics of Spill A spilled formula produces a region of cells with specific roles: The anchor cell is the one cell that contains the formula. It "owns" the result.The spilled cells are the neighboring cells that display the additional values. They do not contain formulas of their own; they mirror slices of the anchor's result. You can reference the entire spilled range from another formula using the # operator. The # reference is not a fixed cell range such as A1:A6; it refers to whatever range the anchor currently spills into. If A1 contains =UNIQUE(...) and the result spills into A1:A6, then =COUNTA(A1#) counts the values in the entire spilled range. If the spill range grows or shrinks, the # reference adjusts automatically. #SPILL! errors. If the range a formula needs to spill into is blocked by an existing value, a merged region, or an Excel Table, the formula cannot spill, and the anchor cell shows #SPILL! instead of a result. Clearing the obstruction allows the formula to complete. Implicit intersection with @. Older Excel silently reduced arrays to single values in many contexts. Modern Excel returns the full array unless the formula uses the @ prefix. For example, =A1:A10 entered in a cell in modern Excel spills the values from A1:A10, while =@A1:A10 applies implicit intersection and returns the value corresponding to the formula's row. Files migrated from older Excel versions often contain automatically inserted @ prefixes to preserve their original behavior. The New Function Family The modern functions commonly associated with Excel's dynamic-array model can be grouped into three broad categories. It is worth understanding the grouping because the groups behave differently. Group 1: Language Features These are not really functions in the traditional sense. They add expression-level constructs to Excel's formula language. LET binds names to intermediate values inside a formula, so you can write =LET(total, SUM(B2:B100), tax, total*0.1, total+tax) instead of repeating SUM(B2:B100) three times.LAMBDA defines a reusable function inside a workbook. Combined with named ranges, LAMBDA effectively adds user-defined functions without VBA.ISOMITTED is used inside LAMBDA to detect whether an optional argument was supplied. These functions do not inherently produce a spilled array. LET returns the result of its calculation, which may itself be an array. Group 2: Dynamic Array Functions These are the functions people usually mean when they talk about "the new Excel functions." These functions are designed to return arrays, and when their results contain multiple values, Excel can spill those results into neighboring cells. UNIQUE returns distinct values from a range.SORT and SORTBY return sorted arrays.FILTER returns rows that match a condition.SEQUENCE generates a sequence of numbers.RANDARRAY generates an array of random numbers. Array-shaping functions form a subset of this group. They take arrays as input and return reshaped arrays: CHOOSECOLS, CHOOSEROWS, DROP, EXPAND, HSTACK, VSTACK, TAKE, TOCOL, TOROW, WRAPCOLS, WRAPROWS. TEXTSPLIT also fits here — it splits a string into an array. Other functions, including BYROW, BYCOL, MAP, REDUCE, and SCAN, build on the same dynamic array model. Group 3: Scalar Functions Added in the Same Era Dynamic arrays are primarily an evaluation model; modern functions are a collection of functions that take advantage of, or coexist with, that model. Group 3 functions were introduced as part of the broader set of modern Excel functions, but they are not themselves primarily array-producing functions. XLOOKUP and XMATCH are modern replacements for VLOOKUP and MATCH. They normally return a single value, though they can return an array when passed an array of lookup values.TEXTAFTER and TEXTBEFORE return substrings.VALUETOTEXT and ARRAYTOTEXT convert values to text (ARRAYTOTEXT takes an array as input but returns a single string). These are often lumped in with dynamic array functions because they arrived together, but their evaluation model is closer to VLOOKUP than to UNIQUE. Why This Is Hard for a Formula Engine Supporting these features is not just a matter of adding new function names to a list. The dynamic array model requires substantial changes to the evaluation engine itself. A traditional one-cell-at-a-time formula model is not sufficient to implement dynamic arrays. An engine must be able to represent a formula whose result has a variable shape and propagate that result across multiple cells. A modern engine has to handle four additional concerns: Array-shaped results. A formula's return value may be a 2D array whose dimensions depend on the input data. =UNIQUE(A1:A100) returns a different number of rows depending on how many unique values the range contains. The engine must determine the result shape at evaluation time, not at parse time. Spill range tracking. The engine must reserve the cells the formula spills into and prevent other content from occupying them. When something occupies a spill target, the anchor must return #SPILL! rather than overwrite the obstruction. The reserved region must also update when the shape of the result changes. Downstream references. Expressions like A1# refer to the entire spilled range. When the shape of the anchor formula changes, every downstream reference must be re-evaluated with the new dimensions. This makes the dependency graph more dynamic than in a one-value-per-cell model. Implicit intersection compatibility. Older Excel silently collapsed arrays to single values in many contexts. Modern Excel returns the whole array. When files authored in older Excel are opened in modern Excel, @ prefixes are inserted automatically to preserve original behavior. An engine that reads modern .xlsx files needs to honor the @ operator, or the imported formulas will produce different results. Adding these behaviors to an engine designed around the one-formula-one-value model is a substantial rewrite, not an incremental feature addition. This is part of why support across the Java ecosystem has been uneven. What Java Developers Should Check Support for these capabilities varies significantly across Java spreadsheet libraries. Some engines were originally designed around traditional one-cell-one-result evaluation and only implement subsets of the modern Excel model. Others have extended or redesigned their evaluators to support dynamic arrays. Rather than relying on feature lists, it is worth validating behavior against workbooks representative of your own application. If your application needs to evaluate modern Excel formulas, the following checks are worth running before committing to a library. Test with a file containing a spilled formula. Create a small .xlsx with =UNIQUE(A1:A100) or =SORT(A1:A100) in a cell. Load it in your candidate library and try to recalculate the anchor cell. A library that supports dynamic arrays will return the array; one that does not will typically throw an exception or return only the first value. Check for the # spill operator. In the same file, add another cell containing =COUNTA(A1#) where A1 is the anchor. This tests whether the library understands spilled range references, which is a separate capability from evaluating the anchor formula itself. Test the @ operator. Add =@A1:A10 in a cell and check whether the library correctly returns the value at the current row rather than the full array. Files migrated from older Excel routinely contain @ prefixes; a library that doesn't handle them will produce different results than Excel. Test with LET and LAMBDA. Write a formula like =LET(total, SUM(A1:A100), total * 1.1) and check both evaluation and .xlsx round-trip. Test LET and LAMBDA independently. Parsing, preserving, and evaluating these functions are separate capabilities, so a library that can read or write the formula text may not necessarily be able to evaluate it correctly. Test round-trip. Save the workbook, reopen it in Excel, and check that the formulas still produce correct results. Some engines strip modern constructs on save. Check what happens on failure. When a library encounters a function it does not implement, does it raise an exception, return an error value, or silently fall back to the cached value from the file? Silent fallback is the most dangerous behavior because it masks the problem during development and only fails in production when the data changes. Conclusion Excel's formula language has changed more in the last few years than in the two decades before it. Dynamic arrays, spill behavior, and the new function set are not experimental — they are standard in Excel 365 and Excel 2021, and they show up in workbooks that Java applications routinely have to process. For Java developers, the practical implication is that "Excel formula support" is no longer a single property that a library either has or doesn't have. There are several distinct capabilities involved, and libraries vary widely on each. As covered in the previous article, the Java spreadsheet landscape spans open source libraries such as Apache POI, commercial headless engines, and embedded spreadsheet components like Keikai. Whichever category fits your use case, the checks above are a reasonable way to verify that a candidate library handles modern Excel behavior against the workbooks your real users produce.
Hadera, Israel, September 8th, 2026, TechnologyWire This article was provided by TechnologyWire and does not represent the editorial content of DZone. RavenDB, a NoSQL document database used by more than 12,000 customers, announced today the launch of its new product, Quill, a context layer for SQL databases that makes them ready for production AI agents without migrating the system of record or architecting a custom AI stack. With AI becoming a board-level mandate, CTOs and VPs of engineering are under pressure to ship AI capabilities fast. But for organizations whose mission-critical data sits in legacy SQL systems, built years before embeddings or agents existed, AI can’t access their data. Modernizing or replacing the systems is expensive, risky, and time-consuming. By the time the system is updated, nobody remembers what the project was supposed to achieve or how it measured ROI. Recently, a Gartner survey of infrastructure and operations leaders found that one in five AI initiatives fail, and only 28% report a positive ROI, which is linked to how well the technology is integrated, governed, and aligned with operational needs, not to the sophistication of the model. As AI becomes the industry standard, organizations have been left without a clear path to deliver until now. "Anyone can stand up an AI demo in an afternoon, but getting that demo into production with data pipelines, semantic search, security, governance, all the plumbing a small proof of concept doesn't need until it has to run at scale, is the hard part," said Oren Eini, founder and CEO of RavenDB. "Quill exists because we'd rather hand teams that plumbing already assembled than watch them rebuild the same project after project. You get access to the live data you need, decide the scope on day one, and change it as you go, instead of building everything from scratch." Quill connects directly to an organization's existing SQL database and adds a context layer on top, making it possible to launch production-ready agents in weeks rather than the 18 to 24 months of a typical in-house build. The source system stays exactly where it is and remains authoritative, and the full AI stack- search, retrieval, and agents that can answer questions- is included. Agents built on Quill support web chat, WhatsApp, Telegram, Slack, and Discord out of the box. "With Quill, the plumbing was already there, so we spent our time building the actual feature," said Hagay Albo, CEO at Albos Technologies and Holdings, an early adopter of Quill. By default, Quill is governed, sitting between the AI and the source system, and it is built on the assumption that the model itself cannot be trusted with unrestricted access, so organizations decide exactly what an agent can and cannot see, independent of the source database's own permissions. In a healthcare setting, for example, an agent can answer a patient's question about an upcoming appointment, while prescription data is never part of the dataset it can query. What is usually a custom security project becomes a configuration choice. Quill is also model-agnostic, so teams can use any AI model, switch providers, or run entirely on their own hardware. Quill is now available for organizations running PostgreSQL, SQL Server, or MySQL, with more databases to be supported in the future, and can be deployed in the cloud or on-premises to meet data-residency or regulatory requirements. To start using Quill today, visit: https://ravendb.net/quill About RavenDB: RavenDB is a hybrid NoSQL document database built for modern application development. Used by more than 12,000 customers across 50 industries, RavenDB helps teams move faster with seamless data management across cloud, on-prem, and edge environments. With full-text search, automatic indexes, and an easy-to-use studio for monitoring and administration, RavenDB is the database developers love and enterprises trust. Learn more at www.ravendb.net
Modern database environments rarely run a single type of workload. Most production systems handle both transactional operations and analytical queries simultaneously. These mixed workloads, often referred to as hybrid workloads, place significant pressure on traditional database indexing and storage strategies. In such environments, disk-based indexes can become a performance bottleneck. When transactional and analytical queries compete for disk I/O, it often results in increased latency, reduced throughput, and inconsistent query performance. To address these challenges, SQL Server leverages memory-optimized tables and indexes as part of its In-Memory OLTP capabilities. These features reduce reliance on disk I/O by enabling data and index access directly from memory, while still maintaining durability through logging and checkpoint mechanisms. This article explores how memory-optimized indexing works and demonstrates how it can significantly improve performance in real-world hybrid workload scenarios. Core Characteristics Mandatory inclusion: Every memory-optimized table must have at least one index, as they serve as the "entry points" for row access.Purely in-memory: Indexes are rebuilt entirely from scratch during database recovery based on their definitions and the data loaded into memory.Non-persistent: Unlike traditional indexes, changes to these indexes are not written to the transaction log, reducing I/O overhead.Fragmentation-free: These structures do not suffer from traditional page fragmentation, eliminating the need for regular REORGANIZE or REBUILD operations. Index TypeBest Use CaseBehaviorHash IndexEquality SearchesUses an array of buckets; highly efficient for point lookups (e.g., WHERE ID = 5).Nonclustered IndexRange QueriesUses a lock-free B-tree structure (Bw-tree); ideal for range scans and sorted results (e.g., WHERE Price > 100). The Challenge With Traditional Indexing Traditionally, database indexes are stored on disk to ensure durability. While this design protects data, it introduces a major limitation: disk I/O latency. In environments with heavy workloads, disk access becomes a bottleneck. This is particularly noticeable when: Large analytical queries scan index rangesTransactional queries require fast point lookupsMany concurrent users access the system When both workloads run together, index operations often compete for disk resources, resulting in slower queries and higher latency. Introducing Memory-First Indexes Memory-First Indexes in SQL Server 2025 take a different approach. Instead of relying primarily on disk-based indexes, the system prioritizes in-memory index access for frequently used data while maintaining a synchronized copy on disk for durability. The key idea is simple: Hot data (frequently accessed index ranges) is kept in memory.Cold data remains on disk.Changes made in memory are synchronized with disk replicas in the background. This approach allows SQL Server to serve many queries directly from memory while still maintaining persistence. The feature also includes monitoring mechanisms that track query patterns. When the system detects frequently accessed index partitions, it moves them into memory automatically. Less frequently accessed portions are pushed back to disk to conserve memory resources. The result is faster query execution without requiring manual tuning from database administrators. Real-World Example: Retail E-Commerce Database To understand the benefits, consider a retail company running an e-commerce platform. The company stores millions of products in a table with the following structure: ProductID – unique identifierProductCategory – category of the productPrice – product priceStockQuantity – available inventory The application runs two types of queries. Transactional Query This query checks stock availability for a specific product. SQL SELECT StockQuantity FROM Products WHERE ProductID = 102345; Analytical Query This query calculates aggregated metrics by product category. SQL SELECT ProductCategory, AVG(Price) AS AvgPrice, SUM(StockQuantity) AS TotalStock FROM Products WHERE Price > 500 GROUP BY ProductCategory; In a traditional setup, both queries rely on disk-based indexes. When concurrency increases, disk access becomes saturated, and query performance suffers. With Memory-First Indexes, the most frequently used index ranges, such as ProductID and ProductCategory, are loaded into memory, allowing much faster lookups. Testing the Feature To evaluate the impact of Memory-First Indexes, we can simulate a large dataset and compare query performance before and after enabling the feature. Step 1: Create the Table SQL CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductCategory NVARCHAR(50), Price DECIMAL(10,2), StockQuantity INT ); Step 2: Populate Test Data The following script generates a large dataset for testing. SQL INSERT INTO Products (ProductID, ProductCategory, Price, StockQuantity) SELECT TOP 50000000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ProductID, CASE WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 5 = 1 THEN 'Electronics' WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 5 = 2 THEN 'Clothing' ELSE 'Home Appliances' END AS ProductCategory, ABS(CHECKSUM(NEWID()) % 1000) + 1.00 AS Price, ABS(CHECKSUM(NEWID()) % 5000) + 1 AS StockQuantity FROM sys.all_objects a CROSS JOIN sys.all_objects b; Step 3: Create Traditional Indexes SQL CREATE INDEX IX_Products_ProductID ON Products (ProductID); CREATE INDEX IX_Products_Category ON Products (ProductCategory); At this stage, run the transactional and analytical queries and capture baseline metrics using Query Store or dynamic management views. Step 4: Enable Memory-First Indexes Next, recreate the indexes with Memory-First enabled. SQL DROP INDEX IX_Products_ProductID ON Products; CREATE INDEX IX_Products_ProductID ON Products (ProductID) WITH (MEMORY_FIRST = ON); DROP INDEX IX_Products_Category ON Products; CREATE INDEX IX_Products_Category ON Products (ProductCategory) WITH (MEMORY_FIRST = ON); Step 5: Execute Test Queries SQL SELECT StockQuantity FROM Products WHERE ProductID = 102345; MS SQL SELECT ProductCategory, AVG(Price) AS AvgPrice, SUM(StockQuantity) AS TotalStock FROM Products WHERE Price > 500 GROUP BY ProductCategory; Record execution time, CPU usage, and disk activity again. Observed Performance Improvements The results typically show noticeable performance gains. For example: Transactional queries Before: ~50 msAfter: ~15 ms Analytical queries Execution time reduced by about 50% System metrics also reveal additional improvements: Disk I/O reduced by more than 70%Memory usage increased only moderatelyCPU utilization became more stable during peak workloads These improvements occur because queries are able to retrieve indexed data directly from memory rather than waiting for disk operations. Why This Matters for Modern Workloads Hybrid workloads are becoming the norm across many industries, including retail, finance, and IoT platforms. Systems must support both real-time transactions and large analytical queries without sacrificing performance. Memory-First Indexes help address this challenge by: Reducing disk I/O bottlenecksImproving response time for critical queriesAutomatically adapting to changing workload patternsMaintaining durability with synchronized disk replicas Final Thoughts Memory-First Indexes represent an important improvement in SQL Server 2025’s indexing architecture. By prioritizing in-memory access for frequently used data, SQL Server can deliver significantly faster query performance while still preserving data durability. For organizations running mixed transactional and analytical workloads, this feature can reduce latency, improve system stability, and make better use of available hardware resources. As hybrid workloads continue to grow, features like Memory-First Indexing will play a key role in helping database platforms keep up with modern application demands.
When I first started building enterprise applications with Large Language Models (LLMs), I fell into a trap that almost every developer encounters. I thought that scaling an AI system simply meant refining a single, massive prompt. I wrote complex system instructions, packed the context window with rules, and expected a single stateless API call to act as a researcher, analyst, and copywriter all at once. In production, this monolithic approach failed repeatedly. When processing dynamic data streams, the model flattened nuanced details, skipped critical execution steps, and regularly generated highly confident hallucinations. Through these failures, I realized the core problem: we are expecting a single inference step to manage an entire engineering workflow. To build predictable, production-grade software, I had to redesign my architecture. I moved away from monolithic prompts and began decoupling complex tasks into role-based, multi-agent frameworks in Python. My Breaking Point: The Competitive Intelligence Engine Failure Problem The necessity of this architectural shift became clear to me during a deployment for an enterprise technology firm. My team was tasked with building a competitive intelligence engine to track daily competitor product launches, analyze changing pricing sheets, and generate technical battlecards for our global sales team. My first iteration used a single, closed-source model wrapper. The prompt instructed the LLM to read raw HTML fragments from target URLs, extract feature updates, compare them against our internal capabilities matrix, and output a structured battlecard. During local testing with a few static URLs, it worked well. But when I went live against a shifting market, the system kept breaking without much notice: The Production Vulnerabilities I Encountered Context flattening: When parsing multiple long competitor pricing tiers, the model routinely dropped nuanced constraints, such as specific seat-count thresholds. It simply averaged out the data. Severe information loss: Instead of extracting the live web data provided in the context window, the model slipped back into its static pre-training data, hallucinating older features that the competitor had deprecated months prior. Prose without substance: Because the model had to handle data extraction, comparative reasoning, and copy editing simultaneously, it prioritized linguistic fluency over technical depth. The output looked like excellent marketing prose, but it was factually useless to our sales engineers. To fix this, I completely dismantled the monolithic prompt. I decoupled the system into three distinct programmatic agents, creating a clear engineering pipeline: Step 1: Establishing a Model-Agnostic Execution Boundary When I design multi-agent systems, my first rule is that agents must be decoupled from specific model providers. A production agent should depend on a stable, programmatic interface. This approach allows me to swap a cloud API like OpenAI for a local, open-weights model running via Ollama without changing a single line of business logic. Here is the standardized execution node I developed for this framework: Python import os from openai import OpenAI # I initialize the client container using environment boundaries client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) def execute_agent_inference(messages: list, target_model: str = "gpt-4o-mini") -> str: """ Provides a standardized execution node for all upstream agents to communicate with the designated model endpoint. """ response = client.chat.completions.create( model=target_model, messages=messages, temperature=0.1 # Low temperature enforces deterministic reasoning ) return response.choices[0].message.content Step 2: The Strategist Agent (Task Decomposition) The execution loop begins with the Strategist Agent. I isolated this node to handle a single cognitive task: ingestion and planning. Its sole job is to break down a broad user request into a chronological sequence of distinct tasks. Python def strategist_agent(user_objective: str) -> list: """ Ingests a broad objective and returns a structured execution plan. """ system_prompt = """ You are a project strategist. Your job is to break down a broad research objective into an ordered, numbered list of specific, non-overlapping data requirements. Do not summarize the topic. Output only the numbered steps. """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Objective: {user_objective}"}, ] raw_plan = execute_agent_inference(messages) # Parse the numbered rows into a clean Python list return [line.strip() for line in raw_plan.split("\n") if line.strip()] By forcing the system to map out its roadmap before running any resource-heavy tasks, I ensure the application maintains a strict operational scope. Step 3: Integrating External Tools With the Extraction Agent An agent is only as good as the data it consumes. I designed the Extraction Agent to never guess or extrapolate. Instead, I equip it with specific Python functions that fetch live, real-world data before it runs an inference cycle. Here, I define a simulated web search utility and an internal vector store look-up tool: Python def fetch_live_web_data(query: str) -> str: """ Simulates a live web lookup via external search providers like Tavily or SerpAPI. """ return f"[Live Web Match] Found current market documentation regarding: {query}" def query_internal_vector_store(query: str) -> str: """ Simulates a vector database query for internal technical specifications. """ return f"[Vector DB Match] Internal baseline spec data for: {query}" def extraction_agent(allocated_task: str, running_context: str) -> str: """ Gathers factual data using external retrieval tools before forming response notes. """ # Execute the tools first to ground the agent's context in real data web_insights = fetch_live_web_data(allocated_task) internal_insights = query_internal_vector_store(allocated_task) system_prompt = """ You are a data extraction agent. Your job is to analyze tool outputs and compile precise, evidence-dense technical notes. Strictly ground your response in the provided tool outputs. Do not extrapolate. """ user_payload = f""" Current Task: {allocated_task} Prior Context: {running_context} Tool Outputs: - {web_insights} - {internal_insights} """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_payload}, ] return execute_agent_inference(messages) Step 4: The Technical Reviewer Agent (Synthesis and Audit) The final step in my pipeline is the Technical Reviewer Agent. I do not use this agent as a passive text formatter. Instead, I design it to act as an internal critic that actively checks the gathered research for missing technical data. Python def technical_reviewer_agent(compiled_research_notes: str) -> str: """ Audits research materials and synthesizes a structured final technical report. """ system_prompt = """ You are a technical reviewer. Synthesize a clean report from the provided research notes. CRITICAL RULES: 1. Organize your output using clear Markdown headings and bullet points. 2. Do not introduce general knowledge or unverified claims. 3. If the data contains gaps, note them explicitly instead of smoothing over them. """ messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Research Notes:\n{compiled_research_notes}"} ] return execute_agent_inference(messages) Step 5: Constructing the Orchestration Loop With all my agents built, I put them together using a central orchestrator function. This loop manages the execution sequence, updates the running memory context between steps, and passes state across agent boundaries. Python def run_intelligence_engine(target_topic: str) -> str: """ Coordinates the execution sequence, updates persistent memory boundaries, and returns the finalized asset. """ print(f"[*] Initializing Strategy Phase for: {target_topic}") execution_steps = strategist_agent(target_topic) accumulated_notes = [] persistent_memory = "" for idx, step in enumerate(execution_steps, 1): print(f"[>] Executing Phase {idx}: {step[:50]}...") # Pass the running context so the agent knows what has been researched so far step_output = extraction_agent(step, persistent_memory) accumulated_notes.append(step_output) # Update the persistent memory to prevent duplicate work in later steps persistent_memory += f"\n[Completed Phase {idx} Info]: {step_output}\n" print("[*] Compiling and Reviewing Final Deliverable...") final_report = technical_reviewer_agent("\n".join(accumulated_notes)) return final_report if __name__ == "__main__": report_output = run_intelligence_engine( "Analyze competitor pricing models for cloud infrastructure shifts" ) print("\n--- Final Report Output ---\n") print(report_output) Resolving the Hidden Challenge: Information Degradation When I first launched a framework like this, I noticed a subtle engineering issue: the information handoff problem. When multiple agents pass unstructured text back and forth, the data risks losing clarity at each step. If the Strategist designs broad steps, the Extractor returns summarized notes, and the Reviewer formats them aggressively, the final output loses its technical precision. To keep your multi-agent networks at their best in production, I recommend implementing these two programmatic practices: 1. Maintain Strict Structural Memory Controls Never pass a raw conversational history across agent boundaries. Instead, require your extraction nodes to return explicit, structured technical updates (such as clear key-value maps or clean markdown bullet records). This approach preserves specific variables like precise pricing values or hardware specs, all the way to the final synthesis step. 2. Implement Automated Validation Gates Do not use an LLM to check if its own output is correct. Instead, place deterministic variable Python validation gates between agent handoffs. I write small programmatic checks to verify that the text matches a required schema, meets minimum character counts, or contains key terms extracted from the retrieval tools before letting the pipeline proceed. Measurable Production Outcomes Transitioning our enterprise tracking engines from monolithic prompt templates to this decoupled multi-agent architecture delivered immediate, verifiable improvements across our core operational metrics: Drastic reduction in hallucination rates: By isolating the extraction agent and grounding its context entirely in live tool calls, our documented hallucination rate fell from 7.2% to less than 0.2%.System traceability: When an output degrades, my team and I no longer dig through thousands of lines of a single prompt history. We simply look at the independent logs of each agent to find exactly where the data chain broke, reducing our Mean Time to Resolution (MTTR) from hours to minutes.Operational maintainability: I can update, optimize, or replace individual components, such as updating a web scraping API or refining the Reviewer's styling guide, without breaking or re-testing the rest of the application ecosystem. Conclusion The true test of an enterprise AI application is not how well it runs a basic query on a local development machine. Real success is defined by how reliably the application handles messy, dynamic data in production over time. By separating monolithic prompts into a coordinated pipeline of role-based agents, I turned unpredictable model outputs into stable, dependable software infrastructure. A perfect framework lies in distributing cognitive responsibility, creating clear interfaces, and engineering strict control boundaries around your models. Thank you for reading. Designing multi-agent AI systems for enterprise LLM workflows goes beyond calling powerful models; it requires thoughtful system design, coordination between agents, strong observability, and scalable architecture that can operate reliably in production.
A pull request arrives. A few hundred lines of Java implementing the new discount rule: tiered thresholds, a regional exception, something about loyalty tiers that nobody can quite explain. It compiles. The tests pass. An LLM wrote it in about forty seconds. Now: who reviews it? The person who owns that rule is in commercial operations. She knows exactly which customers should get the discount and why the regional exception exists, and she cannot read Java. The person who can read Java has no idea whether the thresholds are right. He will check that the code looks reasonable, because that is the only thing he is equipped to check. So the review that happens is not the review that matters. That is the problem I keep coming back to, and it has nothing to do with how good the model is. This Is Not an Argument About Whether the Model Is Good Enough Most objections to generated code are about competence. The model hallucinates an API. It gets an edge case wrong. It writes something that works on the happy path and falls over in production. I find these arguments unconvincing because they expire. Models get better. Any position resting on today's error rate is a position with a shelf life, and people who staked one out three years ago have mostly had to retreat from it. The durable question is different. It is not how well the model writes. It is what the thing it writes is permitted to say. A model that never makes a mistake, handed Java, can still emit Runtime.getRuntime().exec(...). Not because it is malicious or confused — because that sentence is available in the language it was asked to write. Competence and authority are separate axes, and improving the first does nothing to the second. "Write it in Java" Is a Much Bigger Grant Than Anyone Means Consider what you actually authorize when you ask for a discount rule in Java. You authorize file system access. Network sockets. Reflection. Thread creation. Process execution. Every class on the classpath, including the ones that talk to your database, your payment provider, and your secrets manager. You authorize the loading of new code at runtime. Nobody intends to grant any of this. It arrives free with the language, the way a house key also opens the shed. The task needed perhaps six operations — look up an order, total it, check a customer's tier, apply a discount, log the decision, approve or refuse — and the language you handed over contains everything Java contains. That gap, between the authority the task requires and the authority the language confers, is the whole of it. It exists whether or not the model is trustworthy. It exists whether or not anyone acts on it. It is just very large, and it is not visible in the pull request. The Usual Guardrails Are Denial Lists The standard responses all share a shape. Tell the model in the prompt not to touch the file system. Review the generated code. Run static analysis and flag dangerous calls. Run it in a sandbox with a restricted security policy. Every one of these asks you to enumerate what must not happen, over a space of things that can happen which is effectively unbounded. You are writing a deny-list against a general-purpose language. You have to think of exec. Then of reflection reaching exec. Then of the dependency that shells out on your behalf. Then of the next one. We learned this lesson in security a long time ago and reached a settled answer: allow-lists beat deny-lists, because the allow-list is finite and you wrote it. Somehow, when the subject is generated code, we reach for the deny-list again. Shrink the Language, Not the Model The alternative is to stop constraining a powerful language and instead supply a small one. Give the model a vocabulary that contains exactly the operations the domain has — the six from earlier, say — and nothing else. Not a restricted Java. A different, much smaller language, whose entire vocabulary is a list your team wrote in advance, in Java, on purpose. Generated business logic then looks like this: Python PROGRAM ApproveOrder(orderId INTEGER, limit DECIMAL) RETURNS BOOLEAN DECLARE purchase Order DECLARE total DECIMAL purchase = LOAD_ORDER(orderId) total = ORDER_TOTAL(purchase) IF total > limit THEN REJECT purchase, "over limit" RETURN FALSE END IF APPROVE purchase RETURN TRUE END. LOAD_ORDER, ORDER_TOTAL, REJECT and APPROVE are not part of the language. They are Java classes somebody decided to expose. Order is a Java object the program can hold and pass and never look inside — there is no purchase.customer.account.balance here, only the operations the domain chose to have. Two things change, and the second matters more than the first. The obvious one: dangerous programs are no longer forbidden, they are inexpressible. If the model emits DELETE_ALL_ORDERS, nothing rejects it on policy grounds. The name means nothing. The program does not compile, for the same reason a typo does not compile. There is no deny-list because there is nothing to deny. The less obvious one: the commercial operations manager can read the program above. She can tell you whether the threshold is right, whether the rejection reason is the one the contract requires, whether an approval should have been logged. The review moves to the person who owns the rule. That is the review that was missing at the start of this article, and no amount of static analysis over generated Java produces it. A small language buys something else, quietly. With no data structures, one global scope, no null, and a compiler that refuses to run a program that reads a variable before it is set, entire families of subtle wrongness have nowhere to live. Not caught — absent. What It Costs, and What It Does Not Buy I would not trust this argument from someone who only listed the advantages, so here are the bills. You have to design the vocabulary. Somebody sits down and decides that the domain has ORDER_TOTAL and CUSTOMER_RISK and not forty other things. That is real work, done before the first generated line, by someone who understands the domain. And if nobody on your team can write that list, this approach will not help you. It will only show you that the list does not exist. That is worth finding out, but it is not a pleasant morning. Complex algorithms stay in Java. Business rules are algorithms too, and they belong in the small language; that is the point. But route optimization, a scoring model, anything with real computational substance belongs behind a function the small language calls. The signal is usually that you want to build up a data structure, or that you want a helper you can call from three places. Both mean you have wandered out of business logic and should walk back. The boundary bounds naming, not doing. This is the limit people miss, and overstating it is how the idea gets dismissed. A function you expose can do anything Java can do. RUN_SHELL_COMMAND is a perfectly registrable operation. The vocabulary is only as narrow as the operations you chose, and choosing them badly gets you exactly the exposure you were avoiding. There are no resource limits yet. A generated program can still loop forever. This one is a gap rather than a decision: the interpreter walks the program one statement at a time, so a step budget or a deadline is a small addition rather than a redesign, and it will go in when somebody needs it. Until then, untrusted input needs the same containment any untrusted workload needs. What you get is narrower than "safe" and more useful than it sounds: the set of things a generated program can name is finite, written down, and reviewable by a human before anything is generated at all. When I Would Still Write Java If the thing is genuinely computational, write Java. If it is a one-off that will be deleted next week, use whatever is nearest — Java, Python, a shell script — and let the model write it; do not build a vocabulary for something with a life expectancy of days. If the rules change so fast that the vocabulary would be obsolete before it settled, the overhead will not pay for itself. And if your business logic is already reviewed by people who can read it, understand it, and are accountable for it being right — you may not have the problem this solves. Plenty of teams do not. But if you are about to let a model write business rules in Java, ask the question I started with, because the answer is usually uncomfortable. Somebody is going to approve that pull request. Are they the person who knows whether the rule is correct? If not, the language is too big. I have been building a small language along these lines: BUBAS, an orchestration language for subject-matter experts, embedded in Java. The example above is real BUBAS. The idea does not require my implementation, though — the argument is about the size of the language you hand over, and you can shrink yours however you like.
Founder,
Out of the Box Development, LLC