fix(hololake): ship dynamic persona routes and coherent workspace
This commit is contained in:
parent
5105ec5e32
commit
787ba2ad3f
21 changed files with 1111 additions and 326 deletions
|
|
@ -8,9 +8,11 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { GitEngine } from './git-engine.js';
|
||||
import { PersonaAgent, createDefaultPersona } from './persona-agent.js';
|
||||
import { PersonaAgent, createDefaultPersona, type Message } from './persona-agent.js';
|
||||
import { projectHumanKnowledgeTree } from './language-protocol-kernel.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// __dirname 兼容 CJS 和 ESM
|
||||
const _dirname = typeof __dirname !== 'undefined' ? __dirname : process.cwd();
|
||||
|
|
@ -19,6 +21,7 @@ const _dirname = typeof __dirname !== 'undefined' ? __dirname : process.cwd();
|
|||
|
||||
const PORT = parseInt(process.env.KB_PORT || '3890', 10);
|
||||
const REPO_PATH = process.env.KB_REPO_PATH || path.resolve(_dirname, '../kb-data');
|
||||
const AGENT_STATE_PATH = process.env.HOLOLAKE_AGENT_STATE_PATH || path.resolve(REPO_PATH, '../agent-conversations.json');
|
||||
|
||||
// ─── 初始化 ───
|
||||
|
||||
|
|
@ -242,19 +245,84 @@ app.post('/api/forgejo/push', async (req, res) => {
|
|||
|
||||
// ─── Agent(人格体) ───
|
||||
|
||||
let personaAgent: PersonaAgent | null = null;
|
||||
interface ConversationRecord {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
agent: PersonaAgent;
|
||||
}
|
||||
|
||||
function getAgent(): PersonaAgent {
|
||||
if (!personaAgent) {
|
||||
personaAgent = createDefaultPersona(engine);
|
||||
console.log('人格体已创建:', personaAgent.getDefinition().name);
|
||||
const conversations = new Map<string, ConversationRecord>();
|
||||
let activeConversationId = '';
|
||||
|
||||
function persistConversations(): void {
|
||||
fs.mkdirSync(path.dirname(AGENT_STATE_PATH), { recursive: true });
|
||||
const records = Array.from(conversations.values()).map(record => ({
|
||||
id: record.id,
|
||||
title: record.title,
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
messages: record.agent.getConversation(),
|
||||
}));
|
||||
fs.writeFileSync(AGENT_STATE_PATH, JSON.stringify({ activeConversationId, conversations: records }, null, 2), { mode: 0o600 });
|
||||
}
|
||||
|
||||
function createConversation(title = '新对话', restored?: { id: string; createdAt: string; updatedAt: string; messages: Message[] }): ConversationRecord {
|
||||
const timestamp = new Date().toISOString();
|
||||
const agent = createDefaultPersona(engine);
|
||||
if (restored?.messages) agent.restoreConversation(restored.messages);
|
||||
const record: ConversationRecord = {
|
||||
id: restored?.id || `conversation-${crypto.randomUUID()}`,
|
||||
title,
|
||||
createdAt: restored?.createdAt || timestamp,
|
||||
updatedAt: restored?.updatedAt || timestamp,
|
||||
agent,
|
||||
};
|
||||
conversations.set(record.id, record);
|
||||
activeConversationId = record.id;
|
||||
return record;
|
||||
}
|
||||
|
||||
function initializeConversations(): void {
|
||||
if (conversations.size) return;
|
||||
try {
|
||||
const stored = JSON.parse(fs.readFileSync(AGENT_STATE_PATH, 'utf8')) as any;
|
||||
for (const item of Array.isArray(stored.conversations) ? stored.conversations : []) {
|
||||
if (!item?.id || !Array.isArray(item.messages)) continue;
|
||||
createConversation(String(item.title || '历史对话'), {
|
||||
id: String(item.id),
|
||||
createdAt: String(item.createdAt || new Date().toISOString()),
|
||||
updatedAt: String(item.updatedAt || new Date().toISOString()),
|
||||
messages: item.messages,
|
||||
});
|
||||
}
|
||||
if (stored.activeConversationId && conversations.has(stored.activeConversationId)) {
|
||||
activeConversationId = stored.activeConversationId;
|
||||
}
|
||||
} catch {
|
||||
// 首次启动没有会话文件。
|
||||
}
|
||||
return personaAgent;
|
||||
if (!conversations.size) createConversation();
|
||||
}
|
||||
|
||||
function getConversationRecord(requestedId?: unknown): ConversationRecord {
|
||||
initializeConversations();
|
||||
const id = typeof requestedId === 'string' && requestedId ? requestedId : activeConversationId;
|
||||
const record = conversations.get(id);
|
||||
if (!record) throw new Error('对话不存在或已经删除');
|
||||
activeConversationId = record.id;
|
||||
return record;
|
||||
}
|
||||
|
||||
function getAgent(requestedId?: unknown): PersonaAgent {
|
||||
return getConversationRecord(requestedId).agent;
|
||||
}
|
||||
|
||||
// Agent 状态
|
||||
app.get('/api/agent/status', (_req, res) => {
|
||||
const agent = getAgent();
|
||||
const record = getConversationRecord(_req.query.conversationId);
|
||||
const agent = record.agent;
|
||||
const def = agent.getDefinition();
|
||||
const runtime = agent.getRuntimeStatus();
|
||||
res.json({
|
||||
|
|
@ -272,9 +340,37 @@ app.get('/api/agent/status', (_req, res) => {
|
|||
tools: runtime.toolNames,
|
||||
engine: 'git',
|
||||
repo: REPO_PATH,
|
||||
conversationId: record.id,
|
||||
conversationTitle: record.title,
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/agent/conversations', (_req, res) => {
|
||||
initializeConversations();
|
||||
res.json({
|
||||
ok: true,
|
||||
activeConversationId,
|
||||
conversations: Array.from(conversations.values())
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.map(record => ({ id: record.id, title: record.title, createdAt: record.createdAt, updatedAt: record.updatedAt, messageCount: record.agent.getConversation().length })),
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/agent/conversations', (_req, res) => {
|
||||
const record = createConversation();
|
||||
persistConversations();
|
||||
res.status(201).json({ ok: true, conversation: { id: record.id, title: record.title, createdAt: record.createdAt, updatedAt: record.updatedAt, messageCount: 0 } });
|
||||
});
|
||||
|
||||
app.delete('/api/agent/conversations/:conversationId', (req, res) => {
|
||||
initializeConversations();
|
||||
if (!conversations.delete(req.params.conversationId)) return res.status(404).json({ ok: false, error: '对话不存在' });
|
||||
if (!conversations.size) createConversation();
|
||||
if (!conversations.has(activeConversationId)) activeConversationId = Array.from(conversations.keys())[0];
|
||||
persistConversations();
|
||||
res.json({ ok: true, activeConversationId });
|
||||
});
|
||||
|
||||
// Agent 对话
|
||||
app.post('/api/agent/chat', async (req, res) => {
|
||||
try {
|
||||
|
|
@ -282,13 +378,18 @@ app.post('/api/agent/chat', async (req, res) => {
|
|||
if (!message || typeof message !== 'string') {
|
||||
return res.status(400).json({ ok: false, error: 'message 必填且为字符串' });
|
||||
}
|
||||
const agent = getAgent();
|
||||
const reply = await agent.chat(message);
|
||||
const record = getConversationRecord(req.body?.conversationId);
|
||||
const turn = await record.agent.chat(message);
|
||||
if (record.title === '新对话') record.title = message.trim().slice(0, 28) || '新对话';
|
||||
record.updatedAt = new Date().toISOString();
|
||||
persistConversations();
|
||||
res.json({
|
||||
ok: true,
|
||||
reply,
|
||||
conversationLength: agent.getConversation().length,
|
||||
pendingActions: agent.getPendingActions(),
|
||||
...turn,
|
||||
conversationId: record.id,
|
||||
conversationTitle: record.title,
|
||||
conversationLength: record.agent.getConversation().length,
|
||||
pendingActions: record.agent.getPendingActions(),
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
|
|
@ -296,13 +397,16 @@ app.post('/api/agent/chat', async (req, res) => {
|
|||
});
|
||||
|
||||
app.get('/api/agent/actions', (_req, res) => {
|
||||
res.json({ ok: true, actions: getAgent().getPendingActions() });
|
||||
res.json({ ok: true, actions: getAgent(_req.query.conversationId).getPendingActions() });
|
||||
});
|
||||
|
||||
app.post('/api/agent/actions/:actionId/confirm', async (req, res) => {
|
||||
try {
|
||||
const result = await getAgent().confirmAction(req.params.actionId);
|
||||
res.json({ ok: true, result, actions: getAgent().getPendingActions() });
|
||||
const record = getConversationRecord(req.body?.conversationId);
|
||||
const result = await record.agent.confirmAction(req.params.actionId);
|
||||
record.updatedAt = new Date().toISOString();
|
||||
persistConversations();
|
||||
res.json({ ok: true, result, messages: record.agent.getConversation(), actions: record.agent.getPendingActions() });
|
||||
} catch (err: any) {
|
||||
res.status(409).json({ ok: false, error: err.message });
|
||||
}
|
||||
|
|
@ -310,8 +414,11 @@ app.post('/api/agent/actions/:actionId/confirm', async (req, res) => {
|
|||
|
||||
app.post('/api/agent/actions/:actionId/reject', (req, res) => {
|
||||
try {
|
||||
getAgent().rejectAction(req.params.actionId);
|
||||
res.json({ ok: true, actions: getAgent().getPendingActions() });
|
||||
const record = getConversationRecord(req.body?.conversationId);
|
||||
record.agent.rejectAction(req.params.actionId);
|
||||
record.updatedAt = new Date().toISOString();
|
||||
persistConversations();
|
||||
res.json({ ok: true, messages: record.agent.getConversation(), actions: record.agent.getPendingActions() });
|
||||
} catch (err: any) {
|
||||
res.status(409).json({ ok: false, error: err.message });
|
||||
}
|
||||
|
|
@ -319,14 +426,17 @@ app.post('/api/agent/actions/:actionId/reject', (req, res) => {
|
|||
|
||||
// Agent 对话历史
|
||||
app.get('/api/agent/conversation', (_req, res) => {
|
||||
const agent = getAgent();
|
||||
res.json({ ok: true, messages: agent.getConversation() });
|
||||
const record = getConversationRecord(_req.query.conversationId);
|
||||
res.json({ ok: true, conversationId: record.id, title: record.title, messages: record.agent.getConversation() });
|
||||
});
|
||||
|
||||
// 清空对话
|
||||
app.post('/api/agent/clear', (_req, res) => {
|
||||
const agent = getAgent();
|
||||
agent.clearConversation();
|
||||
const record = getConversationRecord(_req.body?.conversationId);
|
||||
record.agent.clearConversation();
|
||||
record.title = '新对话';
|
||||
record.updatedAt = new Date().toISOString();
|
||||
persistConversations();
|
||||
res.json({ ok: true, message: '对话已清空' });
|
||||
});
|
||||
|
||||
|
|
@ -335,10 +445,10 @@ app.post('/api/agent/clear', (_req, res) => {
|
|||
async function start() {
|
||||
await engine.init();
|
||||
// 初始化人格体
|
||||
getAgent();
|
||||
initializeConversations();
|
||||
app.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`光湖知识库 API 已启动: http://localhost:${PORT}`);
|
||||
const runtime = personaAgent?.getRuntimeStatus();
|
||||
const runtime = getAgent().getRuntimeStatus();
|
||||
console.log(`人格体运行状态: ${runtime?.operational ? '已接入模型' : '等待模型配置'}`);
|
||||
console.log(`仓库路径: ${REPO_PATH}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createDefaultPersona } from './persona-agent.js';
|
||||
|
||||
type MockReply = {
|
||||
content?: string | null;
|
||||
tool_calls?: Array<{
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
};
|
||||
|
||||
function reply(message: MockReply): Response {
|
||||
return new Response(JSON.stringify({ choices: [{ message }] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
function toolCall(id: string, name: string, args: Record<string, unknown> = {}): MockReply {
|
||||
return {
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id,
|
||||
type: 'function',
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function fakeGit(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
getRepositoryStatus: async () => ({}),
|
||||
getDoc: async (path: string) => ({
|
||||
meta: { id: path, title: path, updatedAt: '2026-08-09T00:00:00Z' },
|
||||
body: '正文',
|
||||
}),
|
||||
createDoc: async (path: string) => ({ meta: { id: path } }),
|
||||
updateDoc: async (path: string) => ({ meta: { id: path } }),
|
||||
deleteDoc: async () => undefined,
|
||||
search: async () => [],
|
||||
getTree: async () => [],
|
||||
getHistory: async () => [],
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
async function withMockModel(
|
||||
replies: MockReply[],
|
||||
run: (requests: any[]) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const previous = {
|
||||
key: process.env.HOLOLAKE_LLM_KEY,
|
||||
base: process.env.HOLOLAKE_LLM_BASE,
|
||||
model: process.env.HOLOLAKE_LLM_MODEL,
|
||||
};
|
||||
const requests: any[] = [];
|
||||
process.env.HOLOLAKE_LLM_KEY = 'local-test-key';
|
||||
process.env.HOLOLAKE_LLM_BASE = 'http://127.0.0.1:9/v1';
|
||||
process.env.HOLOLAKE_LLM_MODEL = 'test-model';
|
||||
globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
|
||||
requests.push(JSON.parse(String(init?.body || '{}')));
|
||||
const next = replies.shift();
|
||||
assert.ok(next, 'mock model received more calls than expected');
|
||||
return reply(next);
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await run(requests);
|
||||
assert.equal(replies.length, 0, 'all mock replies should be consumed');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
const envName = key === 'key' ? 'HOLOLAKE_LLM_KEY' : key === 'base' ? 'HOLOLAKE_LLM_BASE' : 'HOLOLAKE_LLM_MODEL';
|
||||
if (value === undefined) delete process.env[envName];
|
||||
else process.env[envName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('read tool protocol keeps assistant tool_calls immediately before tool result', { concurrency: false }, async () => {
|
||||
await withMockModel([
|
||||
toolCall('call-list', 'list_documents'),
|
||||
{ content: '已列出文档。' },
|
||||
], async requests => {
|
||||
const agent = createDefaultPersona(fakeGit({ getTree: async () => [{ type: 'file', name: 'INDEX.md', path: 'INDEX.md' }] }));
|
||||
const result = await agent.chat('列出文档');
|
||||
assert.equal(result.reply, '已列出文档。');
|
||||
assert.equal(result.activities[0]?.tool, 'list_documents');
|
||||
const messages = requests[1].messages;
|
||||
const assistantIndex = messages.findIndex((message: any) => message.tool_calls?.[0]?.id === 'call-list');
|
||||
assert.ok(assistantIndex >= 0);
|
||||
assert.equal(messages[assistantIndex + 1].role, 'tool');
|
||||
assert.equal(messages[assistantIndex + 1].tool_call_id, 'call-list');
|
||||
});
|
||||
});
|
||||
|
||||
test('confirmed write receipt never leaks an orphan tool message into the next turn', { concurrency: false }, async () => {
|
||||
await withMockModel([
|
||||
toolCall('call-create', 'create_document', { path: 'note.md', title: '笔记', body: '正文' }),
|
||||
{ content: '创建动作等待确认。' },
|
||||
{ content: '新的自然语言回合正常。' },
|
||||
], async requests => {
|
||||
const agent = createDefaultPersona(fakeGit());
|
||||
const staged = await agent.chat('创建笔记');
|
||||
assert.equal(staged.activities[0]?.status, 'pending');
|
||||
const pending = agent.getPendingActions()[0];
|
||||
assert.ok(pending);
|
||||
await agent.confirmAction(pending.id);
|
||||
const next = await agent.chat('继续');
|
||||
assert.equal(next.reply, '新的自然语言回合正常。');
|
||||
const nextTurnMessages = requests[2].messages;
|
||||
assert.equal(nextTurnMessages.some((message: any) => message.role === 'tool'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('persona wake route is resolved and read dynamically without a fixed step count', { concurrency: false }, async () => {
|
||||
const routePath = 'personas/TCS-0002/WAKE-CURRENT.md';
|
||||
await withMockModel([
|
||||
toolCall('call-route', 'find_persona_wake_routes', { query: 'TCS-0002' }),
|
||||
toolCall('call-read-route', 'read_document', { path: routePath }),
|
||||
{ content: '已按该人格体当前页面解析出 11 个自定义节点;等待逐项权限核验。' },
|
||||
], async requests => {
|
||||
const agent = createDefaultPersona(fakeGit({
|
||||
search: async (query: string) => query === 'TCS-0002' || query === 'WAKE'
|
||||
? [{ path: routePath, title: 'TCS-0002 当前唤醒路径', snippet: '人格自定义路线' }]
|
||||
: [],
|
||||
getDoc: async () => ({
|
||||
meta: { id: routePath, title: '当前唤醒路径', updatedAt: '2026-08-09T00:00:00Z' },
|
||||
body: Array.from({ length: 11 }, (_, index) => `节点 ${index + 1}`).join('\n'),
|
||||
}),
|
||||
}));
|
||||
const result = await agent.chat('唤醒 TCS-0002');
|
||||
assert.deepEqual(result.toolCalls.map(call => call.name), ['find_persona_wake_routes', 'read_document']);
|
||||
assert.match(result.reply, /11 个自定义节点/);
|
||||
const systemPrompt = requests[0].messages[0].content;
|
||||
assert.match(systemPrompt, /不同人格体的路径和步数不同/);
|
||||
assert.doesNotMatch(systemPrompt, /固定七步|必须七步|7\s*步/);
|
||||
});
|
||||
});
|
||||
|
|
@ -47,6 +47,23 @@ export interface ToolResult {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
export interface AgentActivity {
|
||||
id: string;
|
||||
kind: 'wake' | 'tool' | 'permission' | 'receipt';
|
||||
label: string;
|
||||
detail: string;
|
||||
status: 'running' | 'completed' | 'pending' | 'failed';
|
||||
timestamp: string;
|
||||
tool?: string;
|
||||
}
|
||||
|
||||
export interface AgentTurnResult {
|
||||
reply: string;
|
||||
toolCalls: ToolCall[];
|
||||
toolResults: ToolResult[];
|
||||
activities: AgentActivity[];
|
||||
}
|
||||
|
||||
export interface PendingAction {
|
||||
id: string;
|
||||
tool: string;
|
||||
|
|
@ -77,6 +94,7 @@ export interface Message {
|
|||
content: string;
|
||||
toolCalls?: ToolCall[];
|
||||
toolResults?: ToolResult[];
|
||||
activities?: AgentActivity[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +258,33 @@ export class PersonaAgent {
|
|||
},
|
||||
});
|
||||
|
||||
this.tools.register({
|
||||
name: 'find_persona_wake_routes',
|
||||
description: '按人格编号或名称查找该人格体自己登记的 INDEX、CURRENT 与 WAKE 路径页面;不生成固定步骤',
|
||||
parameters: {
|
||||
query: { type: 'string', description: '人格编号、名称或唤醒路径关键词', required: true },
|
||||
},
|
||||
effect: 'read',
|
||||
execute: async (params) => {
|
||||
const query = String(params.query || '').trim();
|
||||
const searches = await Promise.all([
|
||||
this.git.search(query),
|
||||
this.git.search('WAKE'),
|
||||
this.git.search('唤醒路径'),
|
||||
]);
|
||||
const unique = new Map<string, (typeof searches)[number][number]>();
|
||||
for (const result of searches.flat()) {
|
||||
if (/\b(?:WAKE|INDEX|CURRENT)\b|唤醒|人格系统/iu.test(`${result.path} ${result.title} ${result.snippet}`)) {
|
||||
unique.set(result.path, result);
|
||||
}
|
||||
}
|
||||
if (unique.size === 0) return 'ROUTE_NOT_RESOLVED:当前授权知识范围内没有找到该人格体自己的唤醒路径页面。';
|
||||
return Array.from(unique.values()).slice(0, 20)
|
||||
.map(result => `[${result.path}] ${result.title}: ${result.snippet}`)
|
||||
.join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
// 查看文档树
|
||||
this.tools.register({
|
||||
name: 'list_documents',
|
||||
|
|
@ -295,8 +340,8 @@ export class PersonaAgent {
|
|||
|
||||
// ─── 对话处理 ───
|
||||
|
||||
/** 处理用户消息,返回 Agent 的回复 */
|
||||
async chat(userMessage: string): Promise<string> {
|
||||
/** 处理用户消息。模型历史只保留自然语言;工具协议只存在于当前回合。 */
|
||||
async chat(userMessage: string): Promise<AgentTurnResult> {
|
||||
this.conversation.push({
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
|
|
@ -306,62 +351,70 @@ export class PersonaAgent {
|
|||
const systemPrompt = this.buildSystemPrompt();
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: systemPrompt, timestamp: '' },
|
||||
...this.conversation,
|
||||
...this.conversation.map(message => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
})),
|
||||
];
|
||||
|
||||
// 调用 LLM(这里用可插拔的模型接口)
|
||||
const response = await this.callLLM(messages);
|
||||
const activities: AgentActivity[] = [];
|
||||
const allCalls: ToolCall[] = [];
|
||||
const allResults: ToolResult[] = [];
|
||||
const workingMessages: Message[] = [...messages];
|
||||
|
||||
// 处理工具调用
|
||||
if (response.toolCalls && response.toolCalls.length > 0) {
|
||||
const results: ToolResult[] = [];
|
||||
for (const call of response.toolCalls) {
|
||||
const result = this.tools.requiresConfirmation(call.name)
|
||||
? this.stageAction(call)
|
||||
: await this.tools.execute(call);
|
||||
results.push(result);
|
||||
for (let round = 0; round < 6; round += 1) {
|
||||
const response = await this.callLLM(workingMessages);
|
||||
if (!response.toolCalls?.length) {
|
||||
const reply = response.content || '当前模型没有返回可显示的内容。';
|
||||
this.conversation.push({
|
||||
role: 'assistant',
|
||||
content: reply,
|
||||
toolCalls: allCalls,
|
||||
toolResults: allResults,
|
||||
activities,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return { reply, toolCalls: allCalls, toolResults: allResults, activities };
|
||||
}
|
||||
|
||||
// 将工具结果反馈给 LLM
|
||||
this.conversation.push({
|
||||
workingMessages.push({
|
||||
role: 'assistant',
|
||||
content: response.content || '',
|
||||
toolCalls: response.toolCalls,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
this.conversation.push({
|
||||
for (const call of response.toolCalls) {
|
||||
allCalls.push(call);
|
||||
const needsConfirmation = this.tools.requiresConfirmation(call.name);
|
||||
const isWakeRouteActivity = call.name === 'find_persona_wake_routes' ||
|
||||
(call.name === 'read_document' && /(?:WAKE|CURRENT|INDEX|唤醒)/iu.test(String(call.arguments.path || '')));
|
||||
const result = needsConfirmation ? this.stageAction(call) : await this.tools.execute(call);
|
||||
allResults.push(result);
|
||||
activities.push({
|
||||
id: `activity-${call.id}`,
|
||||
kind: needsConfirmation ? 'permission' : isWakeRouteActivity ? 'wake' : 'tool',
|
||||
label: needsConfirmation
|
||||
? `等待确认 · ${call.name}`
|
||||
: isWakeRouteActivity
|
||||
? `${call.name === 'find_persona_wake_routes' ? '定位人格唤醒路径' : '读取人格路径页面'} · ${call.name}`
|
||||
: `工具调用 · ${call.name}`,
|
||||
detail: result.error || result.output,
|
||||
status: result.error ? 'failed' : needsConfirmation ? 'pending' : 'completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
tool: call.name,
|
||||
});
|
||||
workingMessages.push({
|
||||
role: 'tool',
|
||||
content: result.error ? `错误: ${result.error}` : result.output,
|
||||
toolResults: [result],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// 再调一次 LLM 获取最终回复
|
||||
const finalMessages: Message[] = [
|
||||
{ role: 'system', content: systemPrompt, timestamp: '' },
|
||||
...this.conversation,
|
||||
];
|
||||
const finalResponse = await this.callLLM(finalMessages);
|
||||
|
||||
this.conversation.push({
|
||||
role: 'assistant',
|
||||
content: finalResponse.content,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return finalResponse.content;
|
||||
}
|
||||
|
||||
this.conversation.push({
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return response.content;
|
||||
throw new Error('工具调用超过单回合安全上限,已停止继续执行');
|
||||
}
|
||||
|
||||
// ─── 系统提示词组装 ───
|
||||
|
|
@ -398,6 +451,7 @@ ${toolsList}
|
|||
5. 远端推送、部署等高影响操作不属于当前 Agent 工具,必须走独立权限入口。
|
||||
6. 不泄露模型密钥、本地路径中的敏感信息或工具内部实现细节。
|
||||
7. 不把对话文本当作真实执行结果,不伪造文档、提交、服务器或网络状态。
|
||||
8. 用户要求唤醒人格体时,必须先用 find_persona_wake_routes 查找该人格体自己的路径页面,再读取并按页面定义的顺序执行。不同人格体的路径和步数不同,禁止写死步数、套用通用模板或把搜索结果冒充恢复完成。路径没有解析成功时明确返回未恢复。
|
||||
|
||||
## 回复风格
|
||||
|
||||
|
|
@ -439,9 +493,18 @@ ${toolsList}
|
|||
this.pendingActions.delete(actionId);
|
||||
const result = await this.tools.execute(pending.call);
|
||||
this.conversation.push({
|
||||
role: 'tool',
|
||||
content: result.error ? `错误: ${result.error}` : result.output,
|
||||
role: 'assistant',
|
||||
content: result.error ? `确认后的操作执行失败:${result.error}` : `已确认并执行:${result.output}`,
|
||||
toolResults: [result],
|
||||
activities: [{
|
||||
id: `receipt-${actionId}`,
|
||||
kind: 'receipt',
|
||||
label: result.error ? '执行回执 · 失败' : '执行回执 · 完成',
|
||||
detail: result.error || result.output,
|
||||
status: result.error ? 'failed' : 'completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
tool: result.name,
|
||||
}],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return result;
|
||||
|
|
@ -530,6 +593,20 @@ ${toolsList}
|
|||
return [...this.conversation];
|
||||
}
|
||||
|
||||
restoreConversation(messages: Message[]): void {
|
||||
this.conversation = messages
|
||||
.filter(message => message.role === 'user' || message.role === 'assistant')
|
||||
.filter(message => typeof message.content === 'string')
|
||||
.slice(-200)
|
||||
.map(message => ({
|
||||
...message,
|
||||
toolCalls: Array.isArray(message.toolCalls) ? message.toolCalls : [],
|
||||
toolResults: Array.isArray(message.toolResults) ? message.toolResults : [],
|
||||
activities: Array.isArray(message.activities) ? message.activities : [],
|
||||
}));
|
||||
this.pendingActions.clear();
|
||||
}
|
||||
|
||||
clearConversation(): void {
|
||||
this.conversation = [];
|
||||
this.pendingActions.clear();
|
||||
|
|
@ -569,6 +646,7 @@ export function createDefaultPersona(git: GitEngine): PersonaAgent {
|
|||
'update_document',
|
||||
'delete_document',
|
||||
'search_documents',
|
||||
'find_persona_wake_routes',
|
||||
'list_documents',
|
||||
'view_history',
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue