From 16af6d97846531e7985de6887f74f2791826d644 Mon Sep 17 00:00:00 2001 From: tshepomaredi Date: Mon, 11 May 2026 09:46:30 +0200 Subject: [PATCH 1/4] feat(agentic-ai): add AgentCore Harness adapter skeleton Add initial implementation of AgentCoreHarnessAdapter that implements AiFrameworkAdapter interface for AWS Bedrock AgentCore Harness integration. Components: - AgentCoreHarnessAiFrameworkChatResponse: Response record with session ID - HarnessMessageConverter: Converts Camunda messages to/from Harness format - HarnessToolConverter: Converts ToolDefinition to HarnessTool (inline_function) - AgentCoreHarnessAdapter: Main adapter calling InvokeHarness API The adapter uses inline_function tools so Harness returns tool calls back to Camunda for BPMN element activation, preserving visibility and control. --- .../AgentCoreHarnessAdapter.java | 288 ++++++++++++++++++ ...entCoreHarnessAiFrameworkChatResponse.java | 28 ++ .../HarnessMessageConverter.java | 228 ++++++++++++++ .../HarnessToolConverter.java | 87 ++++++ 4 files changed, 631 insertions(+) create mode 100644 connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java create mode 100644 connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAiFrameworkChatResponse.java create mode 100644 connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java create mode 100644 connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverter.java diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java new file mode 100644 index 00000000000..2eab86ddc26 --- /dev/null +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java @@ -0,0 +1,288 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.framework.agentcoreharness; + +import static io.camunda.connector.agenticai.aiagent.agent.AgentErrorCodes.ERROR_CODE_FAILED_MODEL_CALL; + +import io.camunda.connector.agenticai.aiagent.framework.AiFrameworkAdapter; +import io.camunda.connector.agenticai.aiagent.memory.runtime.RuntimeMemory; +import io.camunda.connector.agenticai.aiagent.model.AgentContext; +import io.camunda.connector.agenticai.aiagent.model.AgentExecutionContext; +import io.camunda.connector.agenticai.aiagent.model.AgentMetrics; +import io.camunda.connector.agenticai.model.message.AssistantMessage; +import io.camunda.connector.agenticai.model.message.content.TextContent; +import io.camunda.connector.agenticai.model.tool.ToolCall; +import io.camunda.connector.api.error.ConnectorException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.bedrockagentcore.BedrockAgentCoreAsyncClient; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessContentBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.InvokeHarnessRequest; +import software.amazon.awssdk.services.bedrockagentcore.model.InvokeHarnessResponseHandler; + +/** + * AI Framework adapter for AWS Bedrock AgentCore Harness. + * + *

This adapter calls the InvokeHarness API with inline_function tools, allowing Harness to + * return tool calls back to Camunda for BPMN element activation. + */ +public class AgentCoreHarnessAdapter + implements AiFrameworkAdapter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AgentCoreHarnessAdapter.class); + + private static final String SESSION_ID_PROPERTY = "harnessSessionId"; + + private final BedrockAgentCoreAsyncClient client; + private final HarnessMessageConverter messageConverter; + private final HarnessToolConverter toolConverter; + private final String harnessArn; + + public AgentCoreHarnessAdapter( + BedrockAgentCoreAsyncClient client, + HarnessMessageConverter messageConverter, + HarnessToolConverter toolConverter, + String harnessArn) { + this.client = client; + this.messageConverter = messageConverter; + this.toolConverter = toolConverter; + this.harnessArn = harnessArn; + } + + @Override + public AgentCoreHarnessAiFrameworkChatResponse executeChatRequest( + AgentExecutionContext executionContext, + AgentContext agentContext, + RuntimeMemory runtimeMemory) { + + var messages = runtimeMemory.filteredMessages(); + var systemPrompt = messageConverter.extractSystemPrompt(messages); + var harnessMessages = messageConverter.toHarnessMessages(messages); + var harnessTools = toolConverter.toHarnessTools(agentContext.toolDefinitions()); + + // Get or generate session ID for conversation continuity + String sessionId = getOrCreateSessionId(agentContext); + + var requestBuilder = + InvokeHarnessRequest.builder() + .harnessArn(harnessArn) + .runtimeSessionId(sessionId) + .messages(harnessMessages) + .tools(harnessTools); + + if (!systemPrompt.isEmpty()) { + requestBuilder.systemPrompt(systemPrompt); + } + + var request = requestBuilder.build(); + + LOGGER.debug( + "Invoking Harness {} with {} messages and {} tools", + harnessArn, + harnessMessages.size(), + harnessTools.size()); + + // Process streaming response + var responseData = invokeHarness(request); + + // Build assistant message from response + var assistantMessage = buildAssistantMessage(responseData); + + LOGGER.debug( + "Received response with {} tool calls", + assistantMessage.toolCalls() != null ? assistantMessage.toolCalls().size() : 0); + + // Update metrics + var updatedAgentContext = + agentContext + .withMetrics( + agentContext + .metrics() + .incrementModelCalls(1) + .incrementTokenUsage( + AgentMetrics.TokenUsage.builder() + .inputTokenCount(responseData.inputTokens()) + .outputTokenCount(responseData.outputTokens()) + .build())) + .withProperty(SESSION_ID_PROPERTY, sessionId); + + return new AgentCoreHarnessAiFrameworkChatResponse( + updatedAgentContext, assistantMessage, sessionId); + } + + private String getOrCreateSessionId(AgentContext agentContext) { + return Optional.ofNullable(agentContext.properties()) + .map(props -> props.get(SESSION_ID_PROPERTY)) + .map(Object::toString) + .filter(StringUtils::isNotBlank) + .orElseGet(() -> java.util.UUID.randomUUID().toString()); + } + + private HarnessResponseData invokeHarness(InvokeHarnessRequest request) { + var responseData = new HarnessResponseData(); + var future = new CompletableFuture(); + + var handler = + InvokeHarnessResponseHandler.builder() + .onEventStream( + publisher -> + publisher.subscribe( + event -> { + event.accept( + InvokeHarnessResponseHandler.Visitor.builder() + .onContentBlockStart( + e -> { + if (e.start() != null && e.start().toolUse() != null) { + responseData.startToolUse(e.start().toolUse()); + } + }) + .onContentBlockDelta( + e -> { + if (e.delta() != null) { + if (e.delta().text() != null) { + responseData.appendText(e.delta().text()); + } + if (e.delta().toolUse() != null + && e.delta().toolUse().input() != null) { + responseData.appendToolInput( + e.delta().toolUse().input()); + } + } + }) + .onContentBlockStop(e -> responseData.finishCurrentToolUse()) + .onMetadata( + e -> { + if (e.usage() != null) { + responseData.setTokenUsage( + e.usage().inputTokens(), e.usage().outputTokens()); + } + }) + .build()); + })) + .onError(future::completeExceptionally) + .onComplete(() -> future.complete(null)) + .build(); + + try { + client.invokeHarness(request, handler).join(); + future.join(); + } catch (Exception e) { + var message = + Optional.ofNullable(e.getMessage()) + .filter(StringUtils::isNotBlank) + .orElseGet(() -> e.getClass().getSimpleName()); + + throw new ConnectorException( + ERROR_CODE_FAILED_MODEL_CALL, "Harness invocation failed: %s".formatted(message), e); + } + + return responseData; + } + + private AssistantMessage buildAssistantMessage(HarnessResponseData responseData) { + var builder = AssistantMessage.builder(); + + if (StringUtils.isNotBlank(responseData.getText())) { + builder.content(List.of(TextContent.textContent(responseData.getText()))); + } + + if (!responseData.getToolCalls().isEmpty()) { + builder.toolCalls(responseData.getToolCalls()); + } + + return builder.build(); + } + + /** Accumulates data from streaming Harness response events. */ + private class HarnessResponseData { + private final StringBuilder textBuilder = new StringBuilder(); + private final List toolCalls = new ArrayList<>(); + private final AtomicReference currentToolUseId = new AtomicReference<>(); + private final AtomicReference currentToolName = new AtomicReference<>(); + private final StringBuilder currentToolInputBuilder = new StringBuilder(); + private int inputTokens = 0; + private int outputTokens = 0; + + void appendText(String text) { + textBuilder.append(text); + } + + void startToolUse(HarnessContentBlock.Builder toolUseBuilder) { + // This is called with partial tool use info at start + } + + void startToolUse( + software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseBlockStart toolUse) { + currentToolUseId.set(toolUse.toolUseId()); + currentToolName.set(toolUse.name()); + currentToolInputBuilder.setLength(0); + } + + void appendToolInput(String input) { + currentToolInputBuilder.append(input); + } + + void finishCurrentToolUse() { + if (currentToolUseId.get() != null && currentToolName.get() != null) { + var inputJson = currentToolInputBuilder.toString(); + var arguments = parseToolInput(inputJson); + + toolCalls.add( + ToolCall.builder() + .id(currentToolUseId.get()) + .name(currentToolName.get()) + .arguments(arguments) + .build()); + + currentToolUseId.set(null); + currentToolName.set(null); + currentToolInputBuilder.setLength(0); + } + } + + @SuppressWarnings("unchecked") + private java.util.Map parseToolInput(String inputJson) { + if (StringUtils.isBlank(inputJson)) { + return java.util.Map.of(); + } + try { + return new com.fasterxml.jackson.databind.ObjectMapper() + .readValue(inputJson, java.util.Map.class); + } catch (Exception e) { + LOGGER.warn("Failed to parse tool input JSON: {}", inputJson, e); + return java.util.Map.of(); + } + } + + void setTokenUsage(Integer input, Integer output) { + this.inputTokens = input != null ? input : 0; + this.outputTokens = output != null ? output : 0; + } + + String getText() { + return textBuilder.toString(); + } + + List getToolCalls() { + return toolCalls; + } + + int inputTokens() { + return inputTokens; + } + + int outputTokens() { + return outputTokens; + } + } +} diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAiFrameworkChatResponse.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAiFrameworkChatResponse.java new file mode 100644 index 00000000000..d0747862636 --- /dev/null +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAiFrameworkChatResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.framework.agentcoreharness; + +import io.camunda.connector.agenticai.aiagent.framework.AiFrameworkChatResponse; +import io.camunda.connector.agenticai.aiagent.model.AgentContext; +import io.camunda.connector.agenticai.model.message.AssistantMessage; + +/** + * Response from the AgentCore Harness framework adapter. + * + * @param agentContext the updated agent context with metrics + * @param assistantMessage the assistant message containing text and/or tool calls + * @param runtimeSessionId the Harness session ID for conversation continuity + */ +public record AgentCoreHarnessAiFrameworkChatResponse( + AgentContext agentContext, AssistantMessage assistantMessage, String runtimeSessionId) + implements AiFrameworkChatResponse { + + @Override + public String rawChatResponse() { + return runtimeSessionId; + } +} diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java new file mode 100644 index 00000000000..4ef51d06d7b --- /dev/null +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java @@ -0,0 +1,228 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.framework.agentcoreharness; + +import io.camunda.connector.agenticai.model.message.AssistantMessage; +import io.camunda.connector.agenticai.model.message.ContentMessage; +import io.camunda.connector.agenticai.model.message.Message; +import io.camunda.connector.agenticai.model.message.SystemMessage; +import io.camunda.connector.agenticai.model.message.ToolCallResultMessage; +import io.camunda.connector.agenticai.model.message.UserMessage; +import io.camunda.connector.agenticai.model.message.content.TextContent; +import io.camunda.connector.agenticai.model.tool.ToolCall; +import io.camunda.connector.agenticai.model.tool.ToolCallResult; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessContentBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessConversationRole; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessMessage; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessSystemContentBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolResultBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolResultContentBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseStatus; + +/** Converts between Camunda messages and AWS Harness message formats. */ +public class HarnessMessageConverter { + + /** + * Extracts system prompt content blocks from messages. + * + * @param messages the Camunda messages + * @return list of HarnessSystemContentBlock for system messages + */ + public List extractSystemPrompt(List messages) { + return messages.stream() + .filter(SystemMessage.class::isInstance) + .map(SystemMessage.class::cast) + .flatMap(msg -> toSystemContentBlocks(msg).stream()) + .toList(); + } + + /** + * Converts Camunda messages to Harness messages (excluding system messages). + * + * @param messages the Camunda messages + * @return list of HarnessMessage + */ + public List toHarnessMessages(List messages) { + List result = new ArrayList<>(); + + for (Message message : messages) { + if (message instanceof SystemMessage) { + // System messages are handled separately via systemPrompt + continue; + } else if (message instanceof UserMessage userMessage) { + result.add(toHarnessMessage(userMessage)); + } else if (message instanceof AssistantMessage assistantMessage) { + result.add(toHarnessMessage(assistantMessage)); + } else if (message instanceof ToolCallResultMessage toolResultMessage) { + result.add(toHarnessMessage(toolResultMessage)); + } + } + + return result; + } + + /** + * Converts a Harness tool use block to a Camunda ToolCall. + * + * @param toolUseBlock the Harness tool use block + * @return Camunda ToolCall + */ + public ToolCall toToolCall(HarnessToolUseBlock toolUseBlock) { + return ToolCall.builder() + .id(toolUseBlock.toolUseId()) + .name(toolUseBlock.name()) + .arguments(documentToMap(toolUseBlock.input())) + .build(); + } + + private List toSystemContentBlocks(SystemMessage systemMessage) { + return extractTextContent(systemMessage).stream() + .map(text -> HarnessSystemContentBlock.fromText(text)) + .toList(); + } + + private HarnessMessage toHarnessMessage(UserMessage userMessage) { + List contentBlocks = + extractTextContent(userMessage).stream() + .map(HarnessContentBlock::fromText) + .map(HarnessContentBlock.class::cast) + .toList(); + + return HarnessMessage.builder() + .role(HarnessConversationRole.USER) + .content(contentBlocks) + .build(); + } + + private HarnessMessage toHarnessMessage(AssistantMessage assistantMessage) { + List contentBlocks = new ArrayList<>(); + + // Add text content + for (String text : extractTextContent(assistantMessage)) { + contentBlocks.add(HarnessContentBlock.fromText(text)); + } + + // Add tool use blocks + if (assistantMessage.toolCalls() != null) { + for (ToolCall toolCall : assistantMessage.toolCalls()) { + contentBlocks.add( + HarnessContentBlock.fromToolUse( + HarnessToolUseBlock.builder() + .toolUseId(toolCall.id()) + .name(toolCall.name()) + .input(mapToDocument(toolCall.arguments())) + .build())); + } + } + + return HarnessMessage.builder() + .role(HarnessConversationRole.ASSISTANT) + .content(contentBlocks) + .build(); + } + + private HarnessMessage toHarnessMessage(ToolCallResultMessage toolResultMessage) { + List contentBlocks = + toolResultMessage.results().stream() + .map(this::toToolResultContentBlock) + .map(HarnessContentBlock::fromToolResult) + .toList(); + + return HarnessMessage.builder() + .role(HarnessConversationRole.USER) + .content(contentBlocks) + .build(); + } + + private HarnessToolResultBlock toToolResultContentBlock(ToolCallResult result) { + String contentText = result.content() != null ? result.content().toString() : ""; + boolean isError = + Boolean.TRUE.equals(result.properties().get(ToolCallResult.PROPERTY_INTERRUPTED)); + + return HarnessToolResultBlock.builder() + .toolUseId(result.id()) + .status(isError ? HarnessToolUseStatus.ERROR : HarnessToolUseStatus.SUCCESS) + .content(List.of(HarnessToolResultContentBlock.fromText(contentText))) + .build(); + } + + private List extractTextContent(ContentMessage message) { + if (message.content() == null || message.content().isEmpty()) { + return List.of(); + } + + return message.content().stream() + .filter(TextContent.class::isInstance) + .map(TextContent.class::cast) + .map(TextContent::text) + .toList(); + } + + @SuppressWarnings("unchecked") + private Document mapToDocument(Map map) { + if (map == null || map.isEmpty()) { + return Document.fromMap(Map.of()); + } + return Document.fromMap( + map.entrySet().stream() + .collect( + java.util.stream.Collectors.toMap( + Map.Entry::getKey, e -> valueToDocument(e.getValue())))); + } + + @SuppressWarnings("unchecked") + private Document valueToDocument(Object value) { + if (value == null) { + return Document.fromNull(); + } else if (value instanceof String s) { + return Document.fromString(s); + } else if (value instanceof Number n) { + return Document.fromNumber(n.toString()); + } else if (value instanceof Boolean b) { + return Document.fromBoolean(b); + } else if (value instanceof Map m) { + return mapToDocument((Map) m); + } else if (value instanceof List l) { + return Document.fromList(l.stream().map(this::valueToDocument).toList()); + } else { + return Document.fromString(value.toString()); + } + } + + @SuppressWarnings("unchecked") + private Map documentToMap(Document document) { + if (document == null || document.isNull()) { + return Map.of(); + } + return document.asMap().entrySet().stream() + .collect( + java.util.stream.Collectors.toMap( + Map.Entry::getKey, e -> documentToValue(e.getValue()))); + } + + private Object documentToValue(Document document) { + if (document == null || document.isNull()) { + return null; + } else if (document.isString()) { + return document.asString(); + } else if (document.isNumber()) { + return document.asNumber(); + } else if (document.isBoolean()) { + return document.asBoolean(); + } else if (document.isMap()) { + return documentToMap(document); + } else if (document.isList()) { + return document.asList().stream().map(this::documentToValue).toList(); + } + return null; + } +} diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverter.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverter.java new file mode 100644 index 00000000000..55477008fd4 --- /dev/null +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverter.java @@ -0,0 +1,87 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.framework.agentcoreharness; + +import io.camunda.connector.agenticai.model.tool.ToolDefinition; +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessInlineFunctionConfig; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessTool; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolConfiguration; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolType; + +/** Converts Camunda ToolDefinition to AWS Harness HarnessTool with inline_function type. */ +public class HarnessToolConverter { + + /** + * Converts a list of Camunda tool definitions to Harness tools. + * + * @param toolDefinitions the Camunda tool definitions + * @return list of HarnessTool with inline_function configuration + */ + public List toHarnessTools(List toolDefinitions) { + if (toolDefinitions == null || toolDefinitions.isEmpty()) { + return List.of(); + } + return toolDefinitions.stream().map(this::toHarnessTool).toList(); + } + + /** + * Converts a single Camunda tool definition to a Harness tool. + * + * @param toolDefinition the Camunda tool definition + * @return HarnessTool with inline_function configuration + */ + public HarnessTool toHarnessTool(ToolDefinition toolDefinition) { + var inlineFunctionConfig = + HarnessInlineFunctionConfig.builder() + .description(toolDefinition.description()) + .inputSchema(toDocument(toolDefinition.inputSchema())) + .build(); + + return HarnessTool.builder() + .name(toolDefinition.name()) + .type(HarnessToolType.INLINE_FUNCTION) + .config(HarnessToolConfiguration.fromInlineFunction(inlineFunctionConfig)) + .build(); + } + + private Document toDocument(Map map) { + if (map == null || map.isEmpty()) { + return Document.fromMap(Map.of()); + } + return Document.fromMap(convertToDocumentMap(map)); + } + + @SuppressWarnings("unchecked") + private Map convertToDocumentMap(Map map) { + return map.entrySet().stream() + .collect( + java.util.stream.Collectors.toMap( + Map.Entry::getKey, e -> convertToDocument(e.getValue()))); + } + + @SuppressWarnings("unchecked") + private Document convertToDocument(Object value) { + if (value == null) { + return Document.fromNull(); + } else if (value instanceof String s) { + return Document.fromString(s); + } else if (value instanceof Number n) { + return Document.fromNumber(n.toString()); + } else if (value instanceof Boolean b) { + return Document.fromBoolean(b); + } else if (value instanceof Map m) { + return Document.fromMap(convertToDocumentMap((Map) m)); + } else if (value instanceof List l) { + return Document.fromList(l.stream().map(this::convertToDocument).toList()); + } else { + return Document.fromString(value.toString()); + } + } +} From 10c027bff5444b20a75af15b8d643a2a2b90c7be Mon Sep 17 00:00:00 2001 From: tshepomaredi Date: Thu, 14 May 2026 10:14:27 +0200 Subject: [PATCH 2/4] feat(agentic-ai): complete AgentCore Harness adapter implementation - Add AgentCoreHarnessJobWorker and AgentCoreHarnessRequest for ad-hoc subprocess - Add element template for 'AgentCore Managed Agent' in modeler - Wire up Spring bean configuration in AgenticAiConnectorsAutoConfiguration - Handle stopReason from Harness response (tool_use vs end_turn) - Filter tool calls to only activate BPMN elements for inline_function tools - Built-in tools (shell, browser) are executed by Harness internally - Add allowedTools support to scope which tools Harness can use - Add INFO logging for debugging Harness invocations --- .../agenticai-agentcore-harness.json | 247 ++++++++++++++++++ .../agent/JobWorkerAgentRequestHandler.java | 2 +- .../AgentCoreHarnessJobWorker.java | 137 ++++++++++ .../AgentCoreHarnessRequest.java | 87 ++++++ .../AgentCoreHarnessAdapter.java | 74 +++++- .../AgenticAiConnectorsAutoConfiguration.java | 22 ++ 6 files changed, 556 insertions(+), 13 deletions(-) create mode 100644 connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json create mode 100644 connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java create mode 100644 connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessRequest.java diff --git a/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json b/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json new file mode 100644 index 00000000000..ee5f8c31517 --- /dev/null +++ b/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json @@ -0,0 +1,247 @@ +{ + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "AgentCore Managed Agent", + "id": "io.camunda.connectors.agenticai.agentcore.harness.v1", + "description": "Drive an AWS Bedrock AgentCore Harness through a re-entrant tool-call loop, exposing the AHSP inner elements as inline_function tools.", + "keywords": ["AI", "AI Agent", "AWS", "AgentCore", "Harness", "Bedrock"], + "version": 1, + "category": { + "id": "connectors", + "name": "Connectors" + }, + "appliesTo": ["bpmn:SubProcess"], + "elementType": { + "value": "bpmn:AdHocSubProcess" + }, + "engines": { + "camunda": "^8.9" + }, + "groups": [ + { + "id": "harness", + "label": "Harness", + "openByDefault": true + }, + { + "id": "authentication", + "label": "AWS credentials", + "openByDefault": true + }, + { + "id": "userPrompt", + "label": "User prompt", + "openByDefault": true + }, + { + "id": "advanced", + "label": "Advanced", + "openByDefault": false + }, + { + "id": "output", + "label": "Output mapping" + }, + { + "id": "error", + "label": "Error handling" + } + ], + "properties": [ + { + "value": "io.camunda.agenticai:agentcore-harness:1", + "binding": { + "property": "type", + "type": "zeebe:taskDefinition" + }, + "type": "Hidden" + }, + { + "id": "outputCollection", + "binding": { + "property": "outputCollection", + "type": "zeebe:adHoc" + }, + "value": "toolCallResults", + "type": "Hidden" + }, + { + "id": "outputElement", + "binding": { + "property": "outputElement", + "type": "zeebe:adHoc" + }, + "value": "={\n id: toolCall._meta.id,\n name: toolCall._meta.name,\n content: toolCallResult\n}", + "type": "Hidden" + }, + { + "id": "harness.harnessArn", + "label": "Harness ARN", + "description": "Full ARN of the deployed AgentCore Harness, e.g. arn:aws:bedrock-agentcore:us-west-2:111122223333:harness/helpdesk-9aBcDeFgHi.", + "feel": "optional", + "group": "harness", + "binding": { + "name": "harness.harnessArn", + "type": "zeebe:input" + }, + "constraints": { + "notEmpty": true + }, + "type": "String" + }, + { + "id": "harness.region", + "label": "Region", + "description": "AWS region of the harness (e.g. us-west-2). Inferred from harnessArn when omitted.", + "optional": true, + "feel": "optional", + "group": "harness", + "binding": { + "name": "harness.region", + "type": "zeebe:input" + }, + "type": "String" + }, + { + "id": "harness.allowedTools", + "label": "Allowed tools (FEEL list)", + "description": "Optional list of tool names to scope the harness to (typically the AHSP inner element ids). Leave empty to use harness defaults.", + "optional": true, + "feel": "required", + "group": "harness", + "binding": { + "name": "harness.allowedTools", + "type": "zeebe:input" + }, + "type": "String" + }, + { + "id": "authentication.type", + "label": "Authentication", + "description": "How to authenticate against AWS.", + "value": "credentials", + "group": "authentication", + "binding": { + "name": "authentication.type", + "type": "zeebe:input" + }, + "type": "Dropdown", + "choices": [ + { + "name": "Static credentials", + "value": "credentials" + }, + { + "name": "Default credentials chain", + "value": "defaultCredentialsChain" + } + ] + }, + { + "id": "authentication.accessKey", + "label": "Access key ID", + "description": "AWS access key id, e.g. {{secrets.AWS_ACCESS_KEY_ID}}.", + "feel": "optional", + "group": "authentication", + "binding": { + "name": "authentication.accessKey", + "type": "zeebe:input" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authentication.type", + "equals": "credentials", + "type": "simple" + }, + "type": "String" + }, + { + "id": "authentication.secretKey", + "label": "Secret key", + "description": "AWS secret access key, e.g. {{secrets.AWS_SECRET_ACCESS_KEY}}.", + "feel": "optional", + "group": "authentication", + "binding": { + "name": "authentication.secretKey", + "type": "zeebe:input" + }, + "constraints": { + "notEmpty": true + }, + "condition": { + "property": "authentication.type", + "equals": "credentials", + "type": "simple" + }, + "type": "String" + }, + { + "id": "userPrompt", + "label": "User prompt", + "description": "First-turn user message sent to the harness. Continuation turns are driven by tool results from the AHSP inner elements.", + "feel": "optional", + "group": "userPrompt", + "binding": { + "name": "userPrompt", + "type": "zeebe:input" + }, + "constraints": { + "notEmpty": true + }, + "type": "Text" + }, + { + "id": "maxIterations", + "label": "Max iterations", + "description": "Maximum number of tool-call iterations before forcing completion.", + "optional": true, + "feel": "optional", + "group": "advanced", + "binding": { + "name": "maxIterations", + "type": "zeebe:input" + }, + "type": "String" + }, + { + "id": "resultVariable", + "label": "Result variable", + "description": "Name of variable to store the response in", + "value": "response", + "group": "output", + "binding": { + "source": "=agent", + "type": "zeebe:output" + }, + "type": "String" + }, + { + "id": "resultExpression", + "label": "Result expression", + "description": "Expression to map the response into process variables", + "feel": "required", + "group": "output", + "binding": { + "key": "resultExpression", + "type": "zeebe:taskHeader" + }, + "type": "Text" + }, + { + "id": "errorExpression", + "label": "Error expression", + "description": "Expression to handle errors. Details in the documentation.", + "feel": "required", + "group": "error", + "binding": { + "key": "errorExpression", + "type": "zeebe:taskHeader" + }, + "type": "Text" + } + ], + "icon": { + "contents": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzIzMkYzRSI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTAgMThjLTQuNDEgMC04LTMuNTktOC04czMuNTktOCA4LTggOCAzLjU5IDggOC0zLjU5IDgtOCA4em0tMS0xM2gydjZoLTJ6bTAgOGgydjJoLTJ6Ii8+PC9zdmc+" + } +} diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java index e7b84914188..05d908996fd 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java @@ -170,7 +170,7 @@ private List buildElementActivations(AgentResponse agentRespo if (LOGGER.isTraceEnabled()) { LOGGER.trace("Activating tool {}: {}", toolCall.metadata().name(), toolCall); } else { - LOGGER.debug("Activating tool {}", toolCall.metadata().name()); + LOGGER.info("Activating tool {}", toolCall.metadata().name()); } return (ElementActivation) diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java new file mode 100644 index 00000000000..136d30bc764 --- /dev/null +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java @@ -0,0 +1,137 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.agentcoreharness; + +import io.camunda.connector.agenticai.aiagent.AgentConnectorFunction; +import io.camunda.connector.agenticai.aiagent.AiAgentSubProcessConnectorResponse; +import io.camunda.connector.agenticai.aiagent.agent.AgentInitializer; +import io.camunda.connector.agenticai.aiagent.agent.AgentLimitsValidator; +import io.camunda.connector.agenticai.aiagent.agent.AgentMessagesHandler; +import io.camunda.connector.agenticai.aiagent.agent.AgentResponseHandler; +import io.camunda.connector.agenticai.aiagent.agent.JobWorkerAgentRequestHandler; +import io.camunda.connector.agenticai.aiagent.framework.agentcoreharness.AgentCoreHarnessAdapter; +import io.camunda.connector.agenticai.aiagent.framework.agentcoreharness.HarnessMessageConverter; +import io.camunda.connector.agenticai.aiagent.framework.agentcoreharness.HarnessToolConverter; +import io.camunda.connector.agenticai.aiagent.memory.conversation.ConversationStoreRegistry; +import io.camunda.connector.agenticai.aiagent.model.JobWorkerAgentExecutionContext; +import io.camunda.connector.agenticai.aiagent.model.request.provider.BedrockProviderConfiguration.AwsAuthentication; +import io.camunda.connector.agenticai.aiagent.tool.GatewayToolHandlerRegistry; +import io.camunda.connector.api.annotation.OutboundConnector; +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockagentcore.BedrockAgentCoreAsyncClient; + +/** + * AgentCore Harness job worker implementation (acting on an ad-hoc sub-process). + * + *

Drives an AWS Bedrock AgentCore Harness through a re-entrant tool-call loop, exposing the AHSP + * inner elements as inline_function tools. + */ +@OutboundConnector( + name = AgentCoreHarnessJobWorker.JOB_WORKER_NAME, + type = AgentCoreHarnessJobWorker.JOB_WORKER_TYPE, + inputVariables = { + AgentCoreHarnessJobWorker.AD_HOC_SUB_PROCESS_ELEMENT_VARIABLE, + AgentCoreHarnessJobWorker.AGENT_CONTEXT_VARIABLE, + AgentCoreHarnessJobWorker.TOOL_CALL_RESULTS_VARIABLE, + AgentCoreHarnessJobWorker.HARNESS_VARIABLE, + AgentCoreHarnessJobWorker.AUTHENTICATION_VARIABLE, + AgentCoreHarnessJobWorker.USER_PROMPT_VARIABLE, + AgentCoreHarnessJobWorker.MAX_ITERATIONS_VARIABLE + }) +public class AgentCoreHarnessJobWorker implements AgentConnectorFunction { + + public static final String JOB_WORKER_NAME = "AgentCore Harness Job Worker"; + public static final String JOB_WORKER_TYPE = "io.camunda.agenticai:agentcore-harness:1"; + + public static final String AD_HOC_SUB_PROCESS_ELEMENT_VARIABLE = "adHocSubProcessElements"; + public static final String AGENT_CONTEXT_VARIABLE = "agentContext"; + public static final String TOOL_CALL_RESULTS_VARIABLE = "toolCallResults"; + public static final String HARNESS_VARIABLE = "harness"; + public static final String AUTHENTICATION_VARIABLE = "authentication"; + public static final String USER_PROMPT_VARIABLE = "userPrompt"; + public static final String MAX_ITERATIONS_VARIABLE = "maxIterations"; + + private final AgentInitializer agentInitializer; + private final ConversationStoreRegistry conversationStoreRegistry; + private final AgentLimitsValidator limitsValidator; + private final AgentMessagesHandler messagesHandler; + private final GatewayToolHandlerRegistry gatewayToolHandlers; + private final AgentResponseHandler responseHandler; + + public AgentCoreHarnessJobWorker( + AgentInitializer agentInitializer, + ConversationStoreRegistry conversationStoreRegistry, + AgentLimitsValidator limitsValidator, + AgentMessagesHandler messagesHandler, + GatewayToolHandlerRegistry gatewayToolHandlers, + AgentResponseHandler responseHandler) { + this.agentInitializer = agentInitializer; + this.conversationStoreRegistry = conversationStoreRegistry; + this.limitsValidator = limitsValidator; + this.messagesHandler = messagesHandler; + this.gatewayToolHandlers = gatewayToolHandlers; + this.responseHandler = responseHandler; + } + + @Override + public AiAgentSubProcessConnectorResponse execute(OutboundConnectorContext context) + throws Exception { + var request = context.bindVariables(AgentCoreHarnessRequest.class); + + // Create the Harness adapter with request-specific configuration + try (var asyncClient = createClient(request)) { + + var adapter = + new AgentCoreHarnessAdapter( + asyncClient, + new HarnessMessageConverter(), + new HarnessToolConverter(), + request.harness().harnessArn(), + request.harness().allowedTools()); + + // Create a request handler with the Harness adapter + var requestHandler = + new JobWorkerAgentRequestHandler( + agentInitializer, + conversationStoreRegistry, + limitsValidator, + messagesHandler, + gatewayToolHandlers, + adapter, + responseHandler); + + var executionContext = + new JobWorkerAgentExecutionContext(context.getJobContext(), request.toAgentRequest()); + + return requestHandler.handleRequest(executionContext); + } + } + + private BedrockAgentCoreAsyncClient createClient(AgentCoreHarnessRequest request) { + var builder = + BedrockAgentCoreAsyncClient.builder() + .region(Region.of(request.harness().effectiveRegion())); + + switch (request.authentication()) { + case AwsAuthentication.AwsStaticCredentialsAuthentication staticAuth -> { + var credentials = + AwsBasicCredentials.create(staticAuth.accessKey(), staticAuth.secretKey()); + builder.credentialsProvider(StaticCredentialsProvider.create(credentials)); + } + case AwsAuthentication.AwsDefaultCredentialsChainAuthentication ignored -> + builder.credentialsProvider(DefaultCredentialsProvider.create()); + case AwsAuthentication.AwsApiKeyAuthentication ignored -> + throw new IllegalArgumentException("API Key authentication is not supported for Harness"); + } + + return builder.build(); + } +} diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessRequest.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessRequest.java new file mode 100644 index 00000000000..729fb8d044a --- /dev/null +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessRequest.java @@ -0,0 +1,87 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.agentcoreharness; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.camunda.connector.agenticai.adhoctoolsschema.model.AdHocToolElement; +import io.camunda.connector.agenticai.aiagent.model.AgentContext; +import io.camunda.connector.agenticai.aiagent.model.request.JobWorkerAgentRequest; +import io.camunda.connector.agenticai.aiagent.model.request.JobWorkerAgentRequest.JobWorkerAgentRequestData; +import io.camunda.connector.agenticai.aiagent.model.request.PromptConfiguration.SystemPromptConfiguration; +import io.camunda.connector.agenticai.aiagent.model.request.PromptConfiguration.UserPromptConfiguration; +import io.camunda.connector.agenticai.aiagent.model.request.provider.BedrockProviderConfiguration.AwsAuthentication; +import io.camunda.connector.agenticai.model.tool.ToolCallResult; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * Request model for the AgentCore Harness job worker. + * + * @param toolElements the ad-hoc subprocess inner elements (become inline_function tools) + * @param agentContext the agent context from previous iterations + * @param toolCallResults results from tool executions + * @param harness harness configuration (ARN, region, allowed tools) + * @param authentication AWS authentication configuration + * @param userPrompt the user prompt for the first turn + * @param maxIterations maximum iterations before forcing completion + */ +public record AgentCoreHarnessRequest( + @JsonProperty("adHocSubProcessElements") List toolElements, + @Valid AgentContext agentContext, + List toolCallResults, + @Valid @NotNull HarnessConfiguration harness, + @Valid @NotNull AwsAuthentication authentication, + @NotBlank String userPrompt, + Integer maxIterations) { + + /** + * Harness-specific configuration. + * + * @param harnessArn full ARN of the deployed AgentCore Harness + * @param region AWS region (optional, inferred from ARN if omitted) + * @param allowedTools optional list of tool names to scope the harness to + */ + public record HarnessConfiguration( + @NotBlank String harnessArn, String region, List allowedTools) { + + /** Extract region from ARN if not explicitly provided. */ + public String effectiveRegion() { + if (region != null && !region.isBlank()) { + return region; + } + // ARN format: arn:aws:bedrock-agentcore:REGION:ACCOUNT:harness/NAME + if (harnessArn != null && harnessArn.startsWith("arn:aws:")) { + String[] parts = harnessArn.split(":"); + if (parts.length >= 4) { + return parts[3]; + } + } + return "us-east-1"; // default + } + } + + /** + * Convert to the standard JobWorkerAgentRequest format for compatibility with existing handlers. + */ + public JobWorkerAgentRequest toAgentRequest() { + return new JobWorkerAgentRequest( + toolElements, + agentContext, + toolCallResults, + null, // provider is not used for Harness + new JobWorkerAgentRequestData( + new SystemPromptConfiguration(null), // system prompt comes from Harness config + new UserPromptConfiguration(userPrompt, null), + null, // memory + null, // limits + null, // events + null // response + )); + } +} diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java index 2eab86ddc26..7ec168ec72f 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java @@ -47,16 +47,19 @@ public class AgentCoreHarnessAdapter private final HarnessMessageConverter messageConverter; private final HarnessToolConverter toolConverter; private final String harnessArn; + private final List allowedTools; public AgentCoreHarnessAdapter( BedrockAgentCoreAsyncClient client, HarnessMessageConverter messageConverter, HarnessToolConverter toolConverter, - String harnessArn) { + String harnessArn, + List allowedTools) { this.client = client; this.messageConverter = messageConverter; this.toolConverter = toolConverter; this.harnessArn = harnessArn; + this.allowedTools = allowedTools; } @Override @@ -84,22 +87,38 @@ public AgentCoreHarnessAiFrameworkChatResponse executeChatRequest( requestBuilder.systemPrompt(systemPrompt); } + if (allowedTools != null && !allowedTools.isEmpty()) { + requestBuilder.allowedTools(allowedTools); + } + var request = requestBuilder.build(); - LOGGER.debug( - "Invoking Harness {} with {} messages and {} tools", + LOGGER.info( + "Invoking Harness {} with {} messages and {} tools (names={}), allowedTools={}", harnessArn, harnessMessages.size(), - harnessTools.size()); + harnessTools.size(), + harnessTools.stream().map(t -> t.name()).toList(), + allowedTools); // Process streaming response var responseData = invokeHarness(request); - // Build assistant message from response - var assistantMessage = buildAssistantMessage(responseData); - - LOGGER.debug( - "Received response with {} tool calls", + // Get the names of our inline_function tools (BPMN elements) + var inlineToolNames = + agentContext.toolDefinitions().stream() + .map(td -> td.name()) + .collect(java.util.stream.Collectors.toSet()); + + // Build assistant message from response, filtering to only inline_function tool calls + var assistantMessage = buildAssistantMessage(responseData, inlineToolNames); + + LOGGER.info( + "Received response with stopReason={}, text={}, {} tool calls", + responseData.getStopReason(), + responseData.getText() != null + ? responseData.getText().substring(0, Math.min(100, responseData.getText().length())) + : "null", assistantMessage.toolCalls() != null ? assistantMessage.toolCalls().size() : 0); // Update metrics @@ -160,6 +179,12 @@ private HarnessResponseData invokeHarness(InvokeHarnessRequest request) { } }) .onContentBlockStop(e -> responseData.finishCurrentToolUse()) + .onMessageStop( + e -> { + if (e.stopReason() != null) { + responseData.setStopReason(e.stopReasonAsString()); + } + }) .onMetadata( e -> { if (e.usage() != null) { @@ -189,15 +214,31 @@ private HarnessResponseData invokeHarness(InvokeHarnessRequest request) { return responseData; } - private AssistantMessage buildAssistantMessage(HarnessResponseData responseData) { + private AssistantMessage buildAssistantMessage( + HarnessResponseData responseData, java.util.Set inlineToolNames) { var builder = AssistantMessage.builder(); if (StringUtils.isNotBlank(responseData.getText())) { builder.content(List.of(TextContent.textContent(responseData.getText()))); } - if (!responseData.getToolCalls().isEmpty()) { - builder.toolCalls(responseData.getToolCalls()); + // Only include tool calls if stopReason is "tool_use" and filter to only inline_function tools + // Built-in tools (shell, browser, etc.) are executed by Harness internally + if ("tool_use".equals(responseData.getStopReason()) && !responseData.getToolCalls().isEmpty()) { + var inlineToolCalls = + responseData.getToolCalls().stream() + .filter(tc -> inlineToolNames.contains(tc.name())) + .toList(); + + LOGGER.info( + "Filtered {} tool calls to {} inline_function calls (names={})", + responseData.getToolCalls().size(), + inlineToolCalls.size(), + inlineToolCalls.stream().map(ToolCall::name).toList()); + + if (!inlineToolCalls.isEmpty()) { + builder.toolCalls(inlineToolCalls); + } } return builder.build(); @@ -212,6 +253,7 @@ private class HarnessResponseData { private final StringBuilder currentToolInputBuilder = new StringBuilder(); private int inputTokens = 0; private int outputTokens = 0; + private String stopReason; void appendText(String text) { textBuilder.append(text); @@ -269,6 +311,10 @@ void setTokenUsage(Integer input, Integer output) { this.outputTokens = output != null ? output : 0; } + void setStopReason(String stopReason) { + this.stopReason = stopReason; + } + String getText() { return textBuilder.toString(); } @@ -277,6 +323,10 @@ List getToolCalls() { return toolCalls; } + String getStopReason() { + return stopReason; + } + int inputTokens() { return inputTokens; } diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java index 06f5781dd30..f578a663e0e 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java @@ -38,6 +38,7 @@ import io.camunda.connector.agenticai.aiagent.agent.AgentToolsResolverImpl; import io.camunda.connector.agenticai.aiagent.agent.JobWorkerAgentRequestHandler; import io.camunda.connector.agenticai.aiagent.agent.OutboundConnectorAgentRequestHandler; +import io.camunda.connector.agenticai.aiagent.agentcoreharness.AgentCoreHarnessJobWorker; import io.camunda.connector.agenticai.aiagent.framework.AiFrameworkAdapter; import io.camunda.connector.agenticai.aiagent.framework.langchain4j.ChatModelHttpProxySupport; import io.camunda.connector.agenticai.aiagent.framework.langchain4j.configuration.AgenticAiLangchain4JFrameworkConfiguration; @@ -309,4 +310,25 @@ public JobWorkerAgentRequestHandler aiAgentJobWorkerAgentRequestHandler( public AiAgentJobWorker aiAgentJobWorker(JobWorkerAgentRequestHandler agentRequestHandler) { return new AiAgentJobWorker(agentRequestHandler); } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBooleanProperty( + value = "camunda.connector.agenticai.agentcore-harness.enabled", + matchIfMissing = true) + public AgentCoreHarnessJobWorker agentCoreHarnessJobWorker( + AgentInitializer agentInitializer, + ConversationStoreRegistry conversationStoreRegistry, + AgentLimitsValidator limitsValidator, + AgentMessagesHandler messagesHandler, + GatewayToolHandlerRegistry gatewayToolHandlers, + AgentResponseHandler responseHandler) { + return new AgentCoreHarnessJobWorker( + agentInitializer, + conversationStoreRegistry, + limitsValidator, + messagesHandler, + gatewayToolHandlers, + responseHandler); + } } From 882d64d8a2176d87f1704c9931c4e1e58e78d534 Mon Sep 17 00:00:00 2001 From: tshepomaredi Date: Wed, 20 May 2026 10:31:54 +0200 Subject: [PATCH 3/4] test(agentic-ai): add unit tests for AgentCore Harness converters - Add HarnessToolConverterTest (7 tests) - Add HarnessMessageConverterTest (12 tests) - Cover tool definition conversion, message conversion, and error handling --- .../HarnessMessageConverterTest.java | 228 ++++++++++++++++++ .../HarnessToolConverterTest.java | 129 ++++++++++ 2 files changed, 357 insertions(+) create mode 100644 connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java create mode 100644 connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverterTest.java diff --git a/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java b/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java new file mode 100644 index 00000000000..77b790a6e32 --- /dev/null +++ b/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java @@ -0,0 +1,228 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.framework.agentcoreharness; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.camunda.connector.agenticai.model.message.AssistantMessage; +import io.camunda.connector.agenticai.model.message.Message; +import io.camunda.connector.agenticai.model.message.SystemMessage; +import io.camunda.connector.agenticai.model.message.ToolCallResultMessage; +import io.camunda.connector.agenticai.model.message.UserMessage; +import io.camunda.connector.agenticai.model.message.content.TextContent; +import io.camunda.connector.agenticai.model.tool.ToolCall; +import io.camunda.connector.agenticai.model.tool.ToolCallResult; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessConversationRole; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseBlock; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseStatus; + +class HarnessMessageConverterTest { + + private final HarnessMessageConverter converter = new HarnessMessageConverter(); + + @ParameterizedTest + @NullAndEmptySource + void extractSystemPromptReturnsEmptyForNullOrEmpty(List messages) { + var result = converter.extractSystemPrompt(messages != null ? messages : List.of()); + assertThat(result).isEmpty(); + } + + @Test + void extractSystemPromptExtractsSystemMessages() { + var messages = + List.of(systemMessage("You are a helpful assistant."), userMessage("Hello")); + + var result = converter.extractSystemPrompt(messages); + + assertThat(result).hasSize(1); + assertThat(result.get(0).text()).isEqualTo("You are a helpful assistant."); + } + + @Test + void extractSystemPromptHandlesMultipleSystemMessages() { + var messages = + List.of(systemMessage("First instruction"), systemMessage("Second instruction")); + + var result = converter.extractSystemPrompt(messages); + + assertThat(result).hasSize(2); + assertThat(result.get(0).text()).isEqualTo("First instruction"); + assertThat(result.get(1).text()).isEqualTo("Second instruction"); + } + + @Test + void toHarnessMessagesExcludesSystemMessages() { + var messages = List.of(systemMessage("System prompt"), userMessage("User message")); + + var result = converter.toHarnessMessages(messages); + + assertThat(result).hasSize(1); + assertThat(result.get(0).role()).isEqualTo(HarnessConversationRole.USER); + } + + @Test + void toHarnessMessagesConvertsUserMessage() { + var messages = List.of(userMessage("Hello, how are you?")); + + var result = converter.toHarnessMessages(messages); + + assertThat(result).hasSize(1); + assertThat(result.get(0).role()).isEqualTo(HarnessConversationRole.USER); + assertThat(result.get(0).content()).hasSize(1); + assertThat(result.get(0).content().get(0).text()).isEqualTo("Hello, how are you?"); + } + + @Test + void toHarnessMessagesConvertsAssistantMessage() { + var assistantMessage = + AssistantMessage.builder() + .content(List.of(TextContent.textContent("I'm doing well, thank you!"))) + .build(); + var messages = List.of(assistantMessage); + + var result = converter.toHarnessMessages(messages); + + assertThat(result).hasSize(1); + assertThat(result.get(0).role()).isEqualTo(HarnessConversationRole.ASSISTANT); + assertThat(result.get(0).content().get(0).text()).isEqualTo("I'm doing well, thank you!"); + } + + @Test + void toHarnessMessagesConvertsAssistantMessageWithToolCalls() { + var toolCall = + ToolCall.builder() + .id("call_123") + .name("get_weather") + .arguments(Map.of("location", "Seattle")) + .build(); + var assistantMessage = + AssistantMessage.builder() + .content(List.of(TextContent.textContent("Let me check the weather."))) + .toolCalls(List.of(toolCall)) + .build(); + var messages = List.of(assistantMessage); + + var result = converter.toHarnessMessages(messages); + + assertThat(result).hasSize(1); + assertThat(result.get(0).content()).hasSize(2); + // First content block is text + assertThat(result.get(0).content().get(0).text()).isEqualTo("Let me check the weather."); + // Second content block is tool use + var toolUse = result.get(0).content().get(1).toolUse(); + assertThat(toolUse.toolUseId()).isEqualTo("call_123"); + assertThat(toolUse.name()).isEqualTo("get_weather"); + } + + @Test + void toHarnessMessagesConvertsToolCallResultMessage() { + var toolResult = + ToolCallResult.builder().id("call_123").name("get_weather").content("Sunny, 72°F").build(); + var messages = List.of(toolCallResultMessage(toolResult)); + + var result = converter.toHarnessMessages(messages); + + assertThat(result).hasSize(1); + assertThat(result.get(0).role()).isEqualTo(HarnessConversationRole.USER); + var toolResultBlock = result.get(0).content().get(0).toolResult(); + assertThat(toolResultBlock.toolUseId()).isEqualTo("call_123"); + assertThat(toolResultBlock.status()).isEqualTo(HarnessToolUseStatus.SUCCESS); + } + + @Test + void toHarnessMessagesConvertsErrorToolCallResult() { + var toolResult = + ToolCallResult.builder() + .id("call_456") + .name("failing_tool") + .content("Error occurred") + .properties(Map.of(ToolCallResult.PROPERTY_INTERRUPTED, true)) + .build(); + var messages = List.of(toolCallResultMessage(toolResult)); + + var result = converter.toHarnessMessages(messages); + + var toolResultBlock = result.get(0).content().get(0).toolResult(); + assertThat(toolResultBlock.status()).isEqualTo(HarnessToolUseStatus.ERROR); + } + + @Test + void toToolCallConvertsHarnessToolUseBlock() { + var toolUseBlock = + HarnessToolUseBlock.builder() + .toolUseId("tool_use_abc") + .name("search_database") + .input( + Document.fromMap( + Map.of( + "query", Document.fromString("test query"), + "limit", Document.fromNumber("10")))) + .build(); + + var result = converter.toToolCall(toolUseBlock); + + assertThat(result.id()).isEqualTo("tool_use_abc"); + assertThat(result.name()).isEqualTo("search_database"); + assertThat(result.arguments()).containsEntry("query", "test query"); + } + + @Test + void toHarnessMessagesHandlesConversationFlow() { + var messages = + List.of( + systemMessage("You are helpful."), + userMessage("What's the weather?"), + AssistantMessage.builder() + .content(List.of(TextContent.textContent("Checking..."))) + .toolCalls( + List.of( + ToolCall.builder() + .id("call_1") + .name("get_weather") + .arguments(Map.of("location", "NYC")) + .build())) + .build(), + toolCallResultMessage( + ToolCallResult.builder() + .id("call_1") + .name("get_weather") + .content("Rainy, 55°F") + .build()), + AssistantMessage.builder() + .content(List.of(TextContent.textContent("It's rainy in NYC."))) + .build()); + + var systemPrompt = converter.extractSystemPrompt(messages); + var harnessMessages = converter.toHarnessMessages(messages); + + assertThat(systemPrompt).hasSize(1); + assertThat(harnessMessages).hasSize(4); // user, assistant+tool, tool_result, assistant + assertThat(harnessMessages.get(0).role()).isEqualTo(HarnessConversationRole.USER); + assertThat(harnessMessages.get(1).role()).isEqualTo(HarnessConversationRole.ASSISTANT); + assertThat(harnessMessages.get(2).role()).isEqualTo(HarnessConversationRole.USER); + assertThat(harnessMessages.get(3).role()).isEqualTo(HarnessConversationRole.ASSISTANT); + } + + // Helper methods to create messages + private static SystemMessage systemMessage(String text) { + return SystemMessage.builder().content(List.of(TextContent.textContent(text))).build(); + } + + private static UserMessage userMessage(String text) { + return UserMessage.builder().content(List.of(TextContent.textContent(text))).build(); + } + + private static ToolCallResultMessage toolCallResultMessage(ToolCallResult result) { + return ToolCallResultMessage.builder().results(List.of(result)).build(); + } +} diff --git a/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverterTest.java b/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverterTest.java new file mode 100644 index 00000000000..29861ef6529 --- /dev/null +++ b/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessToolConverterTest.java @@ -0,0 +1,129 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.agenticai.aiagent.framework.agentcoreharness; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.camunda.connector.agenticai.model.tool.ToolDefinition; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolType; + +class HarnessToolConverterTest { + + private final HarnessToolConverter converter = new HarnessToolConverter(); + + @ParameterizedTest + @NullAndEmptySource + void toHarnessToolsReturnsEmptyListForNullOrEmpty(List toolDefinitions) { + assertThat(converter.toHarnessTools(toolDefinitions)).isEmpty(); + } + + @Test + void toHarnessToolConvertsBasicToolDefinition() { + var toolDefinition = + ToolDefinition.builder() + .name("get_weather") + .description("Get the current weather for a location") + .inputSchema( + Map.of( + "type", "object", + "properties", Map.of("location", Map.of("type", "string")), + "required", List.of("location"))) + .build(); + + var result = converter.toHarnessTool(toolDefinition); + + assertThat(result.name()).isEqualTo("get_weather"); + assertThat(result.type()).isEqualTo(HarnessToolType.INLINE_FUNCTION); + assertThat(result.config().inlineFunction()).isNotNull(); + assertThat(result.config().inlineFunction().description()) + .isEqualTo("Get the current weather for a location"); + } + + @Test + void toHarnessToolsConvertsMultipleTools() { + var tool1 = + ToolDefinition.builder() + .name("tool_one") + .description("First tool") + .inputSchema(Map.of("type", "object")) + .build(); + var tool2 = + ToolDefinition.builder() + .name("tool_two") + .description("Second tool") + .inputSchema(Map.of("type", "object")) + .build(); + + var result = converter.toHarnessTools(List.of(tool1, tool2)); + + assertThat(result).hasSize(2); + assertThat(result.get(0).name()).isEqualTo("tool_one"); + assertThat(result.get(1).name()).isEqualTo("tool_two"); + } + + @Test + void toHarnessToolHandlesComplexInputSchema() { + var toolDefinition = + ToolDefinition.builder() + .name("complex_tool") + .description("A tool with complex schema") + .inputSchema( + Map.of( + "type", "object", + "properties", + Map.of( + "name", Map.of("type", "string"), + "count", Map.of("type", "number"), + "enabled", Map.of("type", "boolean"), + "tags", Map.of("type", "array", "items", Map.of("type", "string"))), + "required", List.of("name"))) + .build(); + + var result = converter.toHarnessTool(toolDefinition); + + assertThat(result.name()).isEqualTo("complex_tool"); + assertThat(result.config().inlineFunction().inputSchema()).isNotNull(); + var schema = result.config().inlineFunction().inputSchema(); + assertThat(schema.asMap()).containsKey("type"); + assertThat(schema.asMap().get("type").asString()).isEqualTo("object"); + } + + @Test + void toHarnessToolHandlesEmptyInputSchema() { + var toolDefinition = + ToolDefinition.builder() + .name("no_params_tool") + .description("A tool with no parameters") + .inputSchema(Map.of()) + .build(); + + var result = converter.toHarnessTool(toolDefinition); + + assertThat(result.name()).isEqualTo("no_params_tool"); + assertThat(result.config().inlineFunction().inputSchema().asMap()).isEmpty(); + } + + @Test + void toHarnessToolHandlesNullInputSchema() { + var toolDefinition = + ToolDefinition.builder() + .name("null_schema_tool") + .description("A tool with null schema") + .inputSchema(null) + .build(); + + var result = converter.toHarnessTool(toolDefinition); + + assertThat(result.name()).isEqualTo("null_schema_tool"); + assertThat(result.config().inlineFunction().inputSchema().asMap()).isEmpty(); + } +} From aba3ff44093a833cdab5b3d570b2256b97089f5d Mon Sep 17 00:00:00 2001 From: tshepomaredi Date: Fri, 29 May 2026 11:32:45 +0200 Subject: [PATCH 4/4] fix(agentic-ai): address PR review comments for AgentCore Harness - Rename template to 'Amazon Bedrock AgentCore Managed Agent (Alpha)' - Add [Alpha] prefix to description and 'Amazon' keyword - Update engine version to ^8.10 - Revert log level from INFO to DEBUG - Rename AgentCoreHarnessJobWorker to AgentCoreHarnessSubProcess - Add session timeout documentation comment - Remove unused toToolCall method from HarnessMessageConverter --- .../agenticai-agentcore-harness.json | 8 +++--- .../agent/JobWorkerAgentRequestHandler.java | 2 +- ...r.java => AgentCoreHarnessSubProcess.java} | 26 +++++++++---------- .../AgentCoreHarnessAdapter.java | 7 +++++ .../HarnessMessageConverter.java | 14 ---------- .../AgenticAiConnectorsAutoConfiguration.java | 6 ++--- .../HarnessMessageConverterTest.java | 22 ---------------- 7 files changed, 28 insertions(+), 57 deletions(-) rename connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/{AgentCoreHarnessJobWorker.java => AgentCoreHarnessSubProcess.java} (87%) diff --git a/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json b/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json index ee5f8c31517..4f5f62ebdba 100644 --- a/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json +++ b/connectors/agentic-ai/element-templates/agenticai-agentcore-harness.json @@ -1,9 +1,9 @@ { "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", - "name": "AgentCore Managed Agent", + "name": "Amazon Bedrock AgentCore Managed Agent (Alpha)", "id": "io.camunda.connectors.agenticai.agentcore.harness.v1", - "description": "Drive an AWS Bedrock AgentCore Harness through a re-entrant tool-call loop, exposing the AHSP inner elements as inline_function tools.", - "keywords": ["AI", "AI Agent", "AWS", "AgentCore", "Harness", "Bedrock"], + "description": "[Alpha] Drive an Amazon Bedrock AgentCore Harness through a re-entrant tool-call loop, exposing the AHSP inner elements as inline_function tools.", + "keywords": ["AI", "AI Agent", "Amazon", "AWS", "AgentCore", "Harness", "Bedrock"], "version": 1, "category": { "id": "connectors", @@ -14,7 +14,7 @@ "value": "bpmn:AdHocSubProcess" }, "engines": { - "camunda": "^8.9" + "camunda": "^8.10" }, "groups": [ { diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java index 05d908996fd..e7b84914188 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agent/JobWorkerAgentRequestHandler.java @@ -170,7 +170,7 @@ private List buildElementActivations(AgentResponse agentRespo if (LOGGER.isTraceEnabled()) { LOGGER.trace("Activating tool {}: {}", toolCall.metadata().name(), toolCall); } else { - LOGGER.info("Activating tool {}", toolCall.metadata().name()); + LOGGER.debug("Activating tool {}", toolCall.metadata().name()); } return (ElementActivation) diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessSubProcess.java similarity index 87% rename from connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java rename to connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessSubProcess.java index 136d30bc764..2107bff2b21 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessJobWorker.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/agentcoreharness/AgentCoreHarnessSubProcess.java @@ -35,21 +35,21 @@ * inner elements as inline_function tools. */ @OutboundConnector( - name = AgentCoreHarnessJobWorker.JOB_WORKER_NAME, - type = AgentCoreHarnessJobWorker.JOB_WORKER_TYPE, + name = AgentCoreHarnessSubProcess.CONNECTOR_NAME, + type = AgentCoreHarnessSubProcess.CONNECTOR_TYPE, inputVariables = { - AgentCoreHarnessJobWorker.AD_HOC_SUB_PROCESS_ELEMENT_VARIABLE, - AgentCoreHarnessJobWorker.AGENT_CONTEXT_VARIABLE, - AgentCoreHarnessJobWorker.TOOL_CALL_RESULTS_VARIABLE, - AgentCoreHarnessJobWorker.HARNESS_VARIABLE, - AgentCoreHarnessJobWorker.AUTHENTICATION_VARIABLE, - AgentCoreHarnessJobWorker.USER_PROMPT_VARIABLE, - AgentCoreHarnessJobWorker.MAX_ITERATIONS_VARIABLE + AgentCoreHarnessSubProcess.AD_HOC_SUB_PROCESS_ELEMENT_VARIABLE, + AgentCoreHarnessSubProcess.AGENT_CONTEXT_VARIABLE, + AgentCoreHarnessSubProcess.TOOL_CALL_RESULTS_VARIABLE, + AgentCoreHarnessSubProcess.HARNESS_VARIABLE, + AgentCoreHarnessSubProcess.AUTHENTICATION_VARIABLE, + AgentCoreHarnessSubProcess.USER_PROMPT_VARIABLE, + AgentCoreHarnessSubProcess.MAX_ITERATIONS_VARIABLE }) -public class AgentCoreHarnessJobWorker implements AgentConnectorFunction { +public class AgentCoreHarnessSubProcess implements AgentConnectorFunction { - public static final String JOB_WORKER_NAME = "AgentCore Harness Job Worker"; - public static final String JOB_WORKER_TYPE = "io.camunda.agenticai:agentcore-harness:1"; + public static final String CONNECTOR_NAME = "Amazon Bedrock AgentCore Harness"; + public static final String CONNECTOR_TYPE = "io.camunda.agenticai:agentcore-harness:1"; public static final String AD_HOC_SUB_PROCESS_ELEMENT_VARIABLE = "adHocSubProcessElements"; public static final String AGENT_CONTEXT_VARIABLE = "agentContext"; @@ -66,7 +66,7 @@ public class AgentCoreHarnessJobWorker implements AgentConnectorFunction { private final GatewayToolHandlerRegistry gatewayToolHandlers; private final AgentResponseHandler responseHandler; - public AgentCoreHarnessJobWorker( + public AgentCoreHarnessSubProcess( AgentInitializer agentInitializer, ConversationStoreRegistry conversationStoreRegistry, AgentLimitsValidator limitsValidator, diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java index 7ec168ec72f..9bf7f28704f 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/AgentCoreHarnessAdapter.java @@ -139,6 +139,13 @@ public AgentCoreHarnessAiFrameworkChatResponse executeChatRequest( updatedAgentContext, assistantMessage, sessionId); } + /** + * Gets existing session ID from agent context or creates a new one. + * + *

The runtimeSessionId is used by Harness for conversation continuity across multiple + * InvokeHarness calls. The session is managed by Harness and persists until the Harness + * configuration's idle timeout is reached. Session timeouts are configured in AWS, not here. + */ private String getOrCreateSessionId(AgentContext agentContext) { return Optional.ofNullable(agentContext.properties()) .map(props -> props.get(SESSION_ID_PROPERTY)) diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java index 4ef51d06d7b..cdbb6178851 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverter.java @@ -70,20 +70,6 @@ public List toHarnessMessages(List messages) { return result; } - /** - * Converts a Harness tool use block to a Camunda ToolCall. - * - * @param toolUseBlock the Harness tool use block - * @return Camunda ToolCall - */ - public ToolCall toToolCall(HarnessToolUseBlock toolUseBlock) { - return ToolCall.builder() - .id(toolUseBlock.toolUseId()) - .name(toolUseBlock.name()) - .arguments(documentToMap(toolUseBlock.input())) - .build(); - } - private List toSystemContentBlocks(SystemMessage systemMessage) { return extractTextContent(systemMessage).stream() .map(text -> HarnessSystemContentBlock.fromText(text)) diff --git a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java index f578a663e0e..fc9df0276f1 100644 --- a/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java +++ b/connectors/agentic-ai/src/main/java/io/camunda/connector/agenticai/autoconfigure/AgenticAiConnectorsAutoConfiguration.java @@ -38,7 +38,7 @@ import io.camunda.connector.agenticai.aiagent.agent.AgentToolsResolverImpl; import io.camunda.connector.agenticai.aiagent.agent.JobWorkerAgentRequestHandler; import io.camunda.connector.agenticai.aiagent.agent.OutboundConnectorAgentRequestHandler; -import io.camunda.connector.agenticai.aiagent.agentcoreharness.AgentCoreHarnessJobWorker; +import io.camunda.connector.agenticai.aiagent.agentcoreharness.AgentCoreHarnessSubProcess; import io.camunda.connector.agenticai.aiagent.framework.AiFrameworkAdapter; import io.camunda.connector.agenticai.aiagent.framework.langchain4j.ChatModelHttpProxySupport; import io.camunda.connector.agenticai.aiagent.framework.langchain4j.configuration.AgenticAiLangchain4JFrameworkConfiguration; @@ -316,14 +316,14 @@ public AiAgentJobWorker aiAgentJobWorker(JobWorkerAgentRequestHandler agentReque @ConditionalOnBooleanProperty( value = "camunda.connector.agenticai.agentcore-harness.enabled", matchIfMissing = true) - public AgentCoreHarnessJobWorker agentCoreHarnessJobWorker( + public AgentCoreHarnessSubProcess agentCoreHarnessSubProcess( AgentInitializer agentInitializer, ConversationStoreRegistry conversationStoreRegistry, AgentLimitsValidator limitsValidator, AgentMessagesHandler messagesHandler, GatewayToolHandlerRegistry gatewayToolHandlers, AgentResponseHandler responseHandler) { - return new AgentCoreHarnessJobWorker( + return new AgentCoreHarnessSubProcess( agentInitializer, conversationStoreRegistry, limitsValidator, diff --git a/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java b/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java index 77b790a6e32..737260aae3b 100644 --- a/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java +++ b/connectors/agentic-ai/src/test/java/io/camunda/connector/agenticai/aiagent/framework/agentcoreharness/HarnessMessageConverterTest.java @@ -21,9 +21,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; -import software.amazon.awssdk.core.document.Document; import software.amazon.awssdk.services.bedrockagentcore.model.HarnessConversationRole; -import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseBlock; import software.amazon.awssdk.services.bedrockagentcore.model.HarnessToolUseStatus; class HarnessMessageConverterTest { @@ -156,26 +154,6 @@ void toHarnessMessagesConvertsErrorToolCallResult() { assertThat(toolResultBlock.status()).isEqualTo(HarnessToolUseStatus.ERROR); } - @Test - void toToolCallConvertsHarnessToolUseBlock() { - var toolUseBlock = - HarnessToolUseBlock.builder() - .toolUseId("tool_use_abc") - .name("search_database") - .input( - Document.fromMap( - Map.of( - "query", Document.fromString("test query"), - "limit", Document.fromNumber("10")))) - .build(); - - var result = converter.toToolCall(toolUseBlock); - - assertThat(result.id()).isEqualTo("tool_use_abc"); - assertThat(result.name()).isEqualTo("search_database"); - assertThat(result.arguments()).containsEntry("query", "test query"); - } - @Test void toHarnessMessagesHandlesConversationFlow() { var messages =