mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(agent): auto-redirect skill-as-tool calls to readSkillFile
This commit is contained in:
parent
5789a28e19
commit
aded30bbb1
@ -443,6 +443,17 @@ public class ToolExecutionExecutor {
|
||||
}
|
||||
ToolCallback callback = toolCallbackMap.get(toolName);
|
||||
if (callback == null) {
|
||||
SkillRedirect redirect = tryAutoRedirectSkillCall(toolName, arguments, safeOrigin);
|
||||
if (redirect != null) {
|
||||
// Auto-redirect succeeds with success=true on the SSE event so the
|
||||
// model treats the SKILL.md content as the answer to a different,
|
||||
// valid question (rather than as another failed call to recover from).
|
||||
events.add(GraphEventPublisher.toolComplete(
|
||||
toolCall.id(), toolName, redirect.response(), true));
|
||||
allResponses.add(new ToolResponseMessage.ToolResponse(
|
||||
toolCall.id(), toolName, redirect.response()));
|
||||
continue;
|
||||
}
|
||||
String msg = skillAwareNotFoundMessage(toolName);
|
||||
log.warn("[ToolExecutor] {}", msg);
|
||||
events.add(GraphEventPublisher.toolComplete(toolCall.id(), toolName, msg, false));
|
||||
@ -521,6 +532,18 @@ public class ToolExecutionExecutor {
|
||||
|
||||
ToolCallback callback = toolCallbackMap.get(toolName);
|
||||
if (callback == null) {
|
||||
// Same auto-redirect for pre-approved replays — a stale skill-as-tool
|
||||
// approval shouldn't dead-end the conversation either.
|
||||
ChatOrigin replayOriginForRedirect = ChatOrigin.EMPTY
|
||||
.withConversationId(conversationId)
|
||||
.withWorkspace(null, workspaceBasePath);
|
||||
SkillRedirect redirect = tryAutoRedirectSkillCall(toolName, callArguments, replayOriginForRedirect);
|
||||
if (redirect != null) {
|
||||
events.add(GraphEventPublisher.toolComplete(
|
||||
toolCall.id(), toolName, redirect.response(), true));
|
||||
return new ToolResponseMessage.ToolResponse(
|
||||
toolCall.id(), toolName, redirect.response());
|
||||
}
|
||||
String msg = skillAwareNotFoundMessage(toolName);
|
||||
log.warn("[ToolExecutor] Pre-approved {}", msg);
|
||||
events.add(GraphEventPublisher.toolComplete(toolCall.id(), toolName, msg, false));
|
||||
@ -1049,6 +1072,79 @@ public class ToolExecutionExecutor {
|
||||
return "Tool not found: " + toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder for an auto-redirect outcome: the SKILL.md content (wrapped
|
||||
* with a one-line nudge) that we substitute as the tool response when
|
||||
* the LLM mistakenly calls a skill name as if it were a tool.
|
||||
*
|
||||
* <p>{@code success=true} on the substituted response so the model
|
||||
* doesn't read it as "tool failed, try harder" — semantically we
|
||||
* answered a different question than the one it asked, and we want
|
||||
* the model to follow the redirect rather than thrash.
|
||||
*/
|
||||
private record SkillRedirect(String response) {}
|
||||
|
||||
/**
|
||||
* When the LLM calls a skill name as if it were a tool, transparently
|
||||
* fetch its SKILL.md and return that as the tool response. Smaller
|
||||
* models (qwen-turbo et al.) often can't act on a "not a tool — go
|
||||
* read X first" hint; they keep emitting the same wrong call until the
|
||||
* iteration cap. With auto-redirect, the model receives runnable
|
||||
* instructions on the very first attempt and can copy the runSkillScript
|
||||
* shape from SKILL.md verbatim.
|
||||
*
|
||||
* <p>Returns {@code null} if {@code toolName} isn't a registered skill,
|
||||
* if {@code readSkillFile} isn't available in this agent's tool set, or
|
||||
* if the redirect call itself errored — the caller then falls through
|
||||
* to the usual {@code skillAwareNotFoundMessage} hint.
|
||||
*/
|
||||
private SkillRedirect tryAutoRedirectSkillCall(String toolName, String originalArgs, ChatOrigin origin) {
|
||||
if (skillRuntimeService == null || toolName == null || toolName.isBlank()) return null;
|
||||
try {
|
||||
boolean isSkill = skillRuntimeService.getActiveSkills().stream()
|
||||
.anyMatch(s -> s.getName() != null && s.getName().equalsIgnoreCase(toolName));
|
||||
if (!isSkill) return null;
|
||||
} catch (Exception e) {
|
||||
log.debug("[ToolExecutor] auto-redirect skill lookup failed: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
ToolCallback readSkillFile = toolCallbackMap.get("readSkillFile");
|
||||
if (readSkillFile == null) {
|
||||
log.debug("[ToolExecutor] readSkillFile not bound to this agent — cannot auto-redirect '{}'", toolName);
|
||||
return null;
|
||||
}
|
||||
|
||||
String redirectArgs = "{\"skillName\":\""
|
||||
+ jsonStringEscape(toolName)
|
||||
+ "\",\"filePath\":\"SKILL.md\"}";
|
||||
String skillMd;
|
||||
try {
|
||||
ToolContext ctx = (origin != null ? origin : ChatOrigin.EMPTY).toToolContext();
|
||||
skillMd = readSkillFile.call(redirectArgs, ctx);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ToolExecutor] Auto-redirect readSkillFile failed for '{}': {}", toolName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("[ToolExecutor] Auto-redirected skill-as-tool call '{}' → readSkillFile (returned {} chars)",
|
||||
toolName, skillMd != null ? skillMd.length() : 0);
|
||||
|
||||
String safeArgs = originalArgs == null || originalArgs.isBlank() ? "{}" : originalArgs;
|
||||
String response = String.format(
|
||||
"[auto-redirect] You called '%s' as a tool, but it's a Skill (documentation package). "
|
||||
+ "Its SKILL.md is loaded below — read the script invocation example, then call "
|
||||
+ "`runSkillScript(skillName=\"%s\", scriptPath=\"scripts/<file from SKILL.md>\", args=[...])` "
|
||||
+ "to actually run it. Your original payload was: %s%n%n---%n%s",
|
||||
toolName, toolName, safeArgs, skillMd == null ? "" : skillMd);
|
||||
return new SkillRedirect(response);
|
||||
}
|
||||
|
||||
/** Minimal JSON string escaping for the synthetic readSkillFile arg payload. */
|
||||
private static String jsonStringEscape(String s) {
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
// ==================== 内部数据类 ====================
|
||||
|
||||
private record PreparedToolCall(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user