import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { RpcClient } from './rpc.mjs'; const args = process.argv.slice(2); const live = args.includes('--live'); const handshake = args.includes('--handshake'); if (live && handshake) throw new Error('--live と --handshake は同時に使えません'); const resumeAt = args.indexOf('--resume'); const resumeId = resumeAt >= 0 ? args[resumeAt + 1] : null; if (resumeAt >= 0 && (!resumeId || resumeId.startsWith('--'))) throw new Error('--resume に threadId が必要です'); const proc = live || handshake ? spawn('codex', ['app-server'], { stdio: ['pipe', 'pipe', 'inherit'] }) : spawn(process.execPath, [fileURLToPath(new URL('./mock-server.mjs', import.meta.url))], { stdio: ['pipe', 'pipe', 'inherit'], env: { ...process.env, GUIDE_MOCK_APPROVAL: args.includes('--approval') ? '1' : '0' } }); const completed = new Map(); const messages = new Map(); const key = (threadId, turnId) => JSON.stringify([threadId, turnId]); let rpc; let deadline; try { rpc = new RpcClient(proc, { onServerRequest: m => { if (['item/commandExecution/requestApproval', 'item/fileChange/requestApproval'].includes(m.method)) { console.log(`[approval] ${m.method} → decline(教材の既定値)`); return { decision: 'decline' }; } // 未対応要求を許可したことにしない。RpcClient が -32601 を返す。 }, onNotification: m => { const p = m.params; if (m.method === 'item/agentMessage/delta') { const k = JSON.stringify([p.threadId, p.turnId, p.itemId]); messages.set(k, (messages.get(k) ?? '') + p.delta); process.stdout.write(p.delta); } if (m.method === 'item/completed' && p.item?.type === 'agentMessage') { const k = JSON.stringify([p.threadId, p.turnId, p.item.id]); messages.set(k, p.item.text); // 確定内容で置換。delta に追記しない。 } if (m.method === 'turn/completed') completed.set(key(p.threadId, p.turn.id), p.turn); } }); await rpc.request('initialize', { clientInfo: { name: 'app_server_handson', title: 'App Server Hands-on', version: '1.0.0' } }); rpc.notify('initialized'); console.log('[ready] initialize → initialized'); if (!handshake) { const params = { cwd: process.cwd(), sandbox: 'read-only', approvalPolicy: 'never' }; const result = resumeId ? await rpc.request('thread/resume', { ...params, threadId: resumeId }) : await rpc.request('thread/start', params); const threadId = result.thread.id; console.log(`[thread] ${threadId}`); const { turn } = await rpc.request('turn/start', { threadId, input: [{ type: 'text', text: 'ツールを使わず、日本語で「接続できました」とだけ返してください。', text_elements: [] }] }); const until = Date.now() + 120000; while (!completed.has(key(threadId, turn.id))) { if (rpc.closed) throw new Error('完了通知の前に接続が終了しました'); if (Date.now() > until) { // タイムアウトは完了ではない。停止を要求し、成功と表示しない。 await rpc.request('turn/interrupt', { threadId, turnId: turn.id }).catch(() => {}); throw new Error('turn timeout: 停止要求を送信しました。状態を確認してください'); } await new Promise(resolve => { deadline = setTimeout(resolve, 20); }); } const final = completed.get(key(threadId, turn.id)); console.log(`\n[turn] ${final.id}: ${final.status}`); console.log('[final]', [...messages.values()].join('\n')); if (final.status !== 'completed') throw new Error(`turn ${final.status}: ${final.error?.message ?? ''}`); } } catch (error) { console.error(error.message); process.exitCode = 1; } finally { clearTimeout(deadline); rpc?.close(); if (!rpc) proc.kill(); }