mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
refactor(ai): extract shared Agent core to @openchatlab/node-runtime
- Create runAgentCore() in node-runtime/src/ai/agent/ encapsulating PiAgentCore construction, history conversion, event subscription, abort forwarding, tool round management, and usage collection - Re-export pi-agent-core/pi-ai types (AgentTool, Type, Model, etc.) and functions (completeSimple, streamSimple) from node-runtime - Refactor Server agent.ts to use runAgentCore, removing all direct pi-agent-core/pi-ai imports (~335→~210 lines) - Remove pi-agent-core and pi-ai from Server package.json dependencies - Refactor Electron agent/index.ts and event-handler.ts to use runAgentCore with DI hooks (streamFn, onConvertToLlm, onDebugContext) - Document remaining Electron pi-ai usage points for future migration
This commit is contained in:
1 parent
6348c3f4e8
commit
8dd3e2396c
18 files changed
+559
-408
No files matched your search
@@ -6,6 +6,8 @@
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent-core": "0.54.2",
|
||||
"@mariozechner/pi-ai": "0.54.2",
|
||||
"@openchatlab/core": "workspace:*",
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-tiktoken": "^1.0.21"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Agent Core — 共享的 PiAgentCore 编排逻辑
|
||||
*
|
||||
* 封装:构建 → 历史转换 → 事件订阅 → abort 转发 → prompt 执行 → usage 收集。
|
||||
* Server 和 Electron 通过 AgentCoreOptions DI 注入平台差异。
|
||||
*/
|
||||
|
||||
import { Agent as PiAgentCore } from '@mariozechner/pi-agent-core'
|
||||
import type { AgentEvent as PiAgentEvent } from '@mariozechner/pi-agent-core'
|
||||
import {
|
||||
type Message as PiMessage,
|
||||
type Usage as PiUsage,
|
||||
streamSimple as defaultStreamSimple,
|
||||
} from '@mariozechner/pi-ai'
|
||||
|
||||
import type { AgentCoreOptions, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './types'
|
||||
|
||||
function createEmptyPiUsage(): PiUsage {
|
||||
return {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function toPiHistoryMessages(messages: SimpleHistoryMessage[]): PiMessage[] {
|
||||
return messages.map((msg): PiMessage => {
|
||||
if (msg.role === 'user') {
|
||||
return {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: msg.content || '' }],
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: msg.content || '' }],
|
||||
api: 'openai-completions',
|
||||
provider: 'chatlab',
|
||||
model: 'unknown',
|
||||
usage: createEmptyPiUsage(),
|
||||
stopReason: 'stop',
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function runAgentCore(options: AgentCoreOptions): Promise<AgentCoreResult> {
|
||||
const {
|
||||
piModel,
|
||||
apiKey,
|
||||
systemPrompt,
|
||||
tools,
|
||||
history,
|
||||
userMessage,
|
||||
maxToolRounds = 5,
|
||||
abortSignal,
|
||||
steerMessage = 'Please provide your final answer based on the information gathered.',
|
||||
onEvent,
|
||||
onConvertToLlm,
|
||||
onDebugContext,
|
||||
} = options
|
||||
|
||||
const resolvedStreamFn = (options.streamFn ?? defaultStreamSimple) as typeof defaultStreamSimple
|
||||
|
||||
const totalUsage: AgentTokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
||||
const toolsUsed: string[] = []
|
||||
let toolRounds = 0
|
||||
|
||||
const addPiUsage = (usage?: PiUsage) => {
|
||||
if (!usage) return
|
||||
totalUsage.promptTokens += usage.input || 0
|
||||
totalUsage.completionTokens += usage.output || 0
|
||||
totalUsage.totalTokens += usage.totalTokens || usage.input + usage.output || 0
|
||||
}
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
return { usage: totalUsage, finalMessages: [], toolsUsed: [], toolRounds: 0 }
|
||||
}
|
||||
|
||||
const coreAgent = new PiAgentCore({
|
||||
initialState: {
|
||||
model: piModel,
|
||||
thinkingLevel: piModel.reasoning ? 'medium' : 'off',
|
||||
},
|
||||
getApiKey: () => apiKey,
|
||||
streamFn: resolvedStreamFn,
|
||||
convertToLlm: (messages) => {
|
||||
const filtered = messages.filter(
|
||||
(msg): msg is PiMessage => msg.role === 'user' || msg.role === 'assistant' || msg.role === 'toolResult'
|
||||
)
|
||||
onConvertToLlm?.(filtered)
|
||||
return filtered
|
||||
},
|
||||
})
|
||||
|
||||
coreAgent.setSystemPrompt(systemPrompt)
|
||||
coreAgent.setTools(maxToolRounds > 0 ? tools : [])
|
||||
coreAgent.replaceMessages(toPiHistoryMessages(history))
|
||||
|
||||
let hasReachedToolRoundLimit = false
|
||||
const thinkingStartTime = new Map<number, number>()
|
||||
|
||||
const unsubscribe = coreAgent.subscribe((event: PiAgentEvent) => {
|
||||
if (event.type === 'message_update') {
|
||||
const update = event.assistantMessageEvent
|
||||
if (update.type === 'text_delta') {
|
||||
onEvent({ type: 'content', content: update.delta })
|
||||
} else if (update.type === 'thinking_start') {
|
||||
thinkingStartTime.set(update.contentIndex, Date.now())
|
||||
onEvent({ type: 'thinking_start' })
|
||||
} else if (update.type === 'thinking_delta') {
|
||||
onEvent({ type: 'thinking_delta', content: update.delta })
|
||||
} else if (update.type === 'thinking_end') {
|
||||
const startedAt = thinkingStartTime.get(update.contentIndex)
|
||||
const durationMs = startedAt ? Date.now() - startedAt : undefined
|
||||
thinkingStartTime.delete(update.contentIndex)
|
||||
onEvent({ type: 'thinking_end', durationMs })
|
||||
}
|
||||
} else if (event.type === 'tool_execution_start') {
|
||||
toolsUsed.push(event.toolName)
|
||||
onEvent({
|
||||
type: 'tool_start',
|
||||
toolName: event.toolName,
|
||||
toolParams: (event.args || {}) as Record<string, unknown>,
|
||||
})
|
||||
} else if (event.type === 'tool_execution_end') {
|
||||
onEvent({ type: 'tool_end', toolName: event.toolName, toolResult: event.result })
|
||||
} else if (event.type === 'turn_end') {
|
||||
const hadToolCalls = event.toolResults.length > 0
|
||||
if (hadToolCalls) {
|
||||
toolRounds += 1
|
||||
if (!hasReachedToolRoundLimit && maxToolRounds > 0 && toolRounds >= maxToolRounds) {
|
||||
hasReachedToolRoundLimit = true
|
||||
coreAgent.setTools([])
|
||||
coreAgent.steer({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: steerMessage }],
|
||||
timestamp: Date.now(),
|
||||
} as PiMessage)
|
||||
}
|
||||
}
|
||||
onEvent({ type: 'turn_end', round: toolRounds, hadToolCalls })
|
||||
} else if (event.type === 'message_end') {
|
||||
if (event.message.role === 'assistant') {
|
||||
addPiUsage(event.message.usage)
|
||||
onEvent({ type: 'usage_update', usage: { ...totalUsage } })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const forwardAbort = () => coreAgent.abort()
|
||||
if (abortSignal) {
|
||||
abortSignal.addEventListener('abort', forwardAbort, { once: true })
|
||||
}
|
||||
|
||||
try {
|
||||
if (onDebugContext) {
|
||||
try {
|
||||
const debugMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...history.map((m) => ({
|
||||
role: m.role === 'summary' ? 'assistant' : m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
{ role: 'user', content: userMessage },
|
||||
]
|
||||
onDebugContext(debugMessages)
|
||||
} catch {
|
||||
// silent — debug context is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
await coreAgent.prompt(userMessage)
|
||||
|
||||
return {
|
||||
usage: totalUsage,
|
||||
error: coreAgent.state.error || undefined,
|
||||
finalMessages: [...coreAgent.state.messages],
|
||||
toolsUsed: [...toolsUsed],
|
||||
toolRounds,
|
||||
}
|
||||
} finally {
|
||||
unsubscribe()
|
||||
if (abortSignal) {
|
||||
abortSignal.removeEventListener('abort', forwardAbort)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './types'
|
||||
export { runAgentCore } from './core'
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Agent Core 共享类型
|
||||
*
|
||||
* 定义 runAgentCore 的输入/输出/事件接口,
|
||||
* 供 Server 和 Electron 两端通过 DI 适配。
|
||||
*/
|
||||
|
||||
import type { AgentTool } from '@mariozechner/pi-agent-core'
|
||||
import type { Model, Api, Message } from '@mariozechner/pi-ai'
|
||||
|
||||
export interface AgentTokenUsage {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
export interface SimpleHistoryMessage {
|
||||
role: 'user' | 'assistant' | 'summary'
|
||||
content: string
|
||||
}
|
||||
|
||||
export type AgentCoreEvent =
|
||||
| { type: 'content'; content: string }
|
||||
| { type: 'thinking_start' }
|
||||
| { type: 'thinking_delta'; content: string }
|
||||
| { type: 'thinking_end'; durationMs?: number }
|
||||
| { type: 'tool_start'; toolName: string; toolParams: Record<string, unknown> }
|
||||
| { type: 'tool_end'; toolName: string; toolResult: unknown }
|
||||
| { type: 'turn_end'; round: number; hadToolCalls: boolean }
|
||||
| { type: 'usage_update'; usage: AgentTokenUsage }
|
||||
|
||||
export interface AgentCoreOptions {
|
||||
piModel: Model<Api>
|
||||
apiKey: string
|
||||
systemPrompt: string
|
||||
tools: AgentTool[]
|
||||
history: SimpleHistoryMessage[]
|
||||
userMessage: string
|
||||
maxToolRounds?: number
|
||||
abortSignal?: AbortSignal
|
||||
steerMessage?: string
|
||||
onEvent: (event: AgentCoreEvent) => void
|
||||
/**
|
||||
* 自定义 stream 函数,默认使用 pi-ai 的 streamSimple。
|
||||
* Electron 可传入包装版以捕获 onPayload 用于错误诊断。
|
||||
*/
|
||||
streamFn?: unknown
|
||||
/** 每次 convertToLlm 执行后回调,供 Electron debug 日志使用 */
|
||||
onConvertToLlm?: (filteredMessages: Message[]) => void
|
||||
/** 执行前回调完整的调试上下文(system prompt + history + user message) */
|
||||
onDebugContext?: (messages: Array<{ role: string; content: string }>) => void
|
||||
}
|
||||
|
||||
export interface AgentCoreResult {
|
||||
usage: AgentTokenUsage
|
||||
error?: string
|
||||
finalMessages: Message[]
|
||||
toolsUsed: string[]
|
||||
toolRounds: number
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* AI 模块(Node.js 实现)
|
||||
*
|
||||
* 助手/技能 MD 文件解析器、共享类型、对话管理。
|
||||
* 助手/技能 MD 文件解析器、共享类型、对话管理、Agent Core。
|
||||
*/
|
||||
|
||||
export type { AssistantConfig, AssistantSummary, SkillDef, SkillSummary } from './types'
|
||||
@@ -52,3 +52,21 @@ export {
|
||||
i18nTexts,
|
||||
t,
|
||||
} from './preprocessor'
|
||||
|
||||
// Agent Core
|
||||
export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './agent'
|
||||
export { runAgentCore } from './agent'
|
||||
|
||||
// Re-exports from @mariozechner/pi-agent-core
|
||||
export type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core'
|
||||
|
||||
// Re-exports from @mariozechner/pi-ai
|
||||
export { Type, completeSimple, streamSimple } from '@mariozechner/pi-ai'
|
||||
export type {
|
||||
Model as PiModel,
|
||||
Api as PiApi,
|
||||
Message as PiMessage,
|
||||
Usage as PiUsage,
|
||||
TextContent as PiTextContent,
|
||||
AssistantMessage as PiAssistantMessage,
|
||||
} from '@mariozechner/pi-ai'
|
||||
@@ -76,3 +76,12 @@ export type {
|
||||
TokenUsageData,
|
||||
ConversationManagerLogger,
|
||||
} from './ai'
|
||||
|
||||
// Agent Core
|
||||
export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './ai'
|
||||
export { runAgentCore } from './ai'
|
||||
|
||||
// Re-exports: @mariozechner/pi-agent-core & @mariozechner/pi-ai
|
||||
export type { AgentTool, AgentToolResult } from './ai'
|
||||
export { Type, completeSimple, streamSimple } from './ai'
|
||||
export type { PiModel, PiApi, PiMessage, PiUsage, PiTextContent, PiAssistantMessage } from './ai'
|
||||
Reference in new issue
Block a user