refactor(agent): replace OMP with Claude Code kernel

This commit is contained in:
xiaohan.yuan committed 2026-10-20 14:47:56 +08:00
1 parent 0fb2226f8c
commit 9a1f182397
232 files changed
+2561 -25564

No files matched your search

+81 -95
View File
@@ -13,9 +13,7 @@ interface PersistenceTestState {
promptCalls: number
abortCalls: number
cancelOutputEmitted: boolean
useOmp: boolean
fallbackHistory: ChatMessage[] | null
fallbackWireHistory?: Array<{ role: string; content: string }>
resumeAts: Array<string | undefined>
}
const TEST_STATE_KEY = '__YCODE_CHAT_PERSISTENCE_TEST__'
@@ -73,8 +71,7 @@ async function main(): Promise<void> {
promptCalls: 0,
abortCalls: 0,
cancelOutputEmitted: false,
useOmp: true,
fallbackHistory: null
resumeAts: []
}
await mkdir(state.userData, { recursive: true })
@@ -134,74 +131,74 @@ async function main(): Promise<void> {
export class WebContentsView {}
`
}))
pluginBuild.onResolve({ filter: /^\.\/omp$/ }, () => ({ path: 'omp', namespace: 'mock-omp' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-omp' }, () => ({
pluginBuild.onResolve({ filter: /^\.\/agent\/claude\/claude-kernel$/ }, () => ({ path: 'claude-kernel', namespace: 'mock-claude-kernel' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-claude-kernel' }, () => ({
loader: 'js',
contents: `
import { EventEmitter } from 'node:events';
const state = globalThis.${TEST_STATE_KEY};
const toolId = 'persisted-tool';
const fullOutput = '${'long-output-'.repeat(60)}PERSISTED-OUTPUT-SENTINEL';
const emit = (kernel, frame) => kernel.emit('event', frame);
export class OmpKernel extends EventEmitter {
constructor() { super(); }
setThinkingLevel() { return Promise.resolve(); }
setHostTools() { return Promise.resolve({ type: 'response', success: true, data: { toolNames: [] } }); }
send() { return Promise.resolve({ type: 'response', success: true, data: { messages: [] } }); }
prompt(message) {
const kernels = new Map();
class MockClaudeKernel {
constructor(options) { this.options = options; this.busy = false; this.active = null; }
async ensureStarted() {}
async setPolicy() {}
async runTurn(request) {
state.promptCalls++;
if (message.split('\\n', 1)[0] === 'persist-cancel') {
setImmediate(() => {
emit(this, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'cancel-before' } });
emit(this, { type: 'tool_execution_start', toolCall: { id: toolId, name: 'shell', args: { command: 'long-running' } } });
emit(this, { type: 'tool_execution_update', toolCall: { id: toolId, partialResult: 'cancelled tool output retained' } });
state.cancelOutputEmitted = true;
});
} else {
setImmediate(() => {
emit(this, { type: 'message_end', message: { role: 'assistant', usage: { input: 1200, output: 10, totalTokens: 1210 } } });
emit(this, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'before' } });
emit(this, { type: 'tool_execution_start', toolCall: { id: toolId, name: 'read_file', args: { path: 'large.txt' } } });
emit(this, { type: 'tool_execution_update', toolCall: { id: toolId, partialResult: fullOutput } });
emit(this, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'after' } });
emit(this, { type: 'tool_execution_end', toolCall: { id: toolId, name: 'read_file', result: { content: [{ type: 'text', text: fullOutput }], isError: false }, success: true } });
emit(this, { type: 'message_end', message: { role: 'assistant', usage: { contextTokens: 12800, input: 12000, cacheRead: 800, output: 200, totalTokens: 13000 } } });
emit(this, { type: 'agent_end', yielded: true });
this.busy = true;
const cancelled = request.prompt.startsWith('persist-cancel');
if (cancelled) {
request.emit({ runId: request.runId, type: 'text', delta: 'cancel-before' });
request.emit({ runId: request.runId, type: 'tool_call', call: {
id: toolId, name: 'Read', args: { path: 'large.txt' }, argsRaw: '{"path":"large.txt"}',
status: 'running', output: 'cancelled tool output retained'
} });
state.cancelOutputEmitted = true;
return new Promise((resolve, reject) => {
const abort = () => {
state.abortCalls++;
this.busy = false;
reject(Object.assign(new Error('回合已取消'), { name: 'AbortError' }));
};
if (request.signal.aborted) abort();
else request.signal.addEventListener('abort', abort, { once: true });
});
}
return Promise.resolve({ type: 'response', success: true });
const tool = {
id: toolId, name: 'Read', args: { path: 'large.txt' }, argsRaw: '{"path":"large.txt"}',
status: 'ok', output: fullOutput, summary: fullOutput.slice(0, 600), result: { content: fullOutput }
};
request.emit({ runId: request.runId, type: 'text', delta: 'before' });
request.emit({ runId: request.runId, type: 'tool_call', call: { ...tool, status: 'running', output: undefined, summary: undefined, result: undefined } });
request.emit({ runId: request.runId, type: 'tool_result', call: tool });
request.emit({ runId: request.runId, type: 'text', delta: 'after' });
request.emit({ runId: request.runId, type: 'usage', usage: { inputTokens: 12800, outputTokens: 200, totalTokens: 13000, cachedTokens: 800 } });
this.busy = false;
return { content: 'beforeafter', tools: [tool], status: 'done', entryId: 'mock-claude-entry' };
}
abort() {
state.abortCalls++;
setImmediate(() => emit(this, { type: 'agent_end', yielded: true }));
return Promise.resolve({ type: 'response', success: true });
}
dispose() {}
async rewindFiles() {}
async close() { this.busy = false; }
}
export const detectOmpPath = () => state.useOmp ? 'mock-omp' : null;
export const getOmpInfo = () => ({ available: true, exePath: 'mock-omp', profile: 'ycode', model: 'ycode/test' });
`
}))
pluginBuild.onResolve({ filter: /^\.\/agent\/loop$/ }, () => ({ path: 'agent-loop', namespace: 'mock-loop' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-loop' }, () => ({
loader: 'js',
contents: `
const state = globalThis.${TEST_STATE_KEY};
export async function runTurn(options) {
state.fallbackHistory = options.history;
state.fallbackWireHistory = options.wireHistory;
const content = 'fallback response';
options.emit({ runId: options.runId, type: 'text', delta: content });
options.emit({ runId: options.runId, type: 'done', content, tools: [] });
return { content, tools: [] };
export function getClaudeKernel(options) {
state.resumeAts.push(options.resumeAt);
let kernel = kernels.get(options.sessionId);
if (!kernel) { kernel = new MockClaudeKernel(options); kernels.set(options.sessionId, kernel); }
return kernel;
}
export const cachedClaudeKernel = (sessionId) => kernels.get(sessionId);
export const releaseClaudeKernel = async (sessionId, force) => {
if (!force) return;
const kernel = kernels.get(sessionId);
if (!kernel) return;
kernels.delete(sessionId);
await kernel.close();
};
export const setSessionVisible = () => {};
export const removeSessionView = () => {};
export const disposeClaudeKernels = async () => { for (const kernel of kernels.values()) await kernel.close(); kernels.clear(); };
export const claudeRuntimeInfo = () => ({ available: false, exePath: null, sessionDir: state.userData });
`
}))
pluginBuild.onResolve({ filter: /^\.\/gateway$/ }, () => ({ path: 'gateway', namespace: 'mock-gateway' }))
pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-gateway' }, () => ({
loader: 'js',
contents: 'export class Gateway { constructor() {} }'
}))
}
}
]
@@ -235,6 +232,7 @@ async function main(): Promise<void> {
]
assert.deepEqual(normalAssistant.parts, expectedParts, 'session:messages should retain text/tool/text order')
assert.equal(normalAssistant.status, 'done')
assert.equal(normalAssistant.claudeEntryId, 'mock-claude-entry')
assert.equal(normalAssistant.tools?.length, 1)
const normalTool = normalAssistant.tools?.[0]
assert.ok(normalTool, 'completed tool should be present')
@@ -243,7 +241,8 @@ async function main(): Promise<void> {
assert.ok((normalTool.summary?.length ?? Infinity) < (normalTool.output?.length ?? 0),
'the summary may be truncated while full output remains intact')
const diskNormalAssistant = assistant(await readDiskMessages(state.userData, normalSession.id))
const diskNormalAssistant = await waitForDiskAssistant(state.userData, normalSession.id,
(message) => message.status === 'done' && message.claudeEntryId === 'mock-claude-entry')
assert.deepEqual(normalAssistant.contextUsage, {
usage: { inputTokens: 12800, outputTokens: 200, totalTokens: 13000, cachedTokens: 800 }, contextWindow: 64000,
modelId:'usage-snapshot-model',requestId:normalAssistant.requestId,source:'upstream',sampledAt:normalAssistant.contextUsage?.sampledAt
@@ -252,45 +251,32 @@ async function main(): Promise<void> {
assert.deepEqual(diskNormalAssistant.contextUsage, normalAssistant.contextUsage,
'the checkpoint whitelist must persist the upstream sample and frozen denominator together')
assert.deepEqual(diskNormalAssistant.parts, expectedParts, 'disk JSON should retain text/tool/text order')
assert.equal(diskNormalAssistant.claudeEntryId, 'mock-claude-entry',
'disk JSON should retain the Claude fork point')
assert.ok((diskNormalAssistant.tools?.[0].output?.length ?? 0) > 600,
'disk JSON should retain output beyond the summary limit')
assert.equal(diskNormalAssistant.tools?.[0].output, normalTool.output,
'disk JSON should retain the complete tool output')
assert.ok(diskNormalAssistant.tools?.[0].output?.endsWith('PERSISTED-OUTPUT-SENTINEL'))
const fallbackSession = (await invoke('session:create', 'fallback history')) as { id: string }
const sessions = ipc.sessions as {
appendMessage(sessionId: string, message: ChatMessage): Promise<void>
}
await sessions.appendMessage(fallbackSession.id, {
id: 'fallback-user',
role: 'user',
content: 'old request',
quotes: [{ id: 'saved-table', sourceId: 'table-source', kind: 'table', markdown: '| name | value |\n| --- | --- |\n| A | 42 |', preview: 'A 42' }],
createdAt: Date.now(),
status: 'done'
await invoke('chat:send', { sessionId: normalSession.id, text: 'persist-second' })
const secondMessages = await waitForMessages(invoke, normalSession.id,
(messages) => messages.filter((message) => message.role === 'assistant').length === 2 &&
assistant(messages).status === 'done')
const secondUser = secondMessages.filter((message) => message.role === 'user').at(-1)
assert.ok(secondUser)
await invoke('chat:send', {
sessionId: normalSession.id,
text: 'persist-edited',
fromMessageId: secondUser.id,
action: 'edit',
rollbackArtifacts: false
})
await sessions.appendMessage(fallbackSession.id, {
id: 'fallback-assistant',
role: 'assistant',
content: 'old answer',
parts: [{ type: 'text', text: 'old answer' }],
createdAt: Date.now(),
status: 'done'
})
state.useOmp = false
await invoke('chat:send', { sessionId: fallbackSession.id, text: 'fallback-history' })
const fallbackMessages = await waitForMessages(
invoke,
fallbackSession.id,
(messages) => assistant(messages).content === 'fallback response' && assistant(messages).status === 'done'
)
assert.equal(assistant(fallbackMessages).content, 'fallback response')
assert.deepEqual(state.fallbackHistory?.map((message) => message.content), ['old request', 'old answer'],
'the fallback loop should receive previously persisted user and assistant messages')
assert.equal(state.fallbackWireHistory?.[0].content, 'old request\n\n| name | value |\n| --- | --- |\n| A | 42 |',
'the next fallback request must include the exact previously quoted table snapshot')
state.useOmp = true
await waitForMessages(invoke, normalSession.id,
(messages) => messages.filter((message) => message.role === 'assistant').length === 2 &&
assistant(messages).status === 'done')
assert.equal(state.resumeAts.at(-1), normalAssistant.claudeEntryId,
'editing a prior turn should fork at the last kept Claude chain entry')
const cancelSession = (await invoke('session:create', 'cancelled persistence')) as { id: string }
const cancelledSend = (await invoke('chat:send', {
@@ -298,7 +284,7 @@ async function main(): Promise<void> {
text: 'persist-cancel'
})) as { runId: string }
for (let attempt = 0; attempt < 100 && !state.cancelOutputEmitted; attempt++) await wait(10)
assert.equal(state.cancelOutputEmitted, true, 'fake OMP should emit a running tool and partial output')
assert.equal(state.cancelOutputEmitted, true, 'the mock Claude kernel should emit a running tool and partial output')
const streamingCheckpoint = await waitForDiskAssistant(state.userData, cancelSession.id, (message) =>
message.status === 'streaming' &&
message.content === 'cancel-before' &&
@@ -331,7 +317,7 @@ async function main(): Promise<void> {
assert.equal(diskCancelledAssistant.tools?.[0].status, 'cancelled', 'disk JSON should persist terminal tool status')
assert.equal(diskCancelledAssistant.tools?.[0].output, 'cancelled tool output retained',
'disk JSON should preserve output from a running tool cancelled mid-turn')
assert.equal(state.promptCalls, 2)
assert.equal(state.promptCalls, 4)
assert.equal(state.abortCalls, 1)
console.log('chat-persistence-regression: all checks passed')