AI 大模型飞速进化,不懂 AI、不会用 Agent 的程序员,正在慢慢被行业淘汰。现在 AI 新技术迭代快到离谱:RAG 还没学明白,MCP 接踵而来;工具刚上手,Skills、各类新 Agent 概念又轮番刷屏;Manus还没看懂,就出来OpenClaw,OpenClaw热度还没退,又出来Hermes。越追越乱、越学越焦虑,永远在被动跟风,陷入学不完、跟不上、用不上的内耗。其实焦虑的根本原因,不是新技术太多,而是不懂大模型 Agent 应用底层原理,只会零散跟风,没有通用方法论。
为此我专门推出Agent 应用实战专栏,采用Agent基础 + Agent设计模式 + 工程实践三位一体体系,帮你彻底摆脱 AI 焦虑。
专栏分为三大模块:
- Agent基础:深入浅出拆解AgentScope Java源码,掌握Agent应用基础的三驾马车(提示词、工具和记忆),夯实 Agent 开发底层根基;
- Agent设计模式:基于《agentic-design-patterns》精讲经典架构模式(提示词链、并行、路由、规划、反思、推理、人机协同等等),掌握高阶 AI 应用设计思路;
- 工程实践:手把手从零手搓简易版 OpenClaw,还原真实项目落地全流程。
学完本专栏,你将彻底吃透底层逻辑、掌握架构思维、并具备工程落地能力。未来无论再涌现 MCP、Skills 还是其他任何新技术,你都能一眼看透其本质、快速上手复用,不再盲目跟风,稳稳守住程序员的职业护城河。
注意 :由于框架的不同版本会有些使用的不同,因此本次系列中使用基本框架是agentscope-1.0.11,JDK版本使用的是open-jdk-21(agentscope-java最低要求java版本为17)。
本文章的代码地址: https://github.com/forever1986/agentscope-claw
目录
- [1 OpenClaw整体架构](#1 OpenClaw整体架构)
- [2 Pi-mono](#2 Pi-mono)
- [3 AgentScope实现](#3 AgentScope实现)
-
- [3.1 代码实现](#3.1 代码实现)
- [3.2 运行结果](#3.2 运行结果)
前面已经讲完本专栏的前两个大部分:基础(三驾马车)和Agent设计模式。从本章开始就来学习前段时间非常火的小龙虾,也通过演示如何使用AgentScope手搓一个简单版本的OpenClaw。这里我们并非要完整复刻OpenClaw,而是分析OpenClaw与Agent有关的部分,通过使用AgentScope实现类似功能(不一定按照他们的实现方式),最终达到让大家更深入掌握之前学习的Agent内容,同时在讲解部分内容的时候,可能会将目前行业内的情况给大家说说,可以从宏观上有一个更高层次的理解。最终你会发现其最底层依旧是前面学习的内容,不同的地方就是将工程化做细。
1 OpenClaw整体架构

上图是一张尽量还原OpenClaw整体结构的架构图,虽然有些地方并不准确,但是大差不差。从上面可以看到,openclaw有三个层
- channel:这个是对接外部各种渠道,telegram、WhatsApp、钉钉、飞书等等,同时提供扩展点,可供各个渠道去自己实现
- gateway:这个是一个网关,这是是OpenClaw的消息管理、Node管理、认证&安全的拦截点
- runtime:这个是实际一个Agent运行时的结构,这个是一个核心内容
本次使用AgentScope手搓简单版OpenClaw,主要复刻的是第三层的内容。而前面的Channel、gateway、Node则不是本栏目要讲解的重点。下面就开始第一个内容:Pi-mono。
2 Pi-mono
pi-mono 是由 Mario Zechner 编写的一个Agent框架,而OpenClaw就是使用这个框架来实现底层Agent。现在改名为: pi
这个框架提供一个极简的Agent,它主要核心的模块有以下三个:
| 模块 | 描述 |
|---|---|
| @earendil-works/pi-ai | 统一的多提供商 LLM API。一个接口对接几乎市面上大部分LLM 提供商,包括 OpenAI、Anthropic、Google、Kimi、MiniMax等等。 |
| @earendil-works/pi-agent-core | Agent Runtime。负责工具调用循环、状态管理、上下文维护这些核心逻辑。 |
| @earendil-works/pi-coding-agent | CLI 编程 Agent,通过命令行工具进行Agent操作,包含完整的会话管理、扩展系统等等。 |
如果从上面三个模块的描述,可以看出,其实AgentScope也已经实现对应的能力,虽然不能100%一样,但是底层原理是一致的
| pi-mono | AgentScope |
|---|---|
| @earendil-works/pi-ai | ChatModelBase |
| @earendil-works/pi-agent-core | ReActAgent、ShellCommandTool、ReadFileTool、WriteFileTool |
| @earendil-works/pi-coding-agent | 暂时没有 |
可以看出,除了使用CLI编程命令之外,AgentScope本身就使用了pi-mon的核心能力,接下来就使用AgentScope来实现第一步,一个可以聊天的Agent。
3 AgentScope实现
代码参考:claw-step1-pi-mono
涉及章节 :《Agent应用实践之五 - 基础:AgentScope-模型集成》、《Agent应用实践之四 - 基础:AgentScope-SpringBoot集成源码解析》、《Agent应用实践之十一 - 三驾马车:工具之初级使用》、《Agent应用实践之二十八 - 设计模式:规划》、《Agent应用实践之三十六 - AgentScope高阶用法:AG-UI》
示例说明:通过AgentScope的DashScopeChatModel、ReActAgent以及agui能力,构建一个可视化基本Agent。(注意,本次演示只会使用一个Agent,有兴趣的朋友自己去扩展多Agent管理)
3.1 代码实现
1)在父项目agentscope-claw下,新建claw-step1-pi-mono项目,其pom引入如下:
xml
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>${agentscope.version}</version>
</dependency>
<!-- AgentScope agui -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-agui</artifactId>
<version>${agentscope.version}</version>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-agui-spring-boot-starter</artifactId>
<version>${agentscope.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springframework.boot.version}</version>
</dependency>
</dependencies>
2)在claw-step1-pi-mono项目下,创建配置类AgentConfiguration:
java
package com.lin.claw.config;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.Agent;
import io.agentscope.core.agui.registry.AguiAgentRegistry;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.plan.PlanNotebook;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.coding.ShellCommandTool;
import io.agentscope.core.tool.file.ReadFileTool;
import io.agentscope.core.tool.file.WriteFileTool;
import io.agentscope.spring.boot.agui.common.AguiAgentRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.File;
import java.net.URL;
@Configuration
public class AgentConfiguration {
@Bean
public AguiAgentRegistryCustomizer aguiAgentRegistryCustomizer() {
AguiAgentRegistryCustomizer aguiAgentRegistryCustomizer = new AguiAgentRegistryCustomizer() {
@Override
public void customize(AguiAgentRegistry registry) {
// 这里使用一个默认的agent,本项目不实现维护多个Agent
registry.register("default", createDefaultAgent());
}
};
return aguiAgentRegistryCustomizer;
}
private Agent createDefaultAgent() {
// 定义三大基本工具(读、写和执行)
URL resource = ClassLoader.getSystemResource("workspaces");
File directory = new File(resource.getPath());
// 创建工具
Toolkit toolkit = new Toolkit();
ShellCommandTool shellTool = new ShellCommandTool(directory.getPath(),null, null);
ReadFileTool readTool = new ReadFileTool(directory.getPath());
WriteFileTool writeTool = new WriteFileTool(directory.getPath());
toolkit.registerTool(shellTool);
toolkit.registerTool(readTool);
toolkit.registerTool(writeTool);
// 定义计划PlanNotebook
PlanNotebook planNotebook = PlanNotebook.builder()
.needUserConfirm(false) // 设置不需要询问用户就可以直接执行计划
.build();
// 创建Agent
return ReActAgent.builder()
.name("AG-UI Assistant")
.sysPrompt("""
您是运行在 AgentScope Claw 内部的个人助手。
""")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName(System.getenv("QWEN_MODEL"))
.enableThinking(true)
.build())
.toolkit(toolkit)
.planNotebook(planNotebook)
.memory(new InMemoryMemory())
.maxIters(20)
.build();
}
}
3)在claw-step1-pi-mono项目下,在resources目录下,创建配置文件application.yml
yaml
server:
port: 8080
# AG-UI Configuration
agentscope:
agui:
path-prefix: /agui
cors-enabled: true
cors-allowed-origins:
- "*"
run-timeout: 10m
emit-state-events: true
emit-tool-call-args: true
default-agent-id: default
# Agent ID routing configuration
# Agent ID can be passed via:
# 1. URL path variable: POST /agui/run/{agentId} (highest priority)
# 2. HTTP header: X-Agent-Id (configurable)
# 3. Request body: forwardedProps.agentId
# 4. Default: uses default-agent-id
agent-id-header: X-Agent-Id
enable-path-routing: true
# Server-side memory management
server-side-memory: true
max-thread-sessions: 1000
session-timeout-minutes: 30
enable-reasoning: false
4)在claw-step1-pi-mono项目下,在resources/static目录下,创建配置界面的index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AgentScope AG-UI Demo</title>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #24283b;
--bg-tertiary: #414868;
--text-primary: #c0caf5;
--text-secondary: #9aa5ce;
--accent-blue: #7aa2f7;
--accent-green: #9ece6a;
--accent-orange: #ff9e64;
--accent-red: #f7768e;
--border-color: #414868;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
header {
text-align: center;
padding: 30px 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
}
header h1 {
font-size: 2rem;
font-weight: 600;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
header p {
color: var(--text-secondary);
margin-top: 8px;
font-size: 0.9rem;
}
#messages {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
overflow-y: auto;
min-height: 400px;
max-height: 60vh;
}
.message {
margin-bottom: 16px;
padding: 12px 16px;
border-radius: 8px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
background: var(--bg-tertiary);
margin-left: 40px;
}
.message.assistant {
background: linear-gradient(135deg, rgba(122, 162, 247, 0.1), rgba(158, 206, 106, 0.1));
border-left: 3px solid var(--accent-blue);
margin-right: 40px;
}
.message.tool {
background: rgba(255, 158, 100, 0.1);
border-left: 3px solid var(--accent-orange);
margin: 0 60px 16px 60px;
font-size: 0.85rem;
}
.message.reasoning {
background: rgba(158, 206, 106, 0.08);
border-left: 3px solid var(--accent-green);
margin: 0 60px 16px 60px;
font-size: 0.85rem;
font-style: italic;
opacity: 0.9;
}
.message.error {
background: rgba(247, 118, 142, 0.1);
border-left: 3px solid var(--accent-red);
}
.message-role {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
font-weight: 600;
}
.message.user .message-role { color: var(--text-secondary); }
.message.assistant .message-role { color: var(--accent-blue); }
.message.tool .message-role { color: var(--accent-orange); }
.message.reasoning .message-role { color: var(--accent-green); }
.message-content {
white-space: pre-wrap;
line-height: 1.6;
}
.input-area {
display: flex;
gap: 12px;
margin-top: 20px;
}
#input {
flex: 1;
padding: 14px 18px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
#input:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(122, 162, 247, 0.2);
}
#input::placeholder {
color: var(--text-secondary);
}
#send-btn, #stop-btn {
padding: 14px 28px;
border: none;
border-radius: 8px;
color: white;
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
#send-btn {
background: linear-gradient(135deg, var(--accent-blue), #5a8af7);
}
#send-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(122, 162, 247, 0.3);
}
#send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#stop-btn {
background: linear-gradient(135deg, var(--accent-red), #e05555);
}
#stop-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(247, 118, 142, 0.3);
}
.typing-indicator {
display: inline-flex;
gap: 4px;
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: 16px;
margin-bottom: 12px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: var(--accent-blue);
border-radius: 50%;
animation: bounce 1.4s infinite;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
}
footer {
text-align: center;
padding: 16px;
color: var(--text-secondary);
font-size: 0.8rem;
border-top: 1px solid var(--border-color);
margin-top: 20px;
}
footer a {
color: var(--accent-blue);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.status {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 0.85rem;
color: var(--text-secondary);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
}
.status-dot.disconnected {
background: var(--accent-red);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>AgentScope AG-UI Demo</h1>
<p>Chat with an AI agent via the AG-UI protocol</p>
</header>
<div class="status">
<div class="status-dot" id="status-dot"></div>
<span id="status-text">Ready</span>
</div>
<div id="messages"></div>
<div class="input-area">
<input type="text" id="input" placeholder="Type a message... (Press Enter to send)" autocomplete="off">
<button id="send-btn">Send</button>
<button id="stop-btn" style="display: none;">Stop</button>
</div>
</div>
<footer>
Powered by <a href="https://agentscope.io" target="_blank">AgentScope</a> ·
AG-UI Protocol ·
<a href="https://docs.ag-ui.com" target="_blank">Documentation</a>
</footer>
<script src="/js/agui-client.js"></script>
<script>
const client = new AguiClient('/agui/run');
const input = document.getElementById('input');
const sendBtn = document.getElementById('send-btn');
const stopBtn = document.getElementById('stop-btn');
const messages = document.getElementById('messages');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
let threadId = 'thread-' + Date.now();
let messageHistory = [];
let isRunning = false;
function setStatus(status, text) {
statusDot.className = 'status-dot' + (status === 'error' ? ' disconnected' : '');
statusText.textContent = text;
}
let currentAssistantDiv = null;
function appendMessage(role, content, append = false) {
if (append && role === 'assistant' && currentAssistantDiv) {
// Append to current assistant message
const contentEl = currentAssistantDiv.querySelector('.message-content');
contentEl.textContent += content;
messages.scrollTop = messages.scrollHeight;
return;
}
if (append && role === 'reasoning' && currentReasoningDiv) {
// Append to current reasoning message
const contentEl = currentReasoningDiv.querySelector('.message-content');
contentEl.textContent += content;
messages.scrollTop = messages.scrollHeight;
return;
}
const div = document.createElement('div');
div.className = `message ${role}`;
div.innerHTML = `
<div class="message-role">${role}</div>
<div class="message-content">${escapeHtml(content)}</div>
`;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
if (role === 'assistant') {
currentAssistantDiv = div;
} else if (role === 'reasoning') {
currentReasoningDiv = div;
}
}
function showTypingIndicator() {
const div = document.createElement('div');
div.id = 'typing';
div.className = 'typing-indicator';
div.innerHTML = '<span></span><span></span><span></span>';
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function hideTypingIndicator() {
const typing = document.getElementById('typing');
if (typing) typing.remove();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function sendMessage() {
const text = input.value.trim();
if (!text || isRunning) return;
input.value = '';
isRunning = true;
sendBtn.style.display = 'none';
stopBtn.style.display = 'inline-block';
setStatus('running', 'Running...');
// Add user message
appendMessage('user', text);
messageHistory.push({ id: 'msg-' + Date.now(), role: 'user', content: text });
// Show typing indicator
showTypingIndicator();
let assistantContent = '';
let currentMessageId = null;
let reasoningContent = '';
let currentReasoningMessageId = null;
let currentReasoningDiv = null;
try {
await client.run({
threadId: threadId,
runId: 'run-' + Date.now(),
messages: messageHistory
}, {
onRunStarted: () => {
console.log('Run started');
currentAssistantDiv = null;
currentReasoningDiv = null;
reasoningContent = '';
},
onReasoningMessageStart: (messageId, role) => {
console.log('Reasoning message start:', messageId, role);
hideTypingIndicator();
currentReasoningMessageId = messageId;
reasoningContent = '';
currentReasoningDiv = null;
},
onReasoningContent: (delta, messageId) => {
console.log('Reasoning content delta:', delta);
if (reasoningContent === '') {
appendMessage('reasoning', delta);
} else {
appendMessage('reasoning', delta, true);
}
reasoningContent += delta;
},
onReasoningMessageEnd: (messageId) => {
console.log('Reasoning message end:', messageId);
currentReasoningDiv = null;
reasoningContent = '';
},
onTextMessageStart: (messageId, role) => {
console.log('Text message start:', messageId, role);
hideTypingIndicator();
currentMessageId = messageId;
assistantContent = '';
currentAssistantDiv = null;
},
onTextContent: (delta) => {
console.log('Text content delta:', delta);
if (assistantContent === '') {
appendMessage('assistant', delta);
} else {
appendMessage('assistant', delta, true);
}
assistantContent += delta;
},
onTextMessageEnd: (messageId) => {
console.log('Text message end:', messageId);
if (assistantContent) {
messageHistory.push({
id: messageId,
role: 'assistant',
content: assistantContent
});
}
currentAssistantDiv = null;
},
onToolCallStart: (toolCallId, toolName) => {
hideTypingIndicator();
currentAssistantDiv = null;
appendMessage('tool', `🔧 Calling tool: ${toolName}`);
},
onToolCallEnd: (toolCallId) => {
// Tool call completed
},
onError: (error) => {
hideTypingIndicator();
appendMessage('error', `Error: ${error}`);
},
onRunFinished: () => {
console.log('Run finished');
hideTypingIndicator();
setStatus('ready', 'Ready');
}
});
} catch (error) {
console.error('Error during agent run:', error);
hideTypingIndicator();
// Check if this was a user-initiated abort
if (error.name === 'AbortError') {
console.log('Run was stopped by user');
// Save whatever content we received before stopping
if (assistantContent) {
messageHistory.push({
id: currentMessageId || 'msg-stopped-' + Date.now(),
role: 'assistant',
content: assistantContent + ' [stopped]'
});
}
setStatus('ready', 'Stopped');
} else {
appendMessage('error', `Error: ${error.message}`);
setStatus('error', 'Error');
}
} finally {
console.log('Run complete, resetting state');
isRunning = false;
sendBtn.style.display = 'inline-block';
stopBtn.style.display = 'none';
hideTypingIndicator();
currentAssistantDiv = null;
currentReasoningDiv = null;
reasoningContent = '';
if (statusText.textContent === 'Running...') {
setStatus('ready', 'Ready');
}
}
}
// Stop button handler
function stopGeneration() {
if (isRunning) {
console.log('User requested stop');
setStatus('stopping', 'Stopping...');
client.abort();
}
}
// Event listeners
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
sendBtn.addEventListener('click', sendMessage);
stopBtn.addEventListener('click', stopGeneration);
// Focus input on load
input.focus();
</script>
</body>
</html>
5)在claw-step1-pi-mono项目下,在resources/static/js目录下,创建配置界面的js文件:agui-client.js
javascript
class AguiClient {
/**
* Create a new AG-UI client.
* @param {string} endpoint - The AG-UI run endpoint URL
*/
constructor(endpoint) {
this.endpoint = endpoint;
this.abortController = null;
}
/**
* Abort the current run if one is in progress.
* This will close the SSE connection and trigger agent interruption on the backend.
*/
abort() {
if (this.abortController) {
console.log('Aborting current run...');
this.abortController.abort();
this.abortController = null;
}
}
/**
* Check if a run is currently in progress.
* @returns {boolean} True if running
*/
isRunning() {
return this.abortController !== null;
}
/**
* Run an agent with the given input.
* @param {Object} input - The run input
* @param {string} input.threadId - Thread identifier
* @param {string} input.runId - Run identifier
* @param {Array} input.messages - Array of messages
* @param {Array} [input.tools] - Optional tools
* @param {Array} [input.context] - Optional context
* @param {Object} [input.state] - Optional state
* @param {Object} [input.forwardedProps] - Optional forwarded properties
* @param {Object} callbacks - Event callbacks
* @param {Function} [callbacks.onReasoningMessageStart] - Called when reasoning message starts
* @param {Function} [callbacks.onReasoningContent] - Called with reasoning content delta
* @param {Function} [callbacks.onReasoningMessageEnd] - Called when reasoning message ends
* @returns {Promise} Resolves when the run completes
*/
async run(input, callbacks = {}) {
// Create abort controller for this run
this.abortController = new AbortController();
const signal = this.abortController.signal;
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify(input),
signal: signal
});
if (!response.ok) {
this.abortController = null;
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
console.log('Starting to read SSE stream...');
let eventSequence = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream ended, remaining buffer:', buffer);
break;
}
const chunk = decoder.decode(value, { stream: true });
console.log('Received chunk:', chunk.length, 'bytes');
buffer += chunk;
// Try both \n\n and \r\n\r\n as delimiters
let delimiter = '\n\n';
let delimiterIndex = buffer.indexOf(delimiter);
// If \n\n not found, try \r\n\r\n (Windows-style)
if (delimiterIndex === -1) {
delimiter = '\r\n\r\n';
delimiterIndex = buffer.indexOf(delimiter);
}
// Process complete SSE messages
while (delimiterIndex !== -1) {
const message = buffer.substring(0, delimiterIndex);
buffer = buffer.substring(delimiterIndex + delimiter.length);
console.log('Processing SSE message:', message.substring(0, 100));
// Process each line in the message
const lines = message.split(/\r?\n/);
for (const line of lines) {
if (line.startsWith('data:')) {
try {
// Handle both "data: " and "data:" formats
const jsonStr = line.startsWith('data: ') ? line.substring(6) : line.substring(5);
const event = JSON.parse(jsonStr);
eventSequence++;
console.log(`[${eventSequence}] Received event:`, event.type, event);
this.handleEvent(event, callbacks);
} catch (e) {
console.warn('Failed to parse event:', line, e);
}
}
}
// Check for next delimiter
delimiterIndex = buffer.indexOf('\n\n');
if (delimiterIndex === -1) {
delimiterIndex = buffer.indexOf('\r\n\r\n');
if (delimiterIndex !== -1) delimiter = '\r\n\r\n';
} else {
delimiter = '\n\n';
}
}
}
// Process any remaining data in buffer
if (buffer.trim()) {
console.log('Processing remaining buffer:', buffer);
const lines = buffer.split(/\r?\n/);
for (const line of lines) {
if (line.startsWith('data:')) {
try {
const jsonStr = line.startsWith('data: ') ? line.substring(6) : line.substring(5);
const event = JSON.parse(jsonStr);
console.log('Received final event:', event.type, event);
this.handleEvent(event, callbacks);
} catch (e) {
console.warn('Failed to parse remaining event:', line, e);
}
}
}
}
} finally {
reader.releaseLock();
this.abortController = null;
}
}
/**
* Handle an AG-UI event.
* @param {Object} event - The event object
* @param {Object} callbacks - Event callbacks
*/
handleEvent(event, callbacks) {
if (!event || !event.type) {
console.warn('Invalid event received:', event);
return;
}
const type = event.type;
try {
switch (type) {
case 'RUN_STARTED':
callbacks.onRunStarted?.(event.threadId, event.runId);
break;
case 'RUN_FINISHED':
callbacks.onRunFinished?.(event.threadId, event.runId);
break;
case 'TEXT_MESSAGE_START':
callbacks.onTextMessageStart?.(event.messageId, event.role);
break;
case 'TEXT_MESSAGE_CONTENT':
// Ensure delta is not null/undefined
const delta = event.delta || '';
if (delta) {
callbacks.onTextContent?.(delta, event.messageId);
}
break;
case 'TEXT_MESSAGE_END':
callbacks.onTextMessageEnd?.(event.messageId);
break;
case 'REASONING_MESSAGE_START':
callbacks.onReasoningMessageStart?.(event.messageId, event.role);
break;
case 'REASONING_MESSAGE_CONTENT':
// Ensure delta is not null/undefined
const reasoningDelta = event.delta || '';
if (reasoningDelta) {
callbacks.onReasoningContent?.(reasoningDelta, event.messageId);
}
break;
case 'REASONING_MESSAGE_END':
callbacks.onReasoningMessageEnd?.(event.messageId);
break;
case 'TOOL_CALL_START':
callbacks.onToolCallStart?.(event.toolCallId, event.toolCallName);
break;
case 'TOOL_CALL_ARGS':
callbacks.onToolCallArgs?.(event.toolCallId, event.delta);
break;
case 'TOOL_CALL_END':
callbacks.onToolCallEnd?.(event.toolCallId);
break;
case 'STATE_SNAPSHOT':
callbacks.onStateSnapshot?.(event.snapshot);
break;
case 'STATE_DELTA':
callbacks.onStateDelta?.(event.delta);
break;
case 'RAW':
if (event.rawEvent?.error) {
callbacks.onError?.(event.rawEvent.error);
} else {
callbacks.onRawEvent?.(event.rawEvent);
}
break;
default:
console.log('Unknown event type:', type, event);
}
} catch (error) {
console.error('Error handling event:', type, error);
}
}
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = { AguiClient };
}
6)在claw-step1-pi-mono项目下,创建启动类ClawApplication:
java
package com.lin.claw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ClawApplication {
public static void main(String[] args) {
SpringApplication.run(ClawApplication.class, args);
}
}
3.2 运行结果
1)打开页面地址:http://localhost:8080/index.html 输入:你好

说明:可以看到它会将自己能做的事情(因为赋予了工具)告诉客户
2)让其处理一个文件,在workspace下有一个Alibaba2024.md文件,让其总结并写入到另外的md文件中

说明 :可以看到它调用了各类工具实现最终结果:
总结:本章介绍了OpenClaw的整体框架,并介绍其核心Agent runtime模块中的pi-mono。然后使用AgentScope实现了类似的功能,那么一个基于AgentScope的第一步就已经完成。下一章我们将模拟OpenClaw的提示词以及各种MD文件。
