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
|
|
@ -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*步/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue