From 9612800b4b05ae89acca406413baffbb525e90bb Mon Sep 17 00:00:00 2001 From: "everywhere.z" Date: Thu, 30 Apr 2026 11:49:27 +0800 Subject: [PATCH] feat(react-agent): log ReAct reason/act events with sessionId and config toggle Subscribe to agentscope Pre/PostReasoningEvent, Pre/PostActingEvent and ErrorEvent through a new ReActLoggingHook, surfacing the agent's internal think-act loop in standard logs. Each line carries the LiteFlow agent sessionId so concurrent sessions stay distinguishable. ReActAgentComponent attaches the hook automatically alongside any user-provided hooks. Toggle via liteflow.agent.logging.react-enabled (default true) or override enableReActLogging() per component. Co-Authored-By: Claude Opus 4.7 --- .../liteflow/property/agent/AgentConfig.java | 3 + .../property/agent/LoggingConfig.java | 14 +++ .../agent/component/ReActAgentComponent.java | 26 ++++- .../liteflow/agent/hook/ReActLoggingHook.java | 106 ++++++++++++++++++ 4 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/LoggingConfig.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/hook/ReActLoggingHook.java diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java index 631317dc8..1eaa58b8c 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java @@ -8,6 +8,7 @@ public class AgentConfig { private SessionConfig session = new SessionConfig(); private ShellConfig shell = new ShellConfig(); private DefaultsConfig defaults = new DefaultsConfig(); + private LoggingConfig logging = new LoggingConfig(); private PlatformCredential openai = new PlatformCredential(); private PlatformCredential anthropic = new PlatformCredential(); private PlatformCredential gemini = new PlatformCredential(); @@ -23,6 +24,8 @@ public class AgentConfig { public void setShell(ShellConfig v) { this.shell = v; } public DefaultsConfig getDefaults() { return defaults; } public void setDefaults(DefaultsConfig v) { this.defaults = v; } + public LoggingConfig getLogging() { return logging; } + public void setLogging(LoggingConfig v) { this.logging = v; } public PlatformCredential getOpenai() { return openai; } public void setOpenai(PlatformCredential v) { this.openai = v; } public PlatformCredential getAnthropic() { return anthropic; } diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/LoggingConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/LoggingConfig.java new file mode 100644 index 000000000..d0605ca43 --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/LoggingConfig.java @@ -0,0 +1,14 @@ +package com.yomahub.liteflow.property.agent; + +/** + * ReAct agent 日志开关配置。 + *

对应 {@code liteflow.agent.logging.*} 配置段。 + */ +public class LoggingConfig { + + /** 是否输出 reason / act / error 内部事件日志。默认开启。 */ + private boolean reactEnabled = true; + + public boolean isReactEnabled() { return reactEnabled; } + public void setReactEnabled(boolean reactEnabled) { this.reactEnabled = reactEnabled; } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java index ce1291870..3deee9953 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java @@ -1,8 +1,10 @@ package com.yomahub.liteflow.agent.component; import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.agent.hook.ReActLoggingHook; import com.yomahub.liteflow.agent.session.AgentSession; import com.yomahub.liteflow.agent.session.AgentSessionManager; +import com.yomahub.liteflow.agent.session.NanoIdSessionIdGenerator; import com.yomahub.liteflow.agent.tool.ManagedShellCommandTool; import com.yomahub.liteflow.agent.tool.WorkspaceFileTools; import com.yomahub.liteflow.core.NodeComponent; @@ -21,6 +23,7 @@ import io.agentscope.core.tool.Toolkit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; /** @@ -92,9 +95,11 @@ public abstract class ReActAgentComponent extends NodeComponent { protected List tools(ReActAgentContext ctx) { return List.of(); } /** - * 从当前 slot 推导 session id。默认使用 slot 的 requestId。 + * 从当前 slot 推导 session id。默认生成 {@code YYYYMMDD + NanoId(18)} 格式。 */ - protected String resolveSessionId(Slot slot) { return slot.getRequestId(); } + protected String resolveSessionId(Slot slot) { + return NanoIdSessionIdGenerator.generate(); + } /** * ReAct 最大迭代次数。返回 -1(默认值)表示使用 @@ -117,6 +122,16 @@ public abstract class ReActAgentComponent extends NodeComponent { */ protected List hooks(ReActAgentContext ctx) { return List.of(); } + /** + * 是否在日志中输出 agent 的 reason / act / error 事件。 + *

默认从配置 {@code liteflow.agent.logging.react-enabled} 读取(默认 true), + * 子类可覆写返回 {@code true}/{@code false} 强制开关。 + * 输出在 logger {@code com.yomahub.liteflow.agent.hook.ReActLoggingHook} 上。 + */ + protected boolean enableReActLogging() { + return agentConfig().getLogging().isReactEnabled(); + } + /** * agent 回复后调用。默认实现会把 {@code reply.getTextContent()} * 写入 slot 的响应数据。 @@ -194,6 +209,11 @@ public abstract class ReActAgentComponent extends NodeComponent { toolkit.registerTool(new ManagedShellCommandTool(ctx.getWorkspaceDir(), cfg)); } + List allHooks = new ArrayList<>(hooks(ctx)); + if (enableReActLogging()) { + allHooks.add(new ReActLoggingHook(ctx.getSessionId())); + } + return ReActAgent.builder() .name(getNodeId() == null ? "liteflow-agent" : getNodeId()) .sysPrompt(systemPrompt(ctx)) @@ -201,7 +221,7 @@ public abstract class ReActAgentComponent extends NodeComponent { .toolkit(toolkit) .memory(new InMemoryMemory()) .maxIters(iters) - .hooks(hooks(ctx)) + .hooks(allHooks) .build(); } diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/hook/ReActLoggingHook.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/hook/ReActLoggingHook.java new file mode 100644 index 000000000..50ea1f19c --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/hook/ReActLoggingHook.java @@ -0,0 +1,106 @@ +package com.yomahub.liteflow.agent.hook; + +import io.agentscope.core.hook.ErrorEvent; +import io.agentscope.core.hook.Hook; +import io.agentscope.core.hook.HookEvent; +import io.agentscope.core.hook.PostActingEvent; +import io.agentscope.core.hook.PostReasoningEvent; +import io.agentscope.core.hook.PreActingEvent; +import io.agentscope.core.hook.PreReasoningEvent; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * 把 agentscope ReActAgent 的内部 Reason / Act / Error 事件输出到日志, + * 让 LiteFlow 用户在终端可以直接看到 agent 的思考与工具调用过程。 + * + *

事件 → 日志格式: + *

    + *
  • {@link PreReasoningEvent}:{@code [agent:reason] >>> model=... messages=N}
  • + *
  • {@link PostReasoningEvent}:{@code [agent:reason] <<< text=... toolCalls=[...]}
  • + *
  • {@link PreActingEvent}:{@code [agent:act] >>> tool=... input=...}
  • + *
  • {@link PostActingEvent}:{@code [agent:act] <<< tool=... result=...}
  • + *
  • {@link ErrorEvent}:{@code [agent:error] ...}
  • + *
+ */ +public class ReActLoggingHook implements Hook { + + private static final Logger LOG = LoggerFactory.getLogger(ReActLoggingHook.class); + private static final int MAX_TEXT_LEN = 500; + + private final String sessionId; + + public ReActLoggingHook(String sessionId) { + this.sessionId = sessionId == null ? "-" : sessionId; + } + + @Override + public Mono onEvent(T event) { + try { + if (event instanceof PreReasoningEvent e) { + List msgs = e.getInputMessages(); + LOG.info("[agent:reason][{}] >>> model={} messages={}", + sessionId, e.getModelName(), msgs == null ? 0 : msgs.size()); + } else if (event instanceof PostReasoningEvent e) { + Msg reply = e.getReasoningMessage(); + String text = reply == null ? "" : truncate(reply.getTextContent()); + List tools = reply == null + ? List.of() + : reply.getContentBlocks(ToolUseBlock.class); + if (tools.isEmpty()) { + LOG.info("[agent:reason][{}] <<< text={}", sessionId, text); + } else { + LOG.info("[agent:reason][{}] <<< text={} toolCalls={}", + sessionId, text, summarizeToolUses(tools)); + } + } else if (event instanceof PreActingEvent e) { + ToolUseBlock t = e.getToolUse(); + LOG.info("[agent:act][{}] >>> tool={} input={}", + sessionId, t.getName(), truncate(String.valueOf(t.getInput()))); + } else if (event instanceof PostActingEvent e) { + ToolResultBlock r = e.getToolResult(); + LOG.info("[agent:act][{}] <<< tool={} result={}", + sessionId, r.getName(), truncate(blocksToString(r))); + } else if (event instanceof ErrorEvent e) { + LOG.warn("[agent:error][{}] {}", sessionId, e.getError().toString(), e.getError()); + } + } catch (Throwable logEx) { + LOG.debug("ReActLoggingHook formatting failed", logEx); + } + return Mono.just(event); + } + + @Override + public int priority() { + return 900; + } + + private static String summarizeToolUses(List tools) { + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < tools.size(); i++) { + ToolUseBlock t = tools.get(i); + if (i > 0) sb.append(", "); + sb.append(t.getName()).append("(").append(t.getInput()).append(")"); + } + return truncate(sb.append("]").toString()); + } + + private static String blocksToString(ToolResultBlock r) { + if (r.getOutput() == null) return ""; + StringBuilder sb = new StringBuilder(); + r.getOutput().forEach(b -> sb.append(b)); + return sb.toString(); + } + + private static String truncate(String s) { + if (s == null) return ""; + s = s.replaceAll("\\s+", " ").trim(); + return s.length() <= MAX_TEXT_LEN ? s : s.substring(0, MAX_TEXT_LEN) + "...(truncated)"; + } +}