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:
digua committed 2026-05-14 23:36:11 +08:00
1 parent 6348c3f4e8
commit 8dd3e2396c
18 files changed
+559 -408

No files matched your search

+2
View File
@@ -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"
+192
View File
@@ -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
}
+19 -1
View File
@@ -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'
+9
View File
@@ -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'