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.
This commit is contained in:
User
2026-07-13 00:22:54 +02:00
parent ac333f99ac
commit 34f1fa271f
2 changed files with 15 additions and 12 deletions
+12 -10
View File
@@ -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
});
}
+3 -2
View File
@@ -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({