From 34f1fa271fac318e8891ea4387e751ebd130e222 Mon Sep 17 00:00:00 2001 From: User Date: Mon, 13 Jul 2026 00:22:54 +0200 Subject: [PATCH] fix(bridge): forward tool/lifecycle events so user sees agent progress The SessionBridge only forwarded text_delta and system_event types. All tool_execution_*, agent_start, and turn_end events from the Pi process were silently dropped, so the user saw a blank session even though the agent was actively running (calling tools, querying the DB). Forward: - tool_execution_start/end as info events (visible in stepMessages) - agent_start, turn_end as system_event (lifecycle tracking) Also: log Pi stderr instead of draining silently, for debugging. --- backend/src/bridge/session-bridge.ts | 22 ++++++++++++---------- backend/src/pi/pi-process-manager.ts | 5 +++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/backend/src/bridge/session-bridge.ts b/backend/src/bridge/session-bridge.ts index a9a634c9..e1330f8a 100644 --- a/backend/src/bridge/session-bridge.ts +++ b/backend/src/bridge/session-bridge.ts @@ -18,29 +18,31 @@ export class SessionBridge { constructor(private rpc: RpcClient) { rpc.on("event", (m) => { if (m.type === "extension_ui_request" && m.method === "input") { - let descriptor: any; - try { descriptor = JSON.parse(m.title); } catch { return; } + let descriptor: unknown; + try { descriptor = JSON.parse(m.title as string); } catch { return; } this.pending = descriptor; - this.pendingPiId = m.id; + this.pendingPiId = m.id as string | null; this.fan({ type: "ui_request", ui_request: descriptor }); } else if (m.type === "extension_ui_request" && m.method === "notify") { this.fan({ type: "info", level: m.notifyType ?? "info", text: m.message ?? "" }); } else if (m.type === "message_update" && m.assistantMessageEvent?.type === "text_delta") { - // Real Pi streams assistant text as message_update events whose nested - // assistantMessageEvent carries the incremental `delta`. Map it to the FE's - // text_delta contract. (Top-level text_delta below is what the fake-pi-rpc emits.) this.fan({ type: "text_delta", text: m.assistantMessageEvent.delta ?? "" }); } else if (m.type === "text_delta") { this.fan({ type: "text_delta", text: m.text ?? "" }); + } else if (m.type === "tool_execution_start") { + this.fan({ type: "info", level: "info", text: `▶ ${m.toolName ?? "tool"}: ${JSON.stringify(m.args ?? {}).slice(0, 200)}` }); + } else if (m.type === "tool_execution_end") { + const summary = m.result?.content?.[0]?.text?.slice(0, 200) ?? ""; + this.fan({ type: "info", level: "info", text: summary ? `✓ ${m.toolName ?? "tool"}: ${summary}` : `✓ ${m.toolName ?? "tool"}` }); } else if (m.type === "system_event") { this.fan(m as ClientEvent); } else if (m.type === "agent_end") { - // Fine turno di Pi (pi-agent-core agent-loop): e' l'unico segnale che il turno e' - // concluso. Senza inoltrarlo, il FE resta "working" per sempre quando il turno - // finisce senza un gate successivo (ultimo step del workflow). this.fan({ type: "system_event", event: "agent_end" }); + } else if (m.type === "agent_start") { + this.fan({ type: "system_event", event: "agent_start" }); + } else if (m.type === "turn_end") { + this.fan({ type: "system_event", event: "turn_end" }); } - // altri method nativi (setStatus/setWidget) e altri eventi Pi ignorati in MVP }); } diff --git a/backend/src/pi/pi-process-manager.ts b/backend/src/pi/pi-process-manager.ts index 70e2d6f3..2f95c613 100644 --- a/backend/src/pi/pi-process-manager.ts +++ b/backend/src/pi/pi-process-manager.ts @@ -59,8 +59,8 @@ export class PiProcessManager { cwd: this.cfg.harnessDir, env, }); - // Drain stderr so the child's stderr buffer never blocks the process. - child.stderr.resume?.(); + // Log stderr for debugging (was silently drained) + child.stderr.on("data", (d: Buffer) => console.error(`[pi:${sessionId}] stderr:`, d.toString().trim())); return child; } @@ -95,6 +95,7 @@ export class PiProcessManager { // exit event fires, so reaching this branch with `rt` still mapped means the child // died on its own: tell the client, or the UI spins forever waiting for a turn end. child.on("exit", (code) => { + console.error(`[pi:${sessionId}] exited code=${code ?? "?"} mapped=${this.runtimes.get(sessionId) === rt}`); if (this.runtimes.get(sessionId) === rt) { this.runtimes.delete(sessionId); rt.bridge.emitClientEvent({