heartbeat added

This commit is contained in:
2026-05-12 22:39:36 -04:00
parent 1a93c9246f
commit d256ffb487
+45 -7
View File
@@ -140,18 +140,35 @@ Please follow your instructions to produce the funding outlook brief.`
}
// ----- Pipe agent activity to the client -----
// The agent emits multiple event types. We surface:
// - agent.message text blocks (narration)
// - agent.tool_use for the `write` tool, whose `input.content` IS the
// final brief the agent produces (it writes it as a Markdown file in
// the sandbox instead of streaming it back)
// - lightweight status lines for other tool calls so the UI keeps moving
// Long stretches between agent.message events (model "thinking",
// multi-tool batches, etc.) can silence the response stream long enough
// that Netlify's edge proxy drops the connection. To keep bytes flowing
// we (a) forward extra event types as concise status lines and (b) emit
// a periodic heartbeat space when the upstream goes quiet.
const encoder = new TextEncoder()
const seenEventIds = new Set()
const writtenFiles = new Set() // dedupe writes by path
const HEARTBEAT_MS = 10_000
const stream = new ReadableStream({
async start(controller) {
const send = (s) => controller.enqueue(encoder.encode(s))
let lastSendAt = Date.now()
const send = (s) => {
controller.enqueue(encoder.encode(s))
lastSendAt = Date.now()
}
// Zero-width space — invisible in rendered Markdown but counts as a
// byte on the wire, which is enough to defeat proxy idle-timeouts.
const heartbeat = setInterval(() => {
if (Date.now() - lastSendAt >= HEARTBEAT_MS) {
try {
controller.enqueue(encoder.encode('\u200B'))
lastSendAt = Date.now()
} catch {
/* controller may have closed */
}
}
}, HEARTBEAT_MS / 2)
try {
for await (const event of upstream) {
@@ -170,6 +187,11 @@ Please follow your instructions to produce the funding outlook brief.`
break
}
case 'agent.thinking': {
send(`\n\n_💭 thinking…_\n\n`)
break
}
case 'agent.tool_use': {
// The write tool carries the actual brief in input.content.
if (
@@ -194,6 +216,20 @@ Please follow your instructions to produce the funding outlook brief.`
break
}
case 'agent.tool_result':
case 'agent.mcp_tool_result': {
if (event.is_error) {
const msg =
(Array.isArray(event.content) &&
event.content[0]?.text) ||
'tool error'
send(`\n\n_⚠️ tool error: ${String(msg).slice(0, 200)}_\n\n`)
} else {
send(`\n\n_✓ result_\n\n`)
}
break
}
case 'session.error': {
const msg = event.error?.message || 'unknown session error'
send(`\n\n[session error: ${msg}]`)
@@ -211,8 +247,10 @@ Please follow your instructions to produce the funding outlook brief.`
break
}
}
clearInterval(heartbeat)
controller.close()
} catch (err) {
clearInterval(heartbeat)
send(`\n\n[stream error: ${err.message || err}]`)
controller.close()
}