Skip to content

Commit d942a63

Browse files
authored
🔨 chore: add agent-runtime (#9206)
* add agent runtime * add agent runtime * support finish reason * update workflow * 支持中断 * 支持 token usage 统计 * refactor * add example * add docs * update
1 parent a47ec04 commit d942a63

File tree

16 files changed

+2544
-2
lines changed

16 files changed

+2544
-2
lines changed

.cursor/rules/debug-usage.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
description: 包含添加 debug 日志请求时
3-
globs:
2+
description: 包含添加 console.log 日志请求时
3+
globs:
44
alwaysApply: false
55
---
66
# Debug 包使用指南

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
- electron-server-ipc
2020
- utils
2121
- context-engine
22+
- agent-runtime
2223

2324
name: Test package ${{ matrix.package }}
2425

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"@icons-pack/react-simple-icons": "9.6.0",
144144
"@khmyznikov/pwa-install": "0.3.9",
145145
"@langchain/community": "^0.3.55",
146+
"@lobechat/agent-runtime": "workspace:*",
146147
"@lobechat/const": "workspace:*",
147148
"@lobechat/context-engine": "workspace:*",
148149
"@lobechat/database": "workspace:*",
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// @ts-nocheck
2+
import OpenAI from 'openai';
3+
4+
import { AgentRuntime } from '../src';
5+
import type { Agent, AgentState, RuntimeContext } from '../src';
6+
7+
// OpenAI 模型运行时
8+
async function* openaiRuntime(payload: any) {
9+
const openai = new OpenAI({
10+
apiKey: process.env.OPENAI_API_KEY || '',
11+
});
12+
13+
const { messages, tools } = payload;
14+
15+
const stream = await openai.chat.completions.create({
16+
messages,
17+
model: 'gpt-4.1-mini',
18+
stream: true,
19+
tools,
20+
});
21+
22+
let content = '';
23+
let toolCalls: any[] = [];
24+
25+
for await (const chunk of stream) {
26+
const delta = chunk.choices[0]?.delta;
27+
28+
if (delta?.content) {
29+
content += delta.content;
30+
yield { content: delta.content };
31+
}
32+
33+
if (delta?.tool_calls) {
34+
for (const toolCall of delta.tool_calls) {
35+
if (!toolCalls[toolCall.index]) {
36+
toolCalls[toolCall.index] = {
37+
function: { arguments: '', name: '' },
38+
id: toolCall.id,
39+
type: 'function',
40+
};
41+
}
42+
if (toolCall.function?.name) {
43+
toolCalls[toolCall.index].function.name += toolCall.function.name;
44+
}
45+
if (toolCall.function?.arguments) {
46+
toolCalls[toolCall.index].function.arguments += toolCall.function.arguments;
47+
}
48+
}
49+
}
50+
}
51+
52+
if (toolCalls.length > 0) {
53+
yield { tool_calls: toolCalls.filter(Boolean) };
54+
}
55+
}
56+
57+
// 简单的 Agent 实现
58+
class SimpleAgent implements Agent {
59+
private conversationState: 'waiting_user' | 'processing_llm' | 'executing_tools' | 'done' =
60+
'waiting_user';
61+
private pendingToolCalls: any[] = [];
62+
63+
// Agent 拥有自己的模型运行时
64+
modelRuntime = openaiRuntime;
65+
66+
// 定义可用工具
67+
tools = {
68+
calculate: async ({ expression }: { expression: string }) => {
69+
try {
70+
// 注意:实际应用中应使用安全的数学解析器
71+
const result = new Function(`"use strict"; return (${expression})`)();
72+
return { expression, result };
73+
} catch {
74+
return { error: 'Invalid expression', expression };
75+
}
76+
},
77+
78+
get_time: async () => {
79+
return {
80+
current_time: new Date().toISOString(),
81+
formatted_time: new Date().toLocaleString(),
82+
};
83+
},
84+
};
85+
86+
// 获取工具定义
87+
private getToolDefinitions() {
88+
return [
89+
{
90+
function: {
91+
description: 'Get current date and time',
92+
name: 'get_time',
93+
parameters: { properties: {}, type: 'object' },
94+
},
95+
type: 'function' as const,
96+
},
97+
{
98+
function: {
99+
description: 'Calculate mathematical expressions',
100+
name: 'calculate',
101+
parameters: {
102+
properties: {
103+
expression: { description: 'Math expression', type: 'string' },
104+
},
105+
required: ['expression'],
106+
type: 'object',
107+
},
108+
},
109+
type: 'function' as const,
110+
},
111+
];
112+
}
113+
114+
// Agent 决策逻辑 - 基于执行阶段和上下文
115+
async runner(context: RuntimeContext, state: AgentState) {
116+
console.log(`[${context.phase}] 对话状态: ${this.conversationState}`);
117+
118+
switch (context.phase) {
119+
case 'init': {
120+
// 初始化阶段
121+
this.conversationState = 'waiting_user';
122+
return { reason: 'No action needed', type: 'finish' as const };
123+
}
124+
125+
case 'user_input': {
126+
// 用户输入阶段
127+
const userPayload = context.payload as { isFirstMessage: boolean; message: any };
128+
console.log(`👤 用户消息: ${userPayload.message.content}`);
129+
130+
// 只有在等待用户输入状态时才处理
131+
if (this.conversationState === 'waiting_user') {
132+
this.conversationState = 'processing_llm';
133+
return {
134+
payload: {
135+
messages: state.messages,
136+
tools: this.getToolDefinitions(),
137+
},
138+
type: 'call_llm' as const,
139+
};
140+
}
141+
142+
// 其他状态下不处理用户输入,结束对话
143+
console.log(`⚠️ 忽略用户输入,当前状态: ${this.conversationState}`);
144+
return {
145+
reason: `Not in waiting_user state: ${this.conversationState}`,
146+
type: 'finish' as const,
147+
};
148+
}
149+
150+
case 'llm_result': {
151+
// LLM 结果阶段,检查是否需要工具调用
152+
const llmPayload = context.payload as { hasToolCalls: boolean; result: any };
153+
154+
// 手动添加 assistant 消息到状态中(修复 Runtime 的问题)
155+
const assistantMessage: any = {
156+
content: llmPayload.result.content || null,
157+
role: 'assistant',
158+
};
159+
160+
if (llmPayload.hasToolCalls) {
161+
const toolCalls = llmPayload.result.tool_calls;
162+
assistantMessage.tool_calls = toolCalls;
163+
this.pendingToolCalls = toolCalls;
164+
this.conversationState = 'executing_tools';
165+
166+
console.log(
167+
'🔧 需要执行工具:',
168+
toolCalls.map((call: any) => call.function.name),
169+
);
170+
171+
// 添加包含 tool_calls 的 assistant 消息
172+
state.messages.push(assistantMessage);
173+
174+
// 执行第一个工具调用
175+
return {
176+
toolCall: toolCalls[0],
177+
type: 'call_tool' as const,
178+
};
179+
}
180+
181+
// 没有工具调用,添加普通 assistant 消息
182+
state.messages.push(assistantMessage);
183+
this.conversationState = 'done';
184+
return { reason: 'LLM response completed', type: 'finish' as const };
185+
}
186+
187+
case 'tool_result': {
188+
// 工具执行结果阶段
189+
const toolPayload = context.payload as { result: any; toolMessage: any };
190+
console.log(`🛠️ 工具执行完成: ${JSON.stringify(toolPayload.result)}`);
191+
192+
// 移除已执行的工具
193+
this.pendingToolCalls = this.pendingToolCalls.slice(1);
194+
195+
// 如果还有未执行的工具,继续执行
196+
if (this.pendingToolCalls.length > 0) {
197+
return {
198+
toolCall: this.pendingToolCalls[0],
199+
type: 'call_tool' as const,
200+
};
201+
}
202+
203+
// 所有工具执行完成,调用 LLM 处理结果
204+
this.conversationState = 'processing_llm';
205+
return {
206+
payload: {
207+
messages: state.messages,
208+
tools: this.getToolDefinitions(),
209+
},
210+
type: 'call_llm' as const,
211+
};
212+
}
213+
214+
case 'human_response': {
215+
// 人机交互响应阶段(简化示例中不使用)
216+
return { reason: 'Human interaction not supported', type: 'finish' as const };
217+
}
218+
219+
case 'error': {
220+
// 错误阶段
221+
const errorPayload = context.payload as { error: any };
222+
console.error('❌ 错误状态:', errorPayload.error);
223+
return { reason: 'Error occurred', type: 'finish' as const };
224+
}
225+
226+
default: {
227+
return { reason: 'Unknown phase', type: 'finish' as const };
228+
}
229+
}
230+
}
231+
}
232+
233+
// 主函数
234+
async function main() {
235+
console.log('🚀 简单的 OpenAI Tools Agent 示例\n');
236+
237+
if (!process.env.OPENAI_API_KEY) {
238+
console.error('❌ 请设置 OPENAI_API_KEY 环境变量');
239+
return;
240+
}
241+
242+
// 创建 Agent 和 Runtime
243+
const agent = new SimpleAgent();
244+
const runtime = new AgentRuntime(agent); // modelRuntime 现在在 Agent 中
245+
246+
// 测试消息
247+
const testMessage = process.argv[2] || 'What time is it? Also calculate 15 * 8 + 7';
248+
console.log(`💬 用户: ${testMessage}\n`);
249+
250+
// 创建初始状态
251+
let state = AgentRuntime.createInitialState({
252+
maxSteps: 10,
253+
messages: [{ content: testMessage, role: 'user' }],
254+
sessionId: 'simple-test',
255+
});
256+
257+
console.log('🤖 AI: ');
258+
259+
// 执行对话循环
260+
let nextContext: RuntimeContext | undefined = undefined;
261+
262+
while (state.status !== 'done' && state.status !== 'error') {
263+
const result = await runtime.step(state, nextContext);
264+
265+
// 处理事件
266+
for (const event of result.events) {
267+
switch (event.type) {
268+
case 'llm_stream': {
269+
if ((event as any).chunk.content) {
270+
process.stdout.write((event as any).chunk.content);
271+
}
272+
break;
273+
}
274+
case 'llm_result': {
275+
if ((event as any).result.tool_calls) {
276+
console.log('\n\n🔧 需要调用工具...');
277+
}
278+
break;
279+
}
280+
case 'tool_result': {
281+
console.log(`\n🛠️ 工具执行结果:`, event.result);
282+
console.log('\n🤖 AI: ');
283+
break;
284+
}
285+
case 'done': {
286+
console.log('\n\n✅ 对话完成');
287+
break;
288+
}
289+
case 'error': {
290+
console.error('\n❌ 错误:', event.error);
291+
break;
292+
}
293+
}
294+
}
295+
296+
state = result.newState;
297+
nextContext = result.nextContext; // 使用返回的 nextContext
298+
}
299+
300+
console.log(`\n📊 总共执行了 ${state.stepCount} 个步骤`);
301+
}
302+
303+
main().catch(console.error);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "@lobechat/agent-runtime",
3+
"version": "1.0.0",
4+
"private": true,
5+
"main": "./src/index.ts",
6+
"scripts": {
7+
"simple": "tsx examples/tools-calling.ts",
8+
"test": "vitest",
9+
"test:coverage": "vitest --coverage"
10+
},
11+
"dependencies": {},
12+
"devDependencies": {
13+
"openai": "^4.0.0"
14+
}
15+
}

0 commit comments

Comments
 (0)