diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionPoolStateCodec.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionPoolStateCodec.java index da6bca48..d736d920 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionPoolStateCodec.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionPoolStateCodec.java @@ -50,70 +50,70 @@ final class ActionPoolStateCodec { } private static StateValue.Obj encodeExecutableAction(ExecutableAction action) { - Map actionMap = new LinkedHashMap<>(); - actionMap.put("kind", StateValue.str(action instanceof SchedulableExecutableAction ? "schedulable" : "immediate")); - actionMap.put("uuid", StateValue.str(action.getUuid())); - actionMap.put("source", StateValue.str(action.getSource())); - actionMap.put("reason", StateValue.str(action.getReason())); - actionMap.put("description", StateValue.str(action.getDescription())); - actionMap.put("status", StateValue.str(action.getStatus().name())); - actionMap.put("tendency", StateValue.str(action.getTendency())); - actionMap.put("executing_stage", StateValue.num(action.getExecutingStage())); + Map actionMap = new LinkedHashMap<>(); + actionMap.put("kind", action instanceof SchedulableExecutableAction ? "schedulable" : "immediate"); + actionMap.put("uuid", action.getUuid()); + actionMap.put("source", action.getSource()); + actionMap.put("reason", action.getReason()); + actionMap.put("description", action.getDescription()); + actionMap.put("status", action.getStatus().name()); + actionMap.put("tendency", action.getTendency()); + actionMap.put("executing_stage", action.getExecutingStage()); String result = resolveExecutableResult(action); if (result != null) { - actionMap.put("result", StateValue.str(result)); + actionMap.put("result", result); } if (action instanceof SchedulableExecutableAction schedulableAction) { - actionMap.put("schedule_type", StateValue.str(schedulableAction.getScheduleType().name())); - actionMap.put("schedule_content", StateValue.str(schedulableAction.getScheduleContent())); - actionMap.put("enabled", StateValue.bool(schedulableAction.getEnabled())); - actionMap.put("schedule_histories", StateValue.arr(encodeScheduleHistories(schedulableAction))); + actionMap.put("schedule_type", schedulableAction.getScheduleType().name()); + actionMap.put("schedule_content", schedulableAction.getScheduleContent()); + actionMap.put("enabled", schedulableAction.getEnabled()); + actionMap.put("schedule_histories", encodeScheduleHistories(schedulableAction)); } - List chainStates = action.getActionChain().entrySet().stream() + List chainStates = action.getActionChain().entrySet().stream() .sorted(Map.Entry.comparingByKey()) - .map(entry -> { - Map stageMap = new LinkedHashMap<>(); - stageMap.put("stage", StateValue.num(entry.getKey())); + .map(entry -> { + Map stageMap = new LinkedHashMap<>(); + stageMap.put("stage", entry.getKey()); String stageDescription = action.getStageDescriptions().get(entry.getKey()); if (stageDescription != null && !stageDescription.isBlank()) { - stageMap.put("description", StateValue.str(stageDescription)); + stageMap.put("description", stageDescription); } - stageMap.put("actions", StateValue.arr(entry.getValue().stream() - .map(metaAction -> (StateValue) encodeMetaAction(metaAction)) - .toList())); + stageMap.put("actions", entry.getValue().stream() + .map(ActionPoolStateCodec::encodeMetaAction) + .toList()); return StateValue.obj(stageMap); }).toList(); - actionMap.put("action_chain", StateValue.arr(chainStates)); + actionMap.put("action_chain", chainStates); - actionMap.put("history", StateValue.arr(encodeHistoryStages(action.getHistory()))); + actionMap.put("history", encodeHistoryStages(action.getHistory())); return StateValue.obj(actionMap); } private static StateValue.Obj encodeMetaAction(MetaAction metaAction) { - Map metaMap = new LinkedHashMap<>(); - metaMap.put("name", StateValue.str(metaAction.getName())); - metaMap.put("io", StateValue.bool(metaAction.getIo())); + Map metaMap = new LinkedHashMap<>(); + metaMap.put("name", metaAction.getName()); + metaMap.put("io", metaAction.getIo()); if (metaAction.getLauncher() != null) { - metaMap.put("launcher", StateValue.str(metaAction.getLauncher())); + metaMap.put("launcher", metaAction.getLauncher()); } - metaMap.put("type", StateValue.str(metaAction.getType().name())); - metaMap.put("location", StateValue.str(metaAction.getLocation())); - metaMap.put("params_json", StateValue.str(JSONObject.toJSONString(metaAction.getParams()))); - metaMap.put("result_status", StateValue.str(metaAction.getResult().getStatus().name())); + metaMap.put("type", metaAction.getType().name()); + metaMap.put("location", metaAction.getLocation()); + metaMap.put("params_json", JSONObject.toJSONString(metaAction.getParams())); + metaMap.put("result_status", metaAction.getResult().getStatus().name()); if (metaAction.getResult().getData() != null) { - metaMap.put("result_data", StateValue.str(metaAction.getResult().getData())); + metaMap.put("result_data", metaAction.getResult().getData()); } return StateValue.obj(metaMap); } private static StateValue.Obj encodeHistoryAction(HistoryAction historyAction) { - Map historyMap = new LinkedHashMap<>(); - historyMap.put("action_key", StateValue.str(historyAction.actionKey())); - historyMap.put("description", StateValue.str(historyAction.description())); - historyMap.put("result", StateValue.str(historyAction.result())); + Map historyMap = new LinkedHashMap<>(); + historyMap.put("action_key", historyAction.actionKey()); + historyMap.put("description", historyAction.description()); + historyMap.put("result", historyAction.result()); return StateValue.obj(historyMap); } @@ -288,26 +288,26 @@ final class ActionPoolStateCodec { return restored; } - private static List encodeHistoryStages(Map> historyMap) { + private static List encodeHistoryStages(Map> historyMap) { return historyMap.entrySet().stream() .sorted(Map.Entry.comparingByKey()) - .map(entry -> { - Map stageMap = new LinkedHashMap<>(); - stageMap.put("stage", StateValue.num(entry.getKey())); - stageMap.put("actions", StateValue.arr(entry.getValue().stream() - .map(historyAction -> (StateValue) encodeHistoryAction(historyAction)) - .toList())); + .map(entry -> { + Map stageMap = new LinkedHashMap<>(); + stageMap.put("stage", entry.getKey()); + stageMap.put("actions", entry.getValue().stream() + .map(ActionPoolStateCodec::encodeHistoryAction) + .toList()); return StateValue.obj(stageMap); }).toList(); } - private static List encodeScheduleHistories(SchedulableExecutableAction schedulableAction) { + private static List encodeScheduleHistories(SchedulableExecutableAction schedulableAction) { return schedulableAction.getScheduleHistories().stream() - .map(scheduleHistory -> { - Map historyMap = new LinkedHashMap<>(); - historyMap.put("end_time", StateValue.str(scheduleHistory.getEndTime().toString())); - historyMap.put("result", StateValue.str(scheduleHistory.getResult())); - historyMap.put("history", StateValue.arr(encodeHistoryStages(scheduleHistory.getHistory()))); + .map(scheduleHistory -> { + Map historyMap = new LinkedHashMap<>(); + historyMap.put("end_time", scheduleHistory.getEndTime().toString()); + historyMap.put("result", scheduleHistory.getResult()); + historyMap.put("history", encodeHistoryStages(scheduleHistory.getHistory())); return StateValue.obj(historyMap); }) .toList(); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextCore.java b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextCore.java index 8a53ab21..b4f7050b 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextCore.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextCore.java @@ -200,13 +200,12 @@ public class ContextCore implements StateSerializable { public @NotNull State convert() { State state = new State(); - List convertedMessageList = chatMessages.stream().map(message -> { - Map convertedMap = Map.of( - "role", StateValue.str(message.roleValue()), - "content", StateValue.str(message.getContent()) - ); - return StateValue.obj(convertedMap); - }).toList(); + List convertedMessageList = chatMessages.stream() + .map(message -> StateValue.obj(Map.of( + "role", message.roleValue(), + "content", message.getContent() + ))) + .toList(); state.append("chat_messages", StateValue.arr(convertedMessageList)); return state; diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/memory/MemoryCore.java b/Partner-Core/src/main/java/work/slhaf/partner/core/memory/MemoryCore.java index 4d15af3e..2b68bc4c 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/memory/MemoryCore.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/memory/MemoryCore.java @@ -175,8 +175,7 @@ public class MemoryCore implements StateSerializable { State state = new State(); state.append("memory_session_id", StateValue.str(memorySessionId)); - List unitOverview = memoryUnits.keySet().stream() - .map(StateValue::str) + List unitOverview = memoryUnits.keySet().stream() .toList(); state.append("memory_unit_uuid_set", StateValue.arr(unitOverview)); return state; diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/memory/pojo/MemoryUnit.java b/Partner-Core/src/main/java/work/slhaf/partner/core/memory/pojo/MemoryUnit.java index 06b68fc1..c4054a14 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/memory/pojo/MemoryUnit.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/memory/pojo/MemoryUnit.java @@ -95,25 +95,23 @@ public class MemoryUnit implements StateSerializable { state.append("id", StateValue.str(id)); state.append("update_timestamp", StateValue.num(timestamp)); - List convertedMessageList = conversationMessages.stream().map(message -> { - Map convertedMap = Map.of( - "role", StateValue.str(message.roleValue()), - "content", StateValue.str(message.getContent()) - ); - return StateValue.obj(convertedMap); - }).toList(); + List convertedMessageList = conversationMessages.stream() + .map(message -> StateValue.obj(Map.of( + "role", message.roleValue(), + "content", message.getContent() + ))) + .toList(); state.append("conversation_messages", StateValue.arr(convertedMessageList)); - List convertedSliceList = slices.stream().map(slice -> { - Map convertedMap = Map.of( - "id", StateValue.str(slice.getId()), - "start_index", StateValue.num(slice.getStartIndex()), - "end_index", StateValue.num(slice.getEndIndex()), - "summary", StateValue.str(slice.getSummary()), - "created_timestamp", StateValue.num(slice.getTimestamp()) - ); - return StateValue.obj(convertedMap); - }).toList(); + List convertedSliceList = slices.stream() + .map(slice -> StateValue.obj(Map.of( + "id", slice.getId(), + "start_index", slice.getStartIndex(), + "end_index", slice.getEndIndex(), + "summary", slice.getSummary(), + "created_timestamp", slice.getTimestamp() + ))) + .toList(); state.append("memory_slices", StateValue.arr(convertedSliceList)); return state; } diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeStateCodec.java b/Partner-Core/src/main/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeStateCodec.java index 4b4dde49..682786e9 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeStateCodec.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeStateCodec.java @@ -69,8 +69,8 @@ final class MemoryRuntimeStateCodec { List dateIndexStates = dateIndex.entries().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(entry -> StateValue.obj(Map.of( - "date", StateValue.str(entry.getKey().toString()), - "refs", StateValue.arr(encodeSliceRefs(entry.getValue())) + "date", entry.getKey().toString(), + "refs", encodeSliceRefs(entry.getValue()) ))) .toList(); state.append("date_index", StateValue.arr(dateIndexStates)); @@ -82,8 +82,8 @@ final class MemoryRuntimeStateCodec { TopicMemoryIndex.TopicTreeNode topicNode, List topicStates) { topicStates.add(StateValue.obj(Map.of( - "topic_path", StateValue.str(path), - "bindings", StateValue.arr(encodeTopicBindings(topicNode.bindings())) + "topic_path", path, + "bindings", encodeTopicBindings(topicNode.bindings()) ))); for (Map.Entry childEntry : topicNode.children().entrySet()) { collectTopicStates(path + "->" + childEntry.getKey(), childEntry.getValue(), topicStates); @@ -93,18 +93,16 @@ final class MemoryRuntimeStateCodec { private List encodeTopicBindings(List bindings) { return bindings.stream() .map(binding -> (StateValue) StateValue.obj(Map.of( - "unit_id", StateValue.str(binding.sliceRef().getUnitId()), - "slice_id", StateValue.str(binding.sliceRef().getSliceId()), - "timestamp", StateValue.num(binding.timestamp()), + "unit_id", binding.sliceRef().getUnitId(), + "slice_id", binding.sliceRef().getSliceId(), + "timestamp", binding.timestamp(), "activation_profile", StateValue.obj(Map.of( - "activation_weight", StateValue.num(binding.activationProfile().getActivationWeight()), - "diffusion_weight", StateValue.num(binding.activationProfile().getDiffusionWeight()), + "activation_weight", binding.activationProfile().getActivationWeight(), + "diffusion_weight", binding.activationProfile().getDiffusionWeight(), "context_independence_weight", - StateValue.num(binding.activationProfile().getContextIndependenceWeight()) + binding.activationProfile().getContextIndependenceWeight() )), - "related_topic_paths", StateValue.arr(binding.relatedTopicPaths().stream() - .map(StateValue::str) - .toList()) + "related_topic_paths", binding.relatedTopicPaths() ))) .toList(); } @@ -156,8 +154,8 @@ final class MemoryRuntimeStateCodec { private List encodeSliceRefs(List refs) { return refs.stream() .map(ref -> (StateValue) StateValue.obj(Map.of( - "unit_id", StateValue.str(ref.getUnitId()), - "slice_id", StateValue.str(ref.getSliceId()) + "unit_id", ref.getUnitId(), + "slice_id", ref.getSliceId() ))) .toList(); } diff --git a/Partner-Framework/src/main/java/work/slhaf/partner/framework/agent/state/StateCenter.kt b/Partner-Framework/src/main/java/work/slhaf/partner/framework/agent/state/StateCenter.kt index 87fec298..6d7c5b51 100644 --- a/Partner-Framework/src/main/java/work/slhaf/partner/framework/agent/state/StateCenter.kt +++ b/Partner-Framework/src/main/java/work/slhaf/partner/framework/agent/state/StateCenter.kt @@ -148,10 +148,130 @@ sealed interface StateValue { fun str(value: String) = Str(value) @JvmStatic - fun arr(value: List) = Arr(value) + fun arr(value: List<*>): Arr { + val visiting = java.util.IdentityHashMap() + return Arr(convertList(value, visiting)) + } @JvmStatic - fun obj(value: Map) = Obj(value) + fun obj(value: Map): Obj { + val visiting = java.util.IdentityHashMap() + return Obj(convertMap(value, visiting)) + } + + private fun convertValue( + value: Any?, + visiting: java.util.IdentityHashMap + ): StateValue { + return when (value) { + null -> error("StateValue does not support null") + is StateValue -> normalizeStateValue(value, visiting) + is String -> Str(value) + is Number -> Num(value) + is Boolean -> Bool(value) + is List<*> -> Arr(convertList(value, visiting)) + is Map<*, *> -> Obj(convertGenericMap(value, visiting)) + else -> error("Unsupported state value type: ${value::class.qualifiedName}") + } + } + + private fun normalizeStateValue( + value: StateValue, + visiting: java.util.IdentityHashMap + ): StateValue { + return when (value) { + is Num -> value + is Bool -> value + is Str -> value + is Arr -> Arr(convertStateValueList(value.value, visiting)) + is Obj -> Obj(convertStateValueMap(value.value, visiting)) + } + } + + private fun convertList( + value: List<*>, + visiting: java.util.IdentityHashMap + ): List { + enterContainer(value, visiting) + try { + return value.map { convertValue(it, visiting) } + } finally { + leaveContainer(value, visiting) + } + } + + private fun convertMap( + value: Map, + visiting: java.util.IdentityHashMap + ): Map { + enterContainer(value, visiting) + try { + return value.entries.associateTo(LinkedHashMap()) { (key, mapValue) -> + key to convertValue(mapValue, visiting) + } + } finally { + leaveContainer(value, visiting) + } + } + + private fun convertGenericMap( + value: Map<*, *>, + visiting: java.util.IdentityHashMap + ): Map { + enterContainer(value, visiting) + try { + return value.entries.associateTo(LinkedHashMap()) { (key, mapValue) -> + check(key is String) { + "StateValue object key must be String, but got: ${key?.let { it::class.qualifiedName }}" + } + key to convertValue(mapValue, visiting) + } + } finally { + leaveContainer(value, visiting) + } + } + + private fun convertStateValueList( + value: List, + visiting: java.util.IdentityHashMap + ): List { + enterContainer(value, visiting) + try { + return value.map { normalizeStateValue(it, visiting) } + } finally { + leaveContainer(value, visiting) + } + } + + private fun convertStateValueMap( + value: Map, + visiting: java.util.IdentityHashMap + ): Map { + enterContainer(value, visiting) + try { + return value.entries.associateTo(LinkedHashMap()) { (key, mapValue) -> + key to normalizeStateValue(mapValue, visiting) + } + } finally { + leaveContainer(value, visiting) + } + } + + private fun enterContainer( + container: Any, + visiting: java.util.IdentityHashMap + ) { + check(visiting.put(container, Unit) == null) { + "Circular reference detected while constructing StateValue" + } + } + + private fun leaveContainer( + container: Any, + visiting: java.util.IdentityHashMap + ) { + visiting.remove(container) + } } } diff --git a/Partner-Framework/src/test/java/work/slhaf/partner/framework/agent/state/StateTest.kt b/Partner-Framework/src/test/java/work/slhaf/partner/framework/agent/state/StateTest.kt new file mode 100644 index 00000000..908881f5 --- /dev/null +++ b/Partner-Framework/src/test/java/work/slhaf/partner/framework/agent/state/StateTest.kt @@ -0,0 +1,60 @@ +package work.slhaf.partner.framework.agent.state + +fun main() { + testNormalStateJson() + println() + testCircularReference() +} + +private fun testNormalStateJson() { + val nestedMap = linkedMapOf( + "name" to "partner", + "enabled" to true, + "count" to 3, + "tags" to listOf("agent", "runtime", "state-center"), + "meta" to linkedMapOf( + "version" to "0.1.0", + "experimental" to false + ) + ) + + val state = State() + state.append("root", StateValue.obj(nestedMap)) + state.append( + "arr", + StateValue.arr( + listOf( + "hello", + 123, + true, + linkedMapOf( + "nested" to "value" + ) + ) + ) + ) + + println("=== normal state ===") + println(state.toString()) +} + +private fun testCircularReference() { + val cyclicMap = linkedMapOf() + cyclicMap["name"] = "cyclic" + cyclicMap["self"] = cyclicMap + + println("=== circular reference ===") + + try { + val state = State() + state.append("cyclic", StateValue.obj(cyclicMap)) + + // 如果前面没有抛错,这里再触发最终 JSON 输出 + println(state.toString()) + error("Expected circular reference detection, but no exception was thrown.") + } catch (e: IllegalStateException) { + println("circular reference detected as expected:") + println(e.message) + } +} +