From 2aae1b1f147cd2bc5856b7f524fd4390c391dbb2 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Sat, 11 Apr 2026 18:30:24 +0800 Subject: [PATCH] refactor(action-core): return Result for meta action lookups and adapt runtime callers --- .../partner/core/action/ActionCapability.java | 5 +- .../slhaf/partner/core/action/ActionCore.java | 56 +++++--- .../partner/core/action/entity/Action.kt | 6 +- .../BuiltinInterventionActionProvider.java | 37 ++++- .../action/executor/ActionExecutor.java | 50 ++++++- .../module/action/planner/ActionPlanner.java | 48 +++++-- .../partner/core/action/ActionCoreTest.java | 30 ++++ .../action/executor/ActionExecutorTest.java | 129 +++++++++++++++++- 8 files changed, 321 insertions(+), 40 deletions(-) diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java index bf498f39..838c934b 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java @@ -8,6 +8,7 @@ import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.entity.intervention.MetaIntervention; import work.slhaf.partner.core.action.runner.RunnerClient; import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability; +import work.slhaf.partner.framework.agent.support.Result; import java.util.List; import java.util.Map; @@ -23,9 +24,9 @@ public interface ActionCapability { ExecutorService getExecutor(ActionCore.ExecutorType type); - MetaAction loadMetaAction(@NonNull String actionKey); + Result loadMetaAction(@NonNull String actionKey); - MetaActionInfo loadMetaActionInfo(@NonNull String actionKey); + Result loadMetaActionInfo(@NonNull String actionKey); void registerMetaActions(@NonNull Map metaActions); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java index 02bb91da..7bddcbf7 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java @@ -14,12 +14,14 @@ import work.slhaf.partner.core.action.exception.ActionLookupException; import work.slhaf.partner.core.action.runner.LocalRunnerClient; import work.slhaf.partner.core.action.runner.RunnerClient; import work.slhaf.partner.framework.agent.config.ConfigCenter; +import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler; import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore; import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod; import work.slhaf.partner.framework.agent.factory.context.Shutdown; import work.slhaf.partner.framework.agent.state.State; import work.slhaf.partner.framework.agent.state.StateSerializable; import work.slhaf.partner.framework.agent.state.StateValue; +import work.slhaf.partner.framework.agent.support.Result; import java.io.IOException; import java.nio.file.Path; @@ -114,49 +116,49 @@ public class ActionCore implements StateSerializable { } @CapabilityMethod - public MetaAction loadMetaAction(@NonNull String actionKey) { + public Result loadMetaAction(@NonNull String actionKey) { MetaActionInfo metaActionInfo = existedMetaActions.get(actionKey); if (metaActionInfo == null) { - throw new ActionLookupException( + return Result.failure(new ActionLookupException( "Meta action info not found for action key: " + actionKey, actionKey, "META_ACTION" - ); + )); } String[] split = actionKey.split("::", 2); if (split.length < 2) { - throw new ActionLookupException( + return Result.failure(new ActionLookupException( "Invalid action key format: " + actionKey, actionKey, "META_ACTION" - ); + )); } MetaAction.Type type = switch (split[0]) { case BUILTIN_LOCATION -> MetaAction.Type.BUILTIN; case ORIGIN_LOCATION -> MetaAction.Type.ORIGIN; default -> MetaAction.Type.MCP; }; - return new MetaAction( + return Result.success(new MetaAction( split[1], metaActionInfo.getIo(), metaActionInfo.getLauncher(), type, split[0] - ); + )); } @CapabilityMethod - public MetaActionInfo loadMetaActionInfo(@NonNull String actionKey) { + public Result loadMetaActionInfo(@NonNull String actionKey) { MetaActionInfo info = existedMetaActions.get(actionKey); if (info == null) { - throw new ActionLookupException( + return Result.failure(new ActionLookupException( "Meta action description not found for action key: " + actionKey, actionKey, "META_ACTION_INFO" - ); + )); } - return info; + return Result.success(info); } @CapabilityMethod @@ -175,7 +177,6 @@ public class ActionCore implements StateSerializable { if (executableAction == null) { return; } - // 加锁确保同步 synchronized (executableAction.getStatus()) { applyInterventions(interventions, executableAction); @@ -188,10 +189,12 @@ public class ActionCore implements StateSerializable { interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder)); for (MetaIntervention intervention : interventions) { - List actions = intervention.getActions() - .stream() - .map(this::loadMetaAction) - .toList(); + Result> actionsResult = resolveInterventionActions(intervention); + if (actionsResult.isFailure()) { + reportLookupFailure(actionsResult.exceptionOrNull()); + continue; + } + List actions = actionsResult.getOrNull(); switch (intervention.getType()) { case InterventionType.APPEND -> handleAppend(executableAction, intervention.getOrder(), actions); @@ -210,6 +213,27 @@ public class ActionCore implements StateSerializable { } + private Result> resolveInterventionActions(MetaIntervention intervention) { + List actions = new ArrayList<>(); + for (String actionKey : intervention.getActions()) { + Result metaActionResult = loadMetaAction(actionKey); + if (metaActionResult.isFailure()) { + Throwable throwable = metaActionResult.exceptionOrNull(); + return Result.failure(throwable == null + ? new ActionLookupException("Meta action lookup failed: " + actionKey, actionKey, "META_ACTION") + : throwable); + } + actions.add(metaActionResult.getOrNull()); + } + return Result.success(actions); + } + + private void reportLookupFailure(Throwable throwable) { + if (throwable instanceof ActionLookupException lookupException) { + ExceptionReporterHandler.INSTANCE.report(lookupException); + } + } + /** * 在未进入执行阶段的行动单元组新增新的行动 */ diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/Action.kt b/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/Action.kt index 4ae78e1b..5dbd0eb3 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/Action.kt +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/Action.kt @@ -102,7 +102,7 @@ sealed class ExecutableAction( /** * 行动结果 */ - lateinit var result: String + var result: String? = null val history: MutableMap> = mutableMapOf() @@ -142,7 +142,7 @@ sealed class ExecutableAction( tendency = tendency, actionChainSize = actionChain.size, executingStage = executingStage, - result = if (::result.isInitialized) result else null, + result = result, history = history.mapValues { (_, value) -> value.toList() }, scheduleType = schedulable?.scheduleType, scheduleContent = schedulable?.scheduleContent, @@ -169,7 +169,7 @@ data class SchedulableExecutableAction @JvmOverloads constructor( val scheduleHistories = ArrayList() fun recordAndReset() { - val newHistory = ScheduleHistory(ZonedDateTime.now(), result, history.toMap()) + val newHistory = ScheduleHistory(ZonedDateTime.now(), result ?: "null", history.toMap()) scheduleHistories.add(newHistory) executingStage = 0 diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/action/builtin/BuiltinInterventionActionProvider.java b/Partner-Core/src/main/java/work/slhaf/partner/module/action/builtin/BuiltinInterventionActionProvider.java index d890b230..153da146 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/module/action/builtin/BuiltinInterventionActionProvider.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/action/builtin/BuiltinInterventionActionProvider.java @@ -15,8 +15,11 @@ import work.slhaf.partner.core.cognition.BlockContent; import work.slhaf.partner.core.cognition.CognitionCapability; import work.slhaf.partner.core.cognition.ContextBlock; import work.slhaf.partner.core.cognition.ContextWorkspace; +import work.slhaf.partner.framework.agent.exception.AgentException; +import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler; import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability; import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent; +import work.slhaf.partner.framework.agent.support.Result; import java.util.*; import java.util.function.Function; @@ -291,7 +294,8 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider { Set.of(), true, JSONObject.of( - "result", "Intervene status." + "ok", "Intervene status", + "result", "Intervene result or failed message." ) ); @@ -305,6 +309,15 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider { Integer order = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "order"); List actions = requireActions(params, type); ExecutableAction target = requireTargetAction(targetId); + Result validationResult = validateActionKeys(actions); + if (validationResult.isFailure()) { + reportFailure(validationResult.exceptionOrNull()); + Throwable throwable = validationResult.exceptionOrNull(); + return JSONObject.of( + "ok", false, + "result", throwable == null ? "Intervention action validation failed" : throwable.getLocalizedMessage() + ).toJSONString(); + } MetaIntervention intervention = new MetaIntervention(); intervention.setType(type); @@ -383,4 +396,26 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider { .orElseThrow(() -> new IllegalArgumentException("未找到对应的 Action: " + targetId)); } + private Result validateActionKeys(List actions) { + for (String actionKey : actions) { + Result metaActionResult = actionCapability.loadMetaAction(actionKey); + if (metaActionResult.isFailure()) { + Throwable throwable = metaActionResult.exceptionOrNull(); + return Result.failure(throwable == null + ? new work.slhaf.partner.core.action.exception.ActionLookupException( + "Meta action lookup failed: " + actionKey, + actionKey, + "META_ACTION" + ) : throwable); + } + } + return Result.success(null); + } + + private void reportFailure(Throwable throwable) { + if (throwable instanceof AgentException agentException) { + ExceptionReporterHandler.INSTANCE.report(agentException); + } + } + } diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/action/executor/ActionExecutor.java b/Partner-Core/src/main/java/work/slhaf/partner/module/action/executor/ActionExecutor.java index ac8b8a62..7d5d294e 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/module/action/executor/ActionExecutor.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/action/executor/ActionExecutor.java @@ -7,11 +7,14 @@ import work.slhaf.partner.core.action.ActionCore; import work.slhaf.partner.core.action.entity.*; import work.slhaf.partner.core.action.runner.RunnerClient; import work.slhaf.partner.core.cognition.CognitionCapability; +import work.slhaf.partner.framework.agent.exception.AgentException; +import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler; import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability; import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; import work.slhaf.partner.framework.agent.factory.component.annotation.Init; import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule; import work.slhaf.partner.framework.agent.factory.context.Shutdown; +import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.module.action.executor.entity.*; import java.util.*; @@ -311,7 +314,14 @@ public class ActionExecutor extends AbstractAgentModule.Standalone { val executingStage = actionData.getExecutingStage(); - val extractorInput = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription()); + Result extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription()); + if (extractorInputResult.isFailure()) { + reportFailure(extractorInputResult.exceptionOrNull()); + Throwable throwable = extractorInputResult.exceptionOrNull(); + failureReason = buildAttemptFailureReason("参数提取失败", throwable == null ? null : throwable.getLocalizedMessage()); + break; + } + val extractorInput = extractorInputResult.getOrNull(); ExtractorResult extractorResult = paramsExtractor.execute(extractorInput); if (extractorResult == null || !extractorResult.isOk()) { @@ -331,7 +341,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone { } if (result.getStatus() == MetaAction.Result.Status.SUCCESS) { - val historyAction = new HistoryAction(actionKey, actionCapability.loadMetaActionInfo(actionKey).getDescription(), result.getData()); + val historyAction = new HistoryAction(actionKey, resolveHistoryDescription(actionKey), result.getData()); actionData.getHistory() .computeIfAbsent(executingStage, integer -> new ArrayList<>()) .add(historyAction); @@ -497,6 +507,24 @@ public class ActionExecutor extends AbstractAgentModule.Standalone { } } + private String resolveHistoryDescription(String actionKey) { + Result metaActionInfoResult = actionCapability.loadMetaActionInfo(actionKey); + if (metaActionInfoResult.isFailure()) { + reportFailure(metaActionInfoResult.exceptionOrNull()); + return actionKey; + } + MetaActionInfo metaActionInfo = metaActionInfoResult.getOrNull(); + return metaActionInfo == null || metaActionInfo.getDescription().isBlank() + ? actionKey + : metaActionInfo.getDescription(); + } + + private void reportFailure(Throwable throwable) { + if (throwable instanceof AgentException agentException) { + ExceptionReporterHandler.INSTANCE.report(agentException); + } + } + private record MetaActionsListeningRecord(AtomicBoolean accepting, int phase) { } @@ -511,12 +539,22 @@ public class ActionExecutor extends AbstractAgentModule.Standalone { private AssemblyHelper() { } - private ExtractorInput buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) { + private Result buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) { + Result metaActionInfoResult = actionCapability.loadMetaActionInfo(actionKey); + if (metaActionInfoResult.isFailure()) { + Throwable throwable = metaActionInfoResult.exceptionOrNull(); + return Result.failure(throwable == null + ? new work.slhaf.partner.core.action.exception.ActionLookupException( + "Meta action description not found for action key: " + actionKey, + actionKey, + "META_ACTION_INFO" + ) : throwable); + } ExtractorInput input = new ExtractorInput(); - input.setMetaActionInfo(actionCapability.loadMetaActionInfo(actionKey)); + input.setMetaActionInfo(metaActionInfoResult.getOrNull()); input.setTargetActionId(uuid); input.setTargetActionDesc(description); - return input; + return Result.success(input); } private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) { @@ -525,7 +563,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone { List overviewItems = list.stream() .map(metaAction -> new CorrectorInput.ActionChainItem( metaAction.getKey(), - actionCapability.loadMetaActionInfo(metaAction.getKey()).getDescription(), + resolveHistoryDescription(metaAction.getKey()), metaAction.getResult().getStatus().name().toLowerCase(Locale.ROOT) )) .toList(); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/action/planner/ActionPlanner.java b/Partner-Core/src/main/java/work/slhaf/partner/module/action/planner/ActionPlanner.java index 75224a2e..59e72ac8 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/module/action/planner/ActionPlanner.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/action/planner/ActionPlanner.java @@ -11,10 +11,12 @@ import work.slhaf.partner.core.cognition.BlockContent; import work.slhaf.partner.core.cognition.CognitionCapability; import work.slhaf.partner.core.cognition.CommunicationBlockContent; import work.slhaf.partner.core.cognition.ContextBlock; +import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler; import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability; import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; import work.slhaf.partner.framework.agent.factory.component.annotation.Init; import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule; +import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.module.action.executor.ActionExecutor; import work.slhaf.partner.module.action.planner.evaluator.ActionEvaluator; import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput; @@ -147,6 +149,9 @@ public class ActionPlanner extends AbstractAgentModule.Running> actionChain = getActionChain(evaluatorResult); + if (actionChain == null) { + return null; + } return switch (evaluatorResult.getType()) { case PLANNING -> new SchedulableExecutableAction( evaluatorResult.getTendency(), @@ -366,20 +374,28 @@ public class ActionPlanner extends AbstractAgentModule.Running> getActionChain(EvaluatorResult evaluatorResult) { + private Map> getActionChain(EvaluatorResult evaluatorResult) { Map> actionChain = new HashMap<>(); Map> primaryActionChain = evaluatorResult.getPrimaryActionChain(); - fixDependencies(primaryActionChain); - primaryActionChain.forEach((order, actionKeys) -> { - List metaActions = actionKeys.stream() - .map(actionKey -> actionCapability.loadMetaAction(actionKey)) - .toList(); - actionChain.put(order, metaActions); - }); + if (!fixDependencies(primaryActionChain)) { + return null; + } + for (Map.Entry> entry : primaryActionChain.entrySet()) { + List metaActions = new ArrayList<>(); + for (String actionKey : entry.getValue()) { + Result metaActionResult = actionCapability.loadMetaAction(actionKey); + if (metaActionResult.isFailure()) { + reportFailure(metaActionResult); + return null; + } + metaActions.add(metaActionResult.getOrNull()); + } + actionChain.put(entry.getKey(), metaActions); + } return actionChain; } - private void fixDependencies(Map> primaryActionChain) { + private boolean fixDependencies(Map> primaryActionChain) { // 先将 primaryActionChain 的节点序号修正为从1开始依次增大 fixOrder(primaryActionChain); List fixedOrders = new ArrayList<>(primaryActionChain.keySet().stream().toList()); @@ -392,7 +408,12 @@ public class ActionPlanner extends AbstractAgentModule.Running actionKeys = primaryActionChain.get(fixedOrder); for (String actionKey : actionKeys) { // 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖 - MetaActionInfo metaActionInfo = actionCapability.loadMetaActionInfo(actionKey); + Result metaActionInfoResult = actionCapability.loadMetaActionInfo(actionKey); + if (metaActionInfoResult.isFailure()) { + reportFailure(metaActionInfoResult); + return false; + } + MetaActionInfo metaActionInfo = metaActionInfoResult.getOrNull(); Set preActions = metaActionInfo.getPreActions(); boolean preActionsExist = preActions.isEmpty(); if (!preActionsExist) { @@ -420,6 +441,13 @@ public class ActionPlanner extends AbstractAgentModule.Running result) { + if (result.exceptionOrNull() instanceof work.slhaf.partner.framework.agent.exception.AgentException agentException) { + ExceptionReporterHandler.INSTANCE.report(agentException); + } } private void fixOrder(Map> primaryActionChain) { diff --git a/Partner-Core/src/test/java/work/slhaf/partner/core/action/ActionCoreTest.java b/Partner-Core/src/test/java/work/slhaf/partner/core/action/ActionCoreTest.java index af2516e0..8d396f49 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/core/action/ActionCoreTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/core/action/ActionCoreTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import work.slhaf.partner.core.action.entity.*; +import work.slhaf.partner.core.action.exception.ActionLookupException; +import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.module.action.executor.entity.HistoryAction; import java.nio.file.Path; @@ -331,4 +333,32 @@ class ActionCoreTest { assertEquals(1, schedulableAction.getScheduleHistories().size()); assertEquals("cycle-result", schedulableAction.getScheduleHistories().getFirst().getResult()); } + + @Test + void shouldReturnResultForMetaActionLookup() { + MetaActionInfo metaActionInfo = new MetaActionInfo( + true, + "python", + Map.of(), + "demo", + Set.of(), + Set.of(), + Set.of(), + false, + JSONObject.of() + ); + actionCore.registerMetaActions(Map.of("builtin::demo", metaActionInfo)); + + Result success = actionCore.loadMetaAction("builtin::demo"); + assertTrue(success.isSuccess()); + assertEquals("demo", success.getOrNull().getName()); + + Result failure = actionCore.loadMetaAction("builtin::missing"); + assertTrue(failure.isFailure()); + assertInstanceOf(ActionLookupException.class, failure.exceptionOrNull()); + + Result infoFailure = actionCore.loadMetaActionInfo("builtin::missing"); + assertTrue(infoFailure.isFailure()); + assertInstanceOf(ActionLookupException.class, infoFailure.exceptionOrNull()); + } } diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/action/executor/ActionExecutorTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/action/executor/ActionExecutorTest.java index 3bb524a8..82b4a5d4 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/module/action/executor/ActionExecutorTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/action/executor/ActionExecutorTest.java @@ -9,6 +9,7 @@ import work.slhaf.partner.core.action.entity.*; import work.slhaf.partner.core.action.runner.RunnerClient; import work.slhaf.partner.core.cognition.CognitionCapability; import work.slhaf.partner.core.cognition.ContextWorkspace; +import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.module.action.executor.entity.ExtractorResult; import work.slhaf.partner.module.action.executor.entity.HistoryAction; @@ -121,7 +122,7 @@ class ActionExecutorTest { when(actionCapability.getExecutor(ActionCore.ExecutorType.PLATFORM)).thenReturn(platformExecutor); when(actionCapability.runnerClient()).thenReturn(runnerClient); when(actionCapability.listActions(Action.Status.EXECUTING, null)).thenReturn(Set.of()); - when(actionCapability.loadMetaActionInfo(anyString())).thenReturn(new MetaActionInfo( + when(actionCapability.loadMetaActionInfo(anyString())).thenReturn(Result.success(new MetaActionInfo( false, null, Map.of(), @@ -131,7 +132,7 @@ class ActionExecutorTest { Set.of(), false, new com.alibaba.fastjson2.JSONObject() - )); + ))); when(cognitionCapability.contextWorkspace()).thenReturn(new ContextWorkspace()); ExtractorResult extractorResult = new ExtractorResult(); @@ -186,6 +187,130 @@ class ActionExecutorTest { assertEquals("rerun-ok", metaAction.getResult().getData()); } + @Test + void shouldMarkMetaActionFailedWhenMetaActionInfoLookupFailsBeforeExtraction() throws Exception { + ActionCapability actionCapability = Mockito.mock(ActionCapability.class); + CognitionCapability cognitionCapability = Mockito.mock(CognitionCapability.class); + ParamsExtractor paramsExtractor = Mockito.mock(ParamsExtractor.class); + ActionCorrector actionCorrector = Mockito.mock(ActionCorrector.class); + ActionCorrectionRecognizer actionCorrectionRecognizer = Mockito.mock(ActionCorrectionRecognizer.class); + RunnerClient runnerClient = Mockito.mock(RunnerClient.class); + + ExecutorService virtualExecutor = registerExecutor(Executors.newFixedThreadPool(2)); + ExecutorService platformExecutor = registerExecutor(Executors.newFixedThreadPool(2)); + + when(actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL)).thenReturn(virtualExecutor); + when(actionCapability.getExecutor(ActionCore.ExecutorType.PLATFORM)).thenReturn(platformExecutor); + when(actionCapability.runnerClient()).thenReturn(runnerClient); + when(actionCapability.listActions(Action.Status.EXECUTING, null)).thenReturn(Set.of()); + when(actionCapability.loadMetaActionInfo(anyString())).thenReturn(Result.failure(new work.slhaf.partner.core.action.exception.ActionLookupException( + "missing", + "builtin::command", + "META_ACTION_INFO" + ))); + when(cognitionCapability.contextWorkspace()).thenReturn(new ContextWorkspace()); + + ActionExecutor actionExecutor = new ActionExecutor(); + inject(actionExecutor, "actionCapability", actionCapability); + inject(actionExecutor, "cognitionCapability", cognitionCapability); + inject(actionExecutor, "paramsExtractor", paramsExtractor); + inject(actionExecutor, "actionCorrector", actionCorrector); + inject(actionExecutor, "actionCorrectionRecognizer", actionCorrectionRecognizer); + actionExecutor.init(); + + MetaAction metaAction = metaAction("command"); + ImmediateExecutableAction action = new ImmediateExecutableAction( + "urgent", + actionChain(metaAction), + "reason", + "desc", + "planner", + "lookup-fail-uuid" + ); + + actionExecutor.execute(action); + waitUntilFinished(action); + + verify(paramsExtractor, never()).execute(any()); + verify(runnerClient, never()).submit(any(MetaAction.class)); + assertEquals(Action.Status.SUCCESS, action.getStatus()); + assertEquals(MetaAction.Result.Status.FAILED, metaAction.getResult().getStatus()); + assertTrue(metaAction.getResult().getData().contains("参数提取失败")); + assertEquals(metaAction.getResult().getData(), action.getResult()); + } + + @Test + void shouldFallbackToActionKeyWhenHistoryDescriptionLookupFails() throws Exception { + ActionCapability actionCapability = Mockito.mock(ActionCapability.class); + CognitionCapability cognitionCapability = Mockito.mock(CognitionCapability.class); + ParamsExtractor paramsExtractor = Mockito.mock(ParamsExtractor.class); + ActionCorrector actionCorrector = Mockito.mock(ActionCorrector.class); + ActionCorrectionRecognizer actionCorrectionRecognizer = Mockito.mock(ActionCorrectionRecognizer.class); + RunnerClient runnerClient = Mockito.mock(RunnerClient.class); + + ExecutorService virtualExecutor = registerExecutor(Executors.newFixedThreadPool(2)); + ExecutorService platformExecutor = registerExecutor(Executors.newFixedThreadPool(2)); + + when(actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL)).thenReturn(virtualExecutor); + when(actionCapability.getExecutor(ActionCore.ExecutorType.PLATFORM)).thenReturn(platformExecutor); + when(actionCapability.runnerClient()).thenReturn(runnerClient); + when(actionCapability.listActions(Action.Status.EXECUTING, null)).thenReturn(Set.of()); + when(cognitionCapability.contextWorkspace()).thenReturn(new ContextWorkspace()); + + when(actionCapability.loadMetaActionInfo(anyString())) + .thenReturn(Result.success(new MetaActionInfo( + false, + null, + Map.of(), + "demo action", + Set.of(), + Set.of(), + Set.of(), + false, + new com.alibaba.fastjson2.JSONObject() + ))) + .thenReturn(Result.failure(new work.slhaf.partner.core.action.exception.ActionLookupException( + "missing desc", + "builtin::command", + "META_ACTION_INFO" + ))); + + ExtractorResult extractorResult = new ExtractorResult(); + extractorResult.setOk(true); + extractorResult.setParams(Map.of("fresh", "value")); + when(paramsExtractor.execute(any())).thenReturn(extractorResult); + doAnswer(invocation -> { + MetaAction metaAction = invocation.getArgument(0); + metaAction.getResult().setStatus(MetaAction.Result.Status.SUCCESS); + metaAction.getResult().setData("history-ok"); + return null; + }).when(runnerClient).submit(any(MetaAction.class)); + + ActionExecutor actionExecutor = new ActionExecutor(); + inject(actionExecutor, "actionCapability", actionCapability); + inject(actionExecutor, "cognitionCapability", cognitionCapability); + inject(actionExecutor, "paramsExtractor", paramsExtractor); + inject(actionExecutor, "actionCorrector", actionCorrector); + inject(actionExecutor, "actionCorrectionRecognizer", actionCorrectionRecognizer); + actionExecutor.init(); + + MetaAction metaAction = metaAction("command"); + ImmediateExecutableAction action = new ImmediateExecutableAction( + "urgent", + actionChain(metaAction), + "reason", + "desc", + "planner", + "history-fallback-uuid" + ); + + actionExecutor.execute(action); + waitUntilFinished(action); + + assertEquals(Action.Status.SUCCESS, action.getStatus()); + assertEquals("builtin::command", action.getHistory().get(1).getFirst().description()); + } + private void waitUntilFinished(ImmediateExecutableAction action) throws InterruptedException { long deadline = System.currentTimeMillis() + 3000; while (System.currentTimeMillis() < deadline) {