feat: add structured part events to gateway for teamActivity, media, and error parts

Extends GatewayEvent union with structured_part variants, adds matching
HistoryPart types, wires SSE emission in runtime handleChat, and maps
agent-core content parts to history in session-state.
This commit is contained in:
Harivansh Rathi 2026-03-12 00:40:39 -04:00
parent a5d70ce55e
commit 4dc5e1b376
5 changed files with 165 additions and 2 deletions

View file

@ -7,6 +7,7 @@ import {
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { URL } from "node:url";
import type { AgentMessage } from "@mariozechner/companion-agent-core";
import type { AgentSession, AgentSessionEvent } from "../agent-session.js";
import type { Settings } from "../settings-manager.js";
import { extractMessageText, getLastAssistantText } from "./helpers.js";
@ -29,6 +30,7 @@ import type {
ModelInfo,
} from "./types.js";
import {
createGatewayStructuredPartListener,
createVercelStreamListener,
errorVercelStream,
extractUserText,
@ -567,6 +569,7 @@ export class GatewayRuntime {
sessionKey: managedSession.sessionKey,
text: extractMessageText(event.message),
});
this.emitStructuredParts(managedSession, event.message);
return;
}
if (event.message.role === "toolResult") {
@ -654,6 +657,73 @@ export class GatewayRuntime {
});
}
private emitStructuredParts(
managedSession: ManagedGatewaySession,
message: AgentMessage,
): void {
const content = message.content;
if (!Array.isArray(content)) return;
for (const part of content) {
if (typeof part !== "object" || part === null) continue;
const p = part as Record<string, unknown>;
if (p.type === "teamActivity") {
const teamId = typeof p.teamId === "string" ? p.teamId : "";
const status = typeof p.status === "string" ? p.status : "running";
if (!teamId) continue;
const rawMembers = Array.isArray(p.members) ? p.members : [];
const members = rawMembers
.filter((m): m is Record<string, unknown> => typeof m === "object" && m !== null)
.map((m) => ({
id: typeof m.id === "string" ? m.id : "",
name: typeof m.name === "string" ? m.name : "Teammate",
...(typeof m.role === "string" ? { role: m.role } : {}),
status: typeof m.status === "string" ? m.status : "running",
...(typeof m.message === "string" ? { message: m.message } : {}),
}))
.filter((m) => m.id.length > 0);
this.emit(managedSession, {
type: "structured_part",
sessionKey: managedSession.sessionKey,
partType: "teamActivity",
payload: { teamId, status, members },
});
continue;
}
if (p.type === "image") {
const url = typeof p.url === "string" ? p.url : "";
if (!url) continue;
this.emit(managedSession, {
type: "structured_part",
sessionKey: managedSession.sessionKey,
partType: "media",
payload: {
url,
...(typeof p.mimeType === "string" ? { mimeType: p.mimeType } : {}),
},
});
continue;
}
if (p.type === "error") {
const errorMessage = typeof p.message === "string" ? p.message : "";
if (!errorMessage) continue;
this.emit(managedSession, {
type: "structured_part",
sessionKey: managedSession.sessionKey,
partType: "error",
payload: {
code: typeof p.code === "string" ? p.code : "unknown",
message: errorMessage,
},
});
continue;
}
}
}
private createSessionState(
managedSession: ManagedGatewaySession,
): GatewaySessionState {
@ -1106,7 +1176,9 @@ export class GatewayRuntime {
response.write("\n");
const listener = createVercelStreamListener(response);
const structuredPartListener = createGatewayStructuredPartListener(response);
let unsubscribe: (() => void) | undefined;
let unsubscribeStructured: (() => void) | undefined;
let streamingActive = false;
const stopStreaming = () => {
@ -1114,6 +1186,8 @@ export class GatewayRuntime {
streamingActive = false;
unsubscribe?.();
unsubscribe = undefined;
unsubscribeStructured?.();
unsubscribeStructured = undefined;
};
// Clean up on client disconnect
@ -1135,6 +1209,10 @@ export class GatewayRuntime {
onStart: () => {
if (clientDisconnected || streamingActive) return;
unsubscribe = managedSession.session.subscribe(listener);
managedSession.listeners.add(structuredPartListener);
unsubscribeStructured = () => {
managedSession.listeners.delete(structuredPartListener);
};
streamingActive = true;
},
onFinish: () => {