feat(agent): wire conversation workspace into skill code execution

Pass the ReAct agent context workspace directory through SkillBoxFactory
so SkillBox's code execution runs against the conversation workspace
instead of the process default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-05-15 14:00:37 +08:00
parent f70d4aa1c1
commit 65cbb1ecd5
3 changed files with 44 additions and 7 deletions

View File

@@ -409,7 +409,7 @@ public abstract class ReActAgentComponent extends NodeComponent {
SkillTrackingHook skillTrackingHook = null;
SkillBox skillBox = null;
if (enableSkills()) {
SkillLoadResult skillLoadResult = SkillBoxFactory.build(toolkit, cfg, skills());
SkillLoadResult skillLoadResult = SkillBoxFactory.build(toolkit, cfg, skills(), ctx.getWorkspaceDir());
skillBox = skillLoadResult.skillBox();
skillTrackingHook = new SkillTrackingHook(skillLoadResult.skillIdToName());
allHooks.add(skillTrackingHook);

View File

@@ -30,10 +30,18 @@ public final class SkillBoxFactory {
}
public static SkillLoadResult build(Toolkit toolkit, AgentConfig agentConfig, List<String> allowedSkills) {
return build(toolkit, agentConfig, allowedSkills, null);
}
public static SkillLoadResult build(
Toolkit toolkit,
AgentConfig agentConfig,
List<String> allowedSkills,
Path workspaceDir) {
SkillsConfig skillsConfig = agentConfig.getSkills();
Path root = Path.of(skillsConfig.getPath()).normalize();
if (!Files.isDirectory(root)) {
return handleMissingRoot(root, skillsConfig, toolkit);
return handleMissingRoot(root, skillsConfig, toolkit, workspaceDir);
}
try {
@@ -41,7 +49,7 @@ public final class SkillBoxFactory {
List<AgentSkill> allSkills = repository.getAllSkills();
List<AgentSkill> selected = selectSkills(allSkills, allowedSkills, skillsConfig);
SkillToolManifest manifest = new SkillToolManifest(root, skillsConfig);
SkillBox skillBox = new SkillBox(toolkit);
SkillBox skillBox = createSkillBox(toolkit, workspaceDir);
Map<String, String> skillIdToName = new LinkedHashMap<>();
List<String> skillNames = new ArrayList<>();
@@ -68,17 +76,31 @@ public final class SkillBoxFactory {
throw new AgentConfigException("Failed to load skills from: " + root, e);
}
LOG.warn("Failed to load skills from {}: {}", root, e.getMessage());
return new SkillLoadResult(new SkillBox(toolkit), Map.of(), List.of());
return new SkillLoadResult(createSkillBox(toolkit, workspaceDir), Map.of(), List.of());
}
}
private static SkillLoadResult handleMissingRoot(Path root, SkillsConfig skillsConfig, Toolkit toolkit) {
private static SkillLoadResult handleMissingRoot(
Path root,
SkillsConfig skillsConfig,
Toolkit toolkit,
Path workspaceDir) {
String message = "Skills root not found: " + root;
if (skillsConfig.isStrict()) {
throw new AgentConfigException(message);
}
LOG.warn(message);
return new SkillLoadResult(new SkillBox(toolkit), Map.of(), List.of());
return new SkillLoadResult(createSkillBox(toolkit, workspaceDir), Map.of(), List.of());
}
private static SkillBox createSkillBox(Toolkit toolkit, Path workspaceDir) {
SkillBox skillBox = new SkillBox(toolkit);
if (workspaceDir != null) {
skillBox.codeExecution()
.workDir(workspaceDir.toAbsolutePath().normalize().toString())
.enable();
}
return skillBox;
}
private static List<AgentSkill> selectSkills(

View File

@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
@@ -24,7 +25,11 @@ public class SkillBoxFactoryTest {
public void setUp() {
cfg = new AgentConfig();
cfg.getSkills().setEnabled(true);
cfg.getSkills().setPath("src/test/resources/agent/skills");
Path moduleRelative = Path.of("src/test/resources/agent/skills");
Path rootRelative = Path.of("liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/skills");
cfg.getSkills().setPath(Files.isRegularFile(moduleRelative.resolve("demo/SKILL.md"))
? moduleRelative.toString()
: rootRelative.toString());
cfg.getSkills().setStrict(true);
SkillEchoTool.reset();
}
@@ -72,6 +77,16 @@ public class SkillBoxFactoryTest {
Assertions.assertEquals(1, SkillEchoTool.CONSTRUCT_COUNT.get());
}
@Test
public void testSkillBoxUsesConversationWorkspaceForCodeExecution() throws Exception {
Path workspace = Files.createTempDirectory("liteflow-skill-workspace-test-");
SkillLoadResult result = SkillBoxFactory.build(new Toolkit(), cfg, List.of("demo"), workspace);
Assertions.assertEquals(workspace.toAbsolutePath().normalize(), result.skillBox().getCodeExecutionWorkDir());
Assertions.assertEquals(workspace.toAbsolutePath().normalize().resolve("skills"), result.skillBox().getUploadDir());
}
@Test
public void testMissingSkillsDirectoryFailsInStrictMode() {
cfg.getSkills().setPath(Path.of("target", "missing-skills-dir").toString());