refactor(agent): replace OMP with Claude Code kernel
This commit is contained in:
1 parent
0fb2226f8c
commit
9a1f182397
232 files changed
+2561
-25564
No files matched your search
@@ -0,0 +1,208 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { createSdkMcpServer, query, tool } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { z } from 'zod'
|
||||
|
||||
const executable = resolve(process.env.YCODE_CLAUDE_EXE ?? 'node_modules/@anthropic-ai/claude-agent-sdk-win32-x64/claude.exe')
|
||||
const temp = await mkdtemp(join(tmpdir(), 'ycode-claude-smoke-'))
|
||||
const requests = []
|
||||
|
||||
function sse(res, event, data) {
|
||||
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
||||
}
|
||||
|
||||
function textResponse(res, id, model, text) {
|
||||
sse(res, 'message_start', {
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id, type: 'message', role: 'assistant', model,
|
||||
content: [], stop_reason: null, stop_sequence: null,
|
||||
usage: { input_tokens: 12, output_tokens: 1 }
|
||||
}
|
||||
})
|
||||
sse(res, 'content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } })
|
||||
sse(res, 'content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } })
|
||||
sse(res, 'content_block_stop', { type: 'content_block_stop', index: 0 })
|
||||
sse(res, 'message_delta', {
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'end_turn', stop_sequence: null },
|
||||
usage: { output_tokens: 5 }
|
||||
})
|
||||
sse(res, 'message_stop', { type: 'message_stop' })
|
||||
res.end()
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
let raw = ''
|
||||
req.on('data', chunk => { raw += chunk })
|
||||
req.on('end', () => {
|
||||
let body
|
||||
try { body = JSON.parse(raw || '{}') } catch { body = { raw } }
|
||||
requests.push({ url: req.url, method: req.method, body })
|
||||
if (!req.url?.includes('/messages')) {
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify({ data: [] }))
|
||||
return
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'content-type': 'text/event-stream',
|
||||
'cache-control': 'no-cache',
|
||||
connection: 'keep-alive'
|
||||
})
|
||||
const id = `msg_smoke_${requests.length}`
|
||||
const latestUser = [...(body.messages ?? [])].reverse().find(message => message.role === 'user')
|
||||
const latestUserContent = JSON.stringify(latestUser?.content ?? [])
|
||||
if (process.env.DEBUG_CLAUDE_SMOKE === '1') console.error('smoke request', requests.length, latestUserContent.slice(0, 500))
|
||||
const hasToolResult = latestUserContent.includes('"tool_result"')
|
||||
const wantsRead = latestUserContent.includes('Read the smoke file.')
|
||||
if (hasToolResult) {
|
||||
textResponse(res, id, body.model ?? 'claude-smoke', 'Tool read complete.')
|
||||
return
|
||||
}
|
||||
if (wantsRead && !hasToolResult) {
|
||||
sse(res, 'message_start', {
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id, type: 'message', role: 'assistant', model: body.model ?? 'claude-smoke',
|
||||
content: [], stop_reason: null, stop_sequence: null,
|
||||
usage: { input_tokens: 12, output_tokens: 1 }
|
||||
}
|
||||
})
|
||||
sse(res, 'content_block_start', {
|
||||
type: 'content_block_start', index: 0,
|
||||
content_block: { type: 'tool_use', id: 'tool_smoke', name: 'Read', input: {} }
|
||||
})
|
||||
sse(res, 'content_block_delta', {
|
||||
type: 'content_block_delta', index: 0,
|
||||
delta: { type: 'input_json_delta', partial_json: JSON.stringify({ file_path: join(temp, 'smoke.txt') }) }
|
||||
})
|
||||
sse(res, 'content_block_stop', { type: 'content_block_stop', index: 0 })
|
||||
sse(res, 'message_delta', {
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'tool_use', stop_sequence: null },
|
||||
usage: { output_tokens: 5 }
|
||||
})
|
||||
sse(res, 'message_stop', { type: 'message_stop' })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
textResponse(res, id, body.model ?? 'claude-smoke', 'Claude mock reply.')
|
||||
})
|
||||
})
|
||||
|
||||
await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen))
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') throw new Error('mock server did not bind')
|
||||
|
||||
const sessionId = crypto.randomUUID()
|
||||
let runtimeSessionId = sessionId
|
||||
let releaseSecondTurn
|
||||
const firstTurnComplete = new Promise(resolve => { releaseSecondTurn = resolve })
|
||||
const input = (async function* () {
|
||||
yield {
|
||||
type: 'user',
|
||||
message: { role: 'user', content: [{ type: 'text', text: 'Read the smoke file.' }] },
|
||||
parent_tool_use_id: null,
|
||||
uuid: crypto.randomUUID(),
|
||||
session_id: runtimeSessionId
|
||||
}
|
||||
await firstTurnComplete
|
||||
yield {
|
||||
type: 'user',
|
||||
message: { role: 'user', content: [{ type: 'text', text: 'Reply once more.' }] },
|
||||
parent_tool_use_id: null,
|
||||
uuid: crypto.randomUUID(),
|
||||
session_id: runtimeSessionId
|
||||
}
|
||||
})()
|
||||
|
||||
await writeFile(join(temp, 'smoke.txt'), 'SMOKE_FILE_CONTENTS', 'utf8')
|
||||
|
||||
const messages = []
|
||||
const results = []
|
||||
let instance
|
||||
try {
|
||||
instance = query({
|
||||
prompt: input,
|
||||
options: {
|
||||
cwd: temp,
|
||||
model: 'claude-smoke',
|
||||
pathToClaudeCodeExecutable: executable,
|
||||
env: {
|
||||
...process.env,
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||
ANTHROPIC_API_KEY: 'ycode-smoke-key',
|
||||
CLAUDE_CONFIG_DIR: join(temp, 'claude-config'),
|
||||
CLAUDE_CODE_ENABLE_TODO_TOOLS: '1',
|
||||
CLAUDE_CODE_ENABLE_TASKS: '0',
|
||||
...(process.platform === 'win32' ? { CLAUDE_CODE_USE_POWERSHELL_TOOL: '1' } : {})
|
||||
},
|
||||
includePartialMessages: true,
|
||||
permissionMode: 'default',
|
||||
permissionPrompts: 'host',
|
||||
settingSources: [],
|
||||
strictMcpConfig: true,
|
||||
tools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'PowerShell', 'WebFetch', 'WebSearch',
|
||||
'TodoWrite', 'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode'],
|
||||
allowedTools: [],
|
||||
disallowedTools: ['Agent', 'Task', 'Skill', 'Workflow', 'CronCreate', 'CronDelete', 'CronList'],
|
||||
plugins: [],
|
||||
agents: {},
|
||||
skills: [],
|
||||
mcpServers: {
|
||||
ycode: createSdkMcpServer({
|
||||
name: 'ycode',
|
||||
tools: [tool('ping', 'Test host tool', { value: z.string() }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: value }]
|
||||
}))]
|
||||
})
|
||||
},
|
||||
canUseTool: async (toolName) => toolName === 'Read'
|
||||
? { behavior: 'allow', updatedInput: { file_path: join(temp, 'smoke.txt') } }
|
||||
: { behavior: 'deny', message: 'Only Read is enabled in this smoke test.' }
|
||||
}
|
||||
})
|
||||
for await (const message of instance) {
|
||||
messages.push(message)
|
||||
if (message.type === 'system' && message.subtype === 'init' && message.session_id) {
|
||||
runtimeSessionId = message.session_id
|
||||
}
|
||||
if (message.type === 'result') {
|
||||
results.push(message)
|
||||
if (results.length === 1) releaseSecondTurn()
|
||||
else break
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
instance?.close()
|
||||
await new Promise(resolveClose => server.close(resolveClose))
|
||||
await rm(temp, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
const result = results.at(-1)
|
||||
const streamed = messages.filter(message => message.type === 'stream_event')
|
||||
.map(message => message.event)
|
||||
.filter(event => event?.type === 'content_block_delta' && event.delta?.type === 'text_delta')
|
||||
.map(event => event.delta.text)
|
||||
.join('')
|
||||
const toolResult = messages.find(message => message.type === 'user' &&
|
||||
Array.isArray(message.message?.content) &&
|
||||
message.message.content.some(block => block.type === 'tool_result'))
|
||||
|
||||
assert.ok(requests.some(request => request.url?.includes('/messages')), 'Claude Code should call the Anthropic Messages endpoint')
|
||||
assert.equal(results.length, 2, 'one process should serve two consecutive turns')
|
||||
assert.equal(new Set(results.map(message => message.session_id)).size, 1,
|
||||
'both turns should stay in the same Claude session')
|
||||
assert.equal(streamed, 'Tool read complete.Claude mock reply.')
|
||||
assert.ok(toolResult, 'the native Read tool result should return to Claude')
|
||||
assert.equal(result?.subtype, 'success')
|
||||
assert.equal(result?.result, 'Claude mock reply.')
|
||||
const init = messages.find(message => message.type === 'system' && message.subtype === 'init')
|
||||
assert.ok(init?.tools.includes('TodoWrite'), 'TodoWrite should be enabled')
|
||||
if (process.platform === 'win32') assert.ok(init?.tools.includes('PowerShell'), 'PowerShell should be enabled on Windows')
|
||||
assert.equal(init?.tools.some(tool => ['Agent', 'Task', 'Skill', 'Workflow', 'CronCreate'].includes(tool)), false)
|
||||
|
||||
console.log(JSON.stringify({ result: 'PASS', requests: requests.length, messages: messages.length, turns: results.length, text: result.result }))
|
||||
Reference in new issue
Block a user