mirror of
https://github.com/codeany-ai/open-agent-sdk-typescript.git
synced 2026-04-25 07:00:49 +09:00
67e120b2ed
Open-source Agent SDK with 30+ built-in tools, MCP integration, multi-turn sessions, subagents, and streaming support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
158 lines
4.4 KiB
TypeScript
158 lines
4.4 KiB
TypeScript
/**
|
|
* Web Chat Server
|
|
*
|
|
* A lightweight HTTP server providing:
|
|
* GET / — serves the chat UI
|
|
* POST /api/chat — SSE stream of agent events
|
|
* POST /api/new — resets the session
|
|
*
|
|
* Run: npx tsx examples/web/server.ts
|
|
*/
|
|
|
|
import { createServer, type IncomingMessage, type ServerResponse } from 'http'
|
|
import { readFile } from 'fs/promises'
|
|
import { join, dirname } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
import { createAgent, type Agent } from '../../src/index.js'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const PORT = parseInt(process.env.PORT || '8081')
|
|
|
|
let agent: Agent | null = null
|
|
|
|
function getOrCreateAgent(): Agent {
|
|
if (!agent) {
|
|
agent = createAgent({
|
|
model: process.env.CODEANY_MODEL || 'claude-sonnet-4-6',
|
|
maxTurns: 20,
|
|
})
|
|
}
|
|
return agent
|
|
}
|
|
|
|
function resetAgent(): void {
|
|
agent?.close().catch(() => {})
|
|
agent = null
|
|
}
|
|
|
|
/** Read the full request body as a string. */
|
|
function readBody(req: IncomingMessage): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks: Buffer[] = []
|
|
req.on('data', (c: Buffer) => chunks.push(c))
|
|
req.on('end', () => resolve(Buffer.concat(chunks).toString()))
|
|
req.on('error', reject)
|
|
})
|
|
}
|
|
|
|
/** Handle POST /api/chat — SSE stream */
|
|
async function handleChat(req: IncomingMessage, res: ServerResponse) {
|
|
const body = JSON.parse(await readBody(req))
|
|
const prompt = body.message?.trim()
|
|
if (!prompt) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ error: 'empty message' }))
|
|
return
|
|
}
|
|
|
|
// SSE headers
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Accel-Buffering': 'no',
|
|
})
|
|
|
|
const send = (event: string, data: unknown) => {
|
|
res.write(`data: ${JSON.stringify({ event, data })}\n\n`)
|
|
}
|
|
|
|
const ag = getOrCreateAgent()
|
|
const startMs = Date.now()
|
|
|
|
try {
|
|
for await (const ev of ag.query(prompt)) {
|
|
switch (ev.type) {
|
|
case 'assistant': {
|
|
for (const block of ev.message.content) {
|
|
if (block.type === 'text') {
|
|
send('text', { text: block.text })
|
|
} else if (block.type === 'tool_use') {
|
|
send('tool_use', {
|
|
id: block.id,
|
|
name: block.name,
|
|
input: block.input,
|
|
})
|
|
} else if ('thinking' in block) {
|
|
send('thinking', { thinking: (block as any).thinking })
|
|
}
|
|
}
|
|
break
|
|
}
|
|
case 'tool_result':
|
|
send('tool_result', {
|
|
tool_use_id: ev.result.tool_use_id,
|
|
content: ev.result.output,
|
|
is_error: false,
|
|
})
|
|
break
|
|
case 'result':
|
|
send('result', {
|
|
num_turns: ev.num_turns ?? 0,
|
|
input_tokens: ev.usage?.input_tokens ?? 0,
|
|
output_tokens: ev.usage?.output_tokens ?? 0,
|
|
cost: ev.total_cost_usd ?? ev.cost ?? 0,
|
|
duration_ms: Date.now() - startMs,
|
|
})
|
|
break
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
send('error', { message: err.message })
|
|
}
|
|
|
|
send('done', null)
|
|
res.end()
|
|
}
|
|
|
|
/** Handle POST /api/new */
|
|
function handleNewSession(_req: IncomingMessage, res: ServerResponse) {
|
|
resetAgent()
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ ok: true }))
|
|
}
|
|
|
|
/** Serve the static index.html */
|
|
async function serveIndex(_req: IncomingMessage, res: ServerResponse) {
|
|
const html = await readFile(join(__dirname, 'index.html'), 'utf-8')
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
res.end(html)
|
|
}
|
|
|
|
// --- HTTP Server ---
|
|
|
|
const server = createServer(async (req, res) => {
|
|
const url = req.url || '/'
|
|
const method = req.method || 'GET'
|
|
|
|
try {
|
|
if (url === '/' && method === 'GET') return await serveIndex(req, res)
|
|
if (url === '/api/chat' && method === 'POST') return await handleChat(req, res)
|
|
if (url === '/api/new' && method === 'POST') return handleNewSession(req, res)
|
|
|
|
res.writeHead(404)
|
|
res.end('Not Found')
|
|
} catch (err: any) {
|
|
console.error(err)
|
|
if (!res.headersSent) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
}
|
|
res.end(JSON.stringify({ error: err.message }))
|
|
}
|
|
})
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`\n Open Agent SDK — Web Chat`)
|
|
console.log(` http://localhost:${PORT}\n`)
|
|
})
|