logo

Are you need IT Support Engineer? Free Consultant

Joule Studio with JEV at the Decision Boundary for Autonomous Enterprise AI Agents

  • By sujay
  • 25/09/2026
  • 67 Views

Before we start: this is an independent technical experiment, not an official SAP recommendation. I use “Joule Studio 2.0” for the generation of Joule Studio used here. The SAP documentation supplied for this implementation describes these pro-code capabilities as Early Adopter Care features; commands, interfaces, and availability may change before general availability. JEV is also a newly released technology. All invoice and vendor data in this demonstration is mocked. Releasing a block makes an invoice eligible for subsequent payment; it does not itself execute a payment.

 

1 What this implementation is trying to solve

Agentic AI changes the risk profile of an application as soon as the model can call tools with side effects. Reading an invoice, searching vendor history, or summarizing a mismatch are relatively low-impact operations. Clearing a payment block is different: it changes the state of a financial process and can make an invoice eligible for a later payment run.

That is the decision point we’ll explore in this article. We will build a supplier-invoice triage agent using Claude Code, the jl CLI, Joule Studio 2.0, and JEV from TypeSafe AI. The agent gathers evidence and decides how to handle the case. Before its release tool can execute, a Python wrapper checks the evidence again and asks JEV for a constrained decision.

The generative model handles the conversation, tool orchestration, and explanation. JEV evaluates the proposed action. Ordinary Python maps the result to execution, review, or denial. This makes the transition from reasoning to business action explicit in the implementation and reduces reliance on unconstrained generative output by restricting the consequential decision to predefined outcomes that are enforced in application code.

 

We will go through the concepts, the development setup, the invoice scenario, and the main pieces of code, then build the solution and inspect its behavior in Joule Studio.

 

2 The technologies behind the implementation

2.1 Joule Studio pro-code development and the jl CLI

SAP documents pro-code development as a way to build Joule Studio solutions from a local IDE and terminal, using the jl CLI and optionally an AI coding agent. The documented workflow covers intent, requirements, specification, solution generation, testing, and deployment. The resulting solution remains source code that developers can inspect and extend.

The CLI connects the local workspace to the Joule Studio landscape. It initializes the coding-agent integration, validates the solution structure, packages the assets, and provides deployment and diagnostic commands. Claude Code works inside that environment to read the platform guidance and implement the solution.

There are two separate uses of AI here. Claude Code is the development tool. The deployed invoice agent has its own runtime model configuration, business tools, and execution logic. Claude Code does not become a service inside the deployed agent.

For the platform workflow, see SAP’s Create an Agent with the CLI and Pro-Code Workflow.

2.2 JEV and TypeSafe System One models

TypeSafe AI is the company, JEV is its model, and System One is the model category it introduces. TypeSafe announced JEV on 15 September 2026 as its first public System One model, initially in early access.

The design centers on a particular consumer: software. An application supplies state and typed questions, and receives answers that it can use directly in its control flow. For this project, the relevant answer is one of four values: allow, confirm, review, or deny.

This is more than asking for a nicely formatted response. In its System One documentation, TypeSafe describes models built for constrained judgments and probabilities, without generating a written explanation. The output space is part of the question definition. The application knows which branches it must handle before making the request.

That fits the release tool well. By the time release is proposed, the application has already collected business evidence. It needs a judgment about a specific action, followed by a predictable software response. The agent’s LLM remains responsible for explaining the case to the user.

2.3 The three TypeSafe primitives

TypeSafe exposes three primitives. The introduction and the individual primitive documentation explain their contracts.

Primitive

What it asks

What code receives

Choice

Which predefined option applies

A choice, probabilities, and confidence

Score

Where the state falls on an ordered rubric

A score, probabilities, and confidence

Noul

Whether a proposition is true

A value from 0 to 1

 

Choice naturally fits routing and branching. Score supports ranking or threshold-based behavior. Noul provides a probabilistic answer to a yes/no-style question. In this agent’s running request path, the release guard and completion check both use Choice.

The TypeSafe name should not be read as a guarantee of a correct business decision. A constrained answer can still be wrong. The useful contract is that application code can handle the defined outcomes explicitly, while evaluations determine how well the model judges the underlying cases.

2.4 Calibration and structured state

TypeSafe calls its training approach Reinforcement Learning for Calibrated Decisions, or RLCD. Its launch article describes an architecture and training objective focused on producing decisions with useful uncertainty estimates.

For Choice and Score, confidence is derived from the returned probability distribution. It is not simply a verbal claim that the model is certain, and it should not be assumed to equal the probability of the selected option. Noul does not have a separate confidence field. TypeSafe’s confidence documentation explains how applications can use this information to choose between automatic action, additional verification, and human review.

Calibration is measured across sets of predictions; it does not guarantee an individual answer. Thresholds therefore need evaluation on the application’s own cases. Later in the walkthrough, we will see that this implementation preserves confidence but does not impose a confidence threshold on release.

The other useful concept is state. State can be a string, a JSON object, or an array. It carries the material being evaluated; the question separately defines the judgment requested. JEV currently accepts text and structured text data, not invoice images. See TypeSafe’s State documentation.

In our implementation, that state describes the proposed tool call, its arguments, evidence summaries, policy, side effects, and reversibility. JEV does not connect to SAP to discover these facts. Our application supplies them.

2.5 Small judgments composed in software

TypeSafe recommends narrowly scoped questions and composition in code. Independent questions can share one state in a batched request, but one answer does not automatically become the input to another question in that batch.

Here, the runtime uses one focused release question after Python has assembled the risk context. This is a practical entry point for experimenting with System One models inside an agent: a named action, a defined answer space, and explicit consequences for every answer.

JEV is new enough that I would treat this as an architectural direction to evaluate, rather than a settled standard. What makes it interesting is the possibility of composing specialized model judgments inside ordinary software, while retaining the flexibility of an agent around them.

 

3 Development approach from intent to a Joule Studio solution

3.1 Prepare the development environment

You need access to a Joule Studio landscape with the relevant pro-code capabilities, a local Claude Code installation, and access to TypeSafe’s API. SAP’s EAC guide specifies Node.js 24 or later for the CLI. The mock scenario does not require a live S/4HANA connection, although the deployed agent still needs its platform-provided runtime model access.

Use SAP’s Pro-Code Setup and Anthropic’s Claude Code quickstart for installation and access prerequisites. The EAC CLI installation command in the SAP guide used for this project is:

npm install -g @SAP/joule-work-dev-cli-early-adopters@latest
jl v

This is an EAC installation pattern, not a promise of a permanently available public npm package. Follow the package access instructions for your landscape and check the current SAP guide before installing.

In Joule Studio, open your user settings, go to Develop, and copy the personal Sign-in URL under CLI Authorization. Pass it to the CLI in quotes:

jl login ‘<your Sign-in URL>'
cd supplier-invoice-triage-agent
jl init claude

If the folder does not exist yet, then use:

mkdir supplier-invoice-triage-agent
cd supplier-invoice-triage-agent
jl init claude

jl init claude installs the Joule Studio development skills and creates the MCP configuration used by Claude Code. The coding agent can then work with platform-specific guidance and development tools from this workspace. SAP’s AI Coding Agent documentation explains this integration.

The Jl Cli Prepares The Local Workspace For Joule Studio Development With Claude Code.

 

3.2 Define the business intent and refine the implementation

I used an iterative pro-code workflow. After initializing the workspace, I worked with Claude Code to review and refine the invoice-triage implementation inside the Joule Studio project. The focus was on validating the business behavior, the local test tools, the JEV integration, and the deployable solution structure against the intended design.

The implementation starts from a concise business and technical intent:

Build a Joule Studio agent that triages a blocked supplier invoice. It must retrieve invoice and matching evidence, check recent invoices from the same vendor, and either release the payment block, create an AP-clerk task, or escalate.

Use local test data for the business tools.

Before any release executes:
– reconstruct the relevant invoice and vendor-history evidence;
– evaluate the proposed release with TypeSafe JEV;
– use a Choice decision with allow, confirm, review, or deny;
– and enforce the result in Python so that only an explicit allow can reach the original release tool.

Rather than treating this as a one-shot code-generation exercise, I used Claude Code as a development and review partner. The implementation was inspected against the requirements, with particular attention to the files that define the runtime behavior:

  • app/agent.py
  • app/agent_executor.py
  • app/decision_gates.py
  • app/jev.py
  • app/mcp_providers/agw.py
  • mcp-mock.json
  • asset.yaml
  • solution.yaml

The review focuses on a few concrete questions:

  • Does the agent retrieve the invoice and recent vendor history before proposing a release?
  • Is release_payment_block actually wrapped before the agent receives the tool?
  • Does the wrapper reconstruct evidence independently of the LLM reasoning?
  • Is JEV called with the intended allow, confirm, review, and deny decision space?
  • Do missing, failed, disabled, or invalid JEV decisions hold the release instead of allowing it through?
  • Does only an explicit allow invoke the original release coroutine?

This is the important part of the pro-code workflow: Claude Code can help generate, inspect, and refine the implementation, but the resulting source remains normal application code that can be reviewed against explicit business and technical requirements.

The later sections follow the runtime path in detail and show exactly where these behaviors are implemented.

 
Review this Joule Studio 2.0 invoice-triage agent against the intended implementation below.

Business behavior:
– Triage a blocked supplier invoice using invoice number and fiscal year.
– Retrieve invoice detail and PO / goods-receipt evidence.
– Retrieve recent invoices for the same vendor to identify potential duplicate risk.
– The agent may release the payment block, create an AP-clerk task, or escalate to a manager.

JEV behavior:
– release_payment_block must be protected by a TypeSafe JEV / System One decision gate.
– Before release, reconstruct the relevant invoice and vendor-history evidence independently of the LLM's explanation.
– Ask JEV a Choice question with exactly these alternatives:
allow
confirm
review
deny
– deny must block the release.
– review or confirm must hold the release for human review.
– Missing, failed, disabled, malformed, or invalid JEV decisions must also hold the release.
– Only an explicit allow may invoke the original release_payment_block tool.

Implementation constraints:
– Keep the existing Joule Studio / jl CLI project structure.
– Keep the local test business tools used by this demonstration.
– Do not redesign the architecture unless an actual mismatch is found.
– Preserve SAP/Joule Studio runtime integration code.

Please inspect these files:
– app/agent.py
– app/agent_executor.py
– app/decision_gates.py
– app/jev.py
– app/mcp_providers/agw.py
– mcp-mock.json
– asset.yaml
– solution.yaml

First inspect the implementation and explain whether it satisfies each requirement.
If you find a real mismatch, make only the minimum necessary correction.
If everything already matches, do not change code unnecessarily.

At the end, summarize:
1. files inspected
2. runtime path you verified
3. whether any files were changed
4. whether only an explicit JEV allow reaches release_payment_block

Carlosbasto_0-1790252654118.Png

Carlosbasto_1-1790252698120.Png

Because the implementation had already gone through several rounds of review and refinement before this reproduction exercise, Claude Code did not identify major architectural changes to make. In this step, its role was mainly to validate that the current source still matched the intended business behavior and fail-safe JEV policy.

3.3 Keep the development snapshot reproducible

The development environment used for this implementation was captured with jl 0.1.20-alpha.8, Claude Code 2.1.265, Node.js 26.8.1, and Python 3.14.0. The Joule Studio agent template included in the project is version 1.1.0.

These values describe the development snapshot used for this article. They are not minimum requirements, and they should not be interpreted as the versions used inside the Joule Studio cloud runtime.

The project used throughout the remainder of this walkthrough keeps the following Joule Studio identifiers:

Solution
name: supplierinvoice-triage-agent
version: 1.0.0

Agent asset
name: invoice-triage-jev
version: 1.0.0

Agent template
version: 1.1.0

These values come directly from solution.yaml, asset.yaml, and TEMPLATE_VERSION, and are the identifiers used when validating, building, and deploying the solution later in the walkthrough.

 

4 Invoice triage at the decision boundary

The business scenario used in this experiment is blocked supplier-invoice triage. The agent receives an invoice number and fiscal year, retrieves the invoice and matching evidence, checks the supplier’s recent invoice history, and reasons about whether the case should proceed toward release, require AP-clerk review, or be escalated.

The scenario was designed around an important characteristic of enterprise decision-making: the correct action may not be visible in a single business object. An invoice can look acceptable against its own purchase-order and goods-receipt evidence, while additional context from other transactions changes the risk assessment.

4.1 Why this invoice needs more than a matching check

The example is INV-5500011, fiscal year 2026, for vendor VEND-30012, Nordwind Components GmbH, with a gross amount of USD 8,420. It has a price-variance block, but the variance is 3% against a 5% tolerance. The goods receipt is posted, and both the invoiced and received quantities are 200.

Viewed on its own, the invoice looks like a plausible release candidate. The vendor history adds another fact: INV-5500007, from the same vendor, has the same amount and was posted seven days earlier. That is a reason to investigate a possible duplicate before clearing the block.

Same vendor and amount do not prove duplication. They provide a risk signal for this demonstration. The example lets us inspect how the agent uses cross-document evidence and how the release guard evaluates the action if the agent proposes it.

4.2 Business tools available to the agent

Tool

Role in the scenario

get_blocked_invoices

Find blocked invoices for a company and date range

get_invoice_detail

Retrieve the invoice, block reason, and PO and goods-receipt evidence

get_recent_invoices_for_vendor

Retrieve recent vendor invoices for duplicate-risk analysis

release_payment_block

Clear the block after the JEV guard permits the call

create_clerk_task

Route a case that needs an AP clerk

escalate_to_manager

Escalate a suspected duplicate or other high-severity case

All six tools use local test data in this demonstration. Their names represent the business interface of this solution; they are not being presented as standard SAP APIs.

The Invoice-Level Evidence Looks Acceptable, While Vendor History Introduces A Suspected Duplicate.

 

5 Understand the solution structure

The solution contains one agent asset. solution.yaml references assets/invoice-triage-jev/asset.yaml; the asset directory contains the Python application, dependencies, tests, and mcp-mock.json.

The asset exposes an Agent-to-Agent (A2A) interface. Its container listens on port 5000, while asset.yaml configures startup, liveness, and readiness probes against:

/.well-known/agent-card.json

These files form the platform-facing structure that allows the local pro-code project to later be validated, packaged, and deployed as a Joule Studio solution.

The files worth following are:

app/main.py

SAP AI Core and telemetry initialization, A2A server, and request-token context

app/agent.py

Runtime models, system prompt, agent loop, and conversation memory

app/agent_executor.py

Per-request tool loading, JEV guard installation, and A2A result delivery

app/mcp_providers/agw.py

Local test-tool construction and the Agent Gateway path used outside test mode

app/jev.py

TypeSafe HTTP requests and typed decision helpers

app/decision_gates.py

Execution-time evidence gathering and the release_payment_block wrapper

app/skills/

Invoice-intake and release-policy resources available to the agent

 

5.1 Follow the runtime path

main.py starts the A2A application and establishes the request’s user-token context. For each request, AgentExecutor loads the business tools, adds the skill-resource tools, and installs the JEV guard around release_payment_block. The agent then receives that toolset and runs its reasoning loop.

Normal read operations return business evidence to the agent. If the agent proposes release_payment_block, execution takes a different path. The Python wrapper re-reads the relevant invoice and vendor-history evidence, assembles the decision state, and calls JEV. An explicit allow invokes the original release coroutine; deny returns release_denied, while review, confirm, or the absence of a valid JEV decision returns release_pending_review. The resulting tool status is then returned to the agent so it can explain the outcome or continue the business flow.

4.Png
The release guard sits directly in the runtime tool path. JEV receives application-assembled state through a direct HTTPS call, while Python remains responsible for enforcing the returned decision.

There are also two different integration contexts that should not be confused. The integration created by jl init claude supports the coding agent during development. At runtime, agw.py acts as the business-tool provider: in this demonstration it constructs local tools from mcp-mock.json; outside test mode it discovers MCP tools through SAP Agent Gateway. JEV is independent of both paths and is called separately over HTTPS.

 

6 Implement the agent and its TypeSafe client

The following sections trace the main integration points in the current solution. The excerpts are taken from the actual project, with surrounding code omitted and line wrapping adjusted where necessary for readability. The complete implementation remains available in the corresponding source modules.

6.1 Configure the runtime agent

The runtime agent is configured in app/agent.py.

SAP agent decorators expose selected settings to the Joule Studio configuration model. The primary runtime model is defined with agent_model:

agent_model(
key=”config.model”,
label=”LLM Model”,
description=”The language model powering this agent”,
)
def get_model_name() -> str:
return “sap/anthropic–claude-4.5-sonnet”

The implementation also exposes the model temperature through agent_config:

agent_config(
key=”config.temperature”,
label=”LLM Temperature”,
description=”Controls randomness of responses ”
“(0.0 = deterministic, 1.0 = creative)”,
)
def get_temperature() -> float:
return 0.0

The agent itself is created with LangChain’s create_agent() and uses LangGraph checkpointing for conversation state. Each conversation thread is scoped using both the authenticated user and the A2A context identifier:

config = {
“configurable”: {
“thread_id”: f”{get_user_sub()}:{context_id}”
}
}

The current implementation keeps inactive conversation threads for one hour and introduces history summarization once the conversation exceeds approximately 30,000 tokens. Summarization uses:

sap/anthropic–claude-4.5-haiku

while the main reasoning loop uses Claude Sonnet.

These are implementation choices for this agent, not guarantees that every deployment or future Joule Studio version will use the same model names or runtime settings.

The system prompt establishes the intended business behavior

The system prompt is also defined in agent.py through @prompt_section. For this scenario, several instructions are particularly important. Before proposing a release, the agent must retrieve the invoice detail and check recent invoices from the same vendor:

Gather evidence BEFORE proposing any release:
fetch the invoice detail (…) and check the vendor's
recent invoices for a duplicate.

The prompt also makes the relationship between the agent and the JEV guard explicit:

You may CALL release_payment_block, but it can be
denied or held for human review.

If the tool returns a refusal or hold, relay that to
the requester verbatim — do NOT retry it or claim
the release succeeded.

Finally, the agent may only report a successful release when the tool returns an actual confirmation:

Only claim a block was released if
release_payment_block returns a success payload
with a release confirmation (a release_id).

This distinction is important. The system prompt defines how the agent should behave: which evidence it should collect, when it should propose a release, and how it should communicate the result. The Python guard introduced later defines whether the consequential tool call is actually allowed to execute.

@AGent_model(
key=”config.model”,
label=”LLM Model”,
description=”The language model powering this agent”,
)
…
@agent_config(
key=”config.temperature”,
label=”LLM Temperature”,
description=”Controls randomness of responses (0.0 = deterministic, 1.0 = creative)”,
)
…
def get_system_prompt() -> str:
return “””You are a supplier-invoice triage specialist for an SAP S/4HANA Accounts Payable back office.

Your role is to triage a blocked supplier invoice end-to-end: gather the facts from the available tools, decide whether the payment block should be released, and either release the block through the guarded release tool, route the invoice to an AP clerk, or escalate it to an AP manager.

CORE RULES:
– NEVER fabricate invoice numbers, amounts, PO or goods-receipt data, vendor data, or block-reason text. Always use tools to retrieve the facts.
– Gather evidence BEFORE proposing any release: fetch the invoice detail (header, lines, PO/GR match, vendor master, block reason) and check the vendor's recent invoices for a duplicate.
– The release action (release_payment_block) is STATE-CHANGING — it clears a blocked invoice and makes it eligible for a later payment run, but it does not itself execute payment. It is GUARDED by an external decision layer (JEV). You may CALL release_payment_block, but it can be denied or held for human review. If the tool returns a refusal or hold, relay that to the requester verbatim — do NOT retry it or claim the release succeeded.
– Only claim a block was released if release_payment_block returns a success payload with a release confirmation (a release_id) — never on a hold or denial.
– If the evidence contradicts release (goods not received, invoiced quantity or price outside PO tolerance, a likely duplicate of a recent invoice, or fraud/pressure signals), do NOT release; create an AP-clerk task or escalate to a manager and explain what the evidence shows.
– Relay tool errors verbatim without adding workarounds.
– If a tool returns an empty result, state that explicitly rather than reasoning over missing data.

WORKFLOW:
1. Identify the invoice and its block reason. If the invoice number or fiscal year is missing, ask one targeted clarifying question.
2. Retrieve the invoice detail and its line-level PO / goods-receipt matching facts.
3. Check the vendor's recent invoices for a duplicate (same reference or same amount close in time).
4. Determine whether the evidence justifies release, AP-clerk review, or manager escalation.
5. If release is justified, call release_payment_block with the invoice number, fiscal year, and a short reason. Otherwise call create_clerk_task (ambiguous / needs a human) or escalate_to_manager (fraud / duplicate / high severity).
6. Deliver a clear triage summary: what you found, the decision, the release_id if released or the reason it was held/denied/routed, and the specific evidence for every conclusion. Cite the invoice number.”””

The runtime agent defines its model configuration and business-behavior instructions directly in the Joule Studio pro-code source.

6.2 Load the business tools

The business-tool provider is implemented in app/mcp_providers/agw.py.

get_mcp_tools() selects between two execution paths depending on the IBD_TESTING environment variable:

if os.environ.get(“IBD_TESTING”, “1”) == “1”:
return _build_mock_tools

agw_client = create_client()
mcp_tools = await agw_client.list_mcp_tools(user_token=_get_user_token)

Local/Test Mode

For this demonstration, IBD_TESTING=1 is used.

In that mode, _build_mock_tools() reads mcp-mock.json, converts each tool's JSON input schema into a Pydantic model, and exposes it to the agent as a LangChain StructuredTool.

For example, the tool definition still provides the agent with:

  • a tool name;
  • a natural-language description;
  • a typed argument schema;
  • and an asynchronous coroutine.

The important difference is what happens when that coroutine executes. Instead of calling an external business system, it returns the JSON response configured in mcp-mock.json:

_response = json.dumps(mock_response)

async def _coroutine(
_resp: str = _response,
**kwargs: Any,
) -> str:
return _resp

This keeps the business evidence deterministic and makes the scenario easy to reproduce without an SAP S/4HANA backend.

It also creates an important limitation: these tools simulate interfaces, not a persistent business system. The supplied arguments are validated by the generated schema, but they do not select records from a database. Changing the invoice number in the user prompt therefore does not cause the mock provider to retrieve a different stored invoice, and executing the mocked release_payment_block does not persist a changed invoice state.

For the walkthrough, use the supplied INV-5500011 scenario.

Runtime integration through Agent Gateway

For this public sample, mock mode is enabled by default. If IBD_TESTING is unset or set to a truthy value such as 1, true, yes, or on, the agent loads the local tools defined in mcp-mock.json.

To follow the Agent Gateway path instead, IBD_TESTING must be explicitly set to a non-truthy value such as 0.

agw_client = create_client()

mcp_tools = await agw_client.list_mcp_tools(
user_token=_get_user_token
)

The request's user token is held in a per-request context established by main.py. The discovered MCP tools are then converted into LangChain-compatible tools and supplied to the agent.

Tool invocation also uses that user context:

return await call_mcp_tool_with_retry(
agw_client,
t,
user_token=_get_user_token(),
**kwargs,
)

This is the path intended for connecting the agent to actual business tools through SAP Agent Gateway. A productive setup would additionally require the relevant MCP servers, business-system connectivity, destinations, and user authorizations; simply removing IBD_TESTING=1 does not create those integrations.

One distinction is worth keeping in mind throughout the walkthrough:

IBD_TESTING=1
↓
changes the source of the business tools

It does NOT disable:
– the agent's SAP-bound runtime LLM
– the TypeSafe JEV HTTPS integration

The business tools are local in this demonstration. JEV remains an independent external decision call, and the agent model continues to use its platform-provided runtime binding.

6.3 Send state and a typed question to JEV

The TypeSafe integration is implemented in app/jev.py.

This project deliberately uses the TypeSafe HTTP API directly through httpx rather than introducing a TypeSafe SDK dependency. TypeSafe documents a single System One evaluation endpoint:

POST https://api.typesafe.ai/v1/systemone

The request is authenticated with a bearer API key and contains three main elements:

  • state — the information to evaluate;
  • model — the System One model to use;
  • questions— one or more typed judgments to evaluate against that state.

 

This follows TypeSafe’s official HTTP API reference, which defines the same state, model, and questions request structure. The project sends that request in _system_one():

payload: dict[str, Any] = {
“state”: state,
“model”: model or _default_model(),
“questions”: questions,
}

async with httpx.AsyncClient(
timeout=_timeout_seconds()
) as client:
resp = await client.post(
url,
json=payload,
headers=headers,
)
resp.raise_for_status()
body = resp.json()

The default model is:

jev-latest

and the default request timeout is 15 seconds.

The API key is resolved first from:

TYPESAFE_API_KEY

with JEV_API_KEY retained as a compatibility alias. The fallback value inside the source is intentionally empty so a credential does not need to be embedded in the project.

TypeSafe currently documents jev-latest as an alias for the latest stable official Jev release, rather than a permanently pinned model version. Because an alias can move when a new model release becomes available, the exact model behind the same source code may change over time. TypeSafe recommends pinning a versioned model ID when behavior or calibrated thresholds must remain tied to a specific model revision.

For more detail, see TypeSafe’s Models documentation.

Building a typed Choice question

The current release guard needs one constrained judgment, so the project uses TypeSafe’s Choice primitive.

According to TypeSafe, a Choice question selects one option from a set defined by the application and returns the selected option together with its probability distribution and confidence.

The helper _ask_choice() creates that question:

questions = {
_DECISION_KEY: {
“type”: “choice”,
“instructions”: instructions,
“criteria”: criteria,
}
}

answers = await _system_one(
state,
questions,
model=model,
)

return _read_choice(answers)

_DECISION_KEY is defined as:

_DECISION_KEY = “decision”

This key is not itself part of the model’s reasoning. It is the application-defined identifier that links a question to its corresponding answer.

TypeSafe’s API returns each answer under the same key supplied in questions.

The official API documentation explicitly describes this question-ID-to-answer-ID relationship.

Define the release decision space

For the payment-block release, the project’s tool_guard() helper first assembles a structured state describing the proposed action and its business context.

The state may contain: tool action arguments_summary side_effects safeguards policy reversibility.

It then asks one focused Choice question:

return await _ask_choice(
state,
instructions=(
“Should this consequential tool call ”
“be allowed to run?”
),
criteria={
“allow”:
“Safe and justified — run as-is.”,

“confirm”:
“Run only after explicit human confirmation.”,

“review”:
“Hold for human review before running.”,

“deny”:
“Do not run — unsafe or against policy.”,
},
)

This is the important boundary between the TypeSafe API and the application-specific implementation.

tool_guard() is not a TypeSafe primitive or API endpoint. It is a helper created by this project to express a particular business decision using TypeSafe’s standard Choice primitive.

TypeSafe receives a normal System One request consisting of structured state and a typed Choice question. The names allow, confirm, review, and deny are options defined by this application; they are not built-in TypeSafe verdicts.

For the underlying Choice contract, see TypeSafe’s API reference — Choice and Introduction to System One primitives. TypeSafe’s documentation emphasizes that the application defines the possible outputs in advance and then consumes the resulting typed value in code.

Keep API failure separate from business policy

One final detail is important for understanding the next section.

_system_one() does not throw TypeSafe failures into the agent flow. A missing key, HTTP error, timeout, malformed response, or missing answers map is logged and returned to the caller as None.

For example:

try:
async with httpx.AsyncClient(
timeout=_timeout_seconds()
) as client:
resp = await client.post(
url,
json=payload,
headers=headers,
)
resp.raise_for_status()
body = resp.json()

except Exception as exc:
logger.warning(
“JEV: call to %s failed: %s”,
_SYSTEMONE_PATH,
exc,
)
return None

This client layer therefore does not decide whether a missing JEV response should permit or block the business action. It simply reports valid typed decision OR no decision.

The release wrapper in decision_gates.py defines the business policy for that second case. As we will see next, no decision is never interpreted as approval to release the invoice.

 

6.4 Normalize the response before using it

The System One response contains an answers map keyed by the same question identifiers supplied in the request. For a Choice question, TypeSafe returns the selected choice, the full probability distribution, and a derived confidence value.

In this project, _read_choice() converts that API response into a smaller internal object that the rest of the application can consume:

def _read_choice(
answers: dict[str, Any] | None,
key: str = _DECISION_KEY,
) -> dict[str, Any] | None:
if not isinstance(answers, dict):
return None

ans = answers.get(key)
if not isinstance(ans, dict):
return None

choice = ans.get(“choice”)
if not isinstance(choice, str) or not choice:
return None

out: dict[str, Any] = {
“decision”: choice
}

if “confidence” in ans:
out[“confidence”] = ans[“confidence”]

if “probabilities” in ans:
out[“probabilities”] = ans[“probabilities”]

return out

The normalization is intentionally small. The external field choice becomes the application field decision while confidence and probabilities are preserved when TypeSafe returns them.

This keeps the TypeSafe response format isolated inside jev.py. The release logic in decision_gates.py does not need to understand the complete HTTP response; it only consumes the normalized decision object.

TypeSafe documents choice as the highest-probability option, with probabilities containing the distribution across all defined alternatives and confidence derived from that distribution. See the official TypeSafe HTTP API reference and Confidence documentation.

Invalid responses do not become approvals

The client also normalizes failure into the absence of a decision.

Transport errors, HTTP failures, invalid JSON, missing answers, or a malformed Choice answer ultimately result in None.

There is an important separation of responsibilities here: jev.py -> Can I obtain a valid typed decision? -> decision object OR None decision_gates.py -> What is the business consequence?

_read_choice() itself does not decide whether the invoice may be released. It also does not attempt to enforce that the returned string belongs to this project's expected allow | confirm | review | deny set. It only validates that a non-empty Choice value exists.

The execution policy is intentionally handled later by the release wrapper. In that layer:

  • allow can proceed;
  • deny is rejected;
  • review and confirm are held;
  • None is held;
  • and any unexpected decision value is also held.

This makes the safety property straightforward:

Normalization can produce a decision, but normalization can never authorize the release by itself. Only the execution policy in decision_gates.py can do that.

One additional point is worth noting: although this project preserves TypeSafe's confidence and probabilities, the current release boundary does not use a numerical confidence threshold. The decision value itself controls the branch. We will return to that distinction later when reviewing the implemented confidence policy.

7 Enforce the decision at the release tool

 

7.1 Install the wrapper before the agent runs

The JEV integration becomes operational in app/agent_executor.py.

For each request, the executor first loads the business tools and then adds the skill-resource tools:

tools = [*tools, *self.skill_tools]

Before that toolset is passed to the agent, the release guard is installed:

# Require JEV approval before clearing an invoice for payment.
# Disabled, failed, or missing decisions hold the release for review.
tools = decision_gates.install_release_guard(tools)

The agent does not receive the original release_payment_block tool directly. install_release_guard() first creates a lookup of the complete toolset and replaces only the tool whose name matches:

RELEASE_TOOL_NAME = “release_payment_block”

 

The implementation is intentionally small:

def install_release_guard(
tools: list[BaseTool]
) -> list[BaseTool]:
by_name = {t.name: t for t in tools}

return [
guard_release_tool(t, by_name)
for t in tools
]

Every unrelated tool is returned unchanged. The release tool, however, is replaced by a new StructuredTool that preserves the same:

  • name;
  • description;
  • argument schema;

while substituting the callable coroutine with the guarded implementation.

The wrapper retains a reference to the original release coroutine:

original_coroutine = tool.coroutine

and exposes the guarded version under the same tool identity:

return StructuredTool(
name=tool.name,
description=tool.description,
args_schema=tool.args_schema,
coroutine=_guarded,
handle_tool_error=True,
)

From the agent's perspective, the business capability is still called release_payment_block.

but the execution path has changed:

5.Png

 

This is the key enforcement property of the implementation.

The system prompt can instruct the LLM to behave carefully, but this wrapper creates a software boundary around the consequential action. If the agent chooses release_payment_block, that same named tool now resolves to the guarded implementation.

The protection is deliberately scoped. It applies to this specific tool path; it is not a platform-wide authorization mechanism for every possible write operation in the agent.

That means any additional consequential tool introduced later would need its own corresponding control if the same policy is required.

7.2 Re-read the evidence at execution time

The guard does not rely only on the evidence the agent already retrieved or on the explanation it generated when proposing the release.

Before asking JEV, _gather_risk_context() independently calls the sibling get_invoice_detail and get_recent_invoices_for_vendor tool coroutines again. This reconstructs the decision context at the execution boundary, immediately before release_payment_block could run.

From the invoice detail, the guard extracts evidence such as:

  • goods-receipt status;
  • invoice match status;
  • price variance;
  • invoice amount; and
  • vendor identity.

It then inspects the vendor's recent invoices for an additional duplicate-risk signal.

The current duplicate check looks for another invoice from the same vendor with a different invoice number but the same amount:

same_amount = [
inv
for inv in invoices
if isinstance(inv, dict)
and str(inv.get(“invoice_number”)) != str(invoice_number)
and amount is not None
and str(inv.get(“gross_amount”) or inv.get(“amount”))
== str(amount)
]

For the supplied fixture, this identifies INV-5500007, which has the same amount as INV-5500011.

When such a match exists, the guard adds a suspected-duplicate signal to the context sent to JEV. It also changes the reversibility description to: hard_to_recover_after_payment.

This does not mean that clearing the payment block executes a payment. The release only makes the invoice eligible for a later payment process. The label describes the downstream business risk: if a suspected duplicate is eventually paid, recovering the funds may be difficult.

This is a risk signal, not a duplicate detector

The current comparison is intentionally simple.

It compares string representations of invoice amounts and does not:

  • normalize monetary values;
  • verify that currencies match;
  • compare supplier invoice references;
  • inspect invoice dates or purchase-order references;
  • calculate similarity across line items; or
  • establish that two invoices are actually duplicates.

The result should therefore be interpreted as: same vendor + different invoice number + same amount = possible duplicate signal NOT confirmed duplicate.

That limitation is deliberate for this demonstration. A production duplicate-invoice control would require substantially stronger evidence.

Build structured decision state

Once _gather_risk_context() has assembled the available evidence, the wrapper calls jev.tool_guard() with the proposed action and its surrounding business context.

The JEV helper preserves those inputs as named fields:

state = {
“tool”: tool,
“action”: action,
}

if arguments_summary:
state[“arguments_summary”] = arguments_summary

if side_effects:
state[“side_effects”] = side_effects

if safeguards:
state[“safeguards”] = safeguards

if policy:
state[“policy”] = policy

if reversibility:
state[“reversibility”] = reversibility

That distinction is important: the application determines which facts form the decision state, while JEV evaluates the typed question against that state.

A note about the safeguards field

The current helper parameter is named safeguards, but in this implementation it carries more than protections already in place. It may also include relevant risk evidence, such as the suspected duplicate.

So the name should be understood as an implementation detail of the current client rather than a statement that every item in that field reduces risk.

Evidence gathering is currently best-effort

There is one important limitation in the current implementation.

The evidence lookups are best-effort. If one of the supporting tool calls fails, _gather_risk_context() logs the problem and JEV may receive a reduced decision state.

The current code does not independently fail closed merely because one specific evidence field is unavailable.

For this experiment, that behavior keeps the guard focused on evaluating the available context. For a production release policy, I would make mandatory evidence explicit—for example, requiring invoice detail and duplicate-risk evidence to be successfully retrieved before an automatic release can even be considered.

7.3 Require an explicit allow

The release wrapper follows a deliberately conservative rule:

Only an explicit JEV allow can invoke the original release_payment_block tool.

Before evaluating a decision, the wrapper already holds the release if JEV is unavailable or disabled, or if the underlying release coroutine cannot be executed.

When JEV returns a decision, the result is handled with ordinary Python branches. deny, review, and confirm are handled explicitly. The final check then catches everything else:

# A failed call, absent answer, or unexpected verdict must never
# authorize release. Only an explicit `allow` reaches the tool.
if decision != “allow”:
logger.info(
“JEV.tool_guard: HOLD (missing or invalid verdict %r) ”
“release_payment_block invoice=%s”,
decision,
invoice_number,
)

return json.dumps(
{
“status”: “release_pending_review”,
“invoice_number”: invoice_number,
“reason”: guidance or (
“Release held for human review ”
“(decision layer returned no clear approval).”
),
}
)

The earlier branches distinguish the expected non-approval outcomes:

  • deny → release_denied
  • review → release_pending_review
  • confirm → release_pending_review

Only after those branches, and after the final decision != “allow” check has been passed, can execution reach:

await original_coroutine(**kwargs)

The resulting policy is:

JEV outcome or runtime condition Wrapper behavior
allowInvoke the original release_payment_block coroutine
denyReturn release_denied; do not invoke release
reviewReturn release_pending_review; do not invoke release
confirmReturn release_pending_review; do not invoke release
TypeSafe API key missing or JEV disabledHold the release
JEV request failsHold the release
Missing or malformed answerHold the release
Unexpected decision valueHold the release
Original release coroutine unavailableHold the release

That distinction matters because release_pending_review is only a tool result. It does not mean that a human approval workflow has automatically been created.

The agent may subsequently call create_clerk_task or escalate_to_manager, but the release guard itself does not assign a reviewer, wait for approval, or automatically resume the original release after someone approves the case.

Implementing a full human-in-the-loop approval cycle would require additional workflow state, an assigned reviewer, an approval action, and a controlled mechanism for resuming or re-requesting the release.

7.4 Understand the confidence policy actually implemented

JEV's Choice response may include both confidence and the probability distribution across the available alternatives. As shown earlier, jev.py preserves those values when normalizing the response.

The current release guard, however, does not use them to authorize the action.

Its execution policy depends only on the normalized decision:

decision = verdict.get(“decision”)

and ultimately:

if decision != “allow”:
# hold the release

There is therefore no rule such as: allow + confidence >= 0.90 in the running release path.

An allow is currently treated as an allow regardless of the returned confidence value. Adding a confidence threshold would be a separate business-policy decision, not merely a technical improvement.

But a threshold such as 0.80, 0.90, or 0.95 should not be chosen arbitrarily. It would need to be evaluated against representative invoice cases and the cost of false approvals versus unnecessary human reviews.

This is especially important because TypeSafe's confidence is useful as an uncertainty signal, but it does not guarantee that an individual judgment is correct.

Do not confuse experimental helpers with the live execution path

A fuller development version of this project also included experimental orchestration helpers such as evaluate_case() and triage_invoice(). These helpers explored broader decision patterns, including confidence-based routing, but they are not part of the deployed runtime path and are not included in the public repository.

The public sample intentionally focuses on the two JEV integration points demonstrated in this article: the guarded release_payment_block action and the advisory completion check.

Those functions are useful examples of how richer decision composition could be implemented, but they are not called by the running AgentExecutor.

They should therefore not be represented as additional stages that every invoice currently passes through.

Confidence is retained for observability and future policy evolution, but it is not currently part of the release authorization rule.

 

8 Testing and deploying the solution

8.1 Check the guard before deploying

Before packaging or deploying the solution, the release boundary can be tested locally without calling a real invoice backend or relying on a live JEV decision.

The current project contains two groups of tests:

prebuilt_tests/
tests/

prebuilt_tests/ covers the generated agent structure and local A2A server behavior.

The JEV-specific regression tests are implemented in tests/test_decision_gates.py.

These tests exercise the Python control flow around the release boundary. The JEV calls are replaced with test doubles so the relevant decision branches can be verified deterministically without a live model.

The test cases cover behavior such as:

  • allow invoking the original release coroutine;
  • deny preventing the release;
  • review and confirm holding the release;
  • disabled JEV holding the release;
  • missing or malformed decisions holding the release;
  • unexpected decision values holding the release;
  • timeout and request failures holding the release;
  • preservation of tools that are not release_payment_block.

They also verify the central invariant established earlier: Only an explicit allow can reach the original release coroutine.

Prepare the test environment

From a fresh Python environment, install both the runtime and test dependencies:

cd assets/invoice-triage-jev

python -m pip install \
-r requirements.txt \
-r requirements-test.txt

Installing the test requirements matters because pytest.ini enables plugins such as pytest-timeout and coverage reporting.

Then run the project test suite:

IBD_TESTING=1 python -m pytest

IBD_TESTING=1 keeps the business-tool provider on the bundled local test tools rather than discovering runtime MCP tools through Agent Gateway.

In the environment used for this walkthrough, the development snapshot used for the deployed experiment completed successfully with 63 passed, 1 warning in 34.46s. The sanitized public repository linked with this article contains 60 tests because experimental helpers that were not part of the demonstrated runtime path were removed before publication.

The Current Revision Completes 63 Tests Successfully. The Single Warning Is A Python 3.14/Pydantic V1 Compatibility Warning And Does Not Represent A Failed Test.

The focused tests in tests/test_decision_gates.py do not use a live LLM or live JEV service. They monkeypatch the decision layer and, for failure-path tests, replace the HTTP transport so timeout, HTTP-error, malformed-response, and missing-answer behavior can be reproduced deterministically.

A successful test run verifies the Python enforcement logic implemented by this revision. It does not establish that:

  • JEV will make the correct business judgment on real invoices;
  • Agent Gateway connectivity is correctly configured;
  • a productive SAP backend is reachable;
  • the deployed Joule Studio runtime behaves identically; or
  • the solution has been successfully deployed.

Those require the runtime and deployment checks in the following sections.

8.2 Supply the runtime configuration

The demonstration uses environment-based configuration to control the local business-tool path and the TypeSafe integration.

Setting Role in this demonstration

IBD_TESTING=1Use the six business tools bundled in mcp-mock.json instead of discovering runtime MCP tools through SAP Agent Gateway
TYPESAFE_API_KEYSupply the TypeSafe bearer credential used by the JEV client
TYPESAFE_ENABLEDOptional explicit enable/disable switch for JEV
TYPESAFE_DEFAULT_MODELOptional model override; the source defaults to jev-latest
TYPESAFE_TIMEOUT_SECONDSOptional TypeSafe request timeout; the source defaults to 15 seconds

The TypeSafe credential should be supplied through the secure runtime configuration available in the deployment environment. It should never be committed to jev.py, stored in the repository, or exposed in screenshots.

JEV enablement

The client uses a simple enablement policy.

The primary credential is TYPESAFE_API_KEY with JEV_API_KEY supported as a compatibility alias. If no API key is available, JEV is considered disabled. If a key is available and no explicit enablement flag has been configured, JEV enables automatically. The primary switch is TYPESAFE_ENABLED with JEV_ENABLED supported as its compatibility alias.

This matters because disabling JEV does not make the release path fail open. As described in Section 7, an unavailable or disabled decision layer causes release_payment_block to return a hold status rather than invoking the underlying release coroutine.

Test mode only changes the business-tool source

IBD_TESTING=1 should not be interpreted as an offline mode for the whole agent.

The agent's reasoning model remains provided through its SAP runtime model binding. If JEV is enabled and a valid TypeSafe API key is configured, the decision checks still make external HTTPS calls to TypeSafe.

Compatibility aliases

For compatibility, the current client also accepts:

Primary setting Compatibility alias
TYPESAFE_API_KEYJEV_API_KEY
TYPESAFE_ENABLEDJEV_ENABLED
TYPESAFE_TIMEOUT_SECONDSJEV_TIMEOUT_SECONDS

The article uses the TYPESAFE_* names because they are the primary configuration names in the current implementation.

8.3 Validate and build from the solution root

Once the tests pass, return to the directory containing solution.yaml and validate the Joule Studio solution:

jl solution validate

Carlosbasto_4-1790270099918.Png

SAP documents validation as a local structural check performed before building. It verifies that solution.yaml exists, that referenced assets resolve correctly, and that each asset passes its plugin-specific validation rules.

Then build the deployable solution archive:

jl solution build

Carlosbasto_3-1790270078509.Png

For the revision used in this walkthrough, validation completed successfully and the build produced: build/supplier-invoice-triage-agent-1.0.0.zip.

This ZIP is the artifact used for the Joule Studio deployment shown in the next section. Keeping the test, validation, build, and deployment artifacts aligned is important: the package imported into Joule Studio should come from the same source revision that was reviewed and tested locally.

8.4 Import and deploy the validated build in Joule Studio

SAP also documents deployment through the jl CLI. In the EAC landscape used for this experiment, however, both deployment approaches reached the backend and returned the same HTTP 400 response:

Deploy failed (400):
Element “ID” does not contain a valid UUID

This happened both when deploying the previously built archive: jl solution deploy ./build/supplier-invoice-triage-agent-1.0.0.zip and when running the complete CLI deployment flow: jl solution deploy.

The source itself had already passed Joule Studio validation and had been packaged successfully. A new local solution UUID was also generated before retrying, but the backend deployment response remained unchanged. I therefore used the Joule Studio web workflow for the revision demonstrated in this article.

The locally built archive:

supplierinvoice-triage-agent-1.0.0.zip

was imported into Joule Studio. After import, the complete solution source was available through the normal Solution experience, including solution.yaml, the agent asset, the Python implementation, tests, and the JEV integration.

The Locally Validated Solution Is Imported Into Joule Studio, Inspected In Code View, And Deployed Through The Web Workflow.

 Before deployment, I used the Code view to confirm that the imported package contained the same source revision that had just been tested and built locally. The Deploy action in the Solution page was then used to start the deployment from Joule Studio Web.

 

Joule Studio provides three views that are particularly useful after import:

  • Code exposes the source files contained in the imported solution.
  • Design exposes the agent configuration.
  • Try lets you interact with the deployed agent and inspect its execution traces and logs.

The deployment configuration must also provide the runtime values described earlier. For this demonstration, IBD_TESTING=1 keeps the business-tool path on the bundled test data, while TYPESAFE_API_KEY enables the live JEV decision calls. The TypeSafe credential should remain in secure runtime configuration and must not appear in source code or screenshots.

Deploy the imported solution from Joule Studio Web

After importing supplier-invoice-triage-agent-1.0.0.zip, I completed the deployment from the Joule Studio browser experience:

  1. Verify the imported source. Open the agent in Code and confirm that the expected files are present, particularly app/decision_gates.py, app/jev.py, asset.yaml, and mcp-mock.json. This ensures that the solution about to be deployed is the same revision that was tested and packaged locally.
  2. Provide the runtime configuration. Before running the scenario, make sure the deployment uses the configuration described earlier in this article. For this demonstration, IBD_TESTING=1 selects the bundled business-tool fixtures and TYPESAFE_API_KEY enables the live JEV calls. The credential must remain in secure runtime configuration rather than in the source code.
  3. Start the deployment. From the Solution page, select Deploy in the upper-right corner. Joule Studio asks for the target deployment environment; select the appropriate development environment and choose Deploy again. Joule Studio then validates, builds, provisions the runtime, and checks the deployment status.

Joule Studio confirms the imported development snapshot with 63 tests passed, 0 failed, and a 100% validation score, providing an additional check before deployment. The sanitized public repository linked with this article contains 60 tests after the removal of experimental helpers that were not part of the demonstrated runtime path.

Carlosbasto_7-1790270591422.Png

Verify that the deployment completed. While deployment is running, Joule Studio shows a Integrating… state. A successful deployment changes this to Deployed. The Deployment page can also be used to confirm the target environment, status, timestamp, deployment logs, and runtime endpoint.

The Locally Built Solution Archive Is Imported And Deployed Through The Joule Studio Web Experience.

 

9 Run the invoice scenario and inspect the decision path

With the validated ZIP imported and deployed in Joule Studio, the final step is to exercise the agent through the same runtime that executes the JEV release guard.

The objective of this test is not to force a particular JEV answer. It is to observe how the deployed agent gathers evidence, whether it proposes the consequential release_payment_block action, and how the application translates the JEV decision into deterministic runtime behavior.

9.1 Start with a normal business request

Open the deployed agent in Try and submit:

Triage blocked invoice INV-5500011 for fiscal year 2026 and release the payment block if it is justified.

The prompt gives the agent a business objective without prescribing the conclusion.

In the deployed run used for this article, the agent gathered the expected invoice evidence. The goods receipt was posted for the full quantity, the 3% price variance remained within the configured 5% tolerance, and the three-way match was reported as within_tolerance.

The broader vendor history changed the result. The agent identified INV-5500007, posted seven days earlier for the same vendor and the same USD 8,420 amount, as a potential duplicate-risk signal.

Rather than attempting release_payment_block, the agent retained the block and escalated the case to a manager:

Status: ESCALATED TO MANAGER
Escalation ID: ESC-9032

The final response explains that the matching evidence alone would not justify ignoring the cross-document risk. It recommends management verification before any release is authorized.

Carlosbasto_9-1790272328080.Png

This is an important outcome for the architecture: the agent can recognize sufficient risk during its own reasoning and avoid proposing the consequential action altogether. In this execution, the release_payment_block guard therefore does not need to authorize or reject a release unless the trace shows that the release tool was actually requested.

9.2 Inspect the actual tool path

The Traces view confirms how the deployed agent reached the escalation shown in the previous section.

For the INV-5500011 request, the observed tool sequence was get_invoice_detail -> get_recent_invoices_for_vendor -> escalate_to_manager.

Carlosbasto_10-1790272562873.Png

The agent first retrieved the invoice-level evidence, then inspected the vendor's recent invoice history. After identifying the same-vendor, same-amount invoice as a duplicate-risk signal, it invoked escalate_to_manager.

Importantly, release_payment_block does not appear in this trace.

This means that the agent resolved the case during its own reasoning and never proposed the consequential release action. As a result, the execution-time wrapper around release_payment_block was not entered and JEV.tool_guard was not called during this particular run.

That distinction is important. JEV is not inserted into every reasoning step of the agent. In the current implementation, the release guard is attached specifically to the consequential release_payment_block tool. It becomes active only if the agent attempts that action.

This is itself a valid outcome of the architecture: the reasoning layer can avoid a risky action before the enforcement boundary is reached, while the JEV guard remains available if a release is subsequently proposed.

9.3 Continue the case after manager review

The first run ended with escalation ESC-9032, so I continued the same business case with a second request:

Continue case ESC-9032. The AP manager has completed the review and confirmed that INV-5500007 is a legitimate separate invoice, not a duplicate of INV-5500011, and has explicitly authorized release. Re-check the required invoice detail and vendor history, then call release_payment_block exactly once for INV-5500011, fiscal year 2026. If the release guard holds or denies the action, stop and report the exact tool result without retrying or escalating again.

This follow-up deliberately represents a realistic next step in the process: the suspected duplicate has been reviewed by a manager and the agent now has explicit authorization to proceed.

The agent re-checked the invoice evidence and acknowledged the manager's conclusion that INV-5500007 was a legitimate separate transaction. It then attempted release_payment_block exactly once.

The final response reported:

Carlosbasto_0-1790275203799.Png

The invoice therefore remained blocked despite the manager authorization and otherwise acceptable invoice-level evidence.

9.4 Inspect what actually happened at the release boundary

The Trace for this second execution confirms that the protected action was genuinely reached.

The observed tool sequence was get_invoice_detail, get_recent_invoices_for_vendor and release_payment_block.  Unlike the first run, release_payment_block now appears explicitly in the trace.

The tool result is equally important. Joule Studio shows the wrapped release_payment_block call completing with this business result:

{
“status”: “release_denied”,
“invoice_number”: “INV-5500011”,
“reason”: “Release denied by the decision layer.”
}

Carlosbasto_1-1790275313580.Png

This confirms that the agent did not merely describe a denial in natural language. The protected tool itself returned the typed release_denied result.

The Trace UI may display the technical tool invocation as successful because the wrapper executed correctly. That does not mean the payment block was released. The business result returned by the tool is release_denied.

9.5 Confirm the JEV verdict in the runtime logs

The runtime Logs provide the strongest evidence of the JEV enforcement step.

Before making the decision, the wrapper logged:

 
JEV.tool_guard: evaluating release_payment_block
invoice=INV-5500011 with 3 risk signal(s),
reversibility=hard_to_recover_after_payment

The following entry records the actual verdict:

 
JEV.tool_guard: DENY release_payment_block invoice=INV-5500011

This is the execution-time boundary the implementation was designed to demonstrate.

Even though the manager authorization was included in the agent's second request, the wrapper independently reconstructed the risk context before asking JEV. The decision layer still saw three risk signals and classified the action as hard_to_recover_after_payment.

Carlosbasto_2-1790275386509.Png

JEV then returned DENY, and the Python wrapper translated that verdict into release_denied. The original release operation was therefore not invoked.

9.6 Inspect the advisory completion result

The same log also shows the separate completion evaluation that runs after the agent finishes its work:

 
JEV.completion: decision=complete confidence=0.22 guidance=None

This result serves a different purpose from JEV.tool_guard.

JEV.tool_guard directly influenced execution by preventing release_payment_block from reaching the original release coroutine. JEV.completion, in contrast, evaluated the finished response and classified it as complete.

The reported confidence was only 0.22, but the current implementation does not use completion confidence to retry, repair, or block the response. The result is logged for observability and the final A2A response is still returned normally.

This run therefore demonstrates both JEV integration points in the deployed implementation: an enforcement decision at the consequential tool boundary and a separate advisory assessment after the agent completes its response.

Caption: After the release is denied, JEV separately evaluates the completed agent response; this completion verdict is advisory and does not alter the delivered answer.

This version uses each screenshot for exactly what it proves: Try = business outcome, Trace = actual tool invocation/result, Logs = actual JEV decision and completion verdict.

 

10 What I would evaluate next

The next step is to evaluate the complete decision path against a broader set of representative invoice cases: genuine duplicates, legitimate repeat purchases, incomplete goods receipts, different currencies, missing evidence, borderline tolerances, and external-service failures.

That evaluation should measure more than the JEV verdict itself. As the deployed scenario demonstrated, the agent may detect enough risk during its own reasoning to escalate the case without ever attempting release_payment_block. Other cases may reach the protected tool and therefore exercise the JEV decision boundary. Both paths need to be tested independently.

The evidence model should also become more explicit. The current duplicate signal intentionally uses a simple same-vendor, same-amount comparison. A production implementation should consider additional evidence such as currency, invoice references, dates, purchase orders, quantities, and vendor-specific patterns, while defining which evidence is mandatory before an automatic release can even be considered.

I would also evaluate whether an explicit JEV allow is sufficient for a financial action or whether autonomous release should additionally require a risk-dependent confidence threshold. The current implementation preserves JEV confidence and probabilities but does not use them to authorize the release.

The review path needs similar hardening. release_pending_review currently represents a typed hold, not a complete approval workflow. A production design would need an assigned reviewer, retained evidence, reviewer identity, a controlled approval action, segregation-of-duties rules, and an auditable continuation path.

The advisory completion check also deserves separate evaluation. In one deployed run it returned:

JEV.completion: decision=incomplete confidence=0.82

That result was recorded but did not change the response. Before keeping this check on the critical response path, I would determine whether it should trigger verification or repair, move to asynchronous observability, or remain informational only.

Finally, any use of real invoice, vendor, purchase-order, or user data would require an approved integration and data-handling design before that information is sent to an external decision service.

These are extensions to the demonstrated implementation. Its current enforcement rule remains intentionally narrow: the named release_payment_block tool can execute only after an explicit JEV allow; disabled, failed, missing, or invalid JEV decisions hold the action.

The broader architectural principle is equally important: deterministic facts should remain deterministic. Price-variance arithmetic, quantity comparisons, mandatory fields, and hard policy rules belong in normal application code. The generative agent handles open-ended reasoning and orchestration, while a typed decision layer is most useful for bounded judgments at clearly defined points of consequence.

 

11 Closing thoughts

Joule Studio’s pro-code workflow made this experiment possible inside an implementation that remains inspectable and extensible. Claude Code supported the development work, the jl CLI provided the local validation and packaging workflow, and Joule Studio Web provided the managed runtime used to test the deployed agent. Most importantly, ordinary Python gave us a precise place to introduce an additional decision layer around a consequential business action.

The most interesting characteristic of JEV in this architecture is its software-facing contract. The application supplies structured state, defines a bounded set of possible judgments, receives a typed result, and then decides in normal code what that result is allowed to do. This is fundamentally different from simply adding another instruction to the agent prompt.

The deployed invoice scenario also illustrated an important separation of responsibilities. The generative agent gathered evidence, recognized the suspected duplicate, and escalated the case before attempting the protected release action. The JEV guard remains attached to the consequential release_payment_block boundary for cases that do reach it. In other words, reasoning can prevent an unsafe action early, while the application still retains an explicit enforcement point before that action can execute.

That separation is the main takeaway from this experiment:7.Png

System One models and JEV are still emerging, and this experiment should not be read as evidence that every enterprise decision should be delegated to another model. The more useful question is where a bounded, typed judgment can strengthen an existing software control without replacing deterministic business rules.

For enterprise agents, that leads to a broader design principle: the important question is not only whether an AI agent can perform an action, but what explicit software boundary must be crossed before the system is allowed to perform it.

That is where I see the most interesting potential for combining Joule Studio’s agentic capabilities with decision models such as JEV.

 

Further References

The SAP references below correspond to the EAC documentation used for this implementation. Access and content can vary with the product phase. The TypeSafe links describe the current concepts and API; the repository remains the source for this agent’s actual behavior.

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *

Chat with us on WhatsApp!