Adds A2ANativeAgentFacade and A2AAgentServerResource in the agentspan module
to mirror the workflow-side A2A server at /api/a2a/agent/{name}. Native agents
are listed via AgentService, started via AgentService.start(), and execution
tracking reuses the same workflow-state mapping as the workflow side.
Closes#1407
Renames the default A2A server base path from /a2a to /api/a2a/workflow
to align with Conductor's /api prefix convention and reserve /api/a2a/agent
for future native-agent exposure.
* ci(test-harness): publish a named test-result summary check
The test-harness job passed only report_paths to mikepenz/action-junit-report, so
a run gave no summary of what actually ran — a failure showed up as Gradle's
"There were failing tests" and an exit code, with the counts only reachable by
downloading the report artifact.
Configures the action to surface a "Test-Harness tests" check reading
"N tests run, N passed, N skipped, N failed":
- check_name so the result is its own check rather than folded into the job
- detailed_summary for the per-suite breakdown
- check_title_template for readable annotation titles
- check_retries and flaky_summary to distinguish retried/flaky tests
- include_passed=false to keep the annotation volume down
fail_on_failure is intentionally not set: the Gradle step already fails the job,
so adding it would only mark a second step red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(test-harness): publish a named test-result summary check
The test-harness job's junit-report step parsed results correctly but could not
publish them:
JUnit Test Report - 338 tests run, 306 passed, 31 skipped, 1 failed.
Failed to create checks using the provided token.
(HttpError: Resource not accessible by integration)
ci.yml declares no permissions, so the job ran with the default read-only token
and the check run was rejected. fail_on_failure defaults to false, so the step
still went green and the failure was invisible — leaving Gradle's exit code as
the only signal of which test failed.
Grants the job checks: write, and names the check plus enables the per-suite
summary so the result is visible without opening the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: publish named test-result checks for unit-test, test-harness and e2e
The junit-report steps parsed results correctly but could not publish them:
Unit Test Report - 2802 tests run, 2713 passed, 89 skipped, 0 failed.
Failed to create checks using the provided token.
(HttpError: Resource not accessible by integration)
ci.yml declared no permissions, so these jobs ran with the default read-only
token and every check run was rejected. fail_on_failure defaults to false, so the
steps still went green and the failure was invisible — leaving Gradle's exit code
as the only signal of which test failed.
Grants checks: write to the three jobs that run tests, and names each check plus
enables the per-suite summary. e2e is a matrix, so its check name carries
matrix.name to keep one check per database combination instead of the legs
overwriting each other.
The build job is left alone: it runs with -x test and has no results to publish.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
- mirrors postgres scheduler config (@Qualifier postgresRetryTemplate)
- smoke-test fixture gets a mysqlRetryTemplate bean alongside postgresRetryTemplate
- scheduler flyway shares the schema (own history table, baselineVersion 0); main flyway
must tolerate a non-empty schema the same way or boot depends on bean-init order
- postgres: baselineVersion 0 (default 1 would silently skip V1__initial_schema if second)
- sqlite: baselineOnMigrate + baselineVersion 0 (had neither)
- trade-off: legacy DBs w/ tables but no history now fail on V1 "already exists" instead of
the baseline hint; same operator remedy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PRs touching ai/.../providers/** now run the live media suite (the exact
changes these tests exist to catch), while everything else in the repo
still never triggers a paid run. Concurrency group cancels superseded
runs on rapid pushes so spend cannot stack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- conductor.ai.grok.api-key now falls back XAI_API_KEY -> GROK_API_KEY,
so the org secret (GROK_API_KEY, which the tests already gate on) also
configures the server-side provider; XAI_API_KEY still wins when set.
- BedrockTest.IntegrationTests chat test: legacy bare claude-3-haiku id
-> current us. Haiku 4.5 inference profile (same two failures the
media test hit live: profile required, Legacy model blocked).
- GeminiVertexTest Vertex chat test: retired gemini-1.5-flash ->
gemini-2.5-flash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude-3-haiku is marked Legacy by the provider and Bedrock rejects it
after 30 days of inactivity ("Access denied. This Model is marked by
provider as Legacy"). Use the active Haiku 4.5 US inference profile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bearer-token auth works, but Bedrock rejects the bare model id for
on-demand throughput ("Retry your request with the ID or ARN of an
inference profile") — use the us. cross-region profile id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- OpenAITest / AzureOpenAITest: testChatCompletionWithImageMedia on
gpt-4o-mini — both ride OpenAIResponsesChatModel's input_image path
(pre-existing support; this locks it live at each endpoint).
- BedrockTest: new BearerIntegrationTests nest gated on
AWS_BEARER_TOKEN_BEDROCK (the dedicated org secret, matching the
server's conductor.ai.bedrock.bearerToken binding) — kept separate
from the access-key nest because the CI AWS keys are provisioned for
artifact publishing and may lack Bedrock permissions. Media flows via
Spring AI Converse image blocks; model claude-3-haiku (vision-capable,
same id as the existing bedrock tests).
- Workflow wires the new secrets (OPENAI, AZURE_OPENAI key+endpoint,
AWS_BEARER_TOKEN_BEDROCK, AWS_REGION).
Verified locally: Anthropic and OpenAI pass against the live APIs;
keyless providers skip. Azure/Bedrock get their first live run on this
PR's workflow run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds testChatCompletionWithImageMedia to the key-gated IntegrationTests
nests of Anthropic (#1238), Grok and Perplexity (#1243), and a new
GEMINI_API_KEY-gated ApiKeyIntegrationTests nest for Gemini (#1241,
AI-Studio mode — the mode the server uses with conductor.ai.gemini.api-key),
complementing the existing Cohere test (#1246). Each sends the repo's
token image (MELON7391) to a vision model and asserts transcription —
a pass proves the adapter attached the media; the token cannot be guessed.
Vision models verified against provider docs: claude-haiku-4-5,
gemini-2.5-flash, grok-4.5 (docs.x.ai image understanding), sonar
(Perplexity image attachments, data-URI image_url).
New workflow llm-media-integration.yml runs the suite on GitHub with the
org-level provider secrets (weekly cron + workflow_dispatch + PR runs on
workflow changes). Tests self-skip per absent key (COHERE_API_KEY is not
configured today), so the run stays green with per-provider status.
Verified locally: Anthropic passes against the live API; keyless
providers skip. Full gemini/grok/perplexity verification happens on the
PR's own workflow run via org secrets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cutoff was computed as Instant.now().minus(olderThanDays, DAYS)
with no bounds check. Very large values push the epoch negative, and
the search backend matched recent executions against the negative
bound, hard-deleting them. Non-positive values put the cutoff in the
future, matching every terminal execution. Reject olderThanDays < 1
and clamp the cutoff to epoch start, where it correctly matches
nothing. The cutoff computation is extracted into a VisibleForTesting
helper with an injectable clock for deterministic boundary tests.
Fixes#1331
Context: #1352 binds a toolCalls short-circuit into the same output-guardrail
scripts this branch's escalation fix touches
- neither test suite covered the combination: tool-call turn +
iteration already >= max_retries
Adds 2 tests to GuardrailEscalationScriptTest:
- tool-call turn (non-empty toolCalls) + iteration >= max_retries ->
must still pass
- short-circuit must win over escalate()
- model is mid-tool-use, never a raise candidate
- empty toolCalls array (real final turn) -> must NOT short-circuit,
escalation still fires
Matches the script ordering: toolCalls check runs before escalate().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug: compileToolGuardrailTasks fed every tool guardrail type
(custom/regex/llm) a constant iteration of "1", not the live DoWhile
counter.
- SDK worker / JS scripts check iteration >= maxRetries
- constant "1" -> onFail=RETRY never escalates
- loop runs to maxTurns, workflow completes instead of failing
Fix: pass ${<agent>_loop.output.iteration} instead of "1"
- same live-counter expression as the agent-level path (previous commit)
- one line fixes all guardrail types (fans out to custom SIMPLE task,
regex/llm scripts, normalizer)
Tests added:
- GuardrailCompilerTest: compiler wiring, live ref reaches all 3
guardrail types
- GuardrailEscalationScriptTest (new): executes
customGuardrailNormalizeScript() via real GraalJS
- same evaluator as production
- proves escalation logic itself (retry->raise, fix->raise only if
no fixed_output, tripwire-style raw output)
- no LLM, no workflow executor needed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes:
- live loop counter must be ${<loop>.output.iteration}, not bare ${<loop>.iteration}
- bare form resolves to null mid-loop
- touches: GuardrailCompiler (agent-level output guardrails), TerminationCompiler
(4 sites), MultiAgentCompiler round-robin selector
- customGuardrailNormalizeScript never escalated retry/fix -> raise
- wires iteration + max_retries into the normalize INLINE
- adds escalate() coercion (same as regex/llm scripts already had)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PostConstruct ran too early: ServiceConductorAgentClient could not initialize
because its chain (AgentService → WorkflowExecutorOps → SystemTaskRegistry
@DependsOn workerTaskAnnotationScanner → A2AWorkers) creates a circular
initialization dependency with A2AWorkers still in creation.
Replace @PostConstruct registerAgentClients() with a clients() accessor that
performs a double-checked lazy load on first AGENT/CANCEL_AGENT task execution,
at which point all Spring beans are fully initialized.
ServiceConductorAgentClient is gated on @ConditionalOnProperty(agentspan.embedded=true).
Without this flag the bean is absent, A2AWorkers#agentClients map has no 'conductor' entry,
and all agentType=conductor tasks fall through to executeRemote() → FAILED_WITH_TERMINAL_ERROR.
* Validate SWITCH javascript expressions by syntax only at registration
validateScriptExpression executed the expression via ScriptEvaluator.eval
with inputParameters as bindings - but at registration time those still
hold unresolved ${...} placeholders, so any expression operating on
runtime-bound values (e.g. calling array methods on a value that is a
placeholder string until resolution) threw and wrongly rejected valid
definitions, making the javascript evaluator unusable for dynamic
dispatch. Add ScriptEvaluator.validateScriptSyntax which parses the
source without executing (Context.parse) and rejects only genuine
syntax errors.
Fixes#1311
* Update MetadataServiceTest to a genuine syntax error
1>abcd is syntactically valid javascript that only fails at evaluation,
which registration no longer performs. Use a true syntax error so the
test keeps guarding rejection of malformed expressions.
A2ACallRequest documents agentName as the field for specifying a
conductor agent, but ConductorAgentDelegate re-parses task input as
AgentStartRequest which only has name. Adding @JsonAlias("agentName")
lets both field names work, preventing silent null and the misleading
"AGENT (conductor) requires 'name'" error.
Add conductor.a2a.server.expose-all=true property so every registered
workflow is automatically available as an A2A agent without requiring
per-workflow metadata (a2a.enabled=true) or explicit exposedWorkflows
config. Useful for development and single-tenant deployments.
Both classes are intentionally not annotated with @Component so they do not
auto-register until implemented. Each documents the expected rawConfig keys,
auth approach, and API mapping for the eventual implementor.
Adding BedrockAgentClient and AzureFoundryAgentClient as @Component beans
created three candidates for the unqualified @Autowired field. Qualify it
to serviceConductorAgentClient, which is the in-process implementation the
test is designed to exercise.
- Add agentType() to UnusedConductorAgentClient and FakeConductorAgentClient
(required by the new abstract method on the interface)
- Wrap single ConductorAgentClient args in List.of() in all test constructors
(non-Spring A2AWorkers constructor now takes List<ConductorAgentClient>)
- Stub conductorAgentClient.agentType() to "conductor" in A2ACancelWorkerTest
so the mock is registered under the expected routing key
The 'name' field is conductor-internal (identifies which embedded agent
workflow to call). External agents (Azure Foundry, Bedrock) don't use it
and would fail unnecessarily. Each client's startAgent() already validates
its own required fields. Kept the prompt check which is universal.
The constructor injection of List<ConductorAgentClient> caused a cycle:
A2AWorkers → ServiceConductorAgentClient → AgentService →
WorkflowServiceImpl → WorkflowExecutorOps → A2AWorkers.
Fix: Spring constructor now takes ApplicationContext instead of the
client list. A @PostConstruct method populates the agentClients map via
ApplicationContext.getBeansOfType() after all beans are constructed.
Non-Spring constructor (SDK/tests) is unchanged and still accepts a list.
Replace the placeholder A2A implementation with the real Azure AI Foundry
Agents REST API (OpenAI Assistants-compatible). The client now:
- POST /threads → POST /threads/{id}/messages → POST /threads/{id}/runs
to start an agent and return thread_id as executionId
- GET /threads/{id}/runs/{runId} to poll status
- Maps Azure states: completed→COMPLETED, requires_action→WAITING,
failed/expired→FAILED, queued/in_progress→RUNNING
- GET /threads/{id}/messages on completion to extract the response text
- POST /threads/{id}/runs/{runId}/submit_tool_outputs for tool results
- POST /threads/{id}/runs/{runId}/cancel on cancellation
- rawConfig.assistantId (or agentId) selects which assistant to use
SdkBytes.bytes() does not exist; use asUtf8String() directly on the
SdkBytes returned by PayloadPart.bytes(). Also removed the leftover
ConditionalOnProperty imports from BedrockAgentClient and
AzureFoundryAgentClient (annotations were removed earlier).
Add agentType() to ConductorAgentClient interface so each implementation
self-declares its routing key. A2AWorkers now takes List<ConductorAgentClient>,
builds a map at startup, and routes by agentType on the request — no
conditional logic, no @ConditionalOnProperty flags.
- conductor → ServiceConductorAgentClient (existing)
- bedrock → BedrockAgentClient (new)
- azure-foundry → AzureFoundryAgentClient (new)
- anything else → A2A remote path (existing fallback)
Adding a new platform is now: implement ConductorAgentClient, declare
agentType(), register as a Spring bean — nothing else changes.
- TokenProvider interface + StaticTokenProvider + OAuthTokenProvider (with
Entra ID factory, thread-safe token cache with 60s refresh buffer)
- AgentCredentialConfig model with Platform enum for extensibility
- credentialRef field on ConductorAgentStartRequest so callers can point
to a named secret without embedding credentials in the request
- BedrockAgentClient: streams Bedrock Agent Runtime responses, buffers
output, surfaces returnControl events as WAITING state; falls back to
default AWS credential chain when no credentialRef is set
- AzureFoundryAgentClient: A2A over HTTP with Entra ID bearer token auth;
per-execution state map so getAgentStatus/respond/cancelAgent all have
endpoint context without callers needing to pass it again
- KnownProviderEnvVars: added AZURE_CLIENT_ID/SECRET/TENANT_ID,
AZURE_FOUNDRY_ENDPOINT, BEDROCK_AGENT_ID/ALIAS_ID for auto-seeding