Skip to content

feat(agent): add AgentResult delivery contract with opt-in structured output finalizer (T39) - #173

Merged
yuluo-yx merged 3 commits into
agentic-ai-java:mainfrom
konlue:feat/t39-agent-result-contract
Oct 20, 2026
Merged

yuluo-yx merged 3 commits into
agentic-ai-java:mainfrom
konlue:feat/t39-agent-result-contract

Conversation

@konlue

@konlue konlue commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Describe what this PR does / why we need it

First part of T39 (#142), following the claim agreement: the framework already turns outputType/outputSchema into prompt constraints, but prompt constraints are not validation — ReactAgent.call returns the raw assistant message with no enforced delivery boundary. This PR adds an opt-in structured delivery contract: ReactAgent.callResult(...), mirroring the existing call(...) entries, returns an AgentResult that parses and schema-validates the configured output schema. The existing call/invoke/stream signatures and behavior are untouched.

Does this pull request fix one issue?

NONE

Describe how you did it

  • Minimal AgentResult record (message / status / usage / validated data / issues) with a four-state Status enum (COMPLETED / VALIDATION_FAILED / INTERRUPTED / ERROR) and an OutputValidationIssue record (errorCode / fieldPath / message) in the style of the T31 argument-validation protocol. The status vocabulary is deliberately separate from T38's tool-level ToolOutcome: one classifies a tool call, the other the whole run's delivery.
  • StructuredOutputFinalizer carries the terminal message through unchanged and never regenerates it: parsing uses a strict Jackson 3 mapper (duplicate-key and trailing-token detection), validation uses SchemaRegistry with the same DRAFT_2020_12 dialect the planner codec and ToolSchema.forType already produce — zero new dependencies. The component never throws; every failure mode becomes VALIDATION_FAILED with issues that do not echo the full output content.
  • Every termination path funnels through one boundary: a normal final message, the returnDirect synthesized message and a hook JumpTo.end all surface as the last assistant message and are finalized identically; a run that ends without a terminal message (e.g. interrupted) is reported as INTERRUPTED and never as completed; execution failures and the throwOnModelError=false "Exception: ..." pseudo message are reported as ERROR. Token usage is read from the runner's NodeOutput.tokenUsage() (the runner deliberately keeps the reserved usage key out of the state).

Describe how to verify it

  • StructuredOutputFinalizerTest (6 cases): schema-valid output completes with parsed data; non-JSON / trailing-token output → INVALID_JSON; schema violations carry document-pointer instance paths (/status style); missing schema completes without validation; a malformed schema → SCHEMA_MALFORMED.

  • ReactAgentCallResultTest (7 cases, stub ChatClient end to end): COMPLETED with usage carried; INVALID_JSON and SCHEMA_VIOLATION through the full graph; failing client → ERROR (MODEL_FAILED); a failing run never marks completed; and a guard test asserting the legacy call() still returns the raw message untouched.

  • Run:

    ./mvnw -pl argi-agent-framework -am test -DskipTests=false \
      -Dtest='StructuredOutputFinalizerTest,ReactAgentCallResultTest' \
      -Dsurefire.failIfNoSpecifiedTests=false
  • Full regression: graph-core 829 + agent-framework 1210 tests; only the documented pre-existing Windows-environment failures remain (FileSystem* symlink/virtual-root family and the skills/prompt-template cases, identical on clean main).

Special notes for reviews

  • No new builder switch on purpose: a configured outputSchema signals validation intent and the callResult entry signals delivery intent — a double opt-in, so a permanently-false knob is avoided and legacy call cannot change behavior.
  • The Exception: pseudo-message detection is prefix-based, matching the exact string produced by AgentLlmNode when throwOnModelError=false; a user message that legitimately starts with that prefix would be misclassified as ERROR (the stricter alternative — wiring the failure through the graph — belongs to the streaming follow-up).
  • Streaming delivery (provisional chunks vs the final validated result) and one bounded format repair (re-parse without replaying side-effectful tools) are the follow-up PR that will close the issue.

@yuluo-yx

Copy link
Copy Markdown
Contributor

hi @konlue the ci is failed, please check it, thanks

@konlue

konlue commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

hi @konlue the ci is failed, please check it, thanks

Thanks for pinging! Will check this tomorrow. 🌹

…ts without an output schema do not load the schema validator (T39)
@konlue

konlue commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Found the real root cause — it was on my side, and the earlier transient-download theory was wrong.

The studio module declares json-schema-validator 1.5.7 (test scope, needed by its MCP client for the 1.x API), so studio's test classpath has no 3.x SpecificationVersion.

PR1 eagerly instantiated the StructuredOutputFinalizer in ReactAgent's constructor, whose class initialization touches the 3.x API — so building any agent in studio tests threw NoClassDefFoundError (4 errors in studio's 50 tests).

Pushed a fix: the finalizer is now created lazily on first use, so agents without an output schema never load the validator — consistent with how the planner codec already coexists with studio.

Studio's full module (50 tests) passes locally, along with the PR's own tests.

@yuluo-yx
yuluo-yx merged commit c0a4ab4 into agentic-ai-java:main Oct 20, 2026
7 checks passed
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.

2 participants