chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,710 @@
|
||||
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { createServer, type IncomingMessage } from 'node:http'
|
||||
import { createInterface } from 'node:readline'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { join, parse, relative, resolve, sep } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
interface Frame {
|
||||
type?: string
|
||||
id?: string
|
||||
command?: string
|
||||
success?: boolean
|
||||
data?: unknown
|
||||
error?: { message?: string }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ProbeRun {
|
||||
child: ChildProcessWithoutNullStreams
|
||||
frames: Frame[]
|
||||
rawLines: string[]
|
||||
stderrLines: string[]
|
||||
}
|
||||
|
||||
interface MockRequest {
|
||||
model?: string
|
||||
stream: boolean
|
||||
roles: string[]
|
||||
toolNames: string[]
|
||||
toolProperties: Record<string, string[]>
|
||||
containsImage: boolean
|
||||
messageShape: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
const STARTUP_WAIT_MS = 12_000
|
||||
const COMMAND_WAIT_MS = 20_000
|
||||
const SHUTDOWN_WAIT_MS = 1_500
|
||||
const PROVIDER_ID = 'loopback-probe'
|
||||
const MODEL_ID = 'probe-model'
|
||||
const MODEL = `${PROVIDER_ID}/${MODEL_ID}`
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms))
|
||||
}
|
||||
|
||||
function makeIsolatedEnvironment(home: string, tempRoot: string, profile: string): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = {}
|
||||
for (const name of ['PATH', 'SystemRoot', 'SystemDrive', 'ComSpec', 'WINDIR']) {
|
||||
const value = process.env[name]
|
||||
if (value) env[name] = value
|
||||
}
|
||||
|
||||
const root = parse(home).root
|
||||
env.USERPROFILE = home
|
||||
env.HOME = home
|
||||
env.HOMEDRIVE = root.replace(/[\\/]$/, '')
|
||||
env.HOMEPATH = relative(root, home).replaceAll('/', '\\')
|
||||
env.APPDATA = join(home, 'AppData', 'Roaming')
|
||||
env.LOCALAPPDATA = join(home, 'AppData', 'Local')
|
||||
env.TEMP = tempRoot
|
||||
env.TMP = tempRoot
|
||||
env.OMP_PROFILE = profile
|
||||
return env
|
||||
}
|
||||
|
||||
function summarizeFrame(frame: Frame): Record<string, unknown> {
|
||||
const data = frame.data
|
||||
const dataKeys = data && typeof data === 'object' ? Object.keys(data as object) : []
|
||||
const event = frame.assistantMessageEvent
|
||||
const eventKeys = event && typeof event === 'object' ? Object.keys(event as object) : []
|
||||
return {
|
||||
type: frame.type,
|
||||
frameKeys: Object.keys(frame),
|
||||
id: frame.id,
|
||||
command: frame.command,
|
||||
success: frame.success,
|
||||
status: frame.status,
|
||||
assistantEventType: event && typeof event === 'object' ? (event as Record<string, unknown>).type : undefined,
|
||||
toolCallId: frame.toolCallId,
|
||||
toolName: frame.toolName,
|
||||
argumentKeys: frame.arguments && typeof frame.arguments === 'object' ? Object.keys(frame.arguments as object) : [],
|
||||
eventKeys,
|
||||
dataKeys,
|
||||
error: typeof frame.error === 'string' ? frame.error : frame.error?.message
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForFrame(
|
||||
run: ProbeRun,
|
||||
predicate: (frame: Frame) => boolean,
|
||||
timeoutMs = COMMAND_WAIT_MS,
|
||||
startIndex = 0
|
||||
): Promise<Frame | null> {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
const frame = run.frames.slice(startIndex).find(predicate)
|
||||
if (frame) return frame
|
||||
if (run.child.exitCode !== null || run.child.signalCode !== null) break
|
||||
await delay(20)
|
||||
}
|
||||
return run.frames.slice(startIndex).find(predicate) ?? null
|
||||
}
|
||||
|
||||
async function sendCommand(run: ProbeRun, id: string, command: Record<string, unknown>): Promise<Frame | null> {
|
||||
run.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`)
|
||||
return waitForFrame(run, (frame) => frame.id === id)
|
||||
}
|
||||
|
||||
async function sendLooseCommand(run: ProbeRun, id: string, command: Record<string, unknown>): Promise<Frame | null> {
|
||||
const startIndex = run.frames.length
|
||||
run.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`)
|
||||
return waitForFrame(run, (frame) => frame.id === id || frame.type === 'response', 3_000, startIndex)
|
||||
}
|
||||
|
||||
async function installProbeHostTool(run: ProbeRun, suffix: string): Promise<Frame | null> {
|
||||
return sendCommand(run, `probe-host-tools-${suffix}`, {
|
||||
type: 'set_host_tools',
|
||||
tools: [{
|
||||
name: 'read',
|
||||
label: 'Read local probe file',
|
||||
description: 'Read a file from the isolated protocol probe directory.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { path: { type: 'string' } },
|
||||
required: ['path'],
|
||||
additionalProperties: false
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
async function stopRun(run: ProbeRun): Promise<void> {
|
||||
try {
|
||||
run.child.stdin.end()
|
||||
} catch {
|
||||
// The sidecar may already have exited.
|
||||
}
|
||||
|
||||
await new Promise<void>((resolveExit) => {
|
||||
if (run.child.exitCode !== null || run.child.signalCode !== null) {
|
||||
resolveExit()
|
||||
return
|
||||
}
|
||||
let settled = false
|
||||
const finish = (): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
resolveExit()
|
||||
}
|
||||
const timer = setTimeout(finish, SHUTDOWN_WAIT_MS)
|
||||
run.child.once('exit', finish)
|
||||
})
|
||||
|
||||
if (run.child.exitCode === null && run.child.signalCode === null) {
|
||||
run.child.kill()
|
||||
await new Promise<void>((resolveExit) => {
|
||||
if (run.child.exitCode !== null || run.child.signalCode !== null) resolveExit()
|
||||
else run.child.once('exit', () => resolveExit())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function startRun(
|
||||
exePath: string,
|
||||
tempRoot: string,
|
||||
profile: string,
|
||||
home: string,
|
||||
sessionDir: string,
|
||||
sessionPath: string
|
||||
): Promise<ProbeRun> {
|
||||
const args = [
|
||||
'--mode', 'rpc',
|
||||
'--profile', profile,
|
||||
'--cwd', tempRoot,
|
||||
`--model=${MODEL}`,
|
||||
'--no-tools',
|
||||
`--session-dir=${sessionDir}`,
|
||||
`--resume=${sessionPath}`,
|
||||
'--no-ui',
|
||||
'--no-title',
|
||||
'--no-extensions',
|
||||
'--no-skills',
|
||||
'--no-rules',
|
||||
'--no-lsp'
|
||||
]
|
||||
const child = spawn(exePath, args, {
|
||||
cwd: tempRoot,
|
||||
env: makeIsolatedEnvironment(home, tempRoot, profile),
|
||||
windowsHide: true,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
})
|
||||
const run: ProbeRun = { child, frames: [], rawLines: [], stderrLines: [] }
|
||||
const stdout = createInterface({ input: child.stdout })
|
||||
const stderr = createInterface({ input: child.stderr })
|
||||
stdout.on('line', (line) => {
|
||||
run.rawLines.push(line)
|
||||
try {
|
||||
run.frames.push(JSON.parse(line) as Frame)
|
||||
} catch {
|
||||
// Keep non-JSON stdout separate from RPC frames.
|
||||
}
|
||||
})
|
||||
stderr.on('line', (line) => run.stderrLines.push(line))
|
||||
const ready = await waitForFrame(run, (frame) => frame.type === 'ready', STARTUP_WAIT_MS)
|
||||
if (!ready) {
|
||||
const stderrText = run.stderrLines.join('\n').slice(0, 1000)
|
||||
await stopRun(run)
|
||||
throw new Error(`OMP did not send ready; exit=${child.exitCode}; stderr=${stderrText}`)
|
||||
}
|
||||
return run
|
||||
}
|
||||
|
||||
async function readRequest(request: IncomingMessage): Promise<Record<string, unknown>> {
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of request) chunks.push(Buffer.from(chunk))
|
||||
try {
|
||||
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function toolNamesFrom(body: Record<string, unknown>): string[] {
|
||||
if (!Array.isArray(body.tools)) return []
|
||||
return body.tools.flatMap((tool) => {
|
||||
if (!tool || typeof tool !== 'object') return []
|
||||
const item = tool as Record<string, unknown>
|
||||
const fn = item.function && typeof item.function === 'object' ? item.function as Record<string, unknown> : null
|
||||
const name = fn?.name ?? item.name
|
||||
return typeof name === 'string' ? [name] : []
|
||||
})
|
||||
}
|
||||
|
||||
function requestHasImage(body: Record<string, unknown>): boolean {
|
||||
if (!Array.isArray(body.messages)) return false
|
||||
return body.messages.some((message) => {
|
||||
if (!message || typeof message !== 'object') return false
|
||||
const content = (message as Record<string, unknown>).content
|
||||
return Array.isArray(content) && content.some((part) => {
|
||||
if (!part || typeof part !== 'object') return false
|
||||
const item = part as Record<string, unknown>
|
||||
return item.type === 'image_url' || item.type === 'image'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function startMockProvider(probeFile: string): Promise<{
|
||||
server: ReturnType<typeof createServer>
|
||||
url: string
|
||||
requests: MockRequest[]
|
||||
toolCallSent: boolean
|
||||
}> {
|
||||
const requests: MockRequest[] = []
|
||||
let toolCallSent = false
|
||||
const server = createServer(async (request, response) => {
|
||||
const body = await readRequest(request)
|
||||
const names = toolNamesFrom(body)
|
||||
const messages = Array.isArray(body.messages) ? body.messages as Array<Record<string, unknown>> : []
|
||||
const latestUser = [...messages].reverse().find((message) => message.role === 'user')
|
||||
const latestUserContent = Array.isArray(latestUser?.content)
|
||||
? (latestUser.content as Array<Record<string, unknown>>).map((block) => block.text).filter((value) => typeof value === 'string').join('\n')
|
||||
: typeof latestUser?.content === 'string' ? latestUser.content : ''
|
||||
const toolProperties: Record<string, string[]> = {}
|
||||
for (const tool of Array.isArray(body.tools) ? body.tools : []) {
|
||||
if (!tool || typeof tool !== 'object') continue
|
||||
const item = tool as Record<string, unknown>
|
||||
const fn = item.function && typeof item.function === 'object' ? item.function as Record<string, unknown> : null
|
||||
const name = fn?.name ?? item.name
|
||||
const parameters = fn?.parameters && typeof fn.parameters === 'object' ? fn.parameters as Record<string, unknown> : null
|
||||
const properties = parameters?.properties && typeof parameters.properties === 'object'
|
||||
? parameters.properties as Record<string, unknown> : null
|
||||
if (typeof name === 'string') toolProperties[name] = Object.keys(properties ?? {})
|
||||
}
|
||||
requests.push({
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
stream: body.stream === true,
|
||||
roles: messages.map((message) => typeof message.role === 'string' ? message.role : '?'),
|
||||
toolNames: names,
|
||||
toolProperties,
|
||||
containsImage: requestHasImage(body),
|
||||
messageShape: messages.map((message) => {
|
||||
const content = message.content
|
||||
const blocks = Array.isArray(content) ? content as Array<Record<string, unknown>> : []
|
||||
const calls = Array.isArray(message.tool_calls) ? message.tool_calls as Array<Record<string, unknown>> : []
|
||||
return {
|
||||
role: message.role,
|
||||
contentType: typeof content === 'string' ? 'string' : Array.isArray(content) ? 'array' : typeof content,
|
||||
contentBlockTypes: blocks.map((block) => block.type),
|
||||
toolCallCount: calls.length,
|
||||
hasToolCallId: typeof message.tool_call_id === 'string'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const readToolName = names.find((name) => name === 'read' || name.endsWith('__read'))
|
||||
const sendReadTool = latestUserContent.includes('TOOL_PROBE') && readToolName && !toolCallSent
|
||||
const completion = sendReadTool
|
||||
? {
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
tool_calls: [{
|
||||
id: 'call_local_read_1',
|
||||
type: 'function',
|
||||
function: { name: readToolName, arguments: JSON.stringify({ path: probeFile }) }
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls'
|
||||
}
|
||||
: {
|
||||
message: { role: 'assistant', content: 'PROBE_OK' },
|
||||
finish_reason: 'stop'
|
||||
}
|
||||
if (sendReadTool) toolCallSent = true
|
||||
if (body.stream === true) {
|
||||
response.writeHead(200, {
|
||||
'content-type': 'text/event-stream',
|
||||
'cache-control': 'no-cache',
|
||||
connection: 'keep-alive'
|
||||
})
|
||||
const completionId = `chatcmpl-local-${requests.length}`
|
||||
const writeChunk = (delta: Record<string, unknown>, finishReason: string | null = null): void => {
|
||||
response.write(`data: ${JSON.stringify({
|
||||
id: completionId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: MODEL_ID,
|
||||
choices: [{ index: 0, delta, finish_reason: finishReason }]
|
||||
})}\n\n`)
|
||||
}
|
||||
writeChunk({ role: 'assistant' })
|
||||
if (sendReadTool) {
|
||||
writeChunk({ tool_calls: [{
|
||||
index: 0,
|
||||
id: 'call_local_read_1',
|
||||
type: 'function',
|
||||
function: { name: readToolName, arguments: JSON.stringify({ path: probeFile }) }
|
||||
}] })
|
||||
writeChunk({}, 'tool_calls')
|
||||
} else {
|
||||
writeChunk({ reasoning_content: 'LOCAL_THINKING_PROBE' })
|
||||
writeChunk({ content: 'PROBE_OK' })
|
||||
writeChunk({}, 'stop')
|
||||
}
|
||||
response.write(`data: ${JSON.stringify({
|
||||
id: completionId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: MODEL_ID,
|
||||
choices: [],
|
||||
usage: { prompt_tokens: 17, completion_tokens: 4, total_tokens: 21 }
|
||||
})}\n\n`)
|
||||
response.end('data: [DONE]\n\n')
|
||||
return
|
||||
}
|
||||
response.writeHead(200, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({
|
||||
id: `chatcmpl-local-${requests.length}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: MODEL_ID,
|
||||
choices: [{ index: 0, ...completion }],
|
||||
usage: { prompt_tokens: 17, completion_tokens: 4, total_tokens: 21 }
|
||||
}))
|
||||
})
|
||||
await new Promise<void>((resolveListen, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', reject)
|
||||
resolveListen()
|
||||
})
|
||||
})
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') throw new Error('Loopback mock did not bind to a TCP port')
|
||||
return { server, url: `http://127.0.0.1:${address.port}/v1`, requests, get toolCallSent() { return toolCallSent } }
|
||||
}
|
||||
|
||||
function summarizeMessages(data: unknown): Record<string, unknown> {
|
||||
const root = data && typeof data === 'object' ? data as Record<string, unknown> : {}
|
||||
const possibleArrays = [root.messages, root.entries, root.branchMessages]
|
||||
const records = possibleArrays.find(Array.isArray) as unknown[] | undefined
|
||||
const items = (records ?? (Array.isArray(data) ? data : [])).map((value) => {
|
||||
if (!value || typeof value !== 'object') return { valueType: typeof value }
|
||||
const item = value as Record<string, unknown>
|
||||
const message = item.message && typeof item.message === 'object' ? item.message as Record<string, unknown> : item
|
||||
const content = Array.isArray(message.content) ? message.content as Array<Record<string, unknown>> : []
|
||||
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls as Array<Record<string, unknown>>
|
||||
: Array.isArray(message.tool_calls) ? message.tool_calls as Array<Record<string, unknown>> : []
|
||||
return {
|
||||
itemKeys: Object.keys(item),
|
||||
messageKeys: Object.keys(message),
|
||||
type: item.type,
|
||||
role: message.role,
|
||||
entryId: item.entryId ?? item.id,
|
||||
parentId: item.parentId,
|
||||
textBlocks: content.filter((block) => block.type === 'text').length,
|
||||
toolCalls: toolCalls.map((call) => call.name ?? (call.function as Record<string, unknown> | undefined)?.name)
|
||||
}
|
||||
})
|
||||
return { keys: Object.keys(root), count: items.length, messages: items }
|
||||
}
|
||||
|
||||
async function inspectTranscript(path: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const text = await readFile(path, 'utf8')
|
||||
const records = text.split(/\r?\n/).filter(Boolean).map((record) => JSON.parse(record) as Record<string, unknown>)
|
||||
return {
|
||||
exists: true,
|
||||
records: records.length,
|
||||
entries: records.map((record) => ({
|
||||
type: record.type,
|
||||
id: record.id,
|
||||
parentId: record.parentId,
|
||||
messageRole: (record.message as Record<string, unknown> | undefined)?.role,
|
||||
toolName: (record.toolName ?? (record.message as Record<string, unknown> | undefined)?.toolName) ?? undefined
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
return { exists: false }
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeTree(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(summarizeTree)
|
||||
if (!value || typeof value !== 'object') return value
|
||||
const node = value as Record<string, unknown>
|
||||
const entry = node.entry && typeof node.entry === 'object' ? node.entry as Record<string, unknown> : node
|
||||
const message = entry.message && typeof entry.message === 'object' ? entry.message as Record<string, unknown> : null
|
||||
return {
|
||||
nodeKeys: Object.keys(node),
|
||||
entryKeys: Object.keys(entry),
|
||||
id: entry.id,
|
||||
parentId: entry.parentId,
|
||||
type: entry.type,
|
||||
role: message?.role,
|
||||
label: node.label,
|
||||
children: Array.isArray(node.children) ? summarizeTree(node.children) : []
|
||||
}
|
||||
}
|
||||
|
||||
function findUserNode(value: unknown): { id: string; parentId: string | null } | null {
|
||||
if (Array.isArray(value)) {
|
||||
let latest: { id: string; parentId: string | null } | null = null
|
||||
for (const child of value) {
|
||||
const found = findUserNode(child)
|
||||
if (found) latest = found
|
||||
}
|
||||
return latest
|
||||
}
|
||||
if (!value || typeof value !== 'object') return null
|
||||
const node = value as Record<string, unknown>
|
||||
const entry = node.entry && typeof node.entry === 'object' ? node.entry as Record<string, unknown> : node
|
||||
const message = entry.message && typeof entry.message === 'object' ? entry.message as Record<string, unknown> : null
|
||||
let latest: { id: string; parentId: string | null } | null = null
|
||||
if (entry.type === 'message' && message?.role === 'user' && typeof entry.id === 'string') {
|
||||
latest = { id: entry.id, parentId: typeof entry.parentId === 'string' ? entry.parentId : null }
|
||||
}
|
||||
const child = Array.isArray(node.children) ? findUserNode(node.children) : null
|
||||
return child ?? latest
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const exePath = process.env.YCODE_OMP_PATH ?? 'C:\\Users\\Administrator\\AppData\\Local\\Ygent\\omp.exe'
|
||||
if (!existsSync(exePath)) throw new Error(`OMP executable not found: ${exePath}`)
|
||||
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-omp-protocol-'))
|
||||
const home = join(tempRoot, 'isolated-home')
|
||||
const sessionDir = join(tempRoot, 'sessions')
|
||||
const profile = `protocol-probe-${randomUUID().replaceAll('-', '')}`
|
||||
const sessionPath = join(sessionDir, 'probe.jsonl')
|
||||
const probeFile = join(tempRoot, 'probe.txt')
|
||||
const realProfilePath = resolve(homedir(), '.omp', 'profiles', profile)
|
||||
const realProfileRoot = resolve(homedir(), '.omp', 'profiles')
|
||||
const realProfileRelative = relative(realProfileRoot, realProfilePath)
|
||||
if (!realProfileRelative || realProfileRelative.startsWith(`..${sep}`) || resolve(realProfileRoot, realProfileRelative) !== realProfilePath) {
|
||||
throw new Error('Refusing an invalid isolated profile path')
|
||||
}
|
||||
if (existsSync(realProfilePath)) throw new Error('The unique probe profile unexpectedly exists')
|
||||
|
||||
await mkdir(home, { recursive: true })
|
||||
await mkdir(sessionDir, { recursive: true })
|
||||
await writeFile(probeFile, 'LOCAL_OMP_TOOL_SENTINEL\n', 'utf8')
|
||||
const profileAgentDir = join(home, '.omp', 'profiles', profile, 'agent')
|
||||
await mkdir(profileAgentDir, { recursive: true })
|
||||
const mock = await startMockProvider(probeFile)
|
||||
await writeFile(join(profileAgentDir, 'models.yml'), [
|
||||
'providers:',
|
||||
` ${PROVIDER_ID}:`,
|
||||
` baseUrl: ${mock.url}`,
|
||||
' api: openai-completions',
|
||||
' auth: none',
|
||||
' models:',
|
||||
` - id: ${MODEL_ID}`,
|
||||
' name: Local protocol probe',
|
||||
' reasoning: true',
|
||||
' input: [text, image]',
|
||||
' contextWindow: 64000',
|
||||
' maxTokens: 4096',
|
||||
''
|
||||
].join('\n'), 'utf8')
|
||||
|
||||
const runs: ProbeRun[] = []
|
||||
try {
|
||||
const version = spawnSync(exePath, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
env: makeIsolatedEnvironment(home, tempRoot, profile)
|
||||
})
|
||||
if (version.error) throw version.error
|
||||
if (version.status !== 0) throw new Error(`OMP --version failed with exit ${version.status}`)
|
||||
console.log(`OMP_VERSION: ${version.stdout.trim()}`)
|
||||
console.log(`ISOLATED_PROFILE: ${profile}`)
|
||||
console.log('PROVIDER_NETWORK: loopback only')
|
||||
|
||||
const first = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
|
||||
runs.push(first)
|
||||
const hostToolsFirst = await installProbeHostTool(first, '1')
|
||||
const stateResponse = await sendCommand(first, 'probe-state-1', { type: 'get_state' })
|
||||
console.log(`FIRST_READY: true`)
|
||||
console.log(`SET_HOST_TOOLS: ${JSON.stringify(hostToolsFirst ? summarizeFrame(hostToolsFirst) : null)}`)
|
||||
console.log(`GET_STATE: ${JSON.stringify(stateResponse ? summarizeFrame(stateResponse) : null)}`)
|
||||
|
||||
const image = {
|
||||
type: 'image' as const,
|
||||
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/dXEAAAAASUVORK5CYII=',
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
first.child.stdin.write(`${JSON.stringify({
|
||||
id: 'probe-prompt-1',
|
||||
type: 'prompt',
|
||||
message: 'Reply exactly PROBE_OK.',
|
||||
images: [image]
|
||||
})}\n`)
|
||||
const promptAck = await waitForFrame(first, (frame) => frame.id === 'probe-prompt-1')
|
||||
const turnDone = await waitForFrame(first, (frame) =>
|
||||
frame.type === 'agent_end' || (frame.type === 'prompt_result' && frame.agentInvoked === false), 60_000)
|
||||
const toolTurnStart = first.frames.length
|
||||
first.child.stdin.write(`${JSON.stringify({
|
||||
id: 'probe-prompt-tool',
|
||||
type: 'prompt',
|
||||
message: 'TOOL_PROBE: call the read tool for probe.txt, then reply exactly PROBE_OK.'
|
||||
})}\n`)
|
||||
const toolPromptAck = await waitForFrame(first, (frame) => frame.id === 'probe-prompt-tool')
|
||||
const hostToolCall = await waitForFrame(first, (frame) => frame.type === 'host_tool_call', 20_000, toolTurnStart)
|
||||
let hostToolResponseSent = false
|
||||
if (hostToolCall && typeof hostToolCall.id === 'string') {
|
||||
const args = hostToolCall.arguments && typeof hostToolCall.arguments === 'object'
|
||||
? hostToolCall.arguments as Record<string, unknown> : {}
|
||||
const targetPath = typeof args.path === 'string' ? args.path : probeFile
|
||||
const content = await readFile(targetPath, 'utf8')
|
||||
first.child.stdin.write(`${JSON.stringify({
|
||||
type: 'host_tool_result',
|
||||
id: hostToolCall.id,
|
||||
result: { content: [{ type: 'text', text: content }] }
|
||||
})}\n`)
|
||||
hostToolResponseSent = true
|
||||
}
|
||||
const toolTurnDone = await waitForFrame(first, (frame) =>
|
||||
frame.type === 'agent_end' || (frame.type === 'prompt_result' && frame.agentInvoked === false), 60_000, toolTurnStart)
|
||||
const getMessages = await sendCommand(first, 'probe-messages-1', { type: 'get_messages' })
|
||||
const tree = await sendCommand(first, 'probe-tree-1', { type: 'get_tree' })
|
||||
const branchMessages = await sendCommand(first, 'probe-branch-messages-1', { type: 'get_branch_messages' })
|
||||
const stateAfterTurn = await sendCommand(first, 'probe-state-after-turn', { type: 'get_state' })
|
||||
|
||||
const entries = getMessages ? summarizeMessages(getMessages.data) : null
|
||||
const treeData = tree?.data && typeof tree.data === 'object' ? tree.data as Record<string, unknown> : {}
|
||||
const treeShape = summarizeTree(treeData.tree)
|
||||
const firstUserNode = findUserNode(treeData.tree)
|
||||
const fork = firstUserNode
|
||||
? await sendCommand(first, 'probe-fork-before-branch', { type: 'fork', entryId: firstUserNode.id })
|
||||
: null
|
||||
const stateAfterFork = await sendCommand(first, 'probe-state-after-fork', { type: 'get_state' })
|
||||
const forkStateData = stateAfterFork?.data && typeof stateAfterFork.data === 'object'
|
||||
? stateAfterFork.data as Record<string, unknown> : {}
|
||||
const forkSessionPath = typeof forkStateData.sessionFile === 'string' ? forkStateData.sessionFile : ''
|
||||
const relativeForkSession = forkSessionPath ? relative(tempRoot, resolve(forkSessionPath)) : ''
|
||||
const forkSessionInProbe = !!relativeForkSession && relativeForkSession !== '..' &&
|
||||
!relativeForkSession.startsWith(`..${sep}`) && !relativeForkSession.startsWith(sep)
|
||||
const messagesAfterFork = await sendCommand(first, 'probe-messages-after-fork', { type: 'get_messages' })
|
||||
const treeAfterFork = await sendCommand(first, 'probe-tree-after-fork', { type: 'get_tree' })
|
||||
const treeAfterForkData = treeAfterFork?.data && typeof treeAfterFork.data === 'object'
|
||||
? treeAfterFork.data as Record<string, unknown> : {}
|
||||
const forkData = fork?.data && typeof fork.data === 'object' ? fork.data as Record<string, unknown> : {}
|
||||
const stateBeforeForkData = stateAfterTurn?.data && typeof stateAfterTurn.data === 'object'
|
||||
? stateAfterTurn.data as Record<string, unknown> : {}
|
||||
|
||||
console.log(`PROMPT_ACK: ${JSON.stringify(promptAck ? summarizeFrame(promptAck) : null)}`)
|
||||
console.log(`FIRST_TURN_END_FRAME: ${JSON.stringify(turnDone ? summarizeFrame(turnDone) : null)}`)
|
||||
console.log(`TOOL_PROMPT_ACK: ${JSON.stringify(toolPromptAck ? summarizeFrame(toolPromptAck) : null)}`)
|
||||
console.log(`HOST_TOOL_CALL: ${JSON.stringify(hostToolCall ? summarizeFrame(hostToolCall) : null)}`)
|
||||
console.log(`HOST_TOOL_RESPONSE_SENT: ${hostToolResponseSent}`)
|
||||
console.log(`TOOL_TURN_END_FRAME: ${JSON.stringify(toolTurnDone ? summarizeFrame(toolTurnDone) : null)}`)
|
||||
console.log(`TURN_EVENTS: ${JSON.stringify(first.frames.filter((frame) => [
|
||||
'agent_start', 'message_start', 'message_update', 'message_end', 'tool_execution_start',
|
||||
'tool_execution_update', 'tool_execution_end', 'host_tool_call', 'turn_start', 'turn_end', 'agent_end', 'prompt_result'
|
||||
].includes(frame.type ?? '')).map(summarizeFrame))}`)
|
||||
console.log(`GET_MESSAGES_RESPONSE: ${JSON.stringify(getMessages ? {
|
||||
...summarizeFrame(getMessages),
|
||||
dataSummary: entries
|
||||
} : null)}`)
|
||||
console.log(`GET_TREE_RESPONSE: ${JSON.stringify(tree ? {
|
||||
...summarizeFrame(tree), leafId: treeData.leafId, tree: treeShape
|
||||
} : null)}`)
|
||||
console.log(`GET_BRANCH_MESSAGES_RESPONSE: ${JSON.stringify(branchMessages ? {
|
||||
...summarizeFrame(branchMessages), dataSummary: summarizeMessages(branchMessages.data)
|
||||
} : null)}`)
|
||||
console.log(`FORK_ENTRY_ID: ${JSON.stringify(firstUserNode?.id ?? null)}`)
|
||||
console.log(`FORK_RESPONSE: ${JSON.stringify(fork ? summarizeFrame(fork) : null)}`)
|
||||
console.log(`FORK_CANCELLED: ${forkData.cancelled}`)
|
||||
console.log(`FORK_STATE: ${JSON.stringify(stateAfterFork ? summarizeFrame(stateAfterFork) : null)}`)
|
||||
console.log(`FORK_SESSION_ID_CHANGED: ${stateBeforeForkData.sessionId !== forkStateData.sessionId}`)
|
||||
console.log(`FORK_SESSION_FILE_IS_PROBE_PATH: ${forkSessionInProbe}`)
|
||||
console.log(`FORK_SESSION_FILE_EXISTS: ${forkSessionInProbe ? existsSync(forkSessionPath) : false}`)
|
||||
console.log(`FORK_MESSAGES: ${JSON.stringify(messagesAfterFork ? summarizeMessages(messagesAfterFork.data) : null)}`)
|
||||
console.log(`FORK_TREE_LEAF_ID: ${treeAfterForkData.leafId ?? null}`)
|
||||
console.log(`TRANSCRIPT_AFTER_FORK: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
|
||||
console.log(`STATE_AFTER_TURN: ${JSON.stringify(stateAfterTurn ? summarizeFrame(stateAfterTurn) : null)}`)
|
||||
console.log(`LOOPBACK_REQUESTS: ${JSON.stringify(mock.requests)}`)
|
||||
console.log(`TOOL_CALL_SENT: ${mock.toolCallSent}`)
|
||||
console.log(`TRANSCRIPT_AFTER_TURN: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
|
||||
|
||||
await stopRun(first)
|
||||
const second = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
|
||||
runs.push(second)
|
||||
const hostToolsSecond = await installProbeHostTool(second, '2')
|
||||
const resumedState = await sendCommand(second, 'probe-state-2', { type: 'get_state' })
|
||||
const resumedMessages = await sendCommand(second, 'probe-messages-2', { type: 'get_messages' })
|
||||
console.log(`RESUME_READY: true`)
|
||||
console.log(`RESUME_SET_HOST_TOOLS: ${JSON.stringify(hostToolsSecond ? summarizeFrame(hostToolsSecond) : null)}`)
|
||||
console.log(`RESUME_GET_STATE: ${JSON.stringify(resumedState ? summarizeFrame(resumedState) : null)}`)
|
||||
console.log(`RESUME_GET_MESSAGES: ${JSON.stringify(resumedMessages ? {
|
||||
...summarizeFrame(resumedMessages),
|
||||
dataSummary: summarizeMessages(resumedMessages.data)
|
||||
} : null)}`)
|
||||
console.log(`RESUMED_TRANSCRIPT: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
|
||||
let currentRun: ProbeRun = second
|
||||
|
||||
const resumeTree = await sendCommand(second, 'probe-tree-2', { type: 'get_tree' })
|
||||
const resumeTreeData = resumeTree?.data && typeof resumeTree.data === 'object' ? resumeTree.data as Record<string, unknown> : {}
|
||||
const resumeUserNode = findUserNode(resumeTreeData.tree)
|
||||
console.log(`RESUME_TREE: ${JSON.stringify(resumeTree ? {
|
||||
...summarizeFrame(resumeTree), leafId: resumeTreeData.leafId, tree: summarizeTree(resumeTreeData.tree)
|
||||
} : null)}`)
|
||||
|
||||
if (resumeUserNode) {
|
||||
const branch = await sendCommand(second, 'probe-branch-1', { type: 'branch', entryId: resumeUserNode.id })
|
||||
const branchState = await sendCommand(second, 'probe-state-after-branch', { type: 'get_state' })
|
||||
const branchHistory = await sendCommand(second, 'probe-messages-after-branch', { type: 'get_messages' })
|
||||
const branchTree = await sendCommand(second, 'probe-tree-after-branch', { type: 'get_tree' })
|
||||
const branchTreeData = branchTree?.data && typeof branchTree.data === 'object' ? branchTree.data as Record<string, unknown> : {}
|
||||
console.log(`BRANCH_ENTRY_ID: ${JSON.stringify(resumeUserNode.id)}`)
|
||||
console.log(`BRANCH_RESPONSE: ${JSON.stringify(branch ? summarizeFrame(branch) : null)}`)
|
||||
console.log(`BRANCH_STATE: ${JSON.stringify(branchState ? summarizeFrame(branchState) : null)}`)
|
||||
console.log(`BRANCH_MESSAGES: ${JSON.stringify(branchHistory ? {
|
||||
...summarizeFrame(branchHistory),
|
||||
dataSummary: summarizeMessages(branchHistory.data)
|
||||
} : null)}`)
|
||||
console.log(`BRANCH_TREE: ${JSON.stringify(branchTree ? {
|
||||
...summarizeFrame(branchTree), leafId: branchTreeData.leafId, tree: summarizeTree(branchTreeData.tree)
|
||||
} : null)}`)
|
||||
if (branch?.success === true) {
|
||||
await stopRun(second)
|
||||
const branchResume = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
|
||||
runs.push(branchResume)
|
||||
currentRun = branchResume
|
||||
const resumeHostTools = await installProbeHostTool(branchResume, 'after-branch')
|
||||
const messagesAfterBranchResume = await sendCommand(branchResume, 'probe-messages-after-branch-resume', { type: 'get_messages' })
|
||||
const treeAfterBranchResume = await sendCommand(branchResume, 'probe-tree-after-branch-resume', { type: 'get_tree' })
|
||||
const treeAfterBranchResumeData = treeAfterBranchResume?.data && typeof treeAfterBranchResume.data === 'object'
|
||||
? treeAfterBranchResume.data as Record<string, unknown> : {}
|
||||
console.log(`BRANCH_RESUME_READY: true`)
|
||||
console.log(`BRANCH_RESUME_SET_HOST_TOOLS: ${JSON.stringify(resumeHostTools ? summarizeFrame(resumeHostTools) : null)}`)
|
||||
console.log(`BRANCH_RESUME_MESSAGES: ${JSON.stringify(messagesAfterBranchResume ? summarizeMessages(messagesAfterBranchResume.data) : null)}`)
|
||||
console.log(`BRANCH_RESUME_TREE_LEAF_ID: ${treeAfterBranchResumeData.leafId ?? null}`)
|
||||
console.log(`TRANSCRIPT_AFTER_BRANCH_WITHOUT_PROMPT: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
|
||||
}
|
||||
|
||||
const navigateTree = await sendLooseCommand(currentRun, 'probe-navigate-tree', {
|
||||
type: 'navigate_tree', entryId: resumeUserNode.id
|
||||
})
|
||||
const navigateToEntry = await sendLooseCommand(currentRun, 'probe-navigate-to-entry', {
|
||||
type: 'navigate_to_entry', entryId: resumeUserNode.id
|
||||
})
|
||||
const switchSession = await sendCommand(currentRun, 'probe-switch-session', {
|
||||
type: 'switch_session', sessionPath
|
||||
})
|
||||
const clear = await sendLooseCommand(currentRun, 'probe-clear', { type: 'clear' })
|
||||
console.log(`NAVIGATE_TREE_RESPONSE: ${JSON.stringify(navigateTree ? summarizeFrame(navigateTree) : null)}`)
|
||||
console.log(`NAVIGATE_TO_ENTRY_RESPONSE: ${JSON.stringify(navigateToEntry ? summarizeFrame(navigateToEntry) : null)}`)
|
||||
console.log(`SWITCH_SESSION_SAME_PATH: ${JSON.stringify(switchSession ? summarizeFrame(switchSession) : null)}`)
|
||||
console.log(`CLEAR_RESPONSE: ${JSON.stringify(clear ? summarizeFrame(clear) : null)}`)
|
||||
} else {
|
||||
console.log('BRANCH_ENTRY_ID: unavailable from get_tree parent relation')
|
||||
}
|
||||
await stopRun(currentRun)
|
||||
} finally {
|
||||
for (const run of runs) await stopRun(run)
|
||||
await new Promise<void>((resolveClose) => mock.server.close(() => resolveClose()))
|
||||
await rm(tempRoot, { recursive: true, force: true })
|
||||
if (existsSync(realProfilePath)) await rm(realProfilePath, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error('OMP protocol probe failed:', error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in new issue
Block a user