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 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-04-30 11:49:27 +08:00
parent 5bd1850d99
commit 9612800b4b
4 changed files with 146 additions and 3 deletions

View File

@@ -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; }

View File

@@ -0,0 +1,14 @@
package com.yomahub.liteflow.property.agent;
/**
* ReAct agent 日志开关配置。
* <p>对应 {@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; }
}

View File

@@ -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<Object> 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<Hook> hooks(ReActAgentContext ctx) { return List.of(); }
/**
* 是否在日志中输出 agent 的 reason / act / error 事件。
* <p>默认从配置 {@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<Hook> 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();
}

View File

@@ -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 的思考与工具调用过程。
*
* <p>事件 → 日志格式:
* <ul>
* <li>{@link PreReasoningEvent}{@code [agent:reason] >>> model=... messages=N}</li>
* <li>{@link PostReasoningEvent}{@code [agent:reason] <<< text=... toolCalls=[...]}</li>
* <li>{@link PreActingEvent}{@code [agent:act] >>> tool=... input=...}</li>
* <li>{@link PostActingEvent}{@code [agent:act] <<< tool=... result=...}</li>
* <li>{@link ErrorEvent}{@code [agent:error] ...}</li>
* </ul>
*/
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 <T extends HookEvent> Mono<T> onEvent(T event) {
try {
if (event instanceof PreReasoningEvent e) {
List<Msg> 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<ToolUseBlock> 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<ToolUseBlock> 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)";
}
}