GadgetLogTransportSocket and the drone-to-IDE log
This commit is contained in:
parent
632caf11ed
commit
d7924a9d6f
@ -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 |
|
||||
|
||||
@ -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<string, string> = {
|
||||
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<HTMLDivElement>(null);
|
||||
const [expandedMetadata, setExpandedMetadata] = useState<Set<string>>(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 (
|
||||
<div
|
||||
className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${
|
||||
@ -44,32 +78,46 @@ export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand
|
||||
</div>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-2 font-mono text-xs"
|
||||
className="flex-1 overflow-y-auto p-2 font-mono text-xs leading-relaxed"
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<div className="text-text-muted">No log entries</div>
|
||||
) : (
|
||||
logs.map((entry) => (
|
||||
<div key={entry.id} className="flex gap-2 py-0.5">
|
||||
<span className="text-text-muted shrink-0">
|
||||
{entry.timestamp.toLocaleTimeString()}
|
||||
</span>
|
||||
<span
|
||||
className={`shrink-0 ${
|
||||
entry.level === 'error'
|
||||
? 'text-red-500'
|
||||
: entry.level === 'warn'
|
||||
? 'text-yellow-500'
|
||||
: entry.level === 'info'
|
||||
? 'text-blue-400'
|
||||
: 'text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
[{entry.level.toUpperCase()}]
|
||||
</span>
|
||||
<span className="text-text-primary">{entry.message}</span>
|
||||
</div>
|
||||
))
|
||||
logs.map((entry) => {
|
||||
const levelClass = levelColors[entry.level] || 'text-text-secondary';
|
||||
return (
|
||||
<div key={entry.id} className="flex gap-2 py-0.5 hover:bg-bg-elevated/50">
|
||||
<span className="text-gray-600 shrink-0 select-none">
|
||||
{formatTimestamp(entry.timestamp)}
|
||||
</span>
|
||||
<span className={`shrink-0 font-bold ${levelClass}`}>
|
||||
{entry.level.toUpperCase().padEnd(5)}
|
||||
</span>
|
||||
<span className="text-cyan-400 shrink-0">
|
||||
{entry.component}
|
||||
</span>
|
||||
<span className="text-text-secondary break-all min-w-0">
|
||||
{entry.message}
|
||||
</span>
|
||||
{entry.metadata && (
|
||||
<button
|
||||
onClick={() => toggleMetadata(entry.id)}
|
||||
className="shrink-0 text-text-muted hover:text-text-secondary transition-colors"
|
||||
title={expandedMetadata.has(entry.id) ? 'Hide metadata' : 'Show metadata'}
|
||||
>
|
||||
{expandedMetadata.has(entry.id) ? '⊟' : '⊞'}
|
||||
</button>
|
||||
)}
|
||||
{entry.metadata && expandedMetadata.has(entry.id) && (
|
||||
<div className="w-full text-text-muted pl-[1em] border-l border-border-subtle mt-0.5 mb-1">
|
||||
<pre className="whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(entry.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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() {
|
||||
<LogPanel
|
||||
logs={logs}
|
||||
expanded={logExpanded}
|
||||
workspaceMode={workspaceMode}
|
||||
onToggleExpand={() => setLogExpanded(!logExpanded)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
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<void> {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -90,7 +90,7 @@ class AgentService extends GadgetService {
|
||||
};
|
||||
|
||||
const onStreamChunk = async (chunk: IAiStreamChunk): Promise<void> => {
|
||||
this.log.debug("stream chunk received", { chunk });
|
||||
// this.log.debug("stream chunk received", { chunk });
|
||||
|
||||
switch (chunk.type) {
|
||||
case "thinking":
|
||||
|
||||
@ -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";
|
||||
|
||||
/*
|
||||
|
||||
30
packages/api/src/lib/log-transport-socket.ts
Normal file
30
packages/api/src/lib/log-transport-socket.ts
Normal file
@ -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<void> {
|
||||
this.emit("log", timestamp, component, level, message, metadata);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user