From 1b574a10927274a707820fb61a788275df09c7a9 Mon Sep 17 00:00:00 2001 From: Naveen Saharan Date: Fri, 3 Jul 2026 15:23:30 +0530 Subject: [PATCH 1/4] feat: enhance InterruptBanner and SubAgentIndicator for tool_approval - InterruptBanner: parse structured tool_approval payloads, show subagent name and tool name in approval prompt - SubAgentIndicator: show "Tool Approval Required" with tool name and args when per-subagent tool_approval interrupt fires --- src/frontend/components/InterruptBanner.tsx | 77 ++++++++++++++++--- src/frontend/components/SubAgentIndicator.tsx | 30 +++++++- 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/frontend/components/InterruptBanner.tsx b/src/frontend/components/InterruptBanner.tsx index 777b121..3aceb25 100644 --- a/src/frontend/components/InterruptBanner.tsx +++ b/src/frontend/components/InterruptBanner.tsx @@ -7,6 +7,19 @@ import { import { CheckCircle, XCircle, ShieldCheck } from 'lucide-react'; import type { InterruptInfo, HITLActionRequest, HITLReviewConfig } from '../types/deep-agent'; +interface ToolApprovalInfo { + agentName: string; + toolName: string; +} + +function parseToolApproval(value: string): ToolApprovalInfo | null { + const match = value.match( + /subagent '([^']+)' wants to call '([^']+)'/ + ); + if (!match) return null; + return { agentName: match[1], toolName: match[2] }; +} + interface InterruptBannerProps { readonly interrupt: InterruptInfo; readonly onResume: (decisions: Array<{ type: 'approve' | 'reject'; message?: string }>) => void; @@ -59,6 +72,23 @@ function ToolCard({ request, reviewConfig, index, total }: ToolCardProps) { ); } +/** + * Attempt to extract tool-approval metadata from an action request. + * Checks the request name and all string-valued args for the pattern. + */ +function extractToolApproval(request: HITLActionRequest): ToolApprovalInfo | null { + const fromName = parseToolApproval(request.name); + if (fromName) return fromName; + + for (const v of Object.values(request.args)) { + if (typeof v === 'string') { + const fromArg = parseToolApproval(v); + if (fromArg) return fromArg; + } + } + return null; +} + export function InterruptBanner({ interrupt, onResume, onAlwaysAllow, onDismiss }: InterruptBannerProps) { const { action_requests: actionRequests, review_configs: reviewConfigs } = interrupt.value; @@ -88,28 +118,55 @@ export function InterruptBanner({ interrupt, onResume, onAlwaysAllow, onDismiss onResume(actionRequests.map(() => ({ type: 'approve' }))); }; + // Check if any action request matches the structured tool-approval pattern. + // When there is exactly one request with a parseable approval, render the + // enhanced view; otherwise fall back to the standard ToolCard list. + const singleApproval = + actionRequests.length === 1 ? extractToolApproval(actionRequests[0]) : null; + const toolLabel = actionRequests.length === 1 ? `tool call` : `${actionRequests.length} tool calls`; + const alertTitle = singleApproval + ? 'Tool Approval Required' + : `Action required — approve ${toolLabel}`; + return (
} >
- {actionRequests.map((req, i) => ( - - ))} + {singleApproval ? ( +
+
+ Subagent: + + {singleApproval.agentName} + +
+
+ Tool: + + {singleApproval.toolName} + +
+
+ ) : ( + actionRequests.map((req, i) => ( + + )) + )}
+
)} From 7b0b4da6d5c4d0bad65987197a5c1fb6ec743543 Mon Sep 17 00:00:00 2001 From: Naveen Saharan Date: Mon, 6 Jul 2026 16:07:21 +0530 Subject: [PATCH 2/4] ci: trigger base image build on feat/rhitaif-221 branch --- .github/workflows/build-base-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-base-image.yml b/.github/workflows/build-base-image.yml index 1ec6142..d652685 100644 --- a/.github/workflows/build-base-image.yml +++ b/.github/workflows/build-base-image.yml @@ -7,6 +7,7 @@ on: branches: - main - deep-agent + - feat/rhitaif-221 workflow_dispatch: # Allow manual trigger for testing permissions: From acb48e6ff4ae08e846e4829b7b8acbd50b81c822 Mon Sep 17 00:00:00 2001 From: Naveen Saharan Date: Thu, 16 Jul 2026 12:16:10 +0530 Subject: [PATCH 3/4] feat: add code execution output streaming via SSE custom events - Add 'custom' to stream_mode for LangGraph Platform SSE - Handle code_output custom events in BFF proxy (skip all other custom events to prevent partial text state corruption) - Add code_output chunk type to SSEProcessor - Add onCodeOutput callback to StreamingManager - Wire console logging in useStreamingAPI for code execution output --- src/frontend/hooks/useStreamingAPI.ts | 6 ++++++ src/frontend/lib/streaming/SSEProcessor.ts | 10 +++++++-- .../lib/streaming/StreamingManager.ts | 3 +++ src/server/router/proxy.router.ts | 21 ++++++++++++++++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/frontend/hooks/useStreamingAPI.ts b/src/frontend/hooks/useStreamingAPI.ts index 990c043..7b8243b 100644 --- a/src/frontend/hooks/useStreamingAPI.ts +++ b/src/frontend/hooks/useStreamingAPI.ts @@ -466,6 +466,9 @@ export function useStreamingAPI(threadId: string) { onMetadata(data) { setTraceId(data.trace_id); }, + onCodeOutput(content) { + console.log('[code_execution_output]', content); + }, }; manager.stream(streamRequest, callbacks).then(() => { @@ -620,6 +623,9 @@ export function useStreamingAPI(threadId: string) { onMetadata(data) { setTraceId(data.trace_id); }, + onCodeOutput(content) { + console.log('[code_execution_output]', content); + }, }; await manager.stream(resumeRequest, callbacks); diff --git a/src/frontend/lib/streaming/SSEProcessor.ts b/src/frontend/lib/streaming/SSEProcessor.ts index 3106374..e22e2cc 100644 --- a/src/frontend/lib/streaming/SSEProcessor.ts +++ b/src/frontend/lib/streaming/SSEProcessor.ts @@ -4,7 +4,8 @@ import type { HITLInterruptValue } from '@/types/deep-agent'; export type SSEChunk = | { type: 'token'; content: string; chunk_id: number } | { type: 'message'; content: Message; chunk_id: number } - | { type: 'interrupt'; content: { value: HITLInterruptValue; resumable: boolean }; chunk_id: number }; + | { type: 'interrupt'; content: { value: HITLInterruptValue; resumable: boolean }; chunk_id: number } + | { type: 'code_output'; content: string; chunk_id: number }; export type McpStatusData = { tool: string; @@ -32,7 +33,7 @@ function isRecord(value: unknown): value is Record { function parseSSEChunkPayload(parsed: unknown): SSEChunk | null { if (!isRecord(parsed)) return null; const type = parsed.type; - if (type !== 'token' && type !== 'message' && type !== 'interrupt') return null; + if (type !== 'token' && type !== 'message' && type !== 'interrupt' && type !== 'code_output') return null; const chunkIdRaw = parsed.chunk_id; if (typeof chunkIdRaw !== 'number' || !Number.isFinite(chunkIdRaw)) { return null; @@ -44,6 +45,11 @@ function parseSSEChunkPayload(parsed: unknown): SSEChunk | null { return { type: 'token', content: contentUnknown, chunk_id: chunkIdRaw }; } + if (type === 'code_output') { + if (typeof contentUnknown !== 'string') return null; + return { type: 'code_output', content: contentUnknown, chunk_id: chunkIdRaw }; + } + if (type === 'interrupt') { if (!isRecord(contentUnknown)) return null; let rawValue: unknown = contentUnknown.value; diff --git a/src/frontend/lib/streaming/StreamingManager.ts b/src/frontend/lib/streaming/StreamingManager.ts index e5a46c4..72dfa1b 100644 --- a/src/frontend/lib/streaming/StreamingManager.ts +++ b/src/frontend/lib/streaming/StreamingManager.ts @@ -47,6 +47,7 @@ export type StreamCallback = { onDone: () => void; onMcpStatus?: (event: McpStreamStatusEvent) => void; onMetadata?: (data: StreamMetadataPayload) => void; + onCodeOutput?: (content: string) => void; }; export class StreamingManager { @@ -93,6 +94,8 @@ export class StreamingManager { callbacks.onToken(event.data.content); } else if (event.data.type === 'interrupt') { callbacks.onInterrupt(event.data.content); + } else if (event.data.type === 'code_output') { + callbacks.onCodeOutput?.(event.data.content); } else { callbacks.onMessage(event.data.content); } diff --git a/src/server/router/proxy.router.ts b/src/server/router/proxy.router.ts index b98aea1..1878f08 100644 --- a/src/server/router/proxy.router.ts +++ b/src/server/router/proxy.router.ts @@ -339,7 +339,7 @@ async function proxyRoutes(fastify: FastifyInstance) { const runBody: Record = { assistant_id: 'agent', - stream_mode: ['messages', 'updates'], + stream_mode: ['messages', 'updates', 'custom'], }; if (isResume) { runBody.command = { resume: message }; @@ -489,6 +489,25 @@ async function proxyRoutes(fastify: FastifyInstance) { continue; } + // Code execution output streaming (stream_mode="custom") + // Skip ALL custom events to avoid corrupting partial text state + if (sseType === 'custom') { + if ( + typeof parsed === 'object' && + parsed !== null && + (parsed as Record).type === 'code_output' + ) { + const codeChunk = { + type: 'code_output', + content: (parsed as Record).content ?? '', + chunk_id: chunkId, + }; + reply.raw.write(`event: chunk\ndata: ${JSON.stringify(codeChunk)}\n\n`); + chunkId++; + } + continue; + } + const [uiChunk, nextPartial] = translateMessageEvent( sseType, parsed, From 33b149409509f74bbd54927e52e1e76e35431b85 Mon Sep 17 00:00:00 2001 From: Naveen Saharan Date: Thu, 16 Jul 2026 13:37:45 +0530 Subject: [PATCH 4/4] fix: revert apiUrl to localhost:5002 (local testing override) --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 75222f3..9ba11c9 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,7 @@ } window.APP_DATA = { - apiUrl: "", + apiUrl: "http://localhost:5002", }