Skip to content

feat(agentic-harness): local runnable agentic API-orchestration harness on ReVoman - #407

Open
overfullstack wants to merge 50 commits into
masterfrom
overfullstack/fix-issue-394
Open

feat(agentic-harness): local runnable agentic API-orchestration harness on ReVoman#407
overfullstack wants to merge 50 commits into
masterfrom
overfullstack/fix-issue-394

Conversation

@overfullstack

Copy link
Copy Markdown
Contributor

What & why

Adds agentic-harness — a new, isolated Gradle module that makes the agentic API-orchestration design (production-system-design / reasoning-layer) runnable locally, end-to-end, with no Salesforce org. It demonstrates every production concept as working, tested code on top of ReVoman as the deterministic execution engine.

The ReVoman library is not modified. The only change outside agentic-harness/ is a single include("agentic-harness") line in settings.gradle.kts. New deps (koog) are quarantined to an opt-in source set the default build never compiles.

What's in it

Built in stages, each independently runnable (./gradlew :agentic-harness:runStage*Demo):

  • Stage 1 — deterministic spine: in-memory mock CPQ server (configure/price/quote) + three ReVoman V3 collections chaining via {{var}}, executed by ReVoman.revUp, with a Layer-1 contract test on the Rundown.
  • Stage 2 — probabilistic layer: auto-generated 4-field tool defs, a pluggable LlmClient (deterministic stub + real Claude), router + slot-filler with schema validation before execution, and the orchestrator-workers loop.
  • Stage 3 — evals + calibration: labeled eval set, confusion matrix, live calibration (add a when_not_to_use clause → accuracy 6/7 → 7/7), BFCL slot-fill check, tau-bench final-DB-state check, bounded LLM-as-judge alongside a deterministic ground-truth.
  • Stage 4 — observability + flywheel: OpenTelemetry GenAI-convention spans (invoke_agent/chat/execute_tool), a HITL confirm gate (confirm/edit/reject → labels), and a nightly-batch that grows the eval set + drafts when_not_to_use from rejects.
  • Blue box — unified contract: GraphContract fuses the metadata descriptor (ToolDef) + runtime outcome (Rundown) + data-lineage (which {{var}} came from which step) + a version envelope, with a tiered JSON renderer; plus ContractFidelityCheck (deterministic API-drift catch) and ContractAblationEval (measure accuracy delta from contract enrichment).
  • Green box — reasoning layer (6 scaffolds): router with {graph, confidence, margin}, retrieval pre-filter, the confidence/disambiguation gate (ask-don't-guess: low margin or bad slots → clarify; writes → confirm-required, never auto-execute), and end-to-end ReasoningLayer wiring.

Isolation & safety

  • koog / coroutines live only in src/claude (a source set the default test/build/check never compiles). Grep-verified: no ai.koog.* / kotlinx.coroutines.* under src/main or src/test.
  • No API key required for any test. The real Claude path is gated on env vars and skips cleanly when absent.

Verification

./gradlew :agentic-harness:build   # spotless + detekt + all tests — green
./gradlew :agentic-harness:test    # 32 test classes

Runnable demos (deterministic, no key):

./gradlew :agentic-harness:runStage1Demo    # deterministic spine
./gradlew :agentic-harness:runStage2Demo    # orchestrator-workers loop
./gradlew :agentic-harness:runStage3Demo    # confusion matrix + calibration
./gradlew :agentic-harness:runStage4Demo    # spans + confirm gate + flywheel
./gradlew :agentic-harness:runStage5Demo    # full reasoning layer (proceed/confirm/ask/no-match)
./gradlew :agentic-harness:runContractDemo      # unified GraphContract, 3 verbosity tiers
./gradlew :agentic-harness:runContractEvalDemo  # fidelity + drift catch + ablation

Known limit

The real Claude path compiles and targets this environment's AWS Bedrock proxy; the live call is best-effort (the gateway is reachable but koog's Bedrock client does not complete a request against the proxy's redirect/protocol). The deterministic stub path is the shippable deliverable and exercises the entire design in CI.

Design + implementation notes live under docs/superpowers/specs/ and docs/superpowers/plans/.

🤖 Generated with Claude Code

New agentic-harness Gradle module (koog + :revoman) that makes every
production-system-design.md concept runnable locally, no Salesforce org:
deterministic ReVoman spine, tool-def gen, router/slot-filler, evals +
confusion matrix, OTel spans, HITL confirm gate + flywheel. 4 runnable
stages; STUB LLM in CI, real Claude gated on ANTHROPIC_API_KEY.
Implements Prompt A (Router) and Prompt B (SlotFiller) for the agentic
harness orchestrator. SlotFiller validates every argument against the
tool's typed SlotSchema BEFORE returning, rejecting hallucinated names,
out-of-enum values, non-integer numbers, and missing required slots.
Add Stage 3 Task 1: EvalSet (snakeyaml loader) + ConfusionMatrix with accuracy/render.
- router-eval.yaml: 7 labeled cases with deliberate near-miss (price phrased with "quote")
- EvalCase: simple data class for utterance + expected graph label
- EvalSet: loads YAML resource via snakeyaml, returns List<EvalCase>
- ConfusionMatrix: counts correct/off-diagonal predictions; render() produces labeled grid
- ConfusionMatrices.from(): builder for (expected, predicted) pairs; null -> "none"
Implement two evaluation idioms: BFCL-style AST comparison of predicted (graph, slots)
against gold YAML, and tau-bench-style final database state validation after orchestration.
…e_tool)

Implements GenAiTracer, a dependency-free span model for OpenTelemetry GenAI
conventions with console rendering. Spans nest via block scope using ArrayDeque
stack; child spans become children of their parent via try/finally semantics.
Includes Span data class with render() for indented tree output, NoopTracer
for silent operation, and convention helpers (invokeAgentAttrs/chatAttrs/
executeToolAttrs) for semantic span attributes.
…atch, loop close

Implements the full feedback flywheel with observability:

1. GenAiTracer + instrumented Orchestrator emit OTel GenAI-convention spans
   (invoke_agent, chat, execute_tool) with gen_ai.* attributes to console.

2. ConfirmGate turns proposals into feedback labels (Positive, Negative,
   CorrectionPair) via scripted confirm/reject decisions.

3. NightlyBatch mines labels to grow the eval set and draft when_not_to_use
   clauses from confusion pairs (rejected near-miss turns).

4. The loop closes: re-evaluating the router on the grown set with the
   auto-drafted clauses shows accuracy improve (6/7 → 7/7 in the demo),
   proving the "CI gate tightens itself" flywheel works end-to-end.

FlywheelClosesLoopTest asserts that a rejected near-miss (quote vs price)
→ drafted clause → strictly higher accuracy on the grown eval set.

Also fixes ScoringLlmClient's penalty logic to require ≥2 matching trigger
tokens (not just ANY) to prevent overly-broad penalties on legitimate uses.

Runnable: ./gradlew :agentic-harness:runStage4Demo -q
…ness)

Completes the 6 reliability scaffolds from reasoning-layer.md as a delta on the
existing module: confidence on RouteDecision, retrieval pre-filter, the
confidence/disambiguation gate (ask-don't-guess), a confirm-gate preview object,
end-to-end ReasoningLayer wiring, and native Anthropic tool-use for the real client.
Reuses the ~60% already built (tool-def gen, slot-filler, evals, confusion matrix).
…me + dataFlow)

Implements GraphContractWriter with toJson(Verbosity) extension function that renders
a unified contract (metadata descriptor + runtime outcome) at three tiers:
- SUMMARY: version/graph/slots/stopReason (no descriptive prose)
- STANDARD: adds whenToUse/whenNotToUse and dataFlow lineage
- VERBOSE: adds exampleQueries and full nested Rundown JSON

Hand-built JSON string with defensive quote/backslash escaping per brief.
Implements RL Task 3: the three core types for the reasoning layer's disambiguation gate. ConfidencePolicy handles tunable thresholds (write graphs default to 0.90 financial-services band, reads 0.60), ActionPreview carries proposed-action details for confirmation, and ReasoningOutcome is the sealed type for the gate's decision (NoMatch, Clarify, ConfirmRequired, Proceed).
… etc.) for live demo

The claude source set inherited main's compiled output but not its dependencies, so the
live ClaudeDemo failed loading OAS/graphs (snakeyaml). Add main's compile/runtime
classpaths. Quarantine intact: default build still never compiles this set (verified:
:agentic-harness:test green, no compileClaudeKotlin).

Live Bedrock-proxy call remains best-effort: the SFDC gateway is reachable (HTTP 307) but
koog's Bedrock client does not complete a call against it (redirect/protocol) — the
deterministic layer via StubLlmClient is the shippable deliverable.
@github-actions

Copy link
Copy Markdown

Qodana for JVM

8 new problems were found

Inspection name Severity Problems
Constructor parameter is never used as a property 🔶 Warning 3
Unused import directive 🔶 Warning 2
Unstable API Usage 🔶 Warning 2
Duplicated code fragment ◽️ Notice 1

💡 Qodana analysis was run in the pull request mode: only the changed files were checked
☁️ View the detailed Qodana report

Contact Qodana team

Contact us at qodana-support@jetbrains.com


testing {
suites {
getByName<JvmTestSuite>("test") { useJUnitJupiter(libs.versions.junit.get()) }

testing {
suites {
getByName<JvmTestSuite>("test") { useJUnitJupiter(libs.versions.junit.get()) }
*/
package com.salesforce.revoman.harness

import com.salesforce.revoman.harness.GraphRunner

import com.salesforce.revoman.harness.mock.MockCpqServer
import com.salesforce.revoman.harness.orchestrator.GraphRegistry
import com.salesforce.revoman.harness.reasoning.ConfidencePolicy
* call (graph name + filled slots) against gold by value equality. Rejected (invalid) slot-fills
* predict no slots — a clean fail, never a silent partial pass.
*/
class BfclCheck(private val llm: LlmClient, private val tools: List<ToolDef>) {
class Orchestrator(
private val baseUrl: String,
private val tools: List<ToolDef>,
private val llm: LlmClient,
// ScoringLlmClient populates. A bare LlmClient leaves margin=0.0, causing all decisions to fall
// below threshold and always route to Clarify.
private val llm: ScoringLlmClient = ScoringLlmClient(),
private val policy: ConfidencePolicy = ConfidencePolicy(),
val text =
javaClass.classLoader.getResourceAsStream(resource)?.bufferedReader()?.readText()
?: error("Slot-fill gold not found on classpath: $resource")
@Suppress("UNCHECKED_CAST") val root = Yaml().load<Map<String, Any?>>(text)
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