Agent应用实践之三十八 - OpenClaw:基本框架

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> &middot;
        AG-UI Protocol &middot;
        <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文件。