Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions argi-orchestration/argi-workflow-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ Applications that need declarative independent execution depend on
`argi-workflow-core`, publish a `WorkflowDefinition`, and construct a
`WorkflowAgent` with their agent and tool resolvers.

## Tool step outputs

A successful plain text result remains a string when it contains exactly one
`TEXT` part with a non-null text value, no MIME type or binary data, and no
part-level or result-level metadata. This includes results from `ToolResult.text`.

Other successful results use a map with `content`, `error` (false), and `metadata`.
`content` is an ordered list of maps, each containing `type` (the `ContentPart.Type`
name), `text`, `mimeType`, `data`, and `metadata`. Optional part fields retain null;
binary `data` is a copy of the original byte array. Empty content is an empty list,
so the step output itself is never null. Metadata values retain their runtime types;
they are not converted through JSON or `toString`, and this representation does not
add deep immutability guarantees for runtime data.

The same value is available as the step output and, when configured, under the
step's `outputKey` in state. Existing map/list paths can supply later tool arguments
or agent inputs, for example `new WorkflowValue.StepOutput("fetch", "content.1.text")`
or `WorkflowValue.state("fetch.metadata.receipt")`. JSON parts retain their original
JSON text in `content.0.text`, together with type `JSON` and their MIME type; they
are not automatically parsed into workflow values. Consumers that previously read
the first text from a compound or metadata-bearing result should select its explicit
`content.0.text` path instead. Binary data can be passed directly to a later agent
input; serialization into a later tool's JSON arguments follows the configured
`ObjectMapper` and does not establish end-to-end provider multimodal support.

Preparing arguments, failed invocation stages, and `ToolResult.error` retain the
tool step's retry policy. Projecting a successfully completed result occurs outside
that retry boundary. A malformed null result fails with a diagnostic
`WorkflowExecutionException` without invoking the tool again. This prevents result
handling failures from replaying a successful tool effect; it does not promise
exactly-once effects when a failed invocation has already performed external work.

## Definition data snapshots

Starting with the `2.1.0-RC2-SNAPSHOT` fix for #272, `WorkflowValue.Literal` and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.github.agentic.ai.agent.AgentOutcome;
import io.github.agentic.ai.agent.AgentRequest;
import io.github.agentic.ai.api.chat.ChatMessage;
import io.github.agentic.ai.api.chat.ContentPart;
import io.github.agentic.ai.api.chat.ToolCall;
import io.github.agentic.ai.api.chat.Usage;
import io.github.agentic.ai.api.tool.Tool;
Expand All @@ -30,6 +31,7 @@
import io.github.agentic.ai.api.tool.ToolResult;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -263,7 +265,7 @@ private ExecutionResult executeStep(WorkflowStep step, RunData data, AgentExecut
return executeWithRetry(agent.retry(), () -> executeAgent(agent, data, context), context);
}
if (step instanceof WorkflowStep.Tool tool) {
return executeWithRetry(tool.retry(), () -> executeTool(tool, data, context), context);
return executeTool(tool, data, context);
}
if (step instanceof WorkflowStep.Sequence sequence) {
return executeSteps(sequence.steps(), 0, data, context);
Expand Down Expand Up @@ -351,29 +353,61 @@ private ExecutionResult applyAgentOutput(WorkflowStep.Agent step, AgentOutcome o
}

private ExecutionResult executeTool(WorkflowStep.Tool step, RunData data, AgentExecutionContext context) {
Tool tool = tools.find(step.toolName()).orElseThrow(() ->
new WorkflowExecutionException("Unknown workflow tool: " + step.toolName()));
Map<String, Object> arguments = resolveMap(step.arguments(), data);
String json;
try {
json = mapper.writeValueAsString(arguments);
}
catch (Exception exception) {
throw new WorkflowExecutionException("Workflow tool arguments are not serializable: " + step.id(), exception);
}
ToolResult result = await(tool.execute(new ToolCall(step.id() + ":" + UUID.randomUUID(), step.toolName(), json),
new ToolContext(data.state)).toCompletableFuture());
if (result.error()) {
throw new WorkflowExecutionException("Workflow tool failed: " + step.toolName());
ToolResult result = executeWithRetry(step.retry(), () -> {
Tool tool = tools.find(step.toolName())
.orElseThrow(() -> new WorkflowExecutionException("Unknown workflow tool: " + step.toolName()));
Map<String, Object> arguments = resolveMap(step.arguments(), data);
String json;
try {
json = mapper.writeValueAsString(arguments);
}
catch (Exception exception) {
throw new WorkflowExecutionException("Workflow tool arguments are not serializable: " + step.id(),
exception);
}
ToolResult attempt = await(tool
.execute(new ToolCall(step.id() + ":" + UUID.randomUUID(), step.toolName(), json),
new ToolContext(data.state))
.toCompletableFuture());
if (attempt != null && attempt.error()) {
throw new WorkflowExecutionException("Workflow tool failed: " + step.toolName());
}
return attempt;
}, context);
// A completed tool effect must not be replayed when handling its result fails.
if (result == null) {
throw new WorkflowExecutionException("Workflow tool returned a null result: " + step.toolName());
}
Object output = result.content().isEmpty() ? null : result.content().get(0).text();
Object output = toolOutput(result);
data.stepOutputs.put(step.id(), output);
if (step.outputKey() != null) {
data.state.put(step.outputKey(), output);
}
return ExecutionResult.completed(data);
}

private static Object toolOutput(ToolResult result) {
if (result.content().size() == 1 && result.metadata().isEmpty()) {
ContentPart part = result.content().get(0);
if (part.type() == ContentPart.Type.TEXT && part.text() != null && part.mimeType() == null
&& part.data() == null && part.metadata().isEmpty()) {
return part.text();
}
}
return Map.of("content", result.content().stream().map(WorkflowAgent::toolContent).toList(), "error",
result.error(), "metadata", result.metadata());
}

private static Map<String, Object> toolContent(ContentPart part) {
Map<String, Object> content = new LinkedHashMap<>();
content.put("type", part.type().name());
content.put("text", part.text());
content.put("mimeType", part.mimeType());
content.put("data", part.data());
content.put("metadata", part.metadata());
return Collections.unmodifiableMap(content);
}

private ExecutionResult executeParallel(WorkflowStep.Parallel step, RunData data, AgentExecutionContext context) {
int baseMessageCount = data.messages.size();
Semaphore permits = new Semaphore(step.maxConcurrency());
Expand Down Expand Up @@ -409,8 +443,8 @@ private ExecutionResult executeParallel(WorkflowStep.Parallel step, RunData data
return ExecutionResult.completed(data);
}

private ExecutionResult executeWithRetry(WorkflowStep.RetryPolicy policy, Operation operation,
AgentExecutionContext context) {
private <T> T executeWithRetry(WorkflowStep.RetryPolicy policy, Operation<T> operation,
AgentExecutionContext context) {
RuntimeException failure = null;
for (int attempt = 1; attempt <= policy.maxAttempts(); attempt++) {
try {
Expand All @@ -430,7 +464,8 @@ private ExecutionResult executeWithRetry(WorkflowStep.RetryPolicy policy, Operat
context.control().assertActive();
}
}
throw new WorkflowExecutionException("Workflow step failed after " + policy.maxAttempts() + " attempts", failure);
throw new WorkflowExecutionException("Workflow step failed after " + policy.maxAttempts() + " attempts",
failure);
}

private Map<String, Object> resolveMap(Map<String, WorkflowValue> expressions, RunData data) {
Expand Down Expand Up @@ -542,8 +577,10 @@ private static <T> T await(CompletableFuture<T> future) {
}
}

private interface Operation {
ExecutionResult run();
private interface Operation<T> {

T run();

}

@FunctionalInterface
Expand Down
Loading
Loading