subagent processing updates and fixes

This commit is contained in:
Rob Colbert 2026-05-11 19:07:48 -04:00
parent d7479d54e1
commit c5add0fc7d
10 changed files with 534 additions and 53 deletions

View File

@ -1,31 +1,17 @@
import { useState, memo, useCallback, useEffect, useRef } from "react";
import { memo } from "react";
import { marked } from "marked";
import type { ChatTurn as ChatTurnType, ChatTurnBlock } from "../lib/api";
import type { ChatTurn as ChatTurnType, ChatTurnBlockTool } from "../lib/api";
interface ChatTurnProps {
turn: ChatTurnType;
onUpdate: (turnId: string, updates: Partial<ChatTurnType>) => void;
}
// Configure marked with breaks enabled
marked.setOptions({
marked.use({
breaks: true,
});
const ChatTurn = memo(function ChatTurn({ turn, onUpdate }: ChatTurnProps) {
const [thinkingExpanded, setThinkingExpanded] = useState(false);
const [toolCallsExpanded, setToolCallsExpanded] = useState(true);
const [renderedBlocks, setRenderedBlocks] = useState<Map<number, string>>(new Map());
const blocksEndRef = useRef<HTMLDivElement>(null);
const handleThinkingToggle = useCallback(() => {
setThinkingExpanded((prev) => !prev);
}, []);
const handleToolCallsToggle = useCallback(() => {
setToolCallsExpanded((prev) => !prev);
}, []);
const ChatTurn = memo(function ChatTurn({ turn }: ChatTurnProps) {
const formatDuration = (ms: number): string => {
if (ms < 1000) return `${ms}ms`;
const seconds = (ms / 1000).toFixed(1);
@ -145,15 +131,22 @@ const ChatTurn = memo(function ChatTurn({ turn, onUpdate }: ChatTurnProps) {
);
} else if (block.mode === 'tool') {
const toolCall = block.content;
const subagent = toolCall.subagent;
return (
<div key={idx} className="mb-3">
<div className="flex items-center gap-2 text-xs font-mono text-text-secondary">
<span className="text-brand"></span>
<span>{toolCall.name}</span>
{toolCall.response && (
{toolCall.response && !subagent && (
<span className="text-green-500"></span>
)}
{subagent && subagent.stats && (
<span className="text-green-500"></span>
)}
</div>
{subagent && (
<SubagentDisplay subagent={subagent} />
)}
</div>
);
}
@ -170,4 +163,97 @@ const ChatTurn = memo(function ChatTurn({ turn, onUpdate }: ChatTurnProps) {
);
});
function SubagentDisplay({ subagent }: { subagent: NonNullable<ChatTurnBlockTool['content']['subagent']> }) {
const [expanded, setExpanded] = useState(true);
const toggle = useCallback(() => setExpanded((p) => !p), []);
const formatDuration = (ms: number): string => {
if (ms < 1000) return `${ms}ms`;
const seconds = (ms / 1000).toFixed(1);
return `${seconds}s`;
};
const formatTokenCount = (count: number): string => {
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
};
return (
<div className="mt-2 ml-4 border-l-2 border-brand/30 pl-3">
<button
onClick={toggle}
className="flex items-center gap-1.5 text-xs font-mono text-text-muted hover:text-text-primary transition-colors w-full text-left"
>
<span className="text-brand"></span>
<span>Subagent</span>
{subagent.stats && (
<span className="text-text-muted">
({subagent.toolCalls.length} calls · {formatDuration(subagent.stats.durationMs)})
</span>
)}
</button>
{expanded && (
<div className="mt-2 space-y-2">
{/* Thinking */}
{subagent.thinking && (
<div>
<div className="text-xs text-text-muted font-mono mb-1">Thinking</div>
<div
className="p-2 bg-bg-secondary rounded text-xs text-text-muted whitespace-pre-wrap font-mono"
dangerouslySetInnerHTML={{
__html: marked.parse(subagent.thinking) as string,
}}
/>
</div>
)}
{/* Subagent Tool Calls */}
{subagent.toolCalls.length > 0 && (
<div>
<div className="text-xs text-text-muted font-mono mb-1">
Tool Calls ({subagent.toolCalls.length})
</div>
<div className="space-y-1">
{subagent.toolCalls.map((tc, i) => (
<div key={tc.callId || i} className="flex items-center gap-2 text-xs font-mono text-text-secondary">
<span className="text-yellow-500"></span>
<span>{tc.name}</span>
{tc.response && <span className="text-green-500"></span>}
</div>
))}
</div>
</div>
)}
{/* Response */}
{subagent.response && (
<div>
<div className="text-xs text-text-muted font-mono mb-1">Response</div>
<div
className="text-xs text-text-primary gadget-markdown"
dangerouslySetInnerHTML={{
__html: marked.parse(subagent.response) as string,
}}
/>
</div>
)}
{/* Stats */}
{subagent.stats && (
<div className="flex gap-3 text-xs text-text-muted font-mono pt-1 border-t border-border-subtle">
<span>{formatDuration(subagent.stats.durationMs)}</span>
<span>{formatTokenCount(subagent.stats.inputTokens)} in</span>
<span>{formatTokenCount(subagent.stats.responseTokens)} out</span>
{subagent.stats.thinkingTokenCount > 0 && (
<span>{formatTokenCount(subagent.stats.thinkingTokenCount)} thinking</span>
)}
</div>
)}
</div>
)}
</div>
);
}
export default ChatTurn;

View File

@ -92,6 +92,7 @@ input, textarea {
color: var(--color-text-primary);
line-height: 1.7;
font-size: 14px;
white-space: pre-wrap;
word-break: break-word;
/* Prevent the markdown root from stretching to fill the scroll container
display:inline-block shrinks it to content width, so the

View File

@ -399,6 +399,18 @@ export interface ChatTurnBlockTool {
name: string;
parameters?: string;
response?: string;
subagent?: {
agentId: string;
thinking: string;
response: string;
toolCalls: Array<{
callId: string;
name: string;
parameters?: string;
response?: string;
}>;
stats: ChatTurnStats;
};
};
}
@ -422,6 +434,7 @@ export interface ChatTurn {
name: string;
parameters?: string;
response?: string;
subagent?: ChatTurnBlockTool['content']['subagent'];
}>;
subagents: any[];
stats: ChatTurnStats;

View File

@ -90,7 +90,19 @@ export interface SocketEvents {
tool: string;
result: unknown;
}) => void;
"agent:complete": (data: { agentId: string }) => void;
"agent:complete": (data: {
agentId: string;
response?: string;
subagent?: Record<string, unknown>;
stats?: {
toolCallCount: number;
inputTokens: number;
thinkingTokenCount: number;
responseTokens: number;
durationMs: number;
durationLabel: string;
};
}) => void;
"log:entry": (data: {
level: string;
component: string;
@ -216,6 +228,26 @@ class SocketClient {
},
);
this.socket.on("agent:thinking", (data: unknown) => {
this.emit("agent:thinking", data as { agentId: string; thinking: string });
});
this.socket.on("agent:response", (data: unknown) => {
this.emit("agent:response", data as { agentId: string; chunk: string });
});
this.socket.on("agent:tool-call", (data: unknown) => {
this.emit("agent:tool-call", data as { agentId: string; tool: string; args: unknown });
});
this.socket.on("agent:tool-result", (data: unknown) => {
this.emit("agent:tool-result", data as { agentId: string; tool: string; result: unknown });
});
this.socket.on("agent:complete", (data: unknown) => {
this.emit("agent:complete", data as { agentId: string; response?: string; subagent?: Record<string, unknown>; stats?: Record<string, unknown> });
});
this._socket.on("connect", () => {
this.reconnectAttempts = 0;
this.emit("connect");

View File

@ -25,6 +25,21 @@ interface StreamingState {
currentBlockIndex: number | null;
}
interface SubagentStreamState {
agentId: string;
name: string;
parameters: string;
thinking: string;
response: string;
toolCalls: Array<{
callId: string;
name: string;
parameters?: string;
response?: string;
}>;
stats?: ChatTurnStats;
}
export default function ChatSessionView() {
const { projectId, sessionId } = useParams<{ projectId: string; sessionId: string }>();
const navigate = useNavigate();
@ -63,6 +78,7 @@ export default function ChatSessionView() {
const updateRafRef = useRef<number | null>(null);
const currentTurnIdRef = useRef<string | null>(null);
const streamingStateRef = useRef<Map<string, StreamingState>>(new Map());
const subagentStateRef = useRef<Map<string, SubagentStreamState>>(new Map());
const sessionRef = useRef<ChatSession | null>(null);
const projectRef = useRef<Project | null>(null);
@ -202,6 +218,11 @@ export default function ChatSessionView() {
socketClient.on('sessionUpdated', handleSessionUpdated);
socketClient.on('log:entry', handleLogEntry);
socketClient.on('status', handleStatus);
socketClient.on('agent:thinking', handleAgentThinking);
socketClient.on('agent:response', handleAgentResponse);
socketClient.on('agent:tool-call', handleAgentToolCall);
socketClient.on('agent:tool-result', handleAgentToolResult);
socketClient.on('agent:complete', handleAgentComplete);
};
const cleanupSocketListeners = () => {
@ -213,6 +234,11 @@ export default function ChatSessionView() {
socketClient.off('sessionUpdated', handleSessionUpdated);
socketClient.off('log:entry', handleLogEntry);
socketClient.off('status', handleStatus);
socketClient.off('agent:thinking', handleAgentThinking);
socketClient.off('agent:response', handleAgentResponse);
socketClient.off('agent:tool-call', handleAgentToolCall);
socketClient.off('agent:tool-result', handleAgentToolResult);
socketClient.off('agent:complete', handleAgentComplete);
};
const scheduleUpdate = useCallback(() => {
@ -241,10 +267,18 @@ export default function ChatSessionView() {
const updatedBlocks = [...(oldTurn.blocks || [])];
for (const updateBlock of turnUpdates.blocks) {
let blockIndex = state?.currentBlockIndex ?? null;
let blockIndex: number | null = state?.currentBlockIndex ?? null;
if (updateBlock.mode === 'tool') {
// For tool blocks with callId, find and replace existing
if (updateBlock.content?.callId) {
const existingIndex = updatedBlocks.findIndex(
b => b.mode === 'tool' && b.content?.callId === updateBlock.content?.callId
);
blockIndex = existingIndex !== -1 ? existingIndex : null;
} else {
blockIndex = null;
}
} else if (
blockIndex === null ||
updatedBlocks[blockIndex]?.mode !== updateBlock.mode
@ -300,10 +334,26 @@ export default function ChatSessionView() {
};
if (existing?.blocks || updates.blocks) {
merged.blocks = [
...(existing?.blocks || []),
...(updates.blocks || []),
];
const existingBlocks = [...(existing?.blocks || [])];
const updateBlocks = updates.blocks || [];
// Deduplicate tool blocks by callId: replace existing tool blocks
// that have the same callId as an incoming update block
merged.blocks = [...existingBlocks];
for (const block of updateBlocks) {
if (block.mode === 'tool' && block.content?.callId) {
const existingIdx = merged.blocks.findIndex(
b => b.mode === 'tool' && b.content?.callId === block.content?.callId
);
if (existingIdx !== -1) {
merged.blocks[existingIdx] = block;
} else {
merged.blocks.push(block);
}
} else {
merged.blocks.push(block);
}
}
}
if (existing?.toolCalls || updates.toolCalls) {
@ -435,6 +485,18 @@ export default function ChatSessionView() {
state.currentBlockIndex = null;
}
// Initialize subagent state for subagent tool calls
if (name === 'subagent') {
subagentStateRef.current.set(callId, {
agentId: callId,
name,
parameters: params,
thinking: '',
response: '',
toolCalls: [],
});
}
// Add tool block
mergePendingUpdate(turnId, {
blocks: [{
@ -458,6 +520,12 @@ export default function ChatSessionView() {
}
}
// Clean up subagent state for this turn
const subagentEntries = Array.from(subagentStateRef.current.entries());
for (const [agentId] of subagentEntries) {
subagentStateRef.current.delete(agentId);
}
setTurns(prevTurns =>
prevTurns.map(turn =>
turn._id === turnId
@ -494,6 +562,131 @@ export default function ChatSessionView() {
appContext?.setStatusMessage(content);
}, [appContext]);
const defaultSubagentStats: ChatTurnStats = {
toolCallCount: 0,
inputTokens: 0,
thinkingTokenCount: 0,
responseTokens: 0,
durationMs: 0,
durationLabel: '0ms',
};
const buildSubagentBlock = useCallback((subState: SubagentStreamState): ChatTurnBlock => ({
mode: 'tool' as const,
createdAt: new Date().toISOString(),
content: {
callId: subState.agentId,
name: subState.name,
parameters: subState.parameters,
response: subState.response,
subagent: {
agentId: subState.agentId,
thinking: subState.thinking,
response: subState.response,
toolCalls: subState.toolCalls,
stats: subState.stats || defaultSubagentStats,
},
},
}), []);
const updateSubagentBlock = useCallback((turnId: string, agentId: string) => {
const subState = subagentStateRef.current.get(agentId);
if (!subState) return;
mergePendingUpdate(turnId, {
blocks: [buildSubagentBlock(subState)],
});
scheduleUpdate();
}, [mergePendingUpdate, scheduleUpdate, buildSubagentBlock]);
const handleAgentThinking = useCallback((data: { agentId: string; thinking: string }) => {
const turnId = currentTurnIdRef.current;
if (!turnId) return;
let subState = subagentStateRef.current.get(data.agentId);
if (!subState) {
subState = { agentId: data.agentId, name: '', parameters: '', thinking: '', response: '', toolCalls: [] };
subagentStateRef.current.set(data.agentId, subState);
}
subState.thinking += data.thinking;
updateSubagentBlock(turnId, data.agentId);
}, [updateSubagentBlock]);
const handleAgentResponse = useCallback((data: { agentId: string; chunk: string }) => {
const turnId = currentTurnIdRef.current;
if (!turnId) return;
let subState = subagentStateRef.current.get(data.agentId);
if (!subState) {
subState = { agentId: data.agentId, name: '', parameters: '', thinking: '', response: '', toolCalls: [] };
subagentStateRef.current.set(data.agentId, subState);
}
subState.response += data.chunk;
updateSubagentBlock(turnId, data.agentId);
}, [updateSubagentBlock]);
const handleAgentToolCall = useCallback((data: { agentId: string; tool: string; args: unknown }) => {
const turnId = currentTurnIdRef.current;
if (!turnId) return;
let subState = subagentStateRef.current.get(data.agentId);
if (!subState) {
subState = { agentId: data.agentId, name: '', parameters: '', thinking: '', response: '', toolCalls: [] };
subagentStateRef.current.set(data.agentId, subState);
}
const argsStr = typeof data.args === 'string' ? data.args : JSON.stringify(data.args);
subState.toolCalls.push({ callId: `tc_${Date.now()}_${subState.toolCalls.length}`, name: data.tool, parameters: argsStr });
updateSubagentBlock(turnId, data.agentId);
}, [updateSubagentBlock]);
const handleAgentToolResult = useCallback((data: { agentId: string; tool: string; result: unknown }) => {
const turnId = currentTurnIdRef.current;
if (!turnId) return;
const subState = subagentStateRef.current.get(data.agentId);
if (!subState) return;
const lastToolCall = subState.toolCalls[subState.toolCalls.length - 1];
if (lastToolCall && lastToolCall.name === data.tool) {
lastToolCall.response = typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
}
updateSubagentBlock(turnId, data.agentId);
}, [updateSubagentBlock]);
const handleAgentComplete = useCallback((data: { agentId: string; response?: string; subagent?: Record<string, unknown>; stats?: ChatTurnStats }) => {
const turnId = currentTurnIdRef.current;
if (!turnId) return;
const subState = subagentStateRef.current.get(data.agentId);
if (!subState) return;
// Apply final response if provided
if (data.response) {
subState.response = data.response;
}
// Apply stats from agent:complete
if (data.stats) {
subState.stats = data.stats;
}
// If subagent data was passed directly (e.g., from agent:complete with full payload),
// apply the thinking and toolCalls from it
if (data.subagent) {
const sa = data.subagent as Record<string, unknown>;
if (typeof sa.thinking === 'string') subState.thinking = sa.thinking;
if (typeof sa.response === 'string') subState.response = sa.response;
if (Array.isArray(sa.toolCalls)) subState.toolCalls = sa.toolCalls as SubagentStreamState['toolCalls'];
if (sa.stats) subState.stats = { ...defaultSubagentStats, ...(sa.stats as Record<string, unknown>) } as ChatTurnStats;
}
updateSubagentBlock(turnId, data.agentId);
// Clean up subagent state after final update
requestAnimationFrame(() => {
subagentStateRef.current.delete(data.agentId);
});
}, [updateSubagentBlock]);
const showToast = useCallback((message: string) => {
setToast(message);
if (toastTimerRef.current) {
@ -675,8 +868,7 @@ export default function ChatSessionView() {
mode: session?.mode || 'develop',
status: 'processing',
prompts: { user: promptInput.trim() },
thinking: '',
response: '',
blocks: [],
toolCalls: [],
subagents: [],
stats: {
@ -723,14 +915,6 @@ export default function ChatSessionView() {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleTurnUpdate = useCallback((turnId: string, updates: Partial<ChatTurn>) => {
setTurns(prevTurns =>
prevTurns.map(turn =>
turn._id === turnId ? { ...turn, ...updates } : turn
)
);
}, []);
const promptDisabled = isProcessing || workspaceMode !== WorkspaceMode.Agent || !sessionLocked || !session?.selectedModel || isEditingProvider;
const promptPlaceholder = isEditingProvider
? 'Save or cancel provider changes to continue.'
@ -786,7 +970,6 @@ export default function ChatSessionView() {
<ChatTurnComponent
key={turn._id}
turn={turn}
onUpdate={handleTurnUpdate}
/>
))}
<div ref={messagesEndRef} />

View File

@ -10,6 +10,8 @@ import {
ChatTurnStatus,
GadgetId,
WorkspaceMode,
IChatToolCall,
IChatTurnBlock,
} from "@gadget/api";
import {
GadgetSocket,
@ -62,6 +64,12 @@ export class DroneSession extends SocketSession {
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
this.socket.on("log", this.onLog.bind(this));
this.socket.on("agent:thinking", this.onAgentThinking.bind(this));
this.socket.on("agent:response", this.onAgentResponse.bind(this));
this.socket.on("agent:tool-call", this.onAgentToolCall.bind(this));
this.socket.on("agent:tool-result", this.onAgentToolResult.bind(this));
this.socket.on("agent:complete", this.onAgentComplete.bind(this));
}
async onLog(
@ -350,6 +358,78 @@ export class DroneSession extends SocketSession {
codeSession.onWorkspaceModeChanged(mode);
}
async onAgentThinking(data: { agentId: string; thinking: string }): Promise<void> {
if (!this.chatSessionId) return;
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(this.chatSessionId);
codeSession.socket.emit("agent:thinking", data);
} catch (error) {
this.log.error("failed to route agent:thinking", { error });
}
}
async onAgentResponse(data: { agentId: string; chunk: string }): Promise<void> {
if (!this.chatSessionId) return;
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(this.chatSessionId);
codeSession.socket.emit("agent:response", data);
} catch (error) {
this.log.error("failed to route agent:response", { error });
}
}
async onAgentToolCall(data: { agentId: string; tool: string; args: unknown }): Promise<void> {
if (!this.chatSessionId) return;
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(this.chatSessionId);
codeSession.socket.emit("agent:tool-call", data);
} catch (error) {
this.log.error("failed to route agent:tool-call", { error });
}
}
async onAgentToolResult(data: { agentId: string; tool: string; result: unknown }): Promise<void> {
if (!this.chatSessionId) return;
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(this.chatSessionId);
codeSession.socket.emit("agent:tool-result", data);
} catch (error) {
this.log.error("failed to route agent:tool-result", { error });
}
}
async onAgentComplete(data: { agentId: string; response?: string; subagent?: Record<string, unknown>; stats?: Record<string, unknown> }): Promise<void> {
if (!this.chatSessionId) return;
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(this.chatSessionId);
codeSession.socket.emit("agent:complete", data);
} catch (error) {
this.log.error("failed to route agent:complete to frontend", { error });
}
// Update the persisted tool call with the final response and subagent data
if (this.currentTurnId && data.agentId) {
try {
const turn = await ChatTurn.findById(this.currentTurnId);
if (turn) {
const toolCall = turn.toolCalls.find((tc: IChatToolCall) => tc.callId === data.agentId);
if (toolCall) {
if (data.response) toolCall.response = data.response;
if (data.subagent) (toolCall as any).subagent = data.subagent;
}
const block = turn.blocks.find((b: IChatTurnBlock) => b.mode === 'tool' && (b.content as IChatToolCall).callId === data.agentId);
if (block && block.mode === 'tool') {
if (data.response) (block.content as IChatToolCall).response = data.response;
if (data.subagent) (block.content as any).subagent = data.subagent;
}
await turn.save();
}
} catch (error) {
this.log.error("failed to update subagent tool call in DB", { error });
}
}
}
/**
* Called when the drone requests crash recovery for an incomplete work order.
*/

View File

@ -45,6 +45,7 @@ export const ChatToolCallSchema = new Schema<IChatToolCall>({
name: { type: String, required: true },
parameters: { type: String, required: false },
response: { type: String, required: false },
subagent: { type: Schema.Types.Mixed, required: false } as any,
});
export const ChatSubagentProcessSchema = new Schema<IChatSubagentProcess>({

View File

@ -78,6 +78,7 @@ class AgentService extends GadgetService {
private toolbox = new AiToolbox(toolboxEnv);
private currentWorkOrder: IAgentWorkOrder | null = null;
private currentSocket: DroneSocket | null = null;
private currentToolCallId: string | null = null;
get name(): string {
return "AgentService";
@ -250,18 +251,38 @@ class AgentService extends GadgetService {
});
for (const toolCall of response.toolCalls) {
this.currentToolCallId = toolCall.callId;
// Emit tool call start immediately so frontend shows it
if (toolCall.function.name === "subagent") {
socket.emit("toolCall", toolCall.callId, toolCall.function.name, toolCall.function.arguments, "");
}
const result = await this.executeTool(
toolCall.function.name,
toolCall.function.arguments,
);
socket.emit(
"toolCall",
toolCall.callId,
toolCall.function.name,
toolCall.function.arguments,
result,
);
if (toolCall.function.name === "subagent") {
let responseText = result;
let subagentData: Record<string, unknown> | undefined;
try {
const parsed = JSON.parse(result);
if (parsed.success && parsed.data) {
subagentData = parsed.data;
responseText = parsed.data.response || result;
}
} catch {}
socket.emit("agent:complete", {
agentId: toolCall.callId,
response: responseText,
subagent: subagentData,
});
} else {
socket.emit("toolCall", toolCall.callId, toolCall.function.name, toolCall.function.arguments, result);
}
this.currentToolCallId = null;
messages.push({
createdAt: turn.createdAt,
@ -412,6 +433,19 @@ class AgentService extends GadgetService {
};
}
private makeAgentStreamHandler(socket: DroneSocket, agentId: string): IAiResponseStreamFn {
return async (chunk) => {
switch (chunk.type) {
case "thinking":
socket.emit("agent:thinking", { agentId, thinking: chunk.data });
break;
case "response":
socket.emit("agent:response", { agentId, chunk: chunk.data });
break;
}
};
}
/**
* Builds an IDroneModelConfig by looking up the model's stored settings
* from the provider's cached models array. Falls back to safe defaults
@ -539,9 +573,11 @@ class AgentService extends GadgetService {
{ createdAt: new Date(), role: "user", content: prompt },
];
const agentId = this.currentToolCallId || `sa_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const socket = this.currentSocket;
const tools = this.getToolsForSubagent(agentType);
const streamHandler = this.currentSocket
? this.makeStreamHandler(this.currentSocket)
const streamHandler = socket
? this.makeAgentStreamHandler(socket, agentId)
: undefined;
let fullResponse = "";
@ -604,11 +640,20 @@ class AgentService extends GadgetService {
for (const toolCall of response.toolCalls) {
const toolArgsRaw = toolCall.function.arguments;
if (socket) {
socket.emit("agent:tool-call", { agentId, tool: toolCall.function.name, args: toolArgsRaw });
}
const result = await this.executeTool(
toolCall.function.name,
toolArgsRaw,
);
if (socket) {
socket.emit("agent:tool-result", { agentId, tool: toolCall.function.name, result });
}
subagentToolCalls.push({
callId: toolCall.callId,
name: toolCall.function.name,

View File

@ -52,13 +52,6 @@ export interface IChatTurnStats {
durationLabel: string; // total turn runtime as hh:mm:ss
}
export interface IChatToolCall {
callId: string; // ID of the call so the agent can match response to call
name: string; // tool function name being called
parameters: string; // JSON.stringify of input parameters
response: string; // the tool's response
}
export interface IChatSubagentProcess {
prompt: string;
thinking?: string;
@ -67,6 +60,14 @@ export interface IChatSubagentProcess {
stats: IChatTurnStats;
}
export interface IChatToolCall {
callId: string; // ID of the call so the agent can match response to call
name: string; // tool function name being called
parameters: string; // JSON.stringify of input parameters
response: string; // the tool's response
subagent?: IChatSubagentProcess; // subagent execution details, only present for subagent tool calls
}
/**
* A chat turn is a single prompt/response pair with tool call accounting. It
* stores all data generated by one run of the Agentic Workflow Loop by a Gadget

View File

@ -66,8 +66,42 @@ export interface ClientToServerEvents {
requestCrashRecovery: RequestCrashRecoveryMessage;
requestTermination: RequestTerminationMessage;
workspaceModeChanged: WorkspaceModeChangedMessage;
"agent:thinking": AgentThinkingMessage;
"agent:response": AgentResponseMessage;
"agent:tool-call": AgentToolCallMessage;
"agent:tool-result": AgentToolResultMessage;
"agent:complete": AgentCompleteMessage;
}
export type AgentThinkingMessage = (data: {
agentId: string;
thinking: string;
}) => void;
export type AgentResponseMessage = (data: {
agentId: string;
chunk: string;
}) => void;
export type AgentToolCallMessage = (data: {
agentId: string;
tool: string;
args: unknown;
}) => void;
export type AgentToolResultMessage = (data: {
agentId: string;
tool: string;
result: unknown;
}) => void;
export type AgentCompleteMessage = (data: {
agentId: string;
response?: string;
subagent?: Record<string, unknown>;
stats?: Record<string, unknown>;
}) => void;
export interface ServerToClientEvents {
/*
* gadget-code:web => gadget-drone
@ -93,6 +127,11 @@ export interface ServerToClientEvents {
workOrderComplete: WorkOrderCompleteMessage;
workspaceModeChanged: WorkspaceModeChangedMessage;
sessionUpdated: SessionUpdatedMessage;
"agent:thinking": AgentThinkingMessage;
"agent:response": AgentResponseMessage;
"agent:tool-call": AgentToolCallMessage;
"agent:tool-result": AgentToolResultMessage;
"agent:complete": AgentCompleteMessage;
}
export interface SocketData {