Skip to content

feat(agent): Java Agent builder + durable java_tool worker (Track 9 Phase B)#9

Merged
sunilp merged 7 commits into
mainfrom
feat/adk-track-9b-java-agent
Jun 28, 2026
Merged

feat(agent): Java Agent builder + durable java_tool worker (Track 9 Phase B)#9
sunilp merged 7 commits into
mainfrom
feat/adk-track-9b-java-agent

Conversation

@sunilp

@sunilp sunilp commented Jun 28, 2026

Copy link
Copy Markdown
Member

What

Track 9 Phase B: first-class Java authoring of a governed, durable agent. A new jamjet-agent module gives a JVM developer a fluent Agent builder (model + @Tool + instructions + governance) that compiles to the SAME agent-loop IR the Python ADK builds and runs on the Rust engine over HTTP, with Java @Tool methods executing durably exactly-once via a net-new Java tool-worker draining the java_tool queue.

  • dev.jamjet.agent.Agent builder + @Tool annotation + a ToolRegistry (OpenAI-format tool schemas). agent.runDurable(prompt) compiles the IR, submits it, polls to a terminal state, and extracts the answer into an AgentResult.
  • AgentIrCompiler emits the static-unroll agent-loop WorkflowIr (Model with tool schemas, a tool-gate condition, a JavaFn tool node, looping to the next Model) with start_node always a Model node. Governance knobs compile into the IR policy / token_budget / cost_budget_usd / data_policy fields the engine enforces.
  • JavaToolWorker drains java_tool: claim, heartbeat (threading the lease fence), resolve the @Tool via the registry, invoke it, and complete with the lease fence.
  • jamjet-runtime-core gains a NodeKind.JavaFn + QueueType.JAVA_TOOL (matching the engine), a tools field on the Model node, and a serde-leak fix.
  • A CalculatorAgentExample shows the idiomatic Java authoring and runs the full governed durable loop end to end.

The model side is free: a Java agent emits Model nodes and the engine routes model calls through the governed Python sidecar, so a Java agent inherits the same budget / policy / PII / audit governance as Python.

Engine parity

The Java-emitted IR is proven against a Python-generated golden IR: same node graph, edges, gate condition, Model tool schemas (byte-identical), governance fields, and timeouts, with the only intended difference being java_fn vs python_fn tool nodes. So a Java agent runs the same graph the engine already runs for Python.

Safety

The durable worker is enforcement-adjacent, so it got a dedicated adversarial review plus the whole-branch review. Both confirmed: a stale or reclaimed worker cannot duplicate a completion (it completes with the lease fence, which the engine rejects with a 409 it treats as lost-lease, never failing the reclaimed item; a lost heartbeat aborts the in-flight tool); and tool dispatch is gated to declared @Tool methods through the registry, never Class.forName on a payload field, so a forged tool name cannot invoke arbitrary code. The whole-branch review also cross-checked the transport bodies and the result extraction against the engine routes and the Python client, and confirmed governance is not silently dropped.

Tests

mvn verify green: jamjet-runtime-core 83 tests, jamjet-agent 57 tests (the WireMock transport round-trip, the golden IR parity, the dispatcher and worker adversarial cases, runDurable extraction, and the end-to-end example), 0 failures.

Follow-ups

Phase C (the Spring Boot starter wrapping this, plus the @ConditionalOnClass fix and CI for jamjet-spring). Minor: litellm bare-name parity, a fenced /fail endpoint (shared with the Python worker).

Summary by CodeRabbit

  • New Features

    • Added a Java agent SDK for building, configuring, and running durable agents.
    • Added support for defining tools in Java and dispatching them during agent runs.
    • Added durable run options, budgets, approval controls, and PII-related settings.
    • Added a Java worker and client for executing workflows and handling tool work items.
  • Bug Fixes

    • Improved run result reporting with terminal state and reconstructed tool-call details.
    • Added clearer errors for failed, timed-out, and lease-lost runs.

sunilp added 6 commits June 28, 2026 23:23
…ck round-trip

New jamjet-agent module with JamjetEngineClient: JDK java.net.http on a
virtual-thread executor, JamjetJson snake_case bodies, BARE engine routes
(no /api/v1). Mirrors the canonical Python jamjet.client shapes.

- createWorkflow/startExecution/getExecution/listEvents
- claimWorkItem/completeWorkItem/failWorkItem/heartbeatWorkItem
- lease_fence threaded on heartbeat and complete (engine PR #108); a stale
  fence on complete surfaces as JamjetHttpException with isConflict()==true
  so a reclaimed worker can detect a lost lease
- startExecution uses the workflow_version body key (matches client.py)
- complete includes lease_fence only when non-null

Hermetic WireMock test proves the wire shape: create -> start -> poll to
Completed, plus the work-item path (claim returns lease_fence, complete and
heartbeat post it, null fence is omitted, stubbed 409 surfaces as conflict).
… IR with JavaFn nodes

Add NodeKind.JavaFn (snake_case java_fn: class_name/method/output_schema) + QueueType.JAVA_TOOL (java_tool), matching the Phase A Rust engine, plus a tools field on NodeKind.Model carrying OpenAI schemas. Stop serializing the computed queueType()/isDurable() helpers (@JsonIgnore) so a node matches the Rust shape exactly.

Build the dev.jamjet.agent module: a @tool method annotation, a ToolRegistry that extracts OpenAI-format schemas keyed by class+method (the single source for authoring and B-3 dispatch), and an Agent builder whose compileToIr() emits the static-unroll agent-loop WorkflowIr (Model -> tool gate -> JavaFn dispatch -> next Model, startNode always the first Model node) with governance folded into policy/token_budget/cost_budget_usd/data_policy.

A golden-file parity test asserts the Java IR matches the Python compile_agent_to_ir output structurally (node ids, edges, gate branches, model tool schemas, governance, turn count), with the documented tool-node difference (java_fn vs python_fn) asserted explicitly.
…ated dispatch)

B-3 of Track 9 Phase B. Adds the ToolDispatcher the JavaFn nodes point at and
the durable worker that drains the java_tool queue, mirroring the hardened Python
worker and dispatch_tool_calls.

ToolDispatcher.dispatchToolCalls reads last_model_tool_calls / last_model_output
(with the tool_calls / assistant_content fallbacks) from the work item input,
resolves each call by name through the ToolRegistry only, coerces JSON args to the
typed method params via Jackson, invokes reflectively, and returns the updated
message list as {messages:[...]} (the node output and the state_patch). An unknown
tool yields a clean tool error to the model, never an arbitrary invocation.

JavaToolWorker claims java_tool items and settles them exactly-once:
- completes WITH the claim lease_fence (the engine fences /complete; PR #108)
- treats a 409 on complete as a lost lease no-op, never failing the item
- runs a heartbeat that, on a definitive engine rejection, aborts the in-flight
  tool and skips completion (M3); the engine surfaces a lost heartbeat lease as 500
- gates dispatch to the one fixed ToolDispatcher coordinate, never Class.forName on
  a payload field (the RCE gate)

Tests cover the happy path (complete with fence), fence-lost mid-flight (no
complete), 409 = lost lease (no fail), a forged dispatch coordinate (clean fail,
not RCE), and arg coercion. mvn -pl jamjet-agent -am verify green (50 tests).
Add the durable run path to the Java Agent builder, mirroring the Python
Agent.run_durable: compile -> create_workflow -> start_execution (seeded
with the system+user messages and the tool-resolver map) -> poll
get_execution to a terminal state -> extract the final assistant text +
tool-call trace into an AgentResult.

- AgentResult (output + toolCalls + raw terminal state) and a nested
  ToolCall record, parallel to the Python result shape.
- RunOptions (maxTurns / runtimeUrl / auth / pollInterval / timeout) with
  defaults and withers; timeout derives from the agent when unset.
- AgentRunException (non-completed terminal: failed / cancelled /
  limit_exceeded) and AgentRunTimeoutException, mirroring the Python
  RuntimeError / TimeoutError contract.
- DurableRunner orchestrates create/start/poll/extract. Terminal statuses
  are the snake_case the engine serializes (completed / failed / cancelled
  / limit_exceeded). The answer is current_state.last_model_output (written
  inline by the Rust Model executor's state_patch), falling back to the last
  assistant message; tool calls reconstruct from current_state.messages. The
  existing ExecutionState.currentState carries both, so no record change.
- AgentIrCompiler.buildInitialState / toolsMap seed the run input.
- DRY (B-3 review): AgentIrCompiler.DISPATCH_CLASS/METHOD now reference the
  ToolDispatcher constants the worker's RCE gate checks, so the coordinate
  the builder emits and the coordinate the worker accepts are one literal.

Tests: WireMock-driven create/start/poll/extract, the last-assistant
fallback, terminal failed/limit_exceeded surfacing, the poll timeout, and
the client-owning vs client-injecting overloads. The parity golden stays
green after the DRY fix.
An idiomatic Java example under jamjet-agent: a calculator Agent with a
@tool plus governance defaults (token+cost budget, inline policy with a
blocked tool + model allowlist, an approval gate, PII), authored in clean
Java and runnable durable+governed.

- CalculatorTools: one @tool method (calculate).
- CalculatorAgentExample: buildAgent() is the whole authoring surface; main
  runs it durably with a background JavaToolWorker, documenting the run
  requirement (engine + model sidecar + a java_tool worker). Real shipped
  API only, no fictional methods.
- CalculatorAgentExampleTest: hermetic end-to-end against a WireMock stub of
  the bare engine routes. Builds the agent, asserts governance rode into the
  IR, dispatches the @tool durably through the worker (the tool actually
  ran: calculate(5,3,add) -> 8 in the completion body), and runDurable
  extracts the final assistant text + tool call. The result shape parallels
  the Python run_durable result. Also asserts the worker accepts EXACTLY the
  java_fn coordinate the compiler emits (the claim payload uses the IR's
  emitted coordinate), proving the DRY single-source dispatch coordinate
  end-to-end.
- README: idiomatic authoring + the durable-run requirement.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sunilp, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 15 minutes and 44 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20e43565-2d27-40ae-a3a5-b59549d28340

📥 Commits

Reviewing files that changed from the base of the PR and between 1e6b376 and 74be915.

📒 Files selected for processing (19)
  • jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/Budget.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/RunOptions.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/JamjetEngineClient.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolDispatcher.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolInvocationException.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolRegistry.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/worker/JavaToolWorker.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/AgentBuilderValidationTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/BudgetTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/RunOptionsValidationTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/client/JamjetEngineClientTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorTools.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorToolsTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/tools/ToolDispatcherTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/tools/ToolRegistryTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/worker/JavaToolWorkerTest.java
  • jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/ir/NodeKind.java
  • jamjet-runtime-core/src/test/java/dev/jamjet/runtime/core/ir/NodeKindSerializationTest.java
📝 Walkthrough

Walkthrough

Introduces the jamjet-agent Maven module, a governed durable Java agent SDK. It adds the @Tool annotation, ToolRegistry/ToolDispatcher for reflective invocation, AgentIrCompiler for static loop-unrolling to WorkflowIr, JamjetEngineClient for engine HTTP calls, DurableRunner for poll-based execution, and JavaToolWorker for lease-fenced tool dispatch. Also extends jamjet-runtime-core with NodeKind.JavaFn and QueueType.JAVA_TOOL.

Changes

jamjet-agent SDK

Layer / File(s) Summary
Module descriptor and public data types
pom.xml, jamjet-agent/pom.xml, jamjet-agent/src/main/java/dev/jamjet/agent/Tool.java, jamjet-agent/src/main/java/dev/jamjet/agent/Budget.java, jamjet-agent/src/main/java/dev/jamjet/agent/RunOptions.java, jamjet-agent/src/main/java/dev/jamjet/agent/AgentResult.java, jamjet-agent/src/main/java/dev/jamjet/agent/AgentRunException.java, jamjet-agent/src/main/java/dev/jamjet/agent/AgentRunTimeoutException.java
Registers the module; defines the @Tool annotation, Budget record with validation, RunOptions record with defaults and fluent with* overloads, AgentResult/ToolCall output records, and the AgentRunException/AgentRunTimeoutException exception hierarchy.
Runtime-core IR extensions
jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/QueueType.java, jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/ir/NodeKind.java, jamjet-runtime-core/src/test/java/dev/jamjet/runtime/core/ir/NodeKindSerializationTest.java
Adds JAVA_TOOL queue type, NodeKind.JavaFn record with java_fn JSON discriminator, tools list on NodeKind.Model with back-compat constructor, and @JsonIgnore on helper methods; verified by serialization round-trip tests.
Tool registry and dispatcher
jamjet-agent/src/main/java/dev/jamjet/agent/tools/RegisteredTool.java, jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolRegistry.java, jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolDispatcher.java, jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolInvocationException.java
ToolRegistry discovers @Tool methods via reflection, builds OpenAI function schemas, and resolves by name or class/method key. ToolDispatcher builds OpenAI-style assistant/tool messages, coerces arguments via Jackson, and invokes tools through the registry; unknown coordinates produce clean error messages.
Agent builder and IR compiler
jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java, jamjet-agent/src/main/java/dev/jamjet/agent/AgentIrCompiler.java
Agent.Builder fluent API normalizes inputs and validates required fields. AgentIrCompiler.compile() statically unrolls maxTurns into model→gate→JavaFn node triples, appends a terminal model node, compiles governance IR (policy, budgets, PII), and computes a SHA-256-based content version.
Engine HTTP client
jamjet-agent/src/main/java/dev/jamjet/agent/client/JamjetEngineClient.java, jamjet-agent/src/main/java/dev/jamjet/agent/client/JamjetHttpException.java, jamjet-agent/src/main/java/dev/jamjet/agent/client/CreateWorkflowResult.java, jamjet-agent/src/main/java/dev/jamjet/agent/client/StartExecutionResult.java, jamjet-agent/src/main/java/dev/jamjet/agent/client/ExecutionState.java, jamjet-agent/src/main/java/dev/jamjet/agent/client/ClaimedWorkItem.java
JDK HttpClient-based client covering workflow creation, execution start/poll, event listing, and full work-item protocol (claim/heartbeat/complete/fail with lease-fence propagation). JamjetHttpException carries status and body; isConflict() identifies 409 responses.
DurableRunner and Agent.runDurable
jamjet-agent/src/main/java/dev/jamjet/agent/DurableRunner.java
Orchestrates compile→createWorkflowstartExecution→poll loop→extractResult; reconstructs tool-call trace from message thread; throws AgentRunTimeoutException on deadline and AgentRunException for non-completed terminal states. Agent.runDurable overloads wire into this path.
JavaToolWorker
jamjet-agent/src/main/java/dev/jamjet/agent/worker/JavaToolWorker.java
Continuously claims java_tool items, validates dispatch coordinates against the fixed ToolDispatcher class/method, submits dispatch on virtual threads, runs a scheduled heartbeat that cancels in-flight dispatch on definitive lease rejection (HTTP ≥ 400), and treats HTTP 409 on completion as LOST_LEASE.
Compiler and parity tests
jamjet-agent/src/test/java/dev/jamjet/agent/AgentIrCompilerTest.java, jamjet-agent/src/test/java/dev/jamjet/agent/AgentIrParityTest.java, jamjet-agent/src/test/resources/golden/agent_loop_ir.python.json
AgentIrCompilerTest covers unrolling shape, governance omission, approval glob dedup, versioning stability, and litellmModel normalization. AgentIrParityTest compares Java-compiled IR against a Python golden file across nodes, edges, model schemas, governance, and documented divergences (java_fn vs python_fn).
Tool, client, durable, worker, and example tests
jamjet-agent/src/test/java/dev/jamjet/agent/tools/*, jamjet-agent/src/test/java/dev/jamjet/agent/client/*, jamjet-agent/src/test/java/dev/jamjet/agent/AgentRunDurableTest.java, jamjet-agent/src/test/java/dev/jamjet/agent/worker/JavaToolWorkerTest.java, jamjet-agent/src/test/java/dev/jamjet/agent/examples/*
WireMock-backed tests for ToolRegistry, ToolDispatcher (unknown tools, exceptions, argument coercion), JamjetEngineClient (wire shape, lease fence, 409), DurableRunner (happy path, failures, timeout, output fallback), JavaToolWorker (fence-lost mid-flight, 409 completion, unknown coordinate RCE guard, empty queue), and the CalculatorAgentExample end-to-end scenario.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over Caller,Engine: Agent.runDurable
  end
  participant Caller
  participant Agent
  participant DurableRunner
  participant JamjetEngineClient
  participant Engine as JamJet Engine

  Caller->>Agent: runDurable(prompt, options)
  Agent->>JamjetEngineClient: new(runtimeUrl, auth)
  Agent->>DurableRunner: run(agent, prompt, client, options)
  DurableRunner->>JamjetEngineClient: createWorkflow(WorkflowIr)
  JamjetEngineClient->>Engine: POST /workflows
  Engine-->>JamjetEngineClient: workflowId
  DurableRunner->>JamjetEngineClient: startExecution(workflowId, initialState)
  JamjetEngineClient->>Engine: POST /executions
  Engine-->>JamjetEngineClient: executionId

  loop poll until terminal
    DurableRunner->>JamjetEngineClient: getExecution(executionId)
    JamjetEngineClient->>Engine: GET /executions/{id}
    Engine-->>JamjetEngineClient: ExecutionState(status, currentState)
  end

  DurableRunner->>DurableRunner: extractResult(terminalState)
  DurableRunner-->>Caller: AgentResult(output, toolCalls, terminalState)
Loading
sequenceDiagram
  rect rgba(144, 238, 144, 0.5)
    Note over JavaToolWorker,Tool: JavaToolWorker lease-fenced dispatch
  end
  participant JavaToolWorker
  participant JamjetEngineClient
  participant ToolDispatcher
  participant Tool as `@Tool` method

  JavaToolWorker->>JamjetEngineClient: claimWorkItem(java_tool)
  JamjetEngineClient-->>JavaToolWorker: ClaimedWorkItem(leaseFence)
  JavaToolWorker->>JavaToolWorker: validate dispatch coordinate
  JavaToolWorker->>ToolDispatcher: dispatchToolCalls(input) [virtual thread]
  JavaToolWorker->>JamjetEngineClient: heartbeatWorkItem(leaseFence) [scheduled]
  ToolDispatcher->>Tool: method.invoke(args)
  Tool-->>ToolDispatcher: result
  ToolDispatcher-->>JavaToolWorker: {messages}
  JavaToolWorker->>JamjetEngineClient: completeWorkItem(leaseFence, output)
  Note right of JavaToolWorker: 409 → LOST_LEASE (no-op)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 A hop, a skip, a durable run,
Tools annotated, reflection done!
Lease fences guard the in-flight call,
The compiler unrolls the loop for all.
Java meets Python IR, parity blessed —
This bunny says the agent's passed the test! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change set: a Java Agent builder plus durable java_tool worker support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adk-track-9b-java-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/ir/NodeKind.java (1)

65-70: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Freeze tools recursively, not just the outer list.

List.copyOf(tools) still aliases each schema map/list. Because these tool schemas are nested JSON objects, callers can mutate them after Model construction and change the serialized IR/version under an otherwise immutable node. Recursively copy/freeze the schema tree before storing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/ir/NodeKind.java`
around lines 65 - 70, The Model.tools normalization only freezes the outer list,
while the nested schema maps/lists remain mutable and can still be changed after
construction. Update the Model constructor in NodeKind.Model to recursively
deep-copy and freeze each tool schema object before assigning tools, so the
stored IR cannot be mutated through shared nested references. Use the
Model.tools field and its constructor normalization as the place to apply the
deep-freeze logic.
jamjet-runtime-core/src/test/java/dev/jamjet/runtime/core/ir/NodeKindSerializationTest.java (1)

74-85: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a missing-field deserialize regression.

This only round-trips the new 4-arg constructor. It does not pin the backward-compat path for older model JSON where tools is absent entirely. Add a literal JSON deserialize assertion for that case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@jamjet-runtime-core/src/test/java/dev/jamjet/runtime/core/ir/NodeKindSerializationTest.java`
around lines 74 - 85, The existing NodeKindSerializationTest only covers
round-tripping the 4-arg NodeKind.Model constructor, so add a regression test
for deserializing older model JSON where the tools field is completely missing.
In NodeKindSerializationTest, add a literal JSON deserialize assertion using
NodeKind.Model to verify mapper.readValue handles absent tools the same as an
empty list, and keep the assertion focused on the backward-compat path rather
than serialization output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@jamjet-agent/pom.xml`:
- Around line 33-36: The shared Jackson baseline is still pinned to an older
vulnerable databind release, so update the root `jackson.version` property in
the BOM that `jamjet-agent` inherits from to a patched version such as `2.18.8`
or newer. Make the change in the shared Maven property, not in the
`jackson-databind` dependency declaration itself, so all modules using the BOM
pick up the safer version consistently.

In `@jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java`:
- Around line 311-315: The Builder.timeoutSeconds method in Agent should reject
non-positive values before assigning the field. Add validation in
timeoutSeconds(int timeoutSeconds) to ensure timeoutSeconds > 0, and fail fast
with an appropriate exception so invalid values cannot reach the compiled
workflow timeout or durable-run deadline.
- Around line 317-320: Reject blank model ids in Agent.Builder.build() by
validating the model string after Objects.requireNonNull(model, ...) and before
constructing the Agent. Update build() (and any nearby builder validation
helpers) to trim the value and throw an IllegalArgumentException for empty or
whitespace-only model ids, so invalid input is caught early instead of later in
AgentIrCompiler.litellmModel().

In `@jamjet-agent/src/main/java/dev/jamjet/agent/Budget.java`:
- Around line 16-23: The Budget record constructor currently only rejects
non-positive values, so null/null and non-finite doubles still slip through;
update the compact constructor in Budget to treat an entirely empty budget as
invalid and to reject any non-finite costUsd values (NaN/Infinity) and any
non-finite token values if applicable. Use the existing Budget validation block
to enforce that at least one field is provided and that both tokens and costUsd,
when present, are finite and positive before allowing construction.

In `@jamjet-agent/src/main/java/dev/jamjet/agent/client/JamjetEngineClient.java`:
- Around line 154-163: `heartbeatWorkItem(...)` is forcing `lease_fence` to be
always present, which breaks the nullable fence flow already supported by
`completeWorkItem(...)` and `ClaimedWorkItem.leaseFence()`. Update
`JamjetEngineClient.heartbeatWorkItem` to accept a nullable `Long` lease fence
instead of a primitive `long`, and build the request body so `lease_fence` is
omitted entirely when the value is null. Keep the existing optional behavior
consistent with the completion path and preserve the legacy null fence case
without requiring a sentinel value.

In `@jamjet-agent/src/main/java/dev/jamjet/agent/RunOptions.java`:
- Around line 39-49: RunOptions currently validates pollInterval but allows
non-positive timeout values, so add a timeout validation in the RunOptions
constructor to reject Duration.ZERO and negative durations with an
IllegalArgumentException. Update the existing record validation logic alongside
maxTurns, runtimeUrl, and pollInterval, and ensure the check targets the timeout
field used by the durable runner’s deadline calculation.

In `@jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolDispatcher.java`:
- Around line 150-163: Reject missing required tool arguments before coercing
them in ToolDispatcher.coerceToParameters: the current lookup by
Parameter.getName() silently turns absent fields into null/default values, even
though ToolRegistry.buildInputSchema() marks them required. Add an explicit
arguments.containsKey(...) check for each parameter and fail fast with a clear
validation error before invoking the tool; keep the fix localized to
coerceToParameters and the argument coercion path so bad JSON or omitted fields
cannot reach the tool execution.

In
`@jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolInvocationException.java`:
- Around line 19-21: The ToolInvocationException constructor is exposing raw
cause text by embedding cause.toString() in the public exception message, which
then gets logged and persisted by JavaToolWorker.process and failWorkItem.
Update ToolInvocationException so the surfaced message stays generic while still
passing the original cause to super(...) and preserving toolName, and ensure any
logging in JavaToolWorker.process relies on the attached throwable rather than
the exception message text.

In `@jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolRegistry.java`:
- Around line 101-111: Reject overloaded `@Tool` methods in ToolRegistry before
adding them to the registry maps. The current RegisteredTool.key() used by byKey
only relies on class name and method name, so a second annotated overload in the
same holder can overwrite the first and later make byClassAndMethod() resolve
the wrong Method. Update ToolRegistry.register(...) to either make the durable
key include parameter types or throw an IllegalArgumentException when another
`@Tool` with the same method name is already present for that class, keeping
byName and the exported schemas consistent.

In `@jamjet-agent/src/main/java/dev/jamjet/agent/worker/JavaToolWorker.java`:
- Around line 142-145: The `JavaToolWorker.run()` interruption handling is
incorrectly treating `InterruptedException` from `dispatchFuture.get()` as a
failure path, which can settle the item as `/fail` during shutdown or
cancellation. Update the `run()` control flow to handle `InterruptedException`
separately from other exceptions, restore the thread’s interrupt flag, and exit
without failing or settling the item so it remains reclaimable by another
worker. Keep the existing `leaseLost` logic for genuine lease-expiry cases, but
ensure interruption never reaches the failure branch.

In `@jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorTools.java`:
- Around line 23-35: The CalculatorTools.calculate example still uses binary
floating-point, so it can produce imprecise results and returns Infinity for
zero divisors. Update the calculate method to use decimal arithmetic with
explicit handling for divide-by-zero, and adjust the whole-number rendering
logic accordingly; if you keep double-based math, then the “precise/exact”
wording in the example docs should be relaxed instead.

---

Nitpick comments:
In `@jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/ir/NodeKind.java`:
- Around line 65-70: The Model.tools normalization only freezes the outer list,
while the nested schema maps/lists remain mutable and can still be changed after
construction. Update the Model constructor in NodeKind.Model to recursively
deep-copy and freeze each tool schema object before assigning tools, so the
stored IR cannot be mutated through shared nested references. Use the
Model.tools field and its constructor normalization as the place to apply the
deep-freeze logic.

In
`@jamjet-runtime-core/src/test/java/dev/jamjet/runtime/core/ir/NodeKindSerializationTest.java`:
- Around line 74-85: The existing NodeKindSerializationTest only covers
round-tripping the 4-arg NodeKind.Model constructor, so add a regression test
for deserializing older model JSON where the tools field is completely missing.
In NodeKindSerializationTest, add a literal JSON deserialize assertion using
NodeKind.Model to verify mapper.readValue handles absent tools the same as an
empty list, and keep the assertion focused on the backward-compat path rather
than serialization output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72fa4638-b1db-45dc-b3ff-962b99a66a3b

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5fd86 and 1e6b376.

📒 Files selected for processing (38)
  • jamjet-agent/pom.xml
  • jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/AgentIrCompiler.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/AgentResult.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/AgentRunException.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/AgentRunTimeoutException.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/Budget.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/DurableRunner.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/RunOptions.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/Tool.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/ClaimedWorkItem.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/CreateWorkflowResult.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/ExecutionState.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/JamjetEngineClient.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/JamjetHttpException.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/client/StartExecutionResult.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/RegisteredTool.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolDispatcher.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolInvocationException.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolRegistry.java
  • jamjet-agent/src/main/java/dev/jamjet/agent/worker/JavaToolWorker.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/AgentIrCompilerTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/AgentIrParityTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/AgentRunDurableTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/TestTools.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/client/JamjetEngineClientTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorAgentExample.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorAgentExampleTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorTools.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/examples/README.md
  • jamjet-agent/src/test/java/dev/jamjet/agent/tools/ToolDispatcherTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/tools/ToolRegistryTest.java
  • jamjet-agent/src/test/java/dev/jamjet/agent/worker/JavaToolWorkerTest.java
  • jamjet-agent/src/test/resources/golden/agent_loop_ir.python.json
  • jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/QueueType.java
  • jamjet-runtime-core/src/main/java/dev/jamjet/runtime/core/ir/NodeKind.java
  • jamjet-runtime-core/src/test/java/dev/jamjet/runtime/core/ir/NodeKindSerializationTest.java
  • pom.xml

Comment thread jamjet-agent/pom.xml
Comment on lines +33 to +36
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Jackson version declarations and imports:"
rg -n --glob 'pom.xml' 'jackson-databind|jackson-bom|jackson.version'

echo
echo "POM context around Jackson-related entries:"
fd -a '^pom\.xml$' . -x sh -c '
  if rg -q "jackson-databind|jackson-bom|jackson.version" "$1"; then
    echo "=== $1 ==="
    nl -ba "$1" | sed -n "/jackson/,+20p"
  fi
' sh {}

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Top-level files:"
git ls-files | sed -n '1,200p'

echo
echo "POM files:"
git ls-files '*pom.xml'

echo
echo "Jackson-related matches across tracked POMs:"
rg -n --glob '**/pom.xml' 'jackson-databind|jackson-bom|jackson.version|jackson-core|jackson-annotations' .

echo
echo "jamjet-agent/pom.xml context:"
nl -ba jamjet-agent/pom.xml | sed -n '1,120p'

echo
echo "Parent POM candidates:"
for f in $(git ls-files '*pom.xml'); do
  if grep -q '<packaging>pom</packaging>' "$f" 2>/dev/null; then
    echo "=== $f ==="
    nl -ba "$f" | sed -n '1,220p'
  fi
done

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 16692


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Root pom.xml Jackson section:"
python3 - <<'PY'
from pathlib import Path
p = Path("pom.xml")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 55 <= i <= 85:
        print(f"{i}: {line}")
PY

echo
echo "jamjet-agent/pom.xml around the dependency:"
python3 - <<'PY'
from pathlib import Path
p = Path("jamjet-agent/pom.xml")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 25 <= i <= 45:
        print(f"{i}: {line}")
PY

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 2447


🌐 Web query:

Jackson databind 2.17.2 advisories CVE high severity 2025

💡 Result:

As of June 28, 2026, Jackson Databind 2.17.2 is affected by several high-severity security vulnerabilities discovered in 2025 and 2026. Because 2.17.2 is no longer the latest maintained version, it remains vulnerable to these issues, and users are strongly advised to upgrade to a patched version (such as 2.18.8 or higher) [1][2][3][4]. Key vulnerabilities affecting version 2.17.2 include: 1. PolymorphicTypeValidator (PTV) Bypass (CVE-2026-54512): This high-severity vulnerability allows an attacker to bypass the PTV allow-list by using generic type parameters (e.g., ArrayList<com.evil.Gadget>), potentially leading to arbitrary class instantiation and remote code execution [1][2][5]. It affects all versions from 2.10.0 up to 2.18.7 [1][5]. 2. Array Subtype Allowlist Bypass (CVE-2026-54513): This issue exists in the BasicPolymorphicTypeValidator's allowIfSubTypeIsArray method, which fails to validate the component type of an array, allowing an attacker to instantiate non-allowlisted types [3]. It affects versions 2.10.0 through 2.18.7 [3]. 3. SSRF via InetSocketAddress (CVE-2026-54514): Deserializing InetSocketAddress objects triggers eager DNS resolution, which can be exploited for Server-Side Request Forgery (SSRF) or internal-resolver probing. This affects versions 2.0.0 through 2.18.7 [4]. Note on CVE-2025-52999: While widely reported as a high-severity denial-of-service vulnerability affecting jackson-core, it primarily impacts versions prior to 2.15.0 [6][7]. Jackson Databind 2.17.2 uses a more recent jackson-core and is not directly vulnerable to this specific stack-overflow issue [6]. To remediate these risks, you should update your project dependencies to use Jackson Databind 2.18.8 or a newer release [1][2][3][4].

Citations:


Bump the parent Jackson baseline. jamjet-agent/pom.xml inherits jackson-databind from the root BOM, and pom.xml:61-79 still pins jackson.version to 2.17.2. Upgrade that shared version to a patched release (for example 2.18.8+) so this module and the rest of the build stop pulling the vulnerable databind line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jamjet-agent/pom.xml` around lines 33 - 36, The shared Jackson baseline is
still pinned to an older vulnerable databind release, so update the root
`jackson.version` property in the BOM that `jamjet-agent` inherits from to a
patched version such as `2.18.8` or newer. Make the change in the shared Maven
property, not in the `jackson-databind` dependency declaration itself, so all
modules using the BOM pick up the safer version consistently.

Source: Linters/SAST tools

Comment thread jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java Outdated
Comment thread jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java
Comment thread jamjet-agent/src/main/java/dev/jamjet/agent/Budget.java
Comment thread jamjet-agent/src/main/java/dev/jamjet/agent/tools/ToolRegistry.java
Comment thread jamjet-agent/src/test/java/dev/jamjet/agent/examples/CalculatorTools.java Outdated
Correctness (with tests):
- ToolRegistry: reject overloaded @tool methods that collide on the
  class+method dispatch key, which would silently corrupt byClassAndMethod
- JavaToolWorker: an interrupt mid-dispatch is a reclaimable no-op, not a
  tool failure; restore the interrupt and stop without failing the item
- ToolDispatcher: a tool call missing a required arg yields a clean tool
  error surfaced to the model, not a null-coerced invocation

Input validation:
- Agent.Builder: reject non-positive timeoutSeconds and a blank model
- Budget: reject an entirely-empty budget and a non-finite costUsd
- RunOptions: reject a zero or negative timeout

Hygiene:
- NodeKind.Model: deep-copy and freeze tool schemas, not just the outer list
- JamjetEngineClient.heartbeatWorkItem: accept a nullable Long lease fence
  and include it in the body only when present
- ToolInvocationException: keep the surfaced message generic and retain the
  cause as the exception cause, never embedding raw cause.toString
- CalculatorTools example: BigDecimal arithmetic with a clean divide-by-zero
  error and whole-number rendering without a trailing .0

Adds regression coverage for older Model JSON with no tools field. Full
jamjet-agent and jamjet-runtime-core suites green (Java 21, 163 tests).
@sunilp sunilp merged commit 1720cd0 into main Jun 28, 2026
2 checks passed
@sunilp sunilp deleted the feat/adk-track-9b-java-agent branch June 28, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant