Commit Graph

55 Commits

Author SHA1 Message Date
bradyyie e2ff613424 refactor(agentspan,core): address review nits
- Extract shared buildContextInjectionTask/injectContextIntoUserMessage on
  AgentCompiler and reuse across both compilers (3 duplicated sites); the
  swarm loop now honors the configured context size limits instead of
  hardcoded defaults, and the user-message rewrite uses a ListIterator
  instead of index bookkeeping.
- Extract parentHandsOffOnToolOf predicate for the swarm turn-skip check.
- Join: move the agent output-shaping explanation onto prepareAgentOutput's
  javadoc and return unmodifiable maps for the constructed shapes
  (LinkedHashMap kept over Map.of — tool outputs may contain nulls; the
  pass-through branch stays live to preserve default JOIN behavior).
2026-08-12 15:16:31 -04:00
nicholascole 1393277352 fix(agentspan,ui-next): fix swarm handoff turn-skip scoping and agent diagram rendering
agentspan:
- Only suppress an agent's post-tool-call turn when a parent on_tool_result
  handoff actually watches a tool that agent owns, instead of suppressing it
  for every sub-agent in the swarm whenever any handoff targets any tool.
  Otherwise a sub-agent's own tool result was never summarized in words
  because its loop ended before the LLM saw the output.
- Add a ctx_inject task to the resumed per-turn loop so a turn that does
  keep looping (per the above fix) can see prior state/tool-results instead
  of just the bare prompt, mirroring AgentCompiler's existing wiring.

ui-next:
- Guard AgentExecutionDiagram's response sublabel against undefined text so
  a tool-call-only turn renders an empty sublabel instead of the literal
  string "undefined".
- In agentExecutionUtils, render handoffs (explicit transfer, condition-based
  via handoff_check, or HANDOFF-strategy routing) for real sub-agent turns,
  which previously showed nothing between agents handing off work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 15:09:46 -04:00
nicholascole 09842ab16b revert(agentspan): remove redundant tool result markers 2026-08-12 15:09:46 -04:00
nicholascole 07b6861fda Remove redundant MCP tool-spec secret rewrite 2026-08-12 15:09:46 -04:00
nicholascole 50ac267126 style(agentspan): preserve existing spacing 2026-08-12 15:09:46 -04:00
nicholascole da370dd676 refactor(agentspan): remove OCG-derived MCP options 2026-08-12 15:09:46 -04:00
nicholascole 695b71d108 fix(agentspan): preserve MCP tool outputs 2026-08-12 15:09:46 -04:00
Nicholas Cole 5c16c85322 feat(agents): aggregate execution token usage (#1484) 2026-08-05 11:39:04 -07:00
Shailesh Padave 073720885c fix(#1437): strip whitespace from env-var API keys in AgentspanAIModelProvider
Keys read from environment variables (e.g. via export KEY=$(cat keyfile) or
.env parsers) can carry a trailing newline, producing a cryptic Java HTTP
client error: "Unexpected char 0x0a in Authorization value".

AgentspanAIModelProvider.getSystemEnv() now strips surrounding whitespace
from System.getenv() results. A new package-private readRawEnv() hook makes
the trimming testable without PowerMock; existing tests that override
getSystemEnv() are unaffected.

The direct System.getenv() call inside getModel() is also routed through
getSystemEnv() for consistency.
2026-08-05 12:02:24 +05:30
Shailesh Padave 075caac556 fix: proper 404/400/409 for common agent API client errors (#1332) (#1470) 2026-08-04 09:16:32 -07:00
Shailesh Padave a570c203a0 fix: 404 on SSE stream for nonexistent execution IDs (#1334) (#1469) 2026-08-04 09:16:02 -07:00
Shailesh Padave 71ca791c6c fix: self-hosted models — base_url NPE + OPENAI_BASE_URL silently ignored (#1468) 2026-08-04 09:15:43 -07:00
Viren Baraiya b074897f39 Remove sqlite vector (#1455) 2026-08-02 19:54:39 -07:00
Shailesh Padave 2033ce86eb refactor: plain text comments, no javadoc tags 2026-07-31 17:15:54 +05:30
Shailesh Padave ace5100f91 refactor: extract extractText() helper per review feedback 2026-07-31 17:14:37 +05:30
Shailesh Padave 52ab3da419 fix(azure-foundry): scan content parts for text instead of assuming content[0] is text
Assistants with code interpreter (e.g. analyst agents) return an image_file
part before the text part. Taking content[0].text was always empty for these
assistants. Now iterates parts and returns the first one with type=text.
2026-07-31 14:35:07 +05:30
Manan Bhatt 7bd72be132 fix(agentspan): make host-supplied skill storage optional
SkillRegistryService took SkillPackageStore and SkillMetadataDAO as mandatory
constructor arguments, so a host with no implementation for its configured backend
could not start the agent runtime at all — the context failed with
NoSuchBeanDefinitionException on SkillPackageStore. Skills are a capability, not a
precondition for running an agent.

Both are now ObjectProvider. list() degrades to an empty list, since "which skills
exist" has a correct answer without storage; everything that needs the bytes fails
with a clear UnsupportedOperationException rather than an NPE. isSkillStorageAvailable()
lets callers branch.

Concretely: a host that supplies Postgres skill stores and none for MySQL currently
has to choose between writing a second backend-specific DAO pair or disabling the AI
integration entirely on that backend — and disabling it also removes unrelated AI
surface such as the prompt API. Neither is a reasonable requirement for running
agents without skills.

Adds OptionalSkillStorageTest: the registry starts with no storage, list() is empty,
and lookup fails clearly.

Verified: :conductor-agentspan:test and :conductor-ai:test — 0 failures. spotless clean.
2026-07-29 23:10:59 +05:30
Shailesh Padave 9a8505af78 Merge branch 'main' into fix/azure-foundry-oauth-scope-api-version 2026-07-29 17:21:42 +05:30
Manan Bhatt 0f33f5b8b5 fix(ai): surface agentType collisions; cover the external client wiring
Closes the two loose ends left in this PR so the release does not need a
follow-up.

A2AWorkers indexed clients with agentClients.put(agentType, client), so two
clients claiming one type last-one-wins on discovery order and one disappears
silently. Before #1358 the single-client injection raised
NoUniqueBeanDefinitionException, so this traded a loud failure for an invisible
one — and defaulting agentType() to "conductor" earlier in this PR makes the
collision easier to hit, since an implementation that forgets to override now
matches the built-in client. Keep the first registration and log which client was
ignored, on both the Spring and SDK paths.

Adds ExternalAgentClientGatingTest over the two external clients, which had no
coverage at all: absent when the AI integration is off, registered together with
CredentialResolutionService when on, reporting their own agent types with no
credentials configured anywhere, and — the regression that motivated the
qualifier — Azure Foundry still resolving its OkHttpClient when a second
OkHttpClient bean exists. Verified the last case fails without the qualifier
(NoUniqueBeanDefinitionException: found 2) and passes with it.

The dropped "AGENT (conductor) requires 'name'" validation from #1358 is
deliberate and stays dropped: the delegate now also serves bedrock and
azure-foundry, whose identity comes from rawConfig/assistantId rather than a
name, which is why the remaining messages were genericised to "AGENT requires
'prompt'".

Verified: :conductor-agentspan:test 560 tests, :conductor-ai:test 750 tests, 0
failures; :conductor-test-harness:compileTestJava clean; spotless clean.
2026-07-29 15:39:41 +05:30
Manan Bhatt 40bd05b752 fix(agentspan): qualify AzureFoundryAgentClient's OkHttpClient
Both external agent clients stay on conductor.integrations.ai.enabled, so every
combination is reachable without extra flags: bedrock only, azure-foundry only,
both, or neither — which runtime a workflow uses is decided by its agentType, not
by configuration. Registration is deliberately independent of whether either
service is configured, because neither resolves credentials at construction:
Bedrock reads credentialRef inside buildRuntimeClient (falling back to the
default AWS credential chain) and Azure Foundry inside its token exchange, both
per request. A missing key therefore cannot break startup; it surfaces only if a
workflow actually routes to that runtime.

The one thing that could break the both-registered case is the OkHttpClient
injection, which was unqualified. Only one such bean exists on a stock server
(conductorAiHttpClient), so it resolves today, but any host that defines a second
OkHttpClient bean would fail AzureFoundryAgentClient with
NoUniqueBeanDefinitionException — taking the whole context down, not just that
runtime. Bind it to conductorAiHttpClient explicitly, which is the client the AI
module publishes for exactly this purpose.

Also records the per-request credential behaviour on both clients, since "why is
this bean registered when I never configured Bedrock" is the obvious question.

Verified: ./gradlew :conductor-agentspan:test :conductor-ai:test — 0 failures;
:conductor-test-harness:compileTestJava clean; spotless clean.
2026-07-29 15:29:29 +05:30
Shailesh Padave 6e6097c988 fix(azure-foundry): correct OAuth scope and make API version configurable
Two bugs found during end-to-end local testing against ai-orkes-tests:

1. DEFAULT_SCOPE was management.azure.com — Azure OpenAI Assistants requires
   cognitiveservices.azure.com, causing 401 Unauthorized on every call.

2. API_VERSION was hardcoded to 2025-05-01 which is not yet available on all
   Azure OpenAI resources. Changed default to 2025-01-01-preview (broadly
   supported), configurable via rawConfig.apiVersion for resources that have
   a specific version requirement.

Also stores apiVersion in ExecutionContext so all status/respond/cancel calls
use the same version that was negotiated at start time.

Tested against ai-orkes-tests: greeter (2-4s), summarizer (4-8s), and analyst
with code_interpreter (8-15s) all complete successfully.
2026-07-29 15:03:08 +05:30
Manan Bhatt dd3765d9c6 Merge branch 'main' into fix/restore-agentspan-embedded-gate 2026-07-29 13:55:01 +05:30
Manan Bhatt 04c08bc2cc fix(agentspan): gate the new agent clients on the AI integrations flag
#1358 added BedrockAgentClient and AzureFoundryAgentClient as plain @Component
with no condition, unlike their sibling ServiceConductorAgentClient which is
gated on conductor.integrations.ai.enabled.

All three hard-require CredentialResolutionService, which carries that same
flag. Leaving two of them unconditional means a client can register while the
service it needs does not. On a stock server that is masked, because
AgentSpanAutoConfiguration's scan is gated on the same flag, so the clients are
only ever discovered when the service also exists. It stops being masked for any
host that component-scans the runtime package itself: the clients register, the
service does not, and startup fails with NoSuchBeanDefinitionException.

Give both the same condition as ServiceConductorAgentClient so a client and its
dependency can never register apart. Behaviour on a stock server is unchanged —
they activate exactly when they do today.

BedrockAgentClient's javadoc claimed activation by
conductor.ai.bedrock-agent.enabled, a property that appears nowhere else in the
repo and was never wired; its javadoc now names the flag actually used.

Also drop the dead "agentspan.embedded=true" property from
ConductorAgentEndToEndTest — #1413 folded that flag into
conductor.integrations.ai.enabled (which the test already sets), so this was the
last reference in the repo to a property nothing reads.

Verified: ./gradlew :conductor-agentspan:test — 0 failures;
:conductor-test-harness:compileTestJava clean; spotless clean.
2026-07-29 12:32:46 +05:30
Shailesh Padave de6da295fc Merge branch 'main' into feature/a2a-expose-all 2026-07-29 11:49:02 +05:30
Manan Bhatt 393891501c Merge branch 'main' into feature/external-agent-clients 2026-07-29 10:38:18 +05:30
Nicholas Cole 4cfbd7f97b Merge branch 'main' into feature/fix_unexpected_parent_control 2026-07-28 14:30:31 -07:00
nicholascole aaffdab9be fix(agentspan): default framework handoffs to direct output 2026-07-28 14:22:12 -07:00
nicholascole b0ee0097ab test(agentspan): reproduce handoff synthesis behavior 2026-07-28 13:56:42 -07:00
Shailesh Padave 6e3d61130d Merge branch 'main' into feature/a2a-expose-all 2026-07-29 01:40:50 +05:30
Nicholas Cole 21ee2875ed feat(ui): align agent definitions with workflows (#1411) 2026-07-28 13:08:57 -07:00
Shailesh Padave a9e6316ca1 Merge branch 'main' into feature/a2a-expose-all 2026-07-29 01:22:16 +05:30
nicholascole 1e0072f97d refactor(ai): consolidate agent integration configuration 2026-07-28 11:53:15 -07:00
Shailesh Padave 68d40af152 refactor(a2a): move dispatch, executor, and JSON-RPC helpers to A2ANativeAgentFacade
A2AAgentServerResource is now a thin HTTP adapter — jsonRpc() delegates
entirely to facade.dispatch(). The ExecutorService, switch dispatch, SSE
stream logic, and JSON-RPC envelope builders all live in the facade bean
so Orkes can inject a custom implementation without touching the controller.

Dispatch behavior tests move to A2ANativeAgentFacadeTest; resource test
covers HTTP routing and delegation only.
2026-07-29 00:09:45 +05:30
Shailesh Padave fd542eaf69 fix(a2a): remove HTML tags from Javadoc in new agentspan A2A classes
Replace <ul>/<li>/<p> HTML tags in class-level Javadoc with plain
single-paragraph descriptions so comments are plain text.
2026-07-28 15:43:38 +05:30
Shailesh Padave 1b8d153b3c spotless apply 2026-07-28 14:58:51 +05:30
Shailesh Padave 51efdfcad8 test(a2a): add unit tests for A2ANativeAgentFacade and A2AAgentServerResource
Mirrors the existing A2AWorkflowAgentTest and A2AServerResourceTest patterns
for the new native-agent A2A endpoints at /api/a2a/agent.
2026-07-28 14:09:24 +05:30
Shailesh Padave 1081bab737 spotless apply 2026-07-28 13:48:30 +05:30
Shailesh Padave a3a6198a8a feat(a2a): expose native Conductor agents as A2A agents at /api/a2a/agent
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
2026-07-28 12:49:27 +05:30
Nikhil Bharadwaj Ramashasthri a96c77fd4a Guard pruneExecutions against overflow and non-positive age deleting recent executions
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
2026-07-22 02:40:27 +06:00
Kowser 6994047ad6 test(guardrail): prove the tool-call short-circuit and escalation compose correctly
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>
2026-07-21 09:25:59 -07:00
Kowser 4b28f10f3e fix(guardrail): tool-guardrail iteration ref + escalation script test
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>
2026-07-21 09:24:57 -07:00
Kowser e4cc312c60 fix(guardrail): live loop counter
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>
2026-07-21 00:43:21 -07:00
Shailesh Padave 59c7ac221d Merge branch 'main' into feature/external-agent-clients 2026-07-21 10:48:32 +05:30
Nicholas Cole 5e22ec3740 Fix #1323: output guardrails must pass through on tool-call turns (#1352) 2026-07-20 16:40:01 -07:00
Shailesh Padave 492e628bb6 spotless apply 2026-07-20 20:37:07 +05:30
Shailesh Padave 558fb763df Add placeholder ConductorAgentClient implementations for Vertex AI and OpenAI Assistants
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.
2026-07-20 20:00:46 +05:30
Shailesh Padave d7dee74d8a spotless apply 2026-07-20 19:01:19 +05:30
Shailesh Padave 2f27b8fbda refactor(azure-foundry): rewrite client to use Azure AI Foundry Agents API
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
2026-07-20 17:16:53 +05:30
Shailesh Padave 423b8c783f fix: correct SdkBytes API usage in BedrockAgentClient and remove stale imports
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).
2026-07-20 17:03:39 +05:30
Shailesh Padave 5ca6ad9581 Route external agent clients by agentType — no feature flags needed
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.
2026-07-20 15:58:12 +05:30