From d7924a9d6fb47ae6541b1de43bdc21ca3e53f133 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Sat, 9 May 2026 07:16:50 -0400 Subject: [PATCH] GadgetLogTransportSocket and the drone-to-IDE log --- docs/drone-logger.md | 135 +++++++++-- .../frontend/src/components/LogPanel.tsx | 102 ++++++--- gadget-code/frontend/src/lib/socket.ts | 30 ++- .../frontend/src/pages/ChatSessionView.tsx | 34 ++- gadget-code/src/lib/code-session.ts | 210 ++++++++++-------- gadget-code/src/lib/drone-session.ts | 24 ++ gadget-drone/src/gadget-drone.ts | 14 +- gadget-drone/src/lib/service.ts | 2 +- gadget-drone/src/services/agent.ts | 2 +- packages/api/src/index.ts | 1 + packages/api/src/lib/log-transport-socket.ts | 30 +++ 11 files changed, 444 insertions(+), 140 deletions(-) create mode 100644 packages/api/src/lib/log-transport-socket.ts diff --git a/docs/drone-logger.md b/docs/drone-logger.md index 8dd8aa5..55d1a33 100644 --- a/docs/drone-logger.md +++ b/docs/drone-logger.md @@ -1,32 +1,131 @@ # Gadget Code: GadgetLogTransportSocket -We recently finished a large refactor of GadgetLog into the @gadget/api package, and all components are now consumers of this one unified logging interface. [GadgetLog](../packages/api/src/lib/log.ts) makes use of [GadgetLogTransport](../packages/api/src/lib/log-transport.ts) to provide an abstract log transport. The logging system is working as intended. Console logging works as expected. File logging works as expected. +GadgetLog now ships four transports out of the box: **Console**, **File**, **Socket**, and the abstract base. The socket transport (`GadgetLogTransportSocket`) transmits every log entry from `gadget-drone` → `gadget-code:backend` → `gadget-code:frontend` over Socket.IO, rendering live in the IDE's LogPanel. -We are now going to implement `GadgetLogTransportSocket`, which will transmit a Socket.IO message `log` from `gadget-drone` to `gadget-code:backend`, which will forward the log message directly to [ChatSessionView](../gadget-code/frontend/src/pages/ChatSessionView.tsx) for display in [LogPanel](../gadget-code/frontend/src/components/LogPanel.tsx). +## Message Flow -I have already added the `LogMessage` type to the drone message definitions, and added definitions to the appropriate interfaces we use for extending Socket.IO. It is a fire-n-forget message (no callback, no blocking). +``` +gadget-drone + │ GadgetLogTransportSocket.writeLog() + │ socket.emit("log", timestamp, component, level, message, metadata?) + ▼ +gadget-code:backend (DroneSession.onLog) + │ SocketService.getCodeSessionByChatSessionId(chatSessionId) + │ codeSession.onLog(...) + ▼ +gadget-code:backend (CodeSession.onLog) + │ socket.emit("log", timestamp, component, level, message, metadata?) + ▼ +gadget-code:frontend (SocketClient) + │ raw socket "log" → internal "log:entry" event + │ { level, component, message, metadata, timestamp } + ▼ +ChatSessionView.handleLogEntry + │ setLogs(...) + ▼ +LogPanel — terminal-styled live log viewer +``` -## How It Works +## Transport: `GadgetLogTransportSocket` -You will simply pass the parameters passed to the GadgetLog API as a message over Socket.IO to the Log panel dislay in the IDE. +**File:** `packages/api/src/lib/log-transport-socket.ts` -## Instructions +Accepts a generic emit function (no Socket.IO dependency in `@gadget/api`): -In this session, you will: +```typescript +export type LogEmitFunction = ( + event: string, + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, +) => void; +``` -1. Implement [GadgetLogTransportSocket](../packages/api/src/lib/log-transport-socket.ts) -2. Register it for use as a default transport on the gadget-drone logger instance -3. Implement session message routing in gadget-code:backend to forward the message to gadget-code:frontend (the IDE) -4. Deliver log messages to the Chat Session view's Log panel for display +`writeLog()` calls `this.emit("log", ...)` with the same parameters `GadgetLog.writeLog()` receives. -Log messages should render as closely to the Console Transport's output as possible, matching the colors used, and style. +## Registration in gadget-drone -When you are done, you will re-write this document in the style +Registered in `gadget-drone.ts` `connectSocket()`, after the Socket.IO connection succeeds: -## References +```typescript +const socketTransport = new GadgetLogTransportSocket((event, ...args) => { + (this.socket as any)?.emit(event, ...args); +}); +GadgetLog.addDefaultTransport(socketTransport); +this.log.transports.push(socketTransport); +AgentService.log.transports.push(socketTransport); +AiService.log.transports.push(socketTransport); +PlatformService.log.transports.push(socketTransport); +``` -Always search first in the project's `docs` directories for information and knowledge. Here are some starting points for this session: +The transport is injected into all existing loggers (main process + 3 services) and set as a default for future `GadgetLog` instances. -- [UI Design and Style Guide](../gadget-code/docs/ui-design-guide.md)] -- [System Architecture](./architecture.md) -- [Socket Protocol](./socket-protocol.md) +## Backend Routing + +### DroneSession (`gadget-code/src/lib/drone-session.ts`) + +- Registers `this.socket.on("log", this.onLog.bind(this))` +- `onLog()` guards on `this.chatSessionId`, looks up the `CodeSession` via `SocketService.getCodeSessionByChatSessionId()`, forwards the call. + +### CodeSession (`gadget-code/src/lib/code-session.ts`) + +- `onLog()` calls `this.socket.emit("log", timestamp, component, level, message, metadata)` forwards to the IDE. + +## Frontend Bridging + +### SocketClient (`gadget-code/frontend/src/lib/socket.ts`) + +The raw Socket.IO `log` event is forwarded to the internal `log:entry` event system: + +```typescript +this.socket.on("log", (timestamp, component, level, message, metadata) => { + this.emit("log:entry", { + level, + component: component.name, + message, + metadata, + timestamp: new Date(timestamp).getTime(), + }); +}); +``` + +### ChatSessionView + +`handleLogEntry` callback builds a `LogEntry` with all fields and appends to the `logs` state array. + +## LogPanel Display + +**File:** `gadget-code/frontend/src/components/LogPanel.tsx` + +Styled to match the Console Transport: + +| Element | Console Color | Tailwind Class | +|---------|---------------|----------------| +| Timestamp | `darkGray` | `text-gray-600` | +| Level: debug | `darkGray` | `text-gray-500` | +| Level: info | `green` | `text-green-400` | +| Level: warn | `yellow` | `text-yellow-400` | +| Level: alert | `red` | `text-red-400` | +| Level: error | `bgRed` + `white` | `bg-red-800 text-white` | +| Level: crit | `bgRed` + `yellow` | `bg-red-800 text-yellow-300` | +| Level: fatal | `bgRed` + `darkGray` | `bg-red-800 text-gray-400` | +| Component | `cyan` | `text-cyan-400` | +| Message | `darkGray` | `text-text-secondary` | + +Metadata is displayed as expandable JSON below the log line (toggled with `⊞`/`⊟`). + +## Files Changed + +| File | Action | +|------|--------| +| `packages/api/src/lib/log-transport-socket.ts` | **NEW** — socket transport | +| `packages/api/src/index.ts` | **MODIFY** — export new transport | +| `gadget-drone/src/gadget-drone.ts` | **MODIFY** — register transport on connect | +| `gadget-drone/src/lib/service.ts` | **MODIFY** — `log` public for transport injection | +| `gadget-code/src/lib/drone-session.ts` | **MODIFY** — `onLog` handler | +| `gadget-code/src/lib/code-session.ts` | **MODIFY** — `onLog` forwarding | +| `gadget-code/frontend/src/lib/socket.ts` | **MODIFY** — bridge `log` → `log:entry` | +| `gadget-code/frontend/src/pages/ChatSessionView.tsx` | **MODIFY** — enriched `LogEntry` | +| `gadget-code/frontend/src/components/LogPanel.tsx` | **REWRITE** — console-styled display | diff --git a/gadget-code/frontend/src/components/LogPanel.tsx b/gadget-code/frontend/src/components/LogPanel.tsx index 7fda189..ecbed37 100644 --- a/gadget-code/frontend/src/components/LogPanel.tsx +++ b/gadget-code/frontend/src/components/LogPanel.tsx @@ -1,22 +1,44 @@ -import { useRef, useEffect } from 'react'; -import { WorkspaceMode } from '../lib/types'; +import { useRef, useEffect, useState } from 'react'; interface LogEntry { id: string; timestamp: Date; level: string; + component: string; message: string; + metadata?: unknown; } interface LogPanelProps { logs: LogEntry[]; expanded: boolean; - workspaceMode: WorkspaceMode; onToggleExpand: () => void; } -export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand }: LogPanelProps) { +const levelColors: Record = { + debug: 'text-gray-500', + info: 'text-green-400', + warn: 'text-yellow-400', + alert: 'text-red-400', + error: 'bg-red-800 text-white', + crit: 'bg-red-800 text-yellow-300', + fatal: 'bg-red-800 text-gray-400', +}; + +function formatTimestamp(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + const hh = String(date.getHours()).padStart(2, '0'); + const mm = String(date.getMinutes()).padStart(2, '0'); + const ss = String(date.getSeconds()).padStart(2, '0'); + const ms = String(date.getMilliseconds()).padStart(3, '0'); + return `${y}-${m}-${d} ${hh}:${mm}:${ss}.${ms}`; +} + +export default function LogPanel({ logs, expanded, onToggleExpand }: LogPanelProps) { const scrollRef = useRef(null); + const [expandedMetadata, setExpandedMetadata] = useState>(new Set()); useEffect(() => { if (scrollRef.current) { @@ -24,6 +46,18 @@ export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand } }, [logs]); + const toggleMetadata = (id: string) => { + setExpandedMetadata(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + return (
{logs.length === 0 ? (
No log entries
) : ( - logs.map((entry) => ( -
- - {entry.timestamp.toLocaleTimeString()} - - - [{entry.level.toUpperCase()}] - - {entry.message} -
- )) + logs.map((entry) => { + const levelClass = levelColors[entry.level] || 'text-text-secondary'; + return ( +
+ + {formatTimestamp(entry.timestamp)} + + + {entry.level.toUpperCase().padEnd(5)} + + + {entry.component} + + + {entry.message} + + {entry.metadata && ( + + )} + {entry.metadata && expandedMetadata.has(entry.id) && ( +
+
+                      {JSON.stringify(entry.metadata, null, 2)}
+                    
+
+ )} +
+ ); + }) )}
); -} \ No newline at end of file +} diff --git a/gadget-code/frontend/src/lib/socket.ts b/gadget-code/frontend/src/lib/socket.ts index 7b9f72d..b33cbfe 100644 --- a/gadget-code/frontend/src/lib/socket.ts +++ b/gadget-code/frontend/src/lib/socket.ts @@ -17,6 +17,13 @@ export interface ServerToClientEvents { success: boolean, message?: string, ) => void; + log: ( + timestamp: string, + component: { name: string; slug: string }, + level: string, + message: string, + metadata?: unknown, + ) => void; } export interface ClientToServerEvents { @@ -72,7 +79,9 @@ export interface SocketEvents { "agent:complete": (data: { agentId: string }) => void; "log:entry": (data: { level: string; + component: string; message: string; + metadata?: unknown; timestamp: number; }) => void; "chat:message": (data: { @@ -152,6 +161,25 @@ class SocketClient { this.emit("workspaceModeChanged", mode); }); + this.socket.on( + "log", + ( + timestamp: string, + component: { name: string; slug: string }, + level: string, + message: string, + metadata?: unknown, + ) => { + this.emit("log:entry", { + level, + component: component.name, + message, + metadata, + timestamp: new Date(timestamp).getTime(), + }); + }, + ); + this._socket.on("connect", () => { this.reconnectAttempts = 0; this.emit("connect"); @@ -297,7 +325,7 @@ class SocketClient { startSessionHeartbeat(): void { if (this.heartbeatInterval) return; this.heartbeatInterval = setInterval(() => { - if (this._socket?.connected) { + if (this._socket) { this._socket.emit("sessionHeartbeat", (ack: boolean) => { if (!ack) { console.warn("sessionHeartbeat: drone did not acknowledge"); diff --git a/gadget-code/frontend/src/pages/ChatSessionView.tsx b/gadget-code/frontend/src/pages/ChatSessionView.tsx index c4040a2..4159d5a 100644 --- a/gadget-code/frontend/src/pages/ChatSessionView.tsx +++ b/gadget-code/frontend/src/pages/ChatSessionView.tsx @@ -13,7 +13,9 @@ interface LogEntry { id: string; timestamp: Date; level: string; + component: string; message: string; + metadata?: unknown; } interface StreamingState { @@ -107,6 +109,33 @@ export default function ChatSessionView() { }; }, [session, project]); + // Re-lock on socket reconnect to restore lock on a new CodeSession + const handleSocketReconnect = useCallback(async () => { + if (!sessionRef.current || !projectRef.current) return; + const droneJson = localStorage.getItem('dtp_drone_registration'); + if (!droneJson) return; + try { + const registration = JSON.parse(droneJson); + const success = await socketClient.requestSessionLock( + registration, + projectRef.current, + sessionRef.current, + ); + if (!success) { + console.warn('Failed to re-lock drone after socket reconnect'); + } + } catch (err) { + console.error('Failed to re-lock drone after socket reconnect', err); + } + }, []); + + useEffect(() => { + socketClient.on('connect', handleSocketReconnect); + return () => { + socketClient.off('connect', handleSocketReconnect); + }; + }, [handleSocketReconnect]); + // Release session lock on unmount only useEffect(() => { return () => { @@ -409,14 +438,16 @@ export default function ChatSessionView() { setWorkspaceMode(mode as WorkspaceMode); }, []); - const handleLogEntry = useCallback((data: { level: string; message: string; timestamp: number }) => { + const handleLogEntry = useCallback((data: { level: string; component: string; message: string; metadata?: unknown; timestamp: number }) => { setLogs(prev => [ ...prev, { id: `log-${Date.now()}-${Math.random()}`, timestamp: new Date(data.timestamp), level: data.level, + component: data.component, message: data.message, + metadata: data.metadata, }, ]); }, []); @@ -756,7 +787,6 @@ export default function ChatSessionView() { setLogExpanded(!logExpanded)} /> diff --git a/gadget-code/src/lib/code-session.ts b/gadget-code/src/lib/code-session.ts index 3b76b86..9cc0361 100644 --- a/gadget-code/src/lib/code-session.ts +++ b/gadget-code/src/lib/code-session.ts @@ -8,6 +8,8 @@ import { SocketSessionType, } from "./socket-session.js"; import { + GadgetComponent, + GadgetLogLevel, IChatSession, IDroneRegistration, IProject, @@ -43,10 +45,7 @@ export class CodeSession extends SocketSession { this.onRequestWorkspaceMode.bind(this), ); this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this)); - this.socket.on( - "releaseSessionLock", - this.onReleaseSessionLock.bind(this), - ); + this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this)); this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this)); } @@ -96,23 +95,27 @@ export class CodeSession extends SocketSession { chatSession: IChatSession, cb: (success: boolean, chatSessionId: string) => void, ) { - const droneSession = SocketService.getDroneSession(registration); - droneSession.socket.emit( - "requestSessionLock", - registration, - project, - chatSession, - (success: boolean, chatSessionId: string): void => { - if (success) { - this.selectedDrone = registration; - this.chatSession = chatSession; - this.project = project; - SocketService.registerChatSession(chatSession._id, this); - droneSession.setChatSessionId(chatSession._id); - } - cb(success, chatSessionId); - }, - ); + try { + const droneSession = SocketService.getDroneSession(registration); + droneSession.socket.emit( + "requestSessionLock", + registration, + project, + chatSession, + (success: boolean, chatSessionId: string): void => { + if (success) { + this.selectedDrone = registration; + this.chatSession = chatSession; + this.project = project; + SocketService.registerChatSession(chatSession._id, this); + droneSession.setChatSessionId(chatSession._id); + } + cb(success, chatSessionId); + }, + ); + } catch (error) { + cb(false, ""); + } } /** @@ -136,20 +139,24 @@ export class CodeSession extends SocketSession { return cb(false, this.workspaceMode); } - const droneSession = SocketService.getDroneSession(this.selectedDrone); - droneSession.socket.emit( - "requestWorkspaceMode", - registration, - project, - chatSession, - mode, - (success: boolean, currentMode: WorkspaceMode) => { - if (success) { - this.workspaceMode = currentMode; - } - cb(success, currentMode); - }, - ); + try { + const droneSession = SocketService.getDroneSession(this.selectedDrone); + droneSession.socket.emit( + "requestWorkspaceMode", + registration, + project, + chatSession, + mode, + (success: boolean, currentMode: WorkspaceMode) => { + if (success) { + this.workspaceMode = currentMode; + } + cb(success, currentMode); + }, + ); + } catch (error) { + cb(false, this.workspaceMode); + } } /** @@ -173,43 +180,47 @@ export class CodeSession extends SocketSession { return cb(false, { message: "No project selected" }); } - const droneSession = SocketService.getDroneSession(this.selectedDrone); + try { + const droneSession = SocketService.getDroneSession(this.selectedDrone); - let turn: ChatTurnDocument = await ChatSessionService.createTurn( - this.chatSession, - content, - ); - this.currentTurnId = turn._id; + let turn: ChatTurnDocument = await ChatSessionService.createTurn( + this.chatSession, + content, + ); + this.currentTurnId = turn._id; - this.log.info("ChatTurn created", { - turnId: turn._id, - chatSessionId: this.chatSession._id, - }); + this.log.info("ChatTurn created", { + turnId: turn._id, + chatSessionId: this.chatSession._id, + }); - droneSession.setCurrentTurnId(turn._id); - cb(true, { turnId: turn._id, message: "turn created successfully" }); + droneSession.setCurrentTurnId(turn._id); + cb(true, { turnId: turn._id, message: "turn created successfully" }); - droneSession.socket.emit( - "processWorkOrder", - this.selectedDrone, - turn, - async (success: boolean, message?: string) => { - if (success) { - this.log.info("work order accepted by drone", { - turnId: turn._id, - message, - }); - } else { - this.log.error("work order rejected by drone", { - turnId: turn._id, - message, - }); - turn.status = ChatTurnStatus.Error; - turn.errorMessage = message || "Drone rejected work order"; - await turn.save(); - } - }, - ); + droneSession.socket.emit( + "processWorkOrder", + this.selectedDrone, + turn, + async (success: boolean, message?: string) => { + if (success) { + this.log.info("work order accepted by drone", { + turnId: turn._id, + message, + }); + } else { + this.log.error("work order rejected by drone", { + turnId: turn._id, + message, + }); + turn.status = ChatTurnStatus.Error; + turn.errorMessage = message || "Drone rejected work order"; + await turn.save(); + } + }, + ); + } catch (error) { + cb(false, {}); + } } /** @@ -222,23 +233,27 @@ export class CodeSession extends SocketSession { chatSession: IChatSession, cb: (success: boolean) => void, ) { - const droneSession = SocketService.getDroneSession(registration); - droneSession.socket.emit( - "releaseSessionLock", - registration, - project, - chatSession, - (success: boolean) => { - if (success) { - SocketService.unregisterChatSession(chatSession._id); - droneSession.chatSessionId = undefined; - this.selectedDrone = undefined; - this.chatSession = undefined; - this.project = undefined; - } - cb(success); - }, - ); + try { + const droneSession = SocketService.getDroneSession(registration); + droneSession.socket.emit( + "releaseSessionLock", + registration, + project, + chatSession, + (success: boolean) => { + if (success) { + SocketService.unregisterChatSession(chatSession._id); + droneSession.chatSessionId = undefined; + this.selectedDrone = undefined; + this.chatSession = undefined; + this.project = undefined; + } + cb(success); + }, + ); + } catch (error) { + cb(false); + } } /** @@ -249,10 +264,17 @@ export class CodeSession extends SocketSession { if (!this.selectedDrone) { return cb(false); } - const droneSession = SocketService.getDroneSession(this.selectedDrone); - droneSession.socket.emit("sessionHeartbeat", (ack: boolean) => { - cb(ack); - }); + try { + const droneSession = SocketService.getDroneSession(this.selectedDrone); + droneSession.socket.emit("sessionHeartbeat", (ack: boolean) => { + cb(ack); + }); + } catch (error) { + this.log.error("failed to forward session heartbeat to drone", { + error, + }); + cb(false); + } } /** @@ -268,6 +290,16 @@ export class CodeSession extends SocketSession { this.socket.emit("status", content); } + onLog( + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, + ): void { + this.socket.emit("log", timestamp, component, level, message, metadata); + } + onThinking(content: string): void { this.socket.emit("thinking", content); } diff --git a/gadget-code/src/lib/drone-session.ts b/gadget-code/src/lib/drone-session.ts index 77b8c63..79e93b8 100644 --- a/gadget-code/src/lib/drone-session.ts +++ b/gadget-code/src/lib/drone-session.ts @@ -3,6 +3,8 @@ // All Rights Reserved import { + GadgetComponent, + GadgetLogLevel, IUser, IDroneRegistration, ChatTurnStatus, @@ -58,6 +60,28 @@ export class DroneSession extends SocketSession { ); this.socket.on("requestTermination", this.onRequestTermination.bind(this)); + + this.socket.on("log", this.onLog.bind(this)); + } + + async onLog( + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, + ): Promise { + if (!this.chatSessionId) { + return; + } + try { + const codeSession = SocketService.getCodeSessionByChatSessionId( + this.chatSessionId, + ); + codeSession.onLog(timestamp, component, level, message, metadata); + } catch (error) { + this.log.error("failed to route log message", { error }); + } } async onStatus(message: string): Promise { diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index 1873fad..d203d21 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -12,7 +12,7 @@ import AgentService, { IAgentWorkOrder } from "./services/agent.ts"; import AiService from "./services/ai.ts"; import PlatformService from "./services/platform.ts"; import WorkspaceService from "./services/workspace.ts"; -import { DroneStatus, IUser } from "@gadget/api"; +import { DroneStatus, GadgetLog, GadgetLogTransportSocket, IUser } from "@gadget/api"; import { GadgetProcess } from "./lib/process.ts"; import { @@ -207,6 +207,18 @@ class GadgetDrone extends GadgetProcess { }); this.socket.on("connect", () => { this.log.info("connected to Gadget Code platform."); + + const socketTransport = new GadgetLogTransportSocket( + (event, timestamp, component, level, message, metadata): void => { + (this.socket as any)?.emit(event, timestamp, component, level, message, metadata); + }, + ); + GadgetLog.addDefaultTransport(socketTransport); + this.log.transports.push(socketTransport); + AgentService.log.transports.push(socketTransport); + AiService.log.transports.push(socketTransport); + PlatformService.log.transports.push(socketTransport); + resolve(); }); diff --git a/gadget-drone/src/lib/service.ts b/gadget-drone/src/lib/service.ts index f215517..9f9408e 100644 --- a/gadget-drone/src/lib/service.ts +++ b/gadget-drone/src/lib/service.ts @@ -7,7 +7,7 @@ import { GadgetComponent, GadgetLog } from "@gadget/api"; export abstract class GadgetService implements GadgetComponent { - protected log: GadgetLog; + public log: GadgetLog; constructor() { this.log = new GadgetLog(this); diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index ee9f1b1..cb28273 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -90,7 +90,7 @@ class AgentService extends GadgetService { }; const onStreamChunk = async (chunk: IAiStreamChunk): Promise => { - this.log.debug("stream chunk received", { chunk }); + // this.log.debug("stream chunk received", { chunk }); switch (chunk.type) { case "thinking": diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 538f285..4d77750 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -8,6 +8,7 @@ export * from "./lib/log.ts"; export * from "./lib/log-transport.ts"; export * from "./lib/log-transport-console.ts"; export * from "./lib/log-transport-file.ts"; +export * from "./lib/log-transport-socket.ts"; export * from "./lib/log-file.ts"; /* diff --git a/packages/api/src/lib/log-transport-socket.ts b/packages/api/src/lib/log-transport-socket.ts new file mode 100644 index 0000000..3f0cfb7 --- /dev/null +++ b/packages/api/src/lib/log-transport-socket.ts @@ -0,0 +1,30 @@ +import { GadgetComponent } from "./component.ts"; +import { GadgetLogLevel } from "./log.ts"; +import { GadgetLogTransport } from "./log-transport.ts"; + +export type LogEmitFunction = ( + event: string, + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, +) => void; + +export class GadgetLogTransportSocket implements GadgetLogTransport { + private emit: LogEmitFunction; + + constructor(emit: LogEmitFunction) { + this.emit = emit; + } + + async writeLog( + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, + ): Promise { + this.emit("log", timestamp, component, level, message, metadata); + } +}