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
53 changes: 53 additions & 0 deletions src/main/java/com/openjiuwen/core/application/llm/LlmAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,59 @@ public LlmAgent(LlmAgentConfig agentConfig) {
getController().setEventHandler(eventHandler);
}

/**
* Add tools to this agent dynamically.
* <p>
* Registers each tool in the ability manager and resource manager.
* Mirrors Python's LLMAgent.add_tools.
*
* @param tools the tools to add
* @since 0.1.7
*/
public void addTools(List<Tool> tools) {
if (tools == null || tools.isEmpty()) {
return;
}
String tag = agentConfig != null ? agentConfig.getId() : null;
for (Tool tool : tools) {
if (tool == null || tool.getCard() == null) {
continue;
}
registerPluginSchema(agentConfig, tool.getCard());
this.getAbilityManager().add(tool.getCard());
Runner.resourceMgr().addTool(tool, tag);
}
}

/**
* Add workflows to this agent dynamically.
* <p>
* Registers each workflow in the ability manager and resource manager.
* Mirrors Python's LLMAgent.add_workflows.
*
* @param workflows the workflows to add
* @since 0.1.7
*/
public void addWorkflows(List<Workflow> workflows) {
if (workflows == null || workflows.isEmpty()) {
return;
}
String tag = agentConfig != null ? agentConfig.getId() : null;
for (Workflow workflow : workflows) {
if (workflow == null || workflow.getCard() == null) {
continue;
}
registerWorkflowSchema(agentConfig, workflow.getCard());
this.getAbilityManager().add(workflow.getCard());
WorkflowCard card = workflow.getCard();
String workflowResourceId = WorkflowUtils.generateWorkflowKey(card.getId(), card.getVersion());
WorkflowCard resourceCard =
WorkflowCard.builder().id(workflowResourceId).name(card.getName()).version(card.getVersion())
.description(card.getDescription()).inputParams(card.getInputParams()).build();
Runner.resourceMgr().addWorkflow(resourceCard, () -> workflow, tag);
}
}

/**
* invoke.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,10 @@ private Model getModel() {
}

var modelInfo = agentConfig.getModel().modelInfo();
String provider = agentConfig.getModel().modelProvider();
Loggers.CONTROLLER.info("getModel: provider='{}', apiBase='{}'", provider, modelInfo.getApiBase());
ModelClientConfig clientConfig = ModelClientConfig.builder()
.clientProvider(agentConfig.getModel().modelProvider()).apiKey(modelInfo.getApiKey())
.clientProvider(provider).apiKey(modelInfo.getApiKey())
.apiBase(modelInfo.getApiBase()).timeout(modelInfo.getTimeout()).verifySsl(modelInfo.isVerifySsl())
.sslCert(modelInfo.getSslCert()).headers(modelInfo.getHeaders()).build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ public boolean cancel(boolean isCascade, String reason, String cancelledBy) {
markCancelled();
}
if (isCascade && owner != null) {
owner.cascadeCancel(taskId, "parent_cancelled");
owner.cascadeCancel(taskId, this.cancelReason);
}
if (timeoutHandle != null) {
timeoutHandle.cancel(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,17 @@ public <T> Task createTask(Callable<T> callable, String taskId, String name, Str
*/
public int cancelGroup(String group) {
int count = 0;
for (Task task : registry.getByGroup(group)) {
if (task.cancel(false, "manual_cancel", null)) {
List<Task> groupTasks = registry.getByGroup(group);
for (Task task : groupTasks) {
// Skip tasks whose parent is also in the same group — they will be
// cancelled by the cascade originating from the root task.
boolean hasParentInGroup = task.getParentTaskId() != null
&& registry.get(task.getParentTaskId()) != null
&& group.equals(registry.get(task.getParentTaskId()).getGroup());
if (hasParentInGroup) {
continue;
}
if (task.cancel(true, "manual_cancel", null)) {
count++;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public static Map<String, Object> formatWithSchema(Map<String, Object> data, Map

return dataWithDefaults;
} catch (ValidationError e) {
throw e;
throw new ValidationError(StatusCode.SCHEMA_FORMAT_INVALID, null, null, e,
Map.of("reason", e.getMessage(), "data", String.valueOf(data)));
} catch (Exception e) {
throw new ValidationError(StatusCode.SCHEMA_FORMAT_INVALID, null, null, e,
Map.of("reason", e.getMessage(), "data", String.valueOf(data)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,11 @@ private HttpRequest buildJsonRequest(String suffix, Map<String, Object> body, Fl
* @since 0.1.7
*/
private String normalizedApiBase() {
return modelClientConfig.getApiBase().replaceAll("/+$", "");
String base = modelClientConfig.getApiBase().replaceAll("/+$", "");
if (base.endsWith("/v1/chat/completions")) {
base = base.substring(0, base.length() - "/v1/chat/completions".length());
}
return base;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,11 @@ private static void applyCallTimeout(Call call, Float timeoutOverride) {
* @since 0.1.7
*/
private String normalizedApiBase() {
return modelClientConfig.getApiBase().strip().replaceAll("/+$", "");
String base = modelClientConfig.getApiBase().strip().replaceAll("/+$", "");
if (base.endsWith("/chat/completions")) {
base = base.substring(0, base.length() - "/chat/completions".length());
}
return base;
}

/**
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/com/openjiuwen/core/foundation/tool/ToolCard.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
public class ToolCard extends BaseCard {
private Map<String, Object> inputParams = new HashMap<>();

/**
* Raw inputParams object — can be a Map or a model Class.
* <p>
* Mirrors Python's {@code input_params: Dict[str, Any] | Type[BaseModel]}.
* When set to a non-Map value (e.g., a Class), {@link #getInputParams()} returns
* the default empty Map, while {@link #getInputParamsRaw()} returns the original object.
*
* @since 0.1.14
*/
private Object inputParamsRaw;

/**
* Custom properties map.
*
Expand Down Expand Up @@ -68,6 +79,35 @@ public Map<String, Object> getInputParams() {
*/
public void setInputParams(Map<String, Object> inputParams) {
this.inputParams = inputParams;
this.inputParamsRaw = inputParams;
}

/**
* getInputParamsRaw.
* <p>
* Returns the raw inputParams object, which may be a Map or a model Class.
*
* @return the raw inputParams object, or null if not set
* @since 0.1.14
*/
public Object getInputParamsRaw() {
return inputParamsRaw;
}

/**
* setInputParamsRaw.
* <p>
* Sets the raw inputParams object. When the value is a Map, it also updates
* the typed {@code inputParams} field for backward compatibility.
*
* @param inputParamsRaw the raw inputParams object (Map or model Class)
* @since 0.1.14
*/
public void setInputParamsRaw(Object inputParamsRaw) {
this.inputParamsRaw = inputParamsRaw;
if (inputParamsRaw instanceof Map) {
this.inputParams = (Map<String, Object>) inputParamsRaw;
}
}

/**
Expand Down Expand Up @@ -113,6 +153,13 @@ public static class Builder extends BaseCard.Builder {
*/
protected Map<String, Object> inputParams = new HashMap<>();

/**
* Raw inputParams for model class support.
*
* @since 0.1.14
*/
protected Object inputParamsRaw;

/**
* properties.
*
Expand Down Expand Up @@ -168,6 +215,26 @@ public Builder description(String description) {
*/
public Builder inputParams(Map<String, Object> inputParams) {
this.inputParams = inputParams;
this.inputParamsRaw = inputParams;
return this;
}

/**
* inputParams accepting any Object (Map or model Class).
* <p>
* Mirrors Python's {@code input_params: Dict[str, Any] | Type[BaseModel]}.
* When a non-Map value is passed, it is stored in {@code inputParamsRaw}
* for retrieval via {@link ToolCard#getInputParamsRaw()}.
*
* @param inputParams the input parameters (Map or model Class)
* @return this builder
* @since 0.1.14
*/
public Builder inputParams(Object inputParams) {
this.inputParamsRaw = inputParams;
if (inputParams instanceof Map) {
this.inputParams = (Map<String, Object>) inputParams;
}
return this;
}

Expand Down Expand Up @@ -198,6 +265,9 @@ public ToolCard build() {
card.setName(name);
card.setDescription(description);
card.setInputParams(inputParams);
if (inputParamsRaw != null) {
card.setInputParamsRaw(inputParamsRaw);
}
card.setProperties(properties);
return card;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public Iterator<Object> stream(Map<String, Object> inputs, Map<String, Object> k
*/
private static ToolCard buildCard(String targetId, String targetDescription) {
String toolName = targetId;
String toolId = targetId + "_delegate_tool";
String description = "Delegate a task to " + targetId + " for processing.";
if (targetDescription != null && !targetDescription.isBlank()) {
description += " " + targetDescription;
Expand All @@ -138,6 +139,9 @@ private static ToolCard buildCard(String targetId, String targetDescription) {
inputParams.put("type", "object");
inputParams.put("properties", properties);
inputParams.put("required", List.of("message"));
return ToolCard.builder().id(toolName).name(toolName).description(description).inputParams(inputParams).build();
// Use a distinct tool ID (suffixed with "_delegate_tool") to avoid
// ResourceMgr tagMgr collisions with the agent registered under the
// same ID. The tool name visible to the LLM remains the child agent ID.
return ToolCard.builder().id(toolId).name(toolName).description(description).inputParams(inputParams).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.openjiuwen.core.multiagent.BaseTeam;
import com.openjiuwen.core.multiagent.schema.TeamCard;
import com.openjiuwen.core.runner.Runner;
import com.openjiuwen.core.runner.base.TagMatchStrategy;
import com.openjiuwen.core.session.AgentGroupSessionApi;
import com.openjiuwen.core.singleagent.BaseAgent;
import com.openjiuwen.core.singleagent.schema.AgentCard;
Expand Down Expand Up @@ -113,18 +112,14 @@ private void injectDelegateTools(HierarchicalMsgBusTeamConfig config) {
}
for (String childId : children) {
String toolName = childId;
if (supervisor.getAbilityManager().get(toolName) != null) {
continue;
}
AgentCard childCard = getRuntime().getAgentCard(childId);
String childDescription = childCard != null ? childCard.getDescription() : "";
DelegateTool tool = new DelegateTool(childId, childDescription, getRuntime(), supervisorId, teamId);
supervisor.getAbilityManager().add(tool.getCard());
Object existing =
Runner.resourceMgr().getTool(tool.getCard().getId(), supervisorId, TagMatchStrategy.ALL);
if (existing == null) {
Runner.resourceMgr().addTool(tool, supervisorId);
}
// Always register with refresh=true so stale tool instances from
// prior test runs are replaced. Without refresh, addTool silently
// skips when the toolId already exists, leaving the old instance.
Runner.resourceMgr().addTool(tool, supervisorId, true);
Loggers.MULTI_AGENT.info("[HierarchicalMsgBusTeam:" + teamId + "] injected '" + toolName + "' -> '"
+ supervisorId + "'");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public Iterator<Object> stream(Map<String, Object> inputs, Map<String, Object> k
*/
private static ToolCard buildCard(String targetId, AgentCard targetCard) {
String toolName = targetId;
String toolId = targetId + "_delegate_tool";
String description = "Delegate a task to " + targetId + " for processing.";
if (targetCard != null && targetCard.getDescription() != null && !targetCard.getDescription().isBlank()) {
description = targetCard.getDescription();
Expand All @@ -151,7 +152,10 @@ private static ToolCard buildCard(String targetId, AgentCard targetCard) {
} else {
inputParams = defaultInputParams();
}
return ToolCard.builder().id(toolName).name(toolName).description(description).inputParams(inputParams).build();
// Use a distinct tool ID (suffixed with "_delegate_tool") to avoid
// ResourceMgr tagMgr collisions with the agent registered under the
// same ID. The tool name visible to the LLM remains the child agent ID.
return ToolCard.builder().id(toolId).name(toolName).description(description).inputParams(inputParams).build();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.openjiuwen.core.multiagent.schema.TeamCard;
import com.openjiuwen.core.runner.Runner;
import com.openjiuwen.core.runner.base.AgentProvider;
import com.openjiuwen.core.runner.base.TagMatchStrategy;
import com.openjiuwen.core.session.AgentGroupSessionApi;
import com.openjiuwen.core.session.stream.OutputSchema;
import com.openjiuwen.core.singleagent.BaseAgent;
Expand Down Expand Up @@ -208,9 +207,6 @@ private void injectChildCards(HierarchicalToolsTeamConfig config) {
if (parent == null) {
continue;
}
if (parent.getAbilityManager().get(childId) != null) {
continue;
}
AgentCard childCard = getRuntime().getAgentCard(childId);
if (childCard == null) {
Loggers.MULTI_AGENT.warning("[HierarchicalToolsTeam:" + teamId + "] skip tool injection for '" + childId
Expand All @@ -220,10 +216,12 @@ private void injectChildCards(HierarchicalToolsTeamConfig config) {
HierarchicalDelegateTool tool =
new HierarchicalDelegateTool(childId, childCard, getRuntime(), parentId, teamId);
parent.getAbilityManager().add(tool.getCard());
Object existing = Runner.resourceMgr().getTool(tool.getCard().getId(), parentId, TagMatchStrategy.ALL);
if (existing == null) {
Runner.resourceMgr().addTool(tool, parentId);
}
// Always register with refresh=true so stale tool instances from
// prior test runs are replaced. Without refresh, addTool silently
// skips when the toolId already exists, leaving the old (possibly
// GC'd or wrong-scoped) instance — causing "Tool instance not found"
// errors at execution time.
Runner.resourceMgr().addTool(tool, parentId, true);
Loggers.MULTI_AGENT.info("[HierarchicalToolsTeam:" + teamId + "] registered " + childId + " -> " + parentId
+ ".ability_manager");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ public List<CallbackInfo> getCallbacks() {
return callbacks;
}

/**
* Get the error handler registered for a specific callback, if any.
*
* @param callback the callback function
* @return the error handler, or {@code null} if none registered
* @since 0.1.7
*/
public Function<ExceptionContext, Object> getErrorHandler(
Function<Map<String, Object>, Object> callback) {
return errorHandlers.get(callback);
}

/**
* Add callback to the chain.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ public List<Object> triggerParallel(String event, Object[] args, Map<String, Obj

if (filterResult.getAction() == FilterAction.STOP
|| filterResult.getAction() == FilterAction.SKIP) {
return java.util.Collections.emptyList();
return null;
}

Object[] fa = filterResult.getModifiedArgs() != null ? filterResult.getModifiedArgs() : finalArgs;
Expand Down Expand Up @@ -651,7 +651,7 @@ public List<Object> triggerParallel(String event, Object[] args, Map<String, Obj
log.error("Callback {} failed in parallel execution: {}", callbackInfo.getCallbackDisplayName(),
e.getMessage());
}
return java.util.Collections.emptyList();
return null;
}
}));
}
Expand Down
Loading