Skip to content

Eval observability embed spike - #300

Open
vishesh-orkes wants to merge 75 commits into
agentspan-embed-spikefrom
eval-observability-embed-spike
Open

Eval observability embed spike#300
vishesh-orkes wants to merge 75 commits into
agentspan-embed-spikefrom
eval-observability-embed-spike

Conversation

@vishesh-orkes

Copy link
Copy Markdown

Pull for the eval feature

vishesh-orkes and others added 30 commits May 20, 2026 22:26
- Python SDK: CorrectnessEval posts EvalSuiteResult to server after each run;
  EvalSuiteResult/EvalCaseResult/EvalCheckResult gain to_dict() serialization,
  prompt/output capture per case, strategy/ran_by metadata, name field
- Server: new eval storage layer (schema-eval.sql + schema-eval-postgres.sql)
  with eval_runs/eval_cases/eval_checks/eval_datasets tables; EvalController
  exposes POST/GET /api/eval/runs, /api/eval/runs/{id}, /api/eval/datasets
  and GET /api/eval/datasets/{name}; EvalService persists with @transactional
- Server: AgentService filters eval runs from production search by default;
  isEvalRun() handles both JSON and Conductor Map.toString() serialization
  formats; includeEvalRuns param restores them when requested
- UI: new Experiments section in sidebar with Eval Runs and Datasets pages;
  EvalRunsList shows pass-rate progress bar, Cases pass/fail badges, stats row,
  and search/filter bar; EvalRunDetail shows prompt, agent output, semantic score
  card, strategy/ran_by metadata; DatasetsList/DatasetDetail show read-only
  dataset cases
- UI: Agent Executions table gains Type column (Production/Eval chips) and
  "Show eval runs" toggle next to "Hide sub-agent executions"; eval rows shown
  at reduced opacity; footer links to Experiments → Eval Runs
- Tests: 19 Python unit tests (no LLM), EvalServiceTest for server persistence
Adds an optional OCG (Open Context Graph) retrieval sub-agent that the
main agent's LLM can delegate to when it decides it needs context. Whole
feature is gated on agentspan.ocg.url; unset means every OCG bean stays
out of the context and no behavior changes.

  - 7 OCG_* WorkflowSystemTask beans (OcgRequestTask) — one per OCG
    endpoint (query, get_entity, neighborhood, code_history,
    memory_set/reinforce/delete). Each proxies a single HTTP call with
    field projection + response capping (response-cap-chars, default
    8192) so a large graph traversal can't blow the model's context.
  - _ocg_agent workflow registered at startup by OcgSubAgentService —
    a normal AgentConfig built by OcgAgentFactory with the OCG system
    prompt and the seven ocg_* tools.
  - OcgAgentToolInjector silently appends an ocg_agent agent_tool to
    every top-level AgentConfig at compile time (skips self-injection
    on _ocg_agent and duplicate injection if a user already declared
    it). Main agent's LLM sees it as a peer it can call; tool call
    dispatches SUB_WORKFLOW(_ocg_agent) which runs its own LLM ↔
    ocg_* tool loop and returns a synthesized answer.
  - ToolCompiler TYPE_MAP + enrichment script (both static and
    dynamic variants) get an OCG bucket so ocg_* tools route to the
    right OCG_* task type at runtime.
Wires OCG_API_KEY env → agentspan.ocg.api-key → Authorization: Bearer
header on every OCG_* system task's HTTP request. Empty key keeps the
header off so unauthenticated local OCG instances still work.
Two intertwined changes that ended up in one commit because they touch
the same files:

1) Fix OCG end-to-end dispatch (was broken on the previous tip).

  - Register a TaskDef for each ocg_* tool name in OcgSubAgentService.
    Conductor resolves dynamic-fork tasks by name in the TaskDef
    registry; without the def, dispatch failed with
    "Cannot find task by name ocg_query in the task definitions".
  - Prefix every OCG endpoint with /api/v1 (was hitting
    /agent/query etc., the real paths are /api/v1/agent/query etc.).
    The OCG service returned grpc-gateway 404 NOT_FOUND on every call.

  With both fixes the full chain works: main LLM → ocg_agent
  SUB_WORKFLOW → ocg_query OCG_QUERY task → POST /api/v1/agent/query
  → real citations back.

2) Replace the OCG-specific Injector with a generic compiler hook.

  - AgentCompiler picks up any WorkflowDef whose metadata carries the
    agentspan.autoExposeAsTool flag and appends it as an agent_tool
    on every top-level compile. Self-recursion + duplicate guards live
    inside the merger. Optional @Autowired MetadataDAO keeps existing
    `new AgentCompiler()` test paths working unchanged.
  - OcgSubAgentService stamps the flag on _ocg_agent's metadata at
    startup. That's the *only* line that ties OCG to LLM visibility
    — everything else is generic.
  - OcgAgentToolInjector + its 5-test pinning class are deleted.
    AutoExposedToolsMergeTest (6 tests) replaces them and is
    deliberately not OCG-specific, so future server-side sub-agents
    rely on the same contract.
  - AgentService loses its @Autowired OcgSubAgentService field and
    the maybeInjectOcgAgentTool helper. resolveConfig is back to a
    plain normalize-or-passthrough.

Future server-side sub-agents (`_foo_agent`, `_bar_agent`, …) now drop
in by stamping the same metadata flag and registering their workflow.
No AgentCompiler change, no AgentService change, no per-feature
injection class.
Replace the switch-statement-heavy OcgRequestTask with a per-endpoint
strategy. Each operation owns its URL/method/body/projection in its own
class; the task itself is a thin orchestrator.

  - OcgOperation interface: taskType(), name(), build(), project()
  - Seven concrete operations under runtime/ocg/operation/ — one per
    endpoint (query, get_entity, neighborhood, code_history,
    memory_{set,reinforce,delete})
  - Three shared utilities in the same sub-package:
      OcgInputs — pick/required/intOrDefault/parseJsonLenient/writeJson
      OcgUri    — UriComponentsBuilder rooted at /api/v1
      OcgRequest — HttpRequest factory: base() / postJson() / get() / delete()
  - Apache Commons replaces hand-rolled helpers:
      StringUtils.removeEnd     → trim trailing slash
      StringUtils.abbreviate    → response cap (with custom marker) and
                                  log-body truncation
      Validate.isTrue           → required-input check
      NumberUtils.toInt         → string-to-int fallback
  - Spring UriComponentsBuilder handles URL encoding properly. One
    behavioural improvement: query params no longer over-encode ':' to
    '%3A' (it's not reserved in RFC 3986 query values). Updated the
    memoryDelete test assertion accordingly.
  - OcgRequestTask is now ~100 lines total. Largest method is 16 lines;
    every other method is under 10. No switch statements anywhere.
  - OcgRequestTaskConfig becomes one @bean per operation, each pairing
    a fresh OcgRequestTask with its strategy.

End-to-end smoke test on the live OCG dev instance still works — same
shape, same citations, same token count.
Replace OCG's bespoke @PostConstruct registration with a generic
two-bean registry that any future server-side sub-agent can plug into
without writing per-feature service code.

Generic infrastructure (runtime/registry/, OCG-agnostic):
  - RegisteredAgent          — interface; agentConfig() + autoExpose()
  - RegisteredAgentRegistrar — @PostConstruct picks up every bean,
                                compiles via AgentCompiler, stamps the
                                auto-expose marker when requested, and
                                writes to MetadataDAO
  - RegisteredTaskDefs        — interface; taskDefs()
  - RegisteredTaskDefsRegistrar — runs first via @dependsOn

OCG plug-in (just two @components — no @configuration wrapper):
  - OcgRegisteredAgent      — implements RegisteredAgent
  - OcgRegisteredTaskDefs   — implements RegisteredTaskDefs
  - OcgSubAgentService      — DELETED (responsibilities moved to the
                              two registrars; no per-feature
                              @PostConstruct anywhere now)

Both @components carry @ConditionalOnExpression rather than
@ConditionalOnProperty: the latter treats empty strings as
"present and not false", so empty OCG_URL would still instantiate
the beans and leak the auto-expose registration into the test DB.
The same conditional is now on OcgRequestTaskConfig for consistency.

Two new test classes (6 tests total) pin the contract every future
sub-agent relies on: compile, stamp-if-exposed, persist; no stamp
when autoExpose() returns null; empty supplier lists are no-ops.

End-to-end OCG smoke test on the live dev instance still works
identically — same shape, same citations, same token count.
The method was a 60-line block mashing six concerns: DAO fetch with
try/catch, existing-name collection, per-workflow metadata parsing,
self/duplicate guards, ToolConfig construction, and write-back.

Split into:
  - mergeAutoExposedTools — the loop, ~15 lines, zero control-flow
    keywords inside the body (no continue/break). Reads as: "fetch
    defs, collect taken names, for each def Optional.ifPresent build,
    commit if anything was added."
  - tryBuildAgentTool — per-workflow filter chain with named guard
    clauses; returns Optional<ToolConfig>
  - safelyFetchAllWorkflowDefs — the DAO call + warn-on-failure
  - collectToolNames — existing-name accumulation
  - readAutoExposeSpec — metadata parsing into a typed AutoExposeSpec
    record
  - buildAgentTool — ToolConfig construction
  - appendTools — copy-on-write list mutation

The two-pass "contains then add" dedup collapses into a single
takenNames.add(...) check that does both, halving the guard lines.

Behavior unchanged — 714 tests still green.
The OCG sub-agent's LLM was hallucinating date ranges
(e.g. picking 2023-10-10 as end_time when "today" is 2026-06-09),
filtering out fresh data and producing thin synthesis.

Anchor the LLM on a real date:
  - Add TODAY_PLACEHOLDER ({{TODAY}}) to OCG_SYSTEM_PROMPT
  - OcgAgentFactory.build() replaces it with LocalDate.now(UTC)
    at workflow-compile time
  - Prompt opens with "Today's date is <today> (UTC)" and explicit
    guidance to anchor relative ranges on it and omit ranges when
    none are implied

Refreshes on every server restart since RegisteredAgentRegistrar
recompiles _ocg_agent at startup. Long-running servers will drift —
fix can move to a runtime-resolved template var later if that matters.
The CI guard 'checkNoInlineFQN' caught the inline
java.time.LocalDate.now(java.time.ZoneOffset.UTC) call I added when
baking today's date into the OCG system prompt.
…, lazy cache

Three concerns from the OCG branch code review:

1. mergeAutoExposedTools used to fire on every recursive compile because
   compileSubAgent (and the graph-structure subgraph compile, and
   MultiAgentCompiler's swarm inner-workflow compile) all called back into
   the public compile() entry. The Javadoc claimed top-level-only — the
   code contradicted it. Nested specialist sub-agents silently inherited
   ocg_agent and the DAO got re-queried per nesting level.

   Public compile() now runs the merge then delegates to a new
   package-private compileWithoutAutoExpose() that does the strategy
   dispatch + post-processing. The three internal recursion sites switch
   to the non-merging entry. Pinned by mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion.

2. MetadataDAO was field-injected with @Autowired(required = false),
   forcing AutoExposedToolsMergeTest to use ReflectionTestUtils. Switched
   to constructor injection with a no-arg overload that preserves existing
   `new AgentCompiler()` call sites; AutoExposedToolsMergeTest setUp now
   uses `new AgentCompiler(metadataDAO)`.

3. safelyFetchAllWorkflowDefs hit the DAO on every compile. Registered
   server-side agents are written at @PostConstruct and don't change at
   runtime, so the per-request fetch was wasted work. Added a volatile
   List<AutoExposedEntry> cache with double-checked locking: successes
   cached for the lifetime of the bean, transient DAO failures left
   uncached so the next compile retries (matches the existing "merge is a
   convenience, not a correctness requirement" contract).

Three new tests in AutoExposedToolsMergeTest pin the contracts. Each was
verified failing against the broken state before landing the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Smaller review items from the OCG branch:

- OcgRequestTask.start used to catch Exception, which swallowed
  InterruptedException without re-flagging the current thread. A task
  cancelled mid-http.send would appear to "fail" silently and Conductor's
  executor would never observe the cancellation. Now catches
  InterruptedException separately and calls Thread.currentThread().interrupt()
  before failing. The remaining catch is IOException | RuntimeException so
  Error still propagates.

- OcgRequestTask.send/complete declared `throws Exception`; tightened to
  the actual checked exceptions. OcgOperation.build is also tightened from
  `throws Exception` to `throws IOException` (JsonProcessingException
  extends IOException, so the postJson-using operations still compile).

- Two missing OcgRequestTaskTest cases for the memory_set and
  memory_reinforce body shapes: memory_set must strip the server-side
  __agentspan_ctx__ glob before forwarding; memory_reinforce must only
  forward the four picked fields (no key, no __agentspan_ctx__, no rogue
  fields the LLM might attach). Tests read the actual HttpRequest body via
  a Flow.Subscriber helper.

- OcgAgentFactoryTest now pins the {{TODAY}} substitution and the absence
  of the literal placeholder in the rendered prompt.

Each new test was verified failing against an intentionally broken state
before landing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…he at boot

`RegisteredAgentRegistrar.register()` was calling `agentCompiler.compile()`,
which triggers the auto-expose merger's lazy DAO scan. During the registrar's
own `@PostConstruct` loop the to-be-registered agent isn't yet in the DAO,
so the merger cached an empty list and froze it for the bean's lifetime —
every user compile post-startup saw no auto-exposed tools and the OCG
sub-agent was silently invisible to every LLM. End-to-end smoke against
a live server confirmed the bug, then confirmed the fix.

- Promote `AgentCompiler.compileWithoutAutoExpose` to public; registrar
  now calls it so bootstrap never touches the merger cache.
- Extract `AutoExposedToolsMerger` (was ~140 lines inside `AgentCompiler`)
  as its own `@Component`. AgentCompiler keeps a thin
  `mergeAutoExposedTools` delegate for the existing test API.
- Add `RegisteredAgentBootstrapTest` — stateful in-memory `MetadataDAO`
  exercises register → user-merge ordering. Verified failing on pre-fix
  code, passing post-fix.
- Update `RegisteredAgentRegistrarTest` mocks to track the new entry point.
- Drop duplicate `agentspan.ocg.response-cap-chars` default from
  `application.properties` (already in `OcgProperties`).
- Simplify `OcgMemoryDeleteOperation.addQueryParamIfPresent`: drop the
  redundant `StringUtils.defaultString` wrapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI flagged a handful of line-wrap differences in files touched by the
previous fix. Local palantir-java-format 2.50.0 + Zulu JDK 21 throws
NoSuchMethodError so spotlessApply is unavailable here; applying the
hunks reported in the CI build log by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last hunk CI flagged after the prior fixup landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the silent ``openai/gpt-4o-mini`` default for ``agentspan.ocg.model``.
The right model depends on cost, latency, and the OCG corpus, so it has
to be an explicit operator decision rather than an inherited fallback.
``OcgAgentFactory.build`` now throws ``IllegalArgumentException`` with an
operator-actionable message when ``OCG_URL`` is set but ``OCG_MODEL`` is
blank, so boot fails fast instead of silently routing OCG traffic through
the wrong model.

``OCG_MODEL`` is documented as required (alongside ``OCG_URL``) in the
docs setup table; the optional-knobs table no longer lists it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ans, not a DAO scan

AutoExposedToolsMerger previously discovered auto-exposed sub-agents by
scanning the metadata store for WorkflowDefs stamped with a metadata
marker, caching the result forever. The data it was reading back came
from the same Spring context: the registrar wrote RegisteredAgent beans
into the DAO, then the merger scanned them back out. That round-trip is
what created the bootstrap-ordering trap (a compile during @PostConstruct
froze an empty cache for the bean's lifetime, silently hiding every
registered agent — see 879851f) plus the lazy cache, the
transient-failure retry carve-out, and the untyped metadata parsing.

Now the merger builds its entries from List<RegisteredAgent> at
construction, so it is complete before any compile can run and the trap
is structurally impossible. Deleted along the way:

- the AUTO_EXPOSE_AS_TOOL_METADATA_KEY wire protocol (stamping in the
  registrar, parsing in the merger, the re-export on AgentCompiler)
- the volatile cache + double-checked locking + failure-retry logic
- AgentCompiler's MetadataDAO constructor and mergeAutoExposedTools delegate

The registrar still persists each WorkflowDef — that's needed for
SUB_WORKFLOW dispatch by name — but it's now a dumb compile-and-persist
loop. A blank autoExpose tool name now fails fast at boot instead of
being silently skipped.

Tests rewritten to the bean-list model; the obsolete cache-behavior
tests are replaced by construction-time pins. Validated per CLAUDE.md by
mutating merge() into a no-op: exactly the injection-asserting tests
failed, guards stayed green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…time

OcgAgentFactory baked LocalDate.now() into the registered system prompt,
so the date anchor for "recent" / relative-range queries was the server's
boot date. On a long-running server the prompt drifts until it claims
last week (or last month) is "today", and the LLM bounds every relative
query against the wrong anchor — the exact hallucinated-date failure the
anchor was added to prevent.

Now the agent_tool dispatch script (which runs per execution) injects
__today__ = current UTC date into every sub-workflow's input, and the
OCG prompt references ${workflow.input.__today__}, substituted by
Conductor when the LLM task is scheduled. The injection is generic:
any future sub-agent prompt can use the same input.

Tests written first and confirmed red before the fix, per CLAUDE.md:
- OcgAgentFactoryTest pins the prompt to the runtime expression
- EnrichToolsScriptTest executes the real dispatch script in GraalJS and
  asserts the SUB_WORKFLOW input carries a yyyy-MM-dd __today__

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observed in execution 24fd619d (2026-06-11): asked to "catch me up on
the current state", the sub-agent issued an ocg_query with
end_time=2026-06-01 — ten days before today — silently dropping the
newest data. The __today__ runtime anchor was working (the substituted
prompt read "Today's date is 2026-06-11"); the model still closed the
window early because (a) the prompt's example showed a hardcoded
month-shaped window ending before today, which the model imitated, and
(b) nothing said what end_time should be for open-ended questions.

Prompt changes:
- Explicit rule: ranges extending to the present ("recent", "current
  state", "catch me up") set start_time and OMIT end_time; end_time is
  only for windows that closed in the past.
- The example's literal dates are gone — replaced with a relative
  "<today minus 30 days>" start and no end_time — so there are no
  stale calendar dates in the prompt for the model to anchor on.

Test written first and confirmed red, per CLAUDE.md: pins the omit-rule
text and asserts the prompt contains no literal yyyy-MM-dd dates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ystem_Task

# Conflicts:
#	server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java
#	server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java
#	server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java
NicholasDCole and others added 29 commits June 15, 2026 13:01
Rebased onto main after the credentials->secrets rename and the server
split (#271): server changes now live in conductor-agentspan, and the
Java SDK changes target the org.conductoross.conductor.ai.model package.

AgentHumanTask.start() and AgentService.getStatus() now read the
compiler-emitted tool_calls array (${llm.output.toolCalls}), normalise
each entry to {name, args}, and emit it as pendingTool.toolCalls. Legacy
singular tool_name/parameters keys are preserved (null) for back-compat.

Args precedence now matches the rest of the runtime
(JavaScriptBuilder: tc.inputParameters || tc.input): inputParameters is
canonical, input is the fallback. Server tests use the canonical
inputParameters shape and add explicit precedence coverage.

Java/TS SDKs gain typed PendingToolCall views; Python/C# inherit via the
raw pendingTool map.

Closes #226
…ks (#272)

* refactor(server): split into conductor-agentspan library + thin server; drop auth stack

Invert the AgentSpan/Conductor dependency so AgentSpan is a library Conductor
can depend on, with a thin standalone server over it.

- Two Gradle modules: conductor-agentspan (library) and conductor-agentspan-server
  (OSS runtime + bootJar). Old single-module src/ removed.
- Conductor artifacts are compileOnly in the library so the host supplies the
  engine version; the server brings the runnable OSS Conductor.
- Pin com.networknt:json-schema-validator to 1.0.73 — conductor-ai -> spring-ai
  drags in 2.0.0, which dropped com.networknt.schema.JsonSchema and broke
  conductor-common's JsonSchemaValidator bean (context-refresh hang on startup).
- Remove the unused auth/user enforcement stack (UserRepository, ApiKeyRepository,
  AuthController, AuthUserSeeder, AuthProperties) and the users/api_keys tables;
  auth was off by default and never enforced.
- Collapse the request principal to a String userId, delete User, and relocate the
  carrier from auth/ to a context/ package; AuthFilter sets an anonymous userId.
- Add design docs under server/docs/.

Full build green: 666 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(python-sdk): tokenize CLI command so full command lines work

The run_command tool (cli_allowed_commands) assumed `command` was the bare
executable, but LLMs routinely pass the whole command line
("gh repo list --limit 5"). os.path.basename then returned the entire string,
failing the whitelist check ("Command 'gh repo list ...' is not allowed"), and
non-shell exec tried to run a binary named after the full line. Tokenize with
shlex: validate on the executable, exec tokens + args. Adds tests covering the
full-command-line case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): rename Credentials sidebar entry to Secrets

Match the server-side secrets rename (/api/secrets). Label and id updated;
it already links to SECRETS_URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): remove dead secrets login gate; rebuild bundle for /api/secrets

AuthController and the auth-enforcement stack were removed server-side (OSS runs
anonymous; an embedding host like orkes supplies its own auth), so the secrets
page's login flow is obsolete and pointed at the deleted /api/auth/login.

- Drop LoginDialog, useSecretAuth, and useLogin (and the LoginRequest/Response
  types and their tests); SecretsPage runs anonymously with no token.
- Rebuild the served UI bundle so the browser stops calling the old
  /api/credentials endpoint and uses /api/secrets/v2. Verified: 0 /auth/login
  and 0 /credentials fetch paths in the new bundle.

UI tests: 455 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract storage/secret SPIs to library, push impls to server

Phase 1 of the library split: conductor-agentspan now defines the SPI contracts
+ logic; the concrete implementations live in conductor-agentspan-server, so an
embedding host (orkes) can supply its own.

- New dev.agentspan.runtime.spi package in the library: CredentialStoreProvider,
  SkillPackageStore (+ StoredSkillPackage), and a new SecretOutputMasker.
- Impls + infra moved to the server module: EncryptedDbCredentialStoreProvider,
  FileSystem/ConductorPayload skill stores, MasterKeyConfig, CredentialDataSourceConfig,
  CredentialSchemaMigrator, CredentialEnvSeeder, schema-credentials*.sql, and the
  no-op masker (CredentialOutputMasker -> NoOpSecretOutputMasker).
- Library logic (CredentialResolutionService, SkillRegistryService,
  CredentialMaskingResponseAdvice) depends only on the SPI interfaces; verified the
  library main references no concrete impl.
- Removed SkillRegistryService's convenience constructor that instantiated
  FileSystemSkillPackageStore; tests pass a store explicitly.

Decisions: ExecutionTokenService stays in the library (internal token protocol, not
a swappable backend); credential DataSource keeps @Primary (needed standalone;
embedding-time change deferred to Phase 4). Wiring still flows through @ComponentScan;
Phase 2 converts to auto-config with @ConditionalOnMissingBean.

Full build green: 666 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): self-register the library via Spring auto-configuration

Phase 2 of the library split: conductor-agentspan now registers its beans through
Spring Boot auto-configuration, so an embedding host (orkes) gets them without adding
dev.agentspan to its own component scan.

- New AgentSpanAutoConfiguration (@AutoConfiguration) component-scans dev.agentspan.runtime
  (excluding AgentRuntime and itself), exported via
  META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
- AgentRuntime now scans only the Conductor packages; AgentSpan beans come from the
  auto-config. No double-scan.
- Split-package design makes the scan register library beans always and server-extra
  beans (default SPI impls, web config) only when present — so standalone gets everything
  and an embedding host supplies its own SPI impls.

Phase 3 verification: full build green (666 tests, 0 failures); the standalone bootJar
boots in ~3s and serves (/api/agent/list 200, /api/secrets/v2 200, /actuator/health 200),
confirming the auto-config imports file is read from the nested library jar in the fat jar.

Deferred to Phase 4 (embedding): converting the @Primary task/listener/datasource
overrides to opt-in. They only conflict when embedded in orkes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(server): publish modules to Maven Central; tidy library deps

Publish conductor-agentspan and conductor-agentspan-server to Maven Central under
the org.conductoross.conductor group, mirroring the Java SDK's setup.

- Add com.vanniktech.maven.publish (apply-false at root, applied per module) with
  full POM metadata, coordinates org.conductoross.conductor:<module>:<version>, and
  conditional signing. Group/version set on subprojects; version from gradle.properties
  (default 0.1.0) or -Pversion=X in CI.
- Library jar publishes as conductor-agentspan(.jar); server's plain jar as
  conductor-agentspan-server(.jar) without the "-plain" classifier. The runnable
  fat jar (agentspan-runtime.jar) is still released via the S3/GitHub workflow.
- New release-server-maven.yml: workflow_dispatch with version, maven-central
  environment, publishAndReleaseToMavenCentral (publishes both modules), reusing the
  SONATYPE_*/SIGNING_* secrets.
- Javadoc made lenient (Xdoclint:none + failOnError=false) so Lombok's onConstructor_
  doesn't abort the Central-required javadoc jar.

Dependency hygiene for the published library artifact:
- Drop dead impl-layer deps from the library (spring-jdbc, HikariCP, sqlite-jdbc,
  spring-security-crypto) — moved to the server with the SPI impls in Phase 1 / removed
  with the auth stack.
- Library exports only the slf4j facade (not a log4j2 binding) and no springdoc; both
  are runtime/app concerns now declared explicitly in the server.

Verified: clean build green (666 tests); publishToMavenLocal produces correct
coordinates/POM/sources/javadoc for both modules; bootJar boots and serves
/api-docs, /api/agent/list, /api/secrets/v2 (all 200).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(java-sdk): migrate to Conductor client, fix e2e degradation, add framework coverage

## Core changes
- Migrate worker layer to official conductor-client 5.0.1 (TaskRunnerConfigurer + Worker
  with lease-extend heartbeat); delete hand-rolled WorkerHttp
- Use io.orkes ApiClient with native key/secret auth; remove serverUrl/authKey/authSecret
  from AgentConfig (they don't belong there)
- Move all public/internal classes to org.conductoross.conductor.ai / .internal packages
- Add AgentClient (peer of WorkflowClient) for /api/agent/* control-plane
- Add SseClient riding ApiClient.buildCall for SSE streaming
- Remove HttpApi.java entirely; all HTTP rides the conductor client

## e2e bug fixes
- Fix stateful-domain worker bug: domain change on re-registration now triggers
  runner rebuild so workers poll the correct per-execution queue (not the default)
- Remove redundant no-domain prepareWorkers call from runAsync/streamAsync that
  caused the domain-aware registration to be skipped

## e2e performance fixes (3h → 2m 44s)
- Fix MIN_WORKER_THREADS=16 ignoring configured threadCount; use MIN_THREADS_PER_WORKER=1
  so AgentConfig(100,1) gets 1 thread, not 16 (was 160 req/s to SQLite server)
- Add connectTimeout/readTimeout/writeTimeout to default ApiClient construction
  (was infinite; slow server responses blocked forever)
- waitForResult: fail after 10 consecutive errors with root cause in message instead
  of silently spinning to 600s timeout
- AgentRuntime.close() now calls conductorClient.shutdown() to evict OkHttp pool
- maxParallelForks=3 for e2e task: I/O-bound suites with unique names run concurrently

## Test coverage
- 204 unit tests (up from 39), 0 failures
- 340 e2e tests across 25 suites, 0 failures, 0 skipped
- New: WorkerManagerDomainTest (domain rebuild regression, proven counterfactual)
- New: WorkerManagerThreadCountTest (thread formula regression, proven counterfactual)
- New: AgentHandleErrorTest (fast-fail regression, @Timeout(5) proves counterfactual)
- New: AdkBridgeTest (ADK serialization unit, server-free)
- New: Suite11bOpenAIAgent (OpenAI framework e2e: tagging + compile + runtime)

## Formatting
- Add spotless with palantirJavaFormat 2.50.0; apply to src/main, src/test, e2e

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* updates

* fix(ci): correct server JAR path in e2e workflows

bootJar runs in server/ working-directory, so the jar lands at
conductor-agentspan-server/build/libs/ not build/libs/.
Upload and download paths were pointing at the wrong location,
causing all e2e jobs to fail with "Artifact not found: server-jar".

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk/spring): remove redundant auth props; depend on conductor-client-spring

AgentspanProperties had serverUrl/authKey/authSecret that duplicated what
conductor-client-spring's OrkesConductorClientAutoConfiguration already handles
via conductor.* properties. Removed them.

AgentspanAutoConfiguration no longer creates the ApiClient bean — it takes
the one wired by conductor-client-spring (after = OrkesConductorClientAutoConfiguration).
Our module now only owns the two Agentspan-specific knobs: workerPollIntervalMs
and workerThreadCount.

Add conductor-client-spring 5.0.1 as an api dependency so users get the
ApiClient auto-configuration transitively.

Users configure connectivity once:
  conductor.root-uri=http://localhost:6767/api
  conductor.security.client.key-id=my-key    # optional
  conductor.security.client.secret=my-secret # optional

And Agentspan worker tuning separately:
  agentspan.worker-poll-interval-ms=100
  agentspan.worker-thread-count=1

Also extend spotless coverage to spring/src/**/*.java.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* perf(e2e): parallelize Python and TypeScript e2e to target <5 min

Python: -n 1 → -n 3 --dist=loadgroup
- pytest-xdist was already installed but unused (single worker)
- xdist_group markers already protect credential suites (suite2/3/4/5
  serialize within "credentials" group; suite16 serializes within
  "cli-skills") — loadgroup enforces this correctly
- 13 independent suites spread across 3 workers; ~3× wall-clock reduction

TypeScript: maxForks 2 → 3
- All 23 suites use unique credential names so concurrent forks don't
  conflict (E2E_TS_CRED_A/B, GITHUB_TOKEN, MCP_AUTH_KEY_TS, HTTP_AUTH_KEY_TS)
- Suites 17/18 (the previously-cited "heavy" ones) don't use credentials
- Additional fork reduces the sequential tail per worker

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): add mkdocs documentation for Java SDK

14 pages covering the full public API:

  sdk/java/docs/
  ├── mkdocs.yml          — standalone site config (serves independently)
  ├── index.md            — overview, install, hello world
  ├── getting-started.md  — setup, first agent, first tool, streaming
  ├── spring-boot.md      — auto-configuration, properties, bean overrides
  ├── api-reference.md    — complete method signatures for all public classes
  ├── concepts/
  │   ├── agents.md       — Agent.builder() full reference, AgentRuntime API
  │   ├── tools.md        — @Tool, HTTP, MCP, CLI, human, PDF, media, agent tools
  │   ├── multi-agent.md  — all 7 strategies with examples
  │   ├── guardrails.md   — regex, LLM, custom; positions; OnFail actions
  │   ├── termination.md  — MaxMessage, StopMessage, TextMention, TokenUsage, composition
  │   ├── scheduling.md   — cron deploy, Schedule.builder(), Schedules API
  │   └── skills.md       — SKILL.md format, Skill.skill(), loadSkills()
  └── frameworks/
      ├── langchain4j.md  — @Tool POJO bridge, LangChainBridge
      ├── openai.md       — OpenAIAgent builder, handoffs, structured output
      └── google-adk.md   — AdkBridge.toAgentspan(), agentBuilder(), mapping table

docs/java-sdk → symlink to sdk/java/docs so root mkdocs.yml can include it.
Root mkdocs.yml gains a "Java SDK" nav section before Reference.

Standalone: cd sdk/java/docs && mkdocs serve
Main site:  mkdocs serve  (from repo root)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): add AgentClient control-plane API reference

Covers all 5 public methods with exact HTTP verbs, paths, and field-level
input/output documentation sourced from the server's StartRequest,
CompileResponse, StartResponse, AgentConfig, and ToolConfig DTOs:

  POST /api/agent/compile   → CompileResponse (workflowDef, requiredWorkers)
  POST /api/agent/deploy    → StartResponse (agentName, requiredWorkers)
  POST /api/agent/start     → StartResponse (executionId, agentName, requiredWorkers)
  GET  /api/agent/{id}/status → status, isComplete, isWaiting, pendingTool shape
  POST /api/agent/{id}/respond → HITL resume (204)
  GET  /api/workflow/{id}   → raw Conductor workflow (via WorkflowClient)

Also documents AgentConfig and ToolConfig field tables.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): clarify getWorkflow is post-completion enrichment, not polling

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): remove getWorkflow from AgentClient; use WorkflowClient directly

AgentClient owns the agentspan-proprietary /api/agent/* control-plane.
getWorkflow was fetching from the standard Conductor /api/workflow/* endpoint
via WorkflowClient — that belongs with WorkflowClient, not AgentClient.

Changes:
- AgentClient: remove getWorkflow(), workflowClient field, Workflow/WorkflowClient imports
- AgentHandle: inject WorkflowClient directly; call workflowClient.getWorkflow()
  with the typed Workflow/Task objects — eliminates the JSON roundtrip
  (Workflow → JSON string → Map) that the old delegation required
- AgentRuntime: construct WorkflowClient alongside AgentClient; pass it to
  every new AgentHandle(...)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): add method summary table to AgentClient API reference

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): sync AgentClient API doc with current implementation

- Remove getWorkflow from AgentClient methods table (deleted in previous refactor)
- Rename compile/deploy/start entries to match actual method names (compileAgent etc.)
- Reclassify getWorkflow section as 'WorkflowClient usage' — it is not an AgentClient
  method; AgentHandle calls WorkflowClient.getWorkflow directly
- Tighten AgentClient javadoc: explicit scope (five endpoints only), note that
  standard Conductor endpoints go through their own typed clients

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): rewrite AgentClient API doc with structural proof

Every method now shows exact field names sourced from the server DTOs
and verified against how AgentRuntime/AgentHandle parse the response:

compile  → server CompileResponse (workflowDef, requiredWorkers)
           SDK: plan() returns the raw map; callers get("workflowDef")

deploy   → server StartResponse (agentName, requiredWorkers; no executionId)
           SDK: deploy() reads resp.getOrDefault("agentName", agent.getName())

start    → server StartResponse (executionId, agentName, requiredWorkers)
           SDK: extractExecutionId() tries executionId → workflowId → id → correlationId

getAgentStatus → plain Map built from live Workflow object (not a DTO)
                 source fields documented (workflow.getStatus().name(), etc.)

respond  → void; SDK approve/reject/respond mapped to exact body shapes

Removed incorrect "CompileResponse" class reference in the table;
corrected return type to show the actual JSON shape.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): replace Map returns in AgentClient with typed POJOs

Every AgentClient method now has a proper return type instead of Map<String,Object>:

  compileAgent() → CompileResponse  { workflowDef, requiredWorkers }
  deployAgent()  → StartResponse    { executionId=null, agentName, requiredWorkers }
  startAgent()   → StartResponse    { executionId, agentName, requiredWorkers }
  getAgentStatus() → AgentStatusResponse { status, isComplete, isRunning, isWaiting,
                                           output, reasonForIncompletion, pendingTool }
  respond()      → void (unchanged)

New classes:
  model/CompileResponse.java       — public (returned by AgentRuntime.plan())
  internal/StartResponse.java      — internal; @JsonAlias for legacy executionId keys
  internal/AgentStatusResponse.java — internal; polled by AgentHandle
  internal/PendingTool.java        — internal; nested in AgentStatusResponse

Callers updated:
  AgentRuntime.plan()  → returns CompileResponse
  AgentRuntime.deploy() → reads resp.getAgentName() instead of Map.getOrDefault()
  AgentRuntime.startAsync() → reads resp.getExecutionId() directly; extractExecutionId() deleted
  AgentHandle.waitForResult / isWaiting / waitUntilWaiting / buildResult → typed getters
  AgentStream.waitForResult / buildResultFromStatus → typed getters
  Agentspan.plan() → returns CompileResponse
  BaseTest.getAgentDef(CompileResponse) → uses plan.getWorkflowDef()
  15 e2e suites → Map<String,Object> plan = → CompileResponse plan =

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): sync AgentClient API doc with POJO refactor

Update all stale references from the Map<String,Object> era:
- Methods table now shows Java return types (CompileResponse, StartResponse,
  AgentStatusResponse, void) instead of JSON shape strings
- compileAgent: response section uses CompileResponse getters, not plan.get()
- deployAgent: uses resp.getAgentName() not Map.getOrDefault()
- startAgent: extractExecutionId() reference removed (deleted); shows
  StartResponse.getExecutionId() + @JsonAlias legacy key handling
- getAgentStatus: response described as AgentStatusResponse (typed POJO),
  not 'plain Map<String,Object>'; PendingTool fields added with getters
- respond: return type shown as void, not '204 No Content'
- WorkflowClient section: notes Workflow/Task typed objects, not Map walk

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): type AgentClient input Maps as AgentRequest and RespondBody

AgentClient no longer takes Map<String,Object> as input:

  compileAgent(AgentRequest) — was Map<String,Object>
  deployAgent(AgentRequest)  — was Map<String,Object>
  startAgent(AgentRequest)   — was Map<String,Object>
  respond(String, RespondBody) — was (String, Map<String,Object>)

New internal classes:

  AgentRequest — matches server StartRequest field-for-field:
    - agentConfig / framework + rawConfig (mutually exclusive, native vs framework)
    - prompt, sessionId, runId, staticPlan (@JsonProperty("static_plan"))
    - media, context, idempotencyKey, credentials, skillRef, timeoutSeconds
    - @JsonInclude(NON_NULL) — null fields omitted from wire
    - Factory: AgentRequest.nativeAgent(map) / frameworkAgent(fw, map)

  RespondBody — replaces Map<String,Object> for /respond:
    - RespondBody.approve() / approve(comment) / reject(reason) / of(map)
    - @JsonAnyGetter flattens extra fields to top level (MANUAL strategy)

AgentRuntime: all three payload-building HashMap blocks replaced with
agentRequest(agent, serialized).prompt(...).runId(...).build()

AgentHandle, AgentStream: approveBody/rejectBody helpers deleted;
all respond calls use RespondBody factories.

Structural proof: AgentRequest @JsonProperty names verified against
server StartRequest field names (including "static_plan" alias).

docs(java-sdk): update agent-client-api.md with AgentRequest/RespondBody
  - Methods table shows Java input types
  - AgentRequest field mapping table (SDK → JSON key → server field)
  - RespondBody factory → wire JSON table
  - static_plan mismatch explained (@JsonProperty on both sides)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): replace Map fields in AgentRequest with Agent and Plan types

AgentRequest now holds SDK types instead of pre-serialized Maps:

  agentConfig: Agent  (was Map<String,Object>)
  rawConfig:   Agent  (was Map<String,Object>)
  staticPlan:  Plan   (was Map<String,Object>)

Serialization to the server's wire format is handled by two new
Jackson JsonSerializer inner classes:

  AgentConfigSerializer.AsJson — applied via @JsonSerialize on
    agentConfig and rawConfig fields; calls serialize(agent) and
    writes the result so the server's AgentConfig DTO sees the
    correct camelCase map.

  Plan.AsJson — applied via @JsonSerialize on staticPlan; calls
    plan.toJson() so the server's PAC consumes the same format as
    Python/TypeScript.

AgentRuntime: serialize() calls removed from plan/deploy/startAsync.
The AgentConfigSerializer field and instance are gone from the runtime
— serialization now happens inside AgentRequest/Jackson, not at the
call site. agentRequest(Agent) no longer takes a pre-serialized Map.

context: Map<String,Object> stays — intentionally free-form pass-through.

docs: AgentRequest table updated with Java types and serializer column.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): eliminate agentConfig/rawConfig duplication; add Framework enum

AgentRequest previously had two Agent-typed fields (agentConfig, rawConfig)
that were mutually exclusive and duplicated the @JsonSerialize annotation.
Replaced with a single agent: Agent field + Framework enum discriminator.

AgentRequest.Serializer handles the key decision:
  framework == null  → "agentConfig": serialize(agent)
  framework != null  → "framework": fw.wireValue(), "rawConfig": serialize(agent)

All JSON logic is now in one place. @JsonProperty / @JsonInclude / @JsonSerialize
annotations removed from individual fields — the Serializer owns everything.

Framework enum (enums package, public):
  OPENAI("openai"), GOOGLE_ADK("google_adk"), SKILL("skill")
  @JsonValue on wireValue() → serializes to the server-expected string
  Framework.of(String) → Optional<Framework> for safe conversion

AgentRuntime.agentRequest(Agent):
  Framework.of(agent.getFramework())
    .map(fw -> AgentRequest.frameworkAgent(fw, agent))
    .orElseGet(() -> AgentRequest.nativeAgent(agent))

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* feat(java-sdk): add missing Framework enum values to match all server normalizers

Framework.of(String) already returns Optional.empty() for unknown values,
so adding these is safe — existing code that sets langchain/langgraph via
.framework(string) on Agent will now resolve to the typed enum instead of
falling through to the native path.

Added:
  LANGCHAIN("langchain")       — LangChainNormalizer
  LANGGRAPH("langgraph")       — LangGraphNormalizer
  VERCEL_AI("vercel_ai")       — VercelAINormalizer
  CLAUDE_AGENT_SDK("claude_agent_sdk") — ClaudeAgentSdkNormalizer

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): sync agent-client-api.md with Framework enum + AgentRequest refactors

- AgentRequest section rewritten: single 'agent: Agent' field replaces
  the old agentConfig/rawConfig duplication; Serializer table shows
  how it writes 'agentConfig' vs 'framework'+'rawConfig'
- Framework enum table: all 7 values with wire string and server normalizer
- framework type corrected: String → Framework enum throughout
- Removed stale references: AgentConfigSerializer.AsJson/@JsonSerialize
  on fields, @JsonInclude(NON_NULL) on class, Plan.AsJson on field —
  all of these are now inside AgentRequest.Serializer, not annotations
- compileAgent code example updated to use Framework.OPENAI enum constant
- Structural proof updated to reference gen.writeObjectField('static_plan',...)
  instead of @JsonProperty annotation

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): add AgentRuntime API reference

Covers the full public surface of AgentRuntime — the primary SDK entry point:

  Constructors (4 overloads) + environment variables table
  ApiClient factories: clientFromEnv / client(url) / client(url, key, secret)
    - connectTimeout=10s, readTimeout=30s, writeTimeout=30s baked in

  run / runAsync          — sync + async; Plan overload for PLAN_EXECUTE
  start / startAsync      — fire-and-forget; AgentHandle methods table
  stream / streamAsync    — SSE event iteration; AgentEvent fields table;
                            event-targeted approve/reject for HITL sub-executions
  plan                    — CompileResponse; delegates to AgentClient.compileAgent
  deploy / deployAsync    — idempotent registration; Schedule reconciliation overload
  serve                   — blocking worker mode; SIGTERM shutdown hook
  resume / resumeAsync    — crash recovery / reconnect to existing execution
  schedules               — lazy Schedules accessor
  shutdown / close        — stops workers + releases OkHttp pool

  AgentConfig table: workerPollIntervalMs, workerThreadCount + env vars
  Thread safety: one shared instance per app, shutdown hook pattern

Wired into standalone mkdocs.yml and root mkdocs.yml under "API Reference".

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): add Agent field reference + fix missing Javadoc

Agent.java — three builder methods that had no Javadoc:
  handoffs(List/varargs) — SWARM handoff triggers; reference OnTextMention/OnToolResult
  allowedTransitions(Map) — SWARM transfer restrictions
  framework(String) — reference Framework enum; explain wire value convention
  frameworkConfig(Map) — explain spread-at-top-level behaviour

sessionId field-level comment: explicitly notes it is NOT in agentConfig
(execution parameter, not compilation parameter) — the one subtle field
that users most commonly misplace.

agent-structure.md — complete field-by-field reference:
  - 40-row mapping table: Java field → JSON key → server AgentConfig → Python
  - "NOT in agentConfig" table: sessionId, stateful, framework, frameworkConfig,
    4 callback functions — documents WHY each is excluded
  - "In server but not Java" table: description, memory, reasoningEffort,
    maskedFields, contextWindowBudget
  - "In Python but not Java" table: memory, dependencies, reasoning_effort,
    masked_fields
  - Serialization rules: strategy emission guard, framework dispatch paths,
    synthesize default, plannerContext validation, callback serialization
  - Defaults comparison: Java builder vs server

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): correct gate and enable_planning — both exist in Python

gate (TextGate) and enable_planning are present in the Python Agent class
(sdk/python/src/agentspan/agents/agent.py lines 360, 369, 527, 589).
The 'Not in Python' table was wrong — correct to show Python equivalents:
  gate → gate (TextGate)
  enablePlanning → enable_planning

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): validate and correct 'not in Java' field claims

Verified by reading:
  sdk/python/src/agentspan/agents/agent.py (field definitions)
  sdk/python/src/agentspan/agents/config_serializer.py (serialization)
  server/conductor-agentspan/src/main/java/.../AgentConfig.java

Corrections:
  memory, reasoning_effort, masked_fields, context_window_budget — exist
    in Python Agent AND are serialized to the server; genuine Java gaps;
    moved to 'In Python (and server) but not in Java' table with JSON keys
  description — exists in server AgentConfig ONLY; neither Java nor Python
    Agent exposes it (set by UI/platform); moved to its own table
  gate / enable_planning — already corrected in previous commit

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): correct serialization notes — stateful/sessionId/callbacks ARE emitted

The 'Fields NOT in compiled agentConfig' section was wrong about 6 of 8 entries.
Verified by reading AgentConfigSerializer.java:

  sessionId    → agentMap.put("sessionId", ...) at line 232 — IS emitted
  stateful     → agentMap.put("stateful", true) at line 466 — IS emitted
  4 callbacks  → emitted in agentConfig.callbacks list as task entries

Only framework (dispatch key) and frameworkConfig (merged/spread) are
genuinely not emitted under their own keys in agentConfig.

Renamed section to 'Serialization notes for potentially surprising fields'
and corrected each row to reflect what actually happens. Also fixed the
two stale rows in the main mapping table (stateful, sessionId).

Answer to the user question: this does NOT create bugs. All fields are
intentionally handled. The previous section title was misleading and
most of its content was factually incorrect.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(java-sdk): 3-pass accuracy review — fix all API mismatches vs source

Verified every code claim across all 17 docs against actual source. Fixes:

Tool registration (4 files): @Tool POJOs use ToolRegistry.fromInstance(obj),
  NOT AgentTool.from(obj). AgentTool.from takes an Agent (sub-agent → tool).
  Fixed getting-started, agents, tools, langchain4j + api-reference.

Guardrails (agents, guardrails, google-adk): GuardrailDef.regex()/llm()/of()
  static factories do NOT exist. Rewrote to the real RegexGuardrail.builder()
  / LLMGuardrail.builder() (return GuardrailDef) and GuardrailDef.builder().func()
  for custom. OnFail values corrected: RAISE/RETRY/FIX/HUMAN (not BLOCK/WARN).
  Import corrected: ai.model.GuardrailDef (not ai.guardrail.GuardrailDef).

Credentials: Credentials.get(name) takes no ToolContext arg (4 sites).

CliConfig: package is ai.execution (not ai.tools); builder has no .command()
  — only enabled/allowedCommands/timeout/workingDir/allowShell.

Scheduling: runNow(ScheduleInfo) not runNow(String); previewNext(cron, n) not
  nextNExecutions(wireName, n). Fixed scheduling, api-reference, agent-runtime-api.

Return types: plan() → CompileResponse (not Map); AgentResult.getOutput() → Object
  (+ getOutput(Class)); removed non-existent getRawResult(); AgentEvent.getResult()
  → Object. Removed non-existent no-arg reject(); added isWaiting()/respond(Map).

Strategy enum: added ROUND_ROBIN, RANDOM (9 values total).

PLAN_EXECUTE: Op.generate takes a Generate object, not boolean — rewrote the
  multi-agent example to the real args + Ref("stepId") wiring pattern.

Defaults: timeoutSeconds default is 0 (server applies its own), not 600.

Links: self-hosting link depth corrected (../ not ../../) for the integrated site.

Final sweep confirms: 0 residual defect patterns; all doc imports resolve to
real source files; all framework bridge signatures match.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* feat(java-sdk): add memory, reasoningEffort, maskedFields, contextWindowBudget to Agent

Closes the four field gaps documented in agent-structure.md — Python and the
server had these; Java Agent now does too.

New on Agent.builder():
  .memory(ConversationMemory)   → "memory": {messages, maxMessages}  (server MemoryConfig)
  .reasoningEffort(String)      → "reasoningEffort"   ("low"|"medium"|"high")
  .maskedFields(String.../List) → "maskedFields"      (redacted in history/UI)
  .contextWindowBudget(int)     → "contextWindowBudget" (proactive condensation)

New class model/ConversationMemory.java — messages + maxMessages, with
addUser/addAssistant/addSystem chaining helpers (mirrors Python ConversationMemory).

AgentConfigSerializer emits all four with null/empty guards; JSON keys verified
against server AgentConfig field names and Python config_serializer output.

Tests (SerializerTest, +2): parity_fields_serialized (proven to fail without the
serialization) and parity_fields_absent_when_unset. 206 unit tests, 0 failures.

docs: agent-structure.md moves these into the main mapping table; the
"Python but not Java" section now lists only `dependencies`. api-reference.md
Agent.Builder gains the four methods.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* test(java-sdk/e2e): add round-trip e2e for memory/reasoningEffort/maskedFields/contextWindowBudget

Adds 4 structural plan() tests to Suite17NewParity (Order 19-22), matching the
suite's existing no-LLM pattern: build agent with the field set → runtime.plan()
(real /agent/compile) → assert the field survives the
SDK → server → compiled-output round-trip.

- test_reasoning_effort_serialized  → agentDef.reasoningEffort == "high"
- test_context_window_budget_serialized → agentDef.contextWindowBudget == 8000
- test_masked_fields_serialized → maskedFields (agentDef OR WorkflowDef — server
    maps it to WorkflowDef.maskedFields, so valueFromPlan() checks both)
- test_memory_serialized → agentDef.memory.{maxMessages, messages}

No LLM (CLAUDE.md). Compiles against real APIs. Requires a live server to run;
the exact echo location per field is confirmed on first run (make-fail validation).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor(java-sdk/e2e): rename Suite17NewParity → Suite17ConfigSerialization

"NewParity" described when the tests were added, not what they verify. The suite
asserts that agent config fields/features round-trip into the compiled agentDef via
plan(). Renamed file + class accordingly and fixed the stale "Suite 11" javadoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(java-sdk): trim agent-structure.md to a clean field reference

Drop the "Cross-Layer Proof" framing, the defensive "all fields serialize
correctly / none are missing" reassurances, and the cross-SDK parity-proof
sections. Keep the factual reference: fields, builder methods, JSON keys,
serialization behavior, and defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(java-sdk): add agent-schema.json wire contract + verified proof

Canonical JSON Schema (Draft 2020-12) for the agentConfig that SDKs serialize
and POST to the server. Reconciled from the server AgentConfig model (the
deserialization target) and both SDK serializers, verified in 3 rounds:

1. Static inventory of server model + nested configs + both SDK emit sets.
2. Direct-source verification of cross-SDK discrepancies — corrected the
   onFail enum (retry|raise|fix|human), strategy nullability, sessionId/
   planSource channel divergences, reasoningEffort value set.
3. Empirical: maximal agent serialized by BOTH SDKs validates against the
   schema; 6 negative mutations all rejected (schema has teeth); the
   Java-emitted, schema-valid config compiles on a live server (HTTP 200).

agent-schema.md documents the schema and carries the formal proof
(soundness/completeness/type-consistency) and the known cross-SDK divergences.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(java-sdk/e2e): fix ScheduleIntegrationTest silently skipping under standard env

The suite appends its own "/api" to AGENTSPAN_SERVER_URL, but that env var
conventionally INCLUDES "/api" (per BaseTest and CI). The mismatch produced a
double "/api", so the @EnabledIf scheduler probe 404'd and all 10 tests skipped
under the normal convention — i.e. they never ran. Normalize the base URL by
stripping a trailing "/api" so the suite runs under either form.

Verified: with the standard /api env the suite went from 10 skipped → 10 run, 0
failures; full e2e is now 346 tests, 0 skipped, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(java-sdk): reverse-verify agent-schema via generated dataclass + record

generate.py reverse-engineers agent-schema.json into a Python dataclass and a
Java record, then proves correctness by diffing the generated models against the
server AgentConfig models and validating a generated instance:

  (a) generated fields ≡ schema (root + 16 nested $defs)
  (b) generated instance validates against the schema
  (c) every server field — root + all 13 nested models — is in the schema (0 gaps)

The reverse pass confirms nothing was missed. Only surfaced one undocumented
SDK-only extra, cliConfig.workingDir (Java emits it; server CliConfig ignores it),
now noted in the divergences alongside the tool retry fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(server): OCG sub-agent — system tasks + agent-driven auto-injection

Adds an optional OCG (Open Context Graph) retrieval sub-agent that the
main agent's LLM can delegate to when it decides it needs context. Whole
feature is gated on agentspan.ocg.url; unset means every OCG bean stays
out of the context and no behavior changes.

  - 7 OCG_* WorkflowSystemTask beans (OcgRequestTask) — one per OCG
    endpoint (query, get_entity, neighborhood, code_history,
    memory_set/reinforce/delete). Each proxies a single HTTP call with
    field projection + response capping (response-cap-chars, default
    8192) so a large graph traversal can't blow the model's context.
  - _ocg_agent workflow registered at startup by OcgSubAgentService —
    a normal AgentConfig built by OcgAgentFactory with the OCG system
    prompt and the seven ocg_* tools.
  - OcgAgentToolInjector silently appends an ocg_agent agent_tool to
    every top-level AgentConfig at compile time (skips self-injection
    on _ocg_agent and duplicate injection if a user already declared
    it). Main agent's LLM sees it as a peer it can call; tool call
    dispatches SUB_WORKFLOW(_ocg_agent) which runs its own LLM ↔
    ocg_* tool loop and returns a synthesized answer.
  - ToolCompiler TYPE_MAP + enrichment script (both static and
    dynamic variants) get an OCG bucket so ocg_* tools route to the
    right OCG_* task type at runtime.

* feat(ocg): send bearer auth header on OCG requests

Wires OCG_API_KEY env → agentspan.ocg.api-key → Authorization: Bearer
header on every OCG_* system task's HTTP request. Empty key keeps the
header off so unauthenticated local OCG instances still work.

* refactor(server): generic auto-expose mechanism + fix OCG dispatch

Two intertwined changes that ended up in one commit because they touch
the same files:

1) Fix OCG end-to-end dispatch (was broken on the previous tip).

  - Register a TaskDef for each ocg_* tool name in OcgSubAgentService.
    Conductor resolves dynamic-fork tasks by name in the TaskDef
    registry; without the def, dispatch failed with
    "Cannot find task by name ocg_query in the task definitions".
  - Prefix every OCG endpoint with /api/v1 (was hitting
    /agent/query etc., the real paths are /api/v1/agent/query etc.).
    The OCG service returned grpc-gateway 404 NOT_FOUND on every call.

  With both fixes the full chain works: main LLM → ocg_agent
  SUB_WORKFLOW → ocg_query OCG_QUERY task → POST /api/v1/agent/query
  → real citations back.

2) Replace the OCG-specific Injector with a generic compiler hook.

  - AgentCompiler picks up any WorkflowDef whose metadata carries the
    agentspan.autoExposeAsTool flag and appends it as an agent_tool
    on every top-level compile. Self-recursion + duplicate guards live
    inside the merger. Optional @Autowired MetadataDAO keeps existing
    `new AgentCompiler()` test paths working unchanged.
  - OcgSubAgentService stamps the flag on _ocg_agent's metadata at
    startup. That's the *only* line that ties OCG to LLM visibility
    — everything else is generic.
  - OcgAgentToolInjector + its 5-test pinning class are deleted.
    AutoExposedToolsMergeTest (6 tests) replaces them and is
    deliberately not OCG-specific, so future server-side sub-agents
    rely on the same contract.
  - AgentService loses its @Autowired OcgSubAgentService field and
    the maybeInjectOcgAgentTool helper. resolveConfig is back to a
    plain normalize-or-passthrough.

Future server-side sub-agents (`_foo_agent`, `_bar_agent`, …) now drop
in by stamping the same metadata flag and registering their workflow.
No AgentCompiler change, no AgentService change, no per-feature
injection class.

* refactor(java-sdk): remove Agentspan static facade; AgentRuntime is the entry point

The Agentspan static facade wrapped a process-global singleton AgentRuntime with
a JVM shutdown hook — a crutch that hurt testability/DI and was the odd one out
(AgentRuntime is already AutoCloseable, and the Spring module auto-configures an
agentRuntime bean). Users now either construct an AgentRuntime (try-with-resources)
or inject the Spring bean.

- Move the native-framework drop-in overloads (run/start/stream/deploy/serve/plan/
  resume accepting a raw ADK BaseAgent, LangChain4j ChatModel, or LangGraph4j
  AgentExecutor.Builder) + the coerceAgent/reflection helpers ONTO AgentRuntime as
  instance methods, so the runtime is now the complete API. No capability lost.
- Delete Agentspan.java.
- Rewrite 147 examples to construct a main-local AgentRuntime and call runtime.*.
- Update README, framework-bridge Javadoc, and docs to AgentRuntime.

Builds: core + examples + spring compile; unit tests 206/0/0. AgentRuntime change
is purely additive, and e2e suites already use AgentRuntime directly (unaffected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): remove UserProxyAgent — non-functional parity stub

UserProxyAgent.create(...) only stamped inert metadata (_agent_type=user_proxy,
_human_input_mode, _default_response) that NOTHING consumes — the server has no
user-proxy/human-input-mode handling, so the documented "pauses with a HumanTask
and waits for real human input" behaviour never happened. It produced a plain
LLM agent with misleading metadata, was used only by its own tests (no examples,
no production path), and its class Javadoc example referenced a 1-arg create(String)
overload that didn't even exist. Real HITL is HumanTool / WaitForMessageTool /
MANUAL strategy.

Deletes UserProxyAgent.java + its 2 unit tests (SerializerTest) + 1 e2e test
(Suite17ConfigSerialization) and the stale references.

Builds green; full unit+e2e 343/0/0 against a live server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: remove UserProxyAgent from Python, TypeScript, and C# SDKs

Cross-SDK parity with the Java removal: UserProxyAgent only stamped inert
_agent_type=user_proxy / _human_input_mode / _default_response metadata that the
server never interprets, so the documented "pauses for human input" behaviour
never happened. Real HITL is human_tool / wait-for-message / MANUAL strategy.

Removed in each SDK: the class, its export, its dedicated example (27_*),
its kitchen-sink usage, and its tests — keeping GPTAssistantAgent intact.
Also updated current API docs (docs/python-sdk/*), the SDK READMEs/CHANGELOG,
the python validation group, top-level README, and AGENTS.md.

Left untouched: historical design records under docs/sdk-design/ and
docs/design/ (dated artifacts; flagged separately).

Verified: Python 1653 unit tests pass; TypeScript builds + 822 unit tests pass;
C# builds + 161 tests pass. Repo grep clean outside the design docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: scrub UserProxyAgent from design records (sdk-design + design plans/specs)

Completes the cross-SDK removal: the design docs no longer reference UserProxyAgent
now that it's gone from all SDKs. Removed its sections, feature-matrix/example rows,
source-tree listing mentions, prose list items, and the editorial_reviewer kitchen-sink
participant across the multi-language design, TS/Java/Go/Ruby/Kotlin docs, and the
2026-03 plans/specs. GPTAssistantAgent and all other content kept intact; historical
feature/section numbering left as-is.

Repo-wide grep for UserProxyAgent/user_proxy is now zero (outside build artifacts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor

* refactor(java-sdk): unify credential access onto ToolContext; drop static Credentials

Tool secrets were read via a public static thread-local accessor (Credentials.get),
inconsistent with every other per-call value (ids, shared state) which arrives on the
injected ToolContext — and it leaked framework-only setForCall/clearForCall onto a public
class. Now a tool reads ctx.getCredential(name) / getCredentialOrNull(name) on its
ToolContext, one injected object, no public framework hooks.

- ToolContext gains an IMMUTABLE per-call credential snapshot + getCredential/
  getCredentialOrNull/getCredentials. Multi-threading: because the snapshot lives on the
  context object (not a thread-local), a thread the tool spawns can read it, and it stays
  valid after the worker thread clears the transport — the old static silently failed
  off-thread.
- New internal CredentialContext (thread-local) carries resolved secrets from
  WorkerManager → ToolRegistry on the worker thread (secrets never enter task input/output).
- Deleted the public Credentials class; updated Example16, e2e Suite2, WorkerCredentialFetcher
  javadoc; replaced CredentialsTest with ToolContextCredentialsTest.

Verified: full unit+e2e 344/0/0 (incl. Suite2 credential round-trip on a live server +
secret store). Make-fail: dropping the snapshot fails 4 unit tests incl. the multi-thread
guarantee, confirming the suite has teeth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(java-sdk): remove ClaudeCode — non-functional stub in Java

The Java ClaudeCode class advertised a PermissionMode it silently dropped:
toModelString() encodes only "claude-code/{model}", the serializer never emits
permissionMode, and the server never reads it — so new ClaudeCode("opus", BYPASS)
had zero effect. Worse, Java has no client-side claude-agent-sdk worker (Python runs
claude-code agents via a local claude_agent_sdk worker and emits a passthrough stub;
Java emits a plain native agent and has no worker to execute it), so the class
couldn't deliver a working feature at all. Used only by tests; no examples.

Deletes ClaudeCode.java + its 3 unit tests (SerializerTest) + 2 e2e tests
(Suite17ConfigSerialization) and the import/javadoc references. Targeting a
claude-code-capable server is still possible via a raw model string if ever wired.

Python keeps its ClaudeCode — there permission_mode IS consumed by the
claude_agent_sdk runtime worker, so it's functional and stays.

Compiles (core + e2e + examples); unit tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(java-sdk): drop ClaudeCode test references (completes prior commit)

The previous commit deleted ClaudeCode.java but a failed `git add` left these two
test files out, so that revision didn't compile. This removes the ClaudeCode unit
tests (SerializerTest) and e2e tests + import/javadoc (Suite17ConfigSerialization)
that referenced the deleted class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(java-sdk): keep optional framework types out of AgentRuntime signatures; fix spring prop prefix

CI java-sdk-tests failed: :spring:test threw NoClassDefFoundError for
org.bsc.langgraph4j...AgentExecutor$Builder. Root cause: moving the TYPED
LangChain4j/LangGraph4j drop-in overloads onto AgentRuntime put compileOnly
framework types into the core class's method signatures. Spring introspects the
AgentRuntime bean's methods (resolving every parameter type), which force-loads
those optional types — absent from the spring classpath. The old Agentspan facade
had the same overloads but Spring never loaded it, so the issue was latent.

Fix: AgentRuntime now exposes only Object-typed drop-ins. The native LangChain4j
ChatModel / LangGraph4j AgentExecutor.Builder are detected reflectively in
coerceAgent (by FQN, same pattern as ADK BaseAgent) and built in method BODIES, so
no compileOnly type appears in any signature — Spring introspection is safe. Added
run/runAsync/start/stream(Object, String, Object... tools) for the tool-POJO form;
fixed-arity overloads still win resolution (internal null calls cast to Plan).
Examples calling runtime.run(model|builder, prompt[, tools]) are unchanged.

Also fixes a separate spring-rename mismatch this unmasked: AgentProperties was
@ConfigurationProperties(prefix="conductor.agent") while its tests and own Javadoc
use the agentspan.* prefix — restored prefix to "agentspan".

Verified: :test + :spring:test (5/0/0) green; examples + e2e compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ocg): strategy pattern + Apache Commons cleanup

Replace the switch-statement-heavy OcgRequestTask with a per-endpoint
strategy. Each operation owns its URL/method/body/projection in its own
class; the task itself is a thin orchestrator.

  - OcgOperation interface: taskType(), name(), build(), project()
  - Seven concrete operations under runtime/ocg/operation/ — one per
    endpoint (query, get_entity, neighborhood, code_history,
    memory_{set,reinforce,delete})
  - Three shared utilities in the same sub-package:
      OcgInputs — pick/required/intOrDefault/parseJsonLenient/writeJson
      OcgUri    — UriComponentsBuilder rooted at /api/v1
      OcgRequest — HttpRequest factory: base() / postJson() / get() / delete()
  - Apache Commons replaces hand-rolled helpers:
      StringUtils.removeEnd     → trim trailing slash
      StringUtils.abbreviate    → response cap (with custom marker) and
                                  log-body truncation
      Validate.isTrue           → required-input check
      NumberUtils.toInt         → string-to-int fallback
  - Spring UriComponentsBuilder handles URL encoding properly. One
    behavioural improvement: query params no longer over-encode ':' to
    '%3A' (it's not reserved in RFC 3986 query values). Updated the
    memoryDelete test assertion accordingly.
  - OcgRequestTask is now ~100 lines total. Largest method is 16 lines;
    every other method is under 10. No switch statements anywhere.
  - OcgRequestTaskConfig becomes one @Bean per operation, each pairing
    a fresh OcgRequestTask with its strategy.

End-to-end smoke test on the live OCG dev instance still works — same
shape, same citations, same token count.

* refactor(server): generic registry for server-registered agents

Replace OCG's bespoke @PostConstruct registration with a generic
two-bean registry that any future server-side sub-agent can plug into
without writing per-feature service code.

Generic infrastructure (runtime/registry/, OCG-agnostic):
  - RegisteredAgent          — interface; agentConfig() + autoExpose()
  - RegisteredAgentRegistrar — @PostConstruct picks up every bean,
                                compiles via AgentCompiler, stamps the
                                auto-expose marker when requested, and
                                writes to MetadataDAO
  - RegisteredTaskDefs        — interface; taskDefs()
  - RegisteredTaskDefsRegistrar — runs first via @DependsOn

OCG plug-in (just two @Components — no @Configuration wrapper):
  - OcgRegisteredAgent      — implements RegisteredAgent
  - OcgRegisteredTaskDefs   — implements RegisteredTaskDefs
  - OcgSubAgentService      — DELETED (responsibilities moved to the
                              two registrars; no per-feature
                              @PostConstruct anywhere now)

Both @Components carry @ConditionalOnExpression rather than
@ConditionalOnProperty: the latter treats empty strings as
"present and not false", so empty OCG_URL would still instantiate
the beans and leak the auto-expose registration into the test DB.
The same conditional is now on OcgRequestTaskConfig for consistency.

Two new test classes (6 tests total) pin the contract every future
sub-agent relies on: compile, stamp-if-exposed, persist; no stamp
when autoExpose() returns null; empty supplier lists are no-ops.

End-to-end OCG smoke test on the live dev instance still works
identically — same shape, same citations, same token count.

* refactor(compiler): split mergeAutoExposedTools into focused helpers

The method was a 60-line block mashing six concerns: DAO fetch with
try/catch, existing-name collection, per-workflow metadata parsing,
self/duplicate guards, ToolConfig construction, and write-back.

Split into:
  - mergeAutoExposedTools — the loop, ~15 lines, zero control-flow
    keywords inside the body (no continue/break). Reads as: "fetch
    defs, collect taken names, for each def Optional.ifPresent build,
    commit if anything was added."
  - tryBuildAgentTool — per-workflow filter chain with named guard
    clauses; returns Optional<ToolConfig>
  - safelyFetchAllWorkflowDefs — the DAO call + warn-on-failure
  - collectToolNames — existing-name accumulation
  - readAutoExposeSpec — metadata parsing into a typed AutoExposeSpec
    record
  - buildAgentTool — ToolConfig construction
  - appendTools — copy-on-write list mutation

The two-pass "contains then add" dedup collapses into a single
takenNames.add(...) check that does both, halving the guard lines.

Behavior unchanged — 714 tests still green.

* feat(ocg): bake current date into OCG sub-agent system prompt

The OCG sub-agent's LLM was hallucinating date ranges
(e.g. picking 2023-10-10 as end_time when "today" is 2026-06-09),
filtering out fresh data and producing thin synthesis.

Anchor the LLM on a real date:
  - Add TODAY_PLACEHOLDER ({{TODAY}}) to OCG_SYSTEM_PROMPT
  - OcgAgentFactory.build() replaces it with LocalDate.now(UTC)
    at workflow-compile time
  - Prompt opens with "Today's date is <today> (UTC)" and explicit
    guidance to anchor relative ranges on it and omit ranges when
    none are implied

Refreshes on every server restart since RegisteredAgentRegistrar
recompiles _ocg_agent at startup. Long-running servers will drift —
fix can move to a runtime-resolved template var later if that matters.

* fix(ocg): import java.time classes instead of inline FQN

The CI guard 'checkNoInlineFQN' caught the inline
java.time.LocalDate.now(java.time.ZoneOffset.UTC) call I added when
baking today's date into the OCG system prompt.

* Add docs

* refactor(compiler): top-level-only auto-expose, constructor injection, lazy cache

Three concerns from the OCG branch code review:

1. mergeAutoExposedTools used to fire on every recursive compile because
   compileSubAgent (and the graph-structure subgraph compile, and
   MultiAgentCompiler's swarm inner-workflow compile) all called back into
   the public compile() entry. The Javadoc claimed top-level-only — the
   code contradicted it. Nested specialist sub-agents silently inherited
   ocg_agent and the DAO got re-queried per nesting level.

   Public compile() now runs the merge then delegates to a new
   package-private compileWithoutAutoExpose() that does the strategy
   dispatch + post-processing. The three internal recursion sites switch
   to the non-merging entry. Pinned by mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion.

2. MetadataDAO was field-injected with @Autowired(required = false),
   forcing AutoExposedToolsMergeTest to use ReflectionTestUtils. Switched
   to constructor injection with a no-arg overload that preserves existing
   `new AgentCompiler()` call sites; AutoExposedToolsMergeTest setUp now
   uses `new AgentCompiler(metadataDAO)`.

3. safelyFetchAllWorkflowDefs hit the DAO on every compile. Registered
   server-side agents are written at @PostConstruct and don't change at
   runtime, so the per-request fetch was wasted work. Added a volatile
   List<AutoExposedEntry> cache with double-checked locking: successes
   cached for the lifetime of the bean, transient DAO failures left
   uncached so the next compile retries (matches the existing "merge is a
   convenience, not a correctness requirement" contract).

Three new tests in AutoExposedToolsMergeTest pin the contracts. Each was
verified failing against the broken state before landing the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(ocg): preserve interrupt flag, tighten throws, pin body shapes

Smaller review items from the OCG branch:

- OcgRequestTask.start used to catch Exception, which swallowed
  InterruptedException without re-flagging the current thread. A task
  cancelled mid-http.send would appear to "fail" silently and Conductor's
  executor would never observe the cancellation. Now catches
  InterruptedException separately and calls Thread.currentThread().interrupt()
  before failing. The remaining catch is IOException | RuntimeException so
  Error still propagates.

- OcgRequestTask.send/complete declared `throws Exception`; tightened to
  the actual checked exceptions. OcgOperation.build is also tightened from
  `throws Exception` to `throws IOException` (JsonProcessingException
  extends IOException, so the postJson-using operations still compile).

- Two missing OcgRequestTaskTest cases for the memory_set and
  memory_reinforce body shapes: memory_set must strip the server-side
  __agentspan_ctx__ glob before forwarding; memory_reinforce must only
  forward the four picked fields (no key, no __agentspan_ctx__, no rogue
  fields the LLM might attach). Tests read the actual HttpRequest body via
  a Flow.Subscriber helper.

- OcgAgentFactoryTest now pins the {{TODAY}} substitution and the absence
  of the literal placeholder in the rendered prompt.

Each new test was verified failing against an intentionally broken state
before landing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(compiler): stop registrar from snapshotting empty auto-expose cache at boot

`RegisteredAgentRegistrar.register()` was calling `agentCompiler.compile()`,
which triggers the auto-expose merger's lazy DAO scan. During the registrar's
own `@PostConstruct` loop the to-be-registered agent isn't yet in the DAO,
so the merger cached an empty list and froze it for the bean's lifetime —
every user compile post-startup saw no auto-exposed tools and the OCG
sub-agent was silently invisible to every LLM. End-to-end smoke against
a live server confirmed the bug, then confirmed the fix.

- Promote `AgentCompiler.compileWithoutAutoExpose` to public; registrar
  now calls it so bootstrap never touches the merger cache.
- Extract `AutoExposedToolsMerger` (was ~140 lines inside `AgentCompiler`)
  as its own `@Component`. AgentCompiler keeps a thin
  `mergeAutoExposedTools` delegate for the existing test API.
- Add `RegisteredAgentBootstrapTest` — stateful in-memory `MetadataDAO`
  exercises register → user-merge ordering. Verified failing on pre-fix
  code, passing post-fix.
- Update `RegisteredAgentRegistrarTest` mocks to track the new entry point.
- Drop duplicate `agentspan.ocg.response-cap-chars` default from
  `application.properties` (already in `OcgProperties`).
- Simplify `OcgMemoryDeleteOperation.addQueryParamIfPresent`: drop the
  redundant `StringUtils.defaultString` wrapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: apply spotless / palantir-java-format violations from CI

CI flagged a handful of line-wrap differences in files touched by the
previous fix. Local palantir-java-format 2.50.0 + Zulu JDK 21 throws
NoSuchMethodError so spotlessApply is unavailable here; applying the
hunks reported in the CI build log by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: collapse remaining spotless violation in OcgRequestTaskTest

Last hunk CI flagged after the prior fixup landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ocg): require OCG_MODEL explicitly when OCG is enabled

Drop the silent ``openai/gpt-4o-mini`` default for ``agentspan.ocg.model``.
The right model depends on cost, latency, and the OCG corpus, so it has
to be an explicit operator decision rather than an inherited fallback.
``OcgAgentFactory.build`` now throws ``IllegalArgumentException`` with an
operator-actionable message when ``OCG_URL`` is set but ``OCG_MODEL`` is
blank, so boot fails fast instead of silently routing OCG traffic through
the wrong model.

``OCG_MODEL`` is documented as required (alongside ``OCG_URL``) in the
docs setup table; the optional-knobs table no longer lists it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* agentdef

* Support Agentspan Embedded in Orkes Conductor  (#273)

* Spotless

* Spotless

* refactor(compiler): source auto-exposed tools from RegisteredAgent beans, not a DAO scan

AutoExposedToolsMerger previously discovered auto-exposed sub-agents by
scanning the metadata store for WorkflowDefs stamped with a metadata
marker, caching the result forever. The data it was reading back came
from the same Spring context: the registrar wrote RegisteredAgent beans
into the DAO, then the merger scanned them back out. That round-trip is
what created the bootstrap-ordering trap (a compile during @PostConstruct
froze an empty cache for the bean's lifetime, silently hiding every
registered agent — see 879851f8) plus the lazy cache, the
transient-failure retry carve-out, and the untyped metadata parsing.

Now the merger builds its entries from List<RegisteredAgent> at
construction, so it is complete before any compile can run and the trap
is structurally impossible. Deleted along the way:

- the AUTO_EXPOSE_AS_TOOL_METADATA_KEY wire protocol (stamping in the
  registrar, parsing in the merger, the re-export on AgentCompiler)
- the volatile cache + double-checked locking + failure-retry logic
- AgentCompiler's MetadataDAO constructor and mergeAutoExposedTools delegate

The registrar still persists each WorkflowDef — that's needed for
SUB_WORKFLOW dispatch by name — but it's now a dumb compile-and-persist
loop. A blank autoExpose tool name now fails fast at boot instead of
being silently skipped.

Tests rewritten to the bean-list model; the obsolete cache-behavior
tests are replaced by construction-time pins. Validated per CLAUDE.md by
mutating merge() into a no-op: exactly the injection-asserting tests
failed, guards stayed green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ocg): anchor the sub-agent's "today" at execution time, not boot time

OcgAgentFactory baked LocalDate.now() into the registered system prompt,
so the date anchor for "recent" / relative-range queries was the server's
boot date. On a long-running server the prompt drifts until it claims
last week (or last month) is "today", and the LLM bounds every relative
query against the wrong anchor — the exact hallucinated-date failure the
anchor was added to prevent.

Now the agent_tool dispatch script (which runs per execution) injects
__today__ = current UTC date into every sub-workflow's input, and the
OCG prompt references ${workflow.input.__today__}, substituted by
Conductor when the LLM task is scheduled. The injection is generic:
any future sub-agent prompt can use the same input.

Tests written first and confirmed red before the fix, per CLAUDE.md:
- OcgAgentFactoryTest pins the prompt to the runtime expression
- EnrichToolsScriptTest executes the real dispatch script in GraalJS and
  asserts the SUB_WORKFLOW input carries a yyyy-MM-dd __today__

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ocg): tell the model to omit …
The typescript-unit-tests CI job's `npm audit --omit=dev --audit-level=high`
gate was failing (also red on main) on two high-severity transitive CVEs
that ship to users:

- undici <=7.27.2 (TLS validation bypass, header injection, cache
  poisoning, DoS) — pulled via @io-orkes/conductor-javascript
- ws 8.0.0-8.20.1 (memory-exhaustion DoS) — deduped transitive

Pin both to patched versions via npm `overrides` (undici ^7.28.0,
ws ^8.21.0) — same major, drop-in. Lockfiles regenerated and kept
consistent.

Verified: `npm ci` + `npm run build` + `npx vitest run tests/unit/`
(823 tests) all pass, and the audit gate now exits 0. Remaining
moderate/low advisories (uuid, esbuild) are below the high threshold
and do not fail the gate.
The python-e2e job drives a real server + real LLM, so individual tests
flake nondeterministically on transient conditions — workflow still
RUNNING at the client timeout, tool-call batches not returning, LLM
phrasing variance. A single transient failure currently fails the whole
job; observed runs failed a *different* unrelated test each time
(test_after_tool_callback_executes, test_http_lifecycle, ...).

Add pytest-rerunfailures and run e2e with --reruns 2 --reruns-delay 5.
A genuinely broken test still fails all 3 attempts; a one-off flake
recovers. Does not mask real regressions, just transient infra/LLM
noise.

Dep added to the dev extra; uv.lock updated (pytest-rerunfailures 16.3).
…es) (#278)

The python-e2e suite drives a real server + real LLM, so individual
tests flake nondeterministically on transient conditions — workflow
still RUNNING at the client timeout, tool-call batches not returning,
LLM phrasing variance. A single transient failure currently fails the
whole job; observed runs failed a *different* unrelated test each time
(test_after_tool_callback_executes, test_http_lifecycle@credentials, ...).

Add pytest-rerunfailures and mark every e2e item flaky(reruns=2,
reruns_delay=5) via the e2e conftest. A genuinely broken test still
fails all 3 attempts; a one-off flake recovers. Configured in conftest
(not the CI yaml) so it also covers local e2e runs and needs no
workflow-file change.

dev extra + uv.lock updated (pytest-rerunfailures 16.3).
# Conflicts:
#	sdk/python/src/agentspan/agents/ocg.py
…282)

release-server-maven.yml only had a workflow_dispatch trigger, so creating a release (e.g. v0.2.0) never published the server modules. Add a release: [created] trigger with the same version-extraction the Python/JS SDK workflows use, so it fires automatically on release and stays manually dispatchable.

Also publish both modules (conductor-agentspan, conductor-agentspan-server) to GitHub Packages: add a GitHubPackages maven repository to the server Gradle build (publishAllPublicationsToGitHubPackagesRepository) and a separate least-privilege publish-github-packages job (packages: write + GITHUB_TOKEN, no Sonatype/signing secrets).
The guardrail matrix's #1 aout_regex_retry hit TIMEOUT in CI while its
raise/fix siblings passed. It's the only CC-regex spec that drives the
agent-level retry loop, and INST_CC unconditionally ordered the model to
emit the card number verbatim with no redact-on-retry clause — so every
retry re-emitted the CC, the regex re-blocked, and it burned all maxTurns.
Combined with the new LLM_CHAT_COMPLETE task-level retry (retryCount=3,
exponential backoff) under 27-way concurrency, that tipped it past the
300s polling budget.

- Make INST_CC retry-friendly (echo verbatim first, redact on retry) so #1
  converges in ~2 turns — same pattern already used for INST_PROC/INST_SECRET.
- Widen the poll budget 300s -> 480s for headroom on the LLM retry backoff,
  still under the 600s beforeAll/describe timeouts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite design-doc §9.2 around the three concrete ways a consumer picks
an AgentSpan + Conductor pair, replacing the open-ended "every consumer
of the published library" framing:

- Mode A (standalone server): drift-free by construction (single
  conductorVersion, lib+server same commit).
- Mode B (external OSS self-embed): build from source against your
  engine (recommended) inherits Mode A's by-construction guarantee;
  take-the-published-jar + self-certify is the best-effort fallback,
  with a Conductor-Built-Against manifest breadcrumb for diagnosability.
- Mode C (orkes enterprise embed): one host-pinned pair, host-certified.

Add a forward-reference from §6 (build/classpath mechanics) to the §9.2
mode table (ownership per deployment) so the sections stay in sync.

The README "Releasing" section documents the matching release-time
convention (single conductorVersion, build-against fact not a range).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §9.3 (upgrade & adoption): version upgrade = data + ops (two schema
  lifecycles, in-flight/HITL deserialization, restore-from-backup);
  adoption = additive with >= same-major engine-direction rule
  (Mode A check-or-fall-back-to-B, B aligned-by-construction, C
  auto-enforced by orkes' release line); SPI fail-fast; removal asymmetry.
- New agentspan-validation-readiness.md: gap analysis for the
  testing/deployment/integration-validation task. Notes the SDK e2e
  suite is already black-box/endpoint-parameterizable (instrument
  exists), documents the three orkes-reuse options (A works today, C
  conformance container does not exist yet), and flags security
  validation + post-deploy smoke as the real uncovered gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, CLI, python -m) (#279)

* fix(python-sdk): support `python -m agentspan` via __main__.py

Windows users (esp. when the install's Scripts dir isn't on PATH) fall back
to `python -m agentspan ...` and hit:
  No module named agentspan.__main__; 'agentspan' is a package and cannot be
  directly executed
because there was no __main__.py — the CLI was only reachable through the
`agentspan` console script (entry point agentspan.cli:main).

Add agentspan/__main__.py that calls cli.main(), so `python -m agentspan doctor`
is equivalent to `agentspan doctor` and works without the Scripts/bin dir on PATH.
Verified locally: `python -m agentspan doctor` and `--help` now run (exit 0).

Adds a deterministic test that __main__ re-exports cli.main and that importing
it is side-effect free (validated fail-then-pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(python-sdk): harden Windows/cross-platform developer experience

Follow-up to the __main__.py fix, from a full audit of the fresh-developer
path (install -> invoke -> run). The Windows CLI binary IS published; the
defects were in our Python layer.

- pyproject: cap requires-python to >=3.10,<3.14 and drop the stale 3.9
  classifier. Native deps (grpc via conductor-python, pydantic-core) lack
  wheels for brand-new Python, so `pip install` on 3.14 fell back to source
  builds and failed — the exact error the Windows dev on 3.14 hit.
- cli binary download (cli/__init__.py): was the fragile core of `agentspan`.
  Now: atomic download (temp file + os.replace) so a partial/interrupted
  download can't leave a corrupt binary cached and executed forever; friendly,
  actionable errors for 404 (unsupported/unpublished platform) and
  network/proxy failures instead of raw tracebacks; skip POSIX chmod on
  Windows; treat zero-byte cache as missing; AGENTSPAN_FORCE_DOWNLOAD=1 to
  recover from a bad cache; main() prints a clean error instead of crashing.
- README: Windows venv activation + Python version guidance + the
  `python -m agentspan` fallback.

Tests (deterministic, no network; validated fail-then-pass): atomic success
leaves no temp, partial download is cleaned up (no corrupt binary), and 404 /
network failures map to friendly RuntimeErrors.

Deferred (separate PR): 13 asyncio.get_event_loop() call sites that are
deprecated and break on 3.14 — needs a careful, tested sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion-alignment

docs(server): scope version-drift to three consumption modes
Updates package.json name, all import references in src/, tests/,
examples/, docs/, and root-level design/README files.
…ty-215

# Conflicts:
#	sdk/python/src/agentspan/agents/runtime/http_client.py
#	server/conductor-agentspan-server/src/main/resources/schema-eval-postgres.sql
#	server/conductor-agentspan-server/src/main/resources/schema-eval.sql
#	ui/src/components/Sidebar/sidebarCoreItems.tsx
#	ui/src/routes/routes.tsx
#	ui/src/utils/constants/route.ts
…reated-by

- Thread a `dataset` field end-to-end (SDK run(dataset=...) → server eval_runs
  column + migration → EvalRunDto → UI) so an eval run links back to the stored
  dataset it came from; eval-run detail shows a clickable "Dataset: X"
- Auto-run semantic_criteria as an LLM-judge check: semantic.judge_output()
  returns (score, reason); _run_case records an EvalCheckResult with score +
  reasoning, skips gracefully when litellm is absent, and surfaces judge errors
  as a failed check
- Store the anonymous OSS user id as null so the UI shows the friendly
  "Ran by: <script>" instead of an all-zeros UUID
- Add pizza_support_eval.py: one script that pushes a dataset and runs the eval
  linked to it
- Tests: dataset round-trip (EvalServiceTest), semantic wiring (4 new unit
  tests, LLM judge patched — no real calls)
…bed-spike

# Conflicts:
#	sdk/python/src/agentspan/agents/runtime/http_client.py
#	sdk/python/src/conductor/ai/agents/_internal/token_utils.py
#	sdk/python/src/conductor/ai/agents/frameworks/claude_agent_sdk.py
#	sdk/python/src/conductor/ai/agents/frameworks/langchain.py
#	sdk/python/src/conductor/ai/agents/frameworks/langgraph.py
#	sdk/python/src/conductor/ai/agents/runtime/runtime.py
#	sdk/python/tests/unit/test_sse_client.py
#	sdk/python/tests/unit/test_token_utils.py
#	server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java
#	server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java
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.

6 participants