Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions helm/kagent/files/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Origin $scheme://$host;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }};
proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }};
proxy_buffering off;
}

Expand All @@ -63,6 +63,10 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Origin $scheme://$host;
proxy_cache_bypass $http_upgrade;
# Increased timeouts for streaming endpoints
proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }};
proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }};
proxy_buffering off;
}

location /health {
Expand All @@ -80,8 +84,8 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }};
proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }};
proxy_buffering off;
}
}
Expand Down
4 changes: 4 additions & 0 deletions helm/kagent/templates/openshift-route.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ kind: Route
metadata:
name: {{ include "kagent.fullname" . }}-ui
namespace: {{ include "kagent.namespace" . }}
{{- with .Values.ui.openshiftRoute.annotations }}
annotations:
{{- toYaml . | nindent 6 }}
{{- end }}
spec:
to:
kind: Service
Expand Down
4 changes: 4 additions & 0 deletions helm/kagent/templates/ui-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ spec:
- name: SSO_REDIRECT_PATH
value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }}
{{- end }}
{{- if .Values.ui.streamTimeoutSeconds }}
- name: KAGENT_STREAM_TIMEOUT_MS
value: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }}
{{- end }}
{{- with .Values.ui.additionalForwardedHeaders }}
- name: KAGENT_ADDITIONAL_FORWARDED_HEADERS
value: {{ join "," . | quote }}
Expand Down
18 changes: 18 additions & 0 deletions helm/kagent/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,17 @@ ui:
# Path to redirect users to when they click "Sign in with SSO" on the login page
# Default: /oauth2/start (oauth2-proxy's authentication start endpoint)
ssoRedirectPath: "/oauth2/start"
# -- Client-side chat stream inactivity timeout (seconds). The browser aborts a
# streaming response if no event is received within this window. Should be >=
# ui.nginx.proxyReadTimeout so nginx isn't the silent limit. Default 1800 (30m).
streamTimeoutSeconds: 1800
# -- Nginx proxy timeout configuration for the UI sidecar (values are passed
# directly to the corresponding nginx directives, e.g. "1800s").
nginx:
# -- proxy_read_timeout: max time between two successive reads from the upstream.
proxyReadTimeout: 1800s
# -- proxy_send_timeout: max time between two successive writes to the upstream.
proxySendTimeout: 1800s
env: {} # Additional configuration key-value pairs for the ui ConfigMap
# -- Additional request headers (beyond Authorization) the UI proxy will forward
# to the backend. Names are case-insensitive. Hop-by-hop headers (Connection,
Expand Down Expand Up @@ -390,6 +401,13 @@ ui:
# path: /health
# port: http
# periodSeconds: 15

# OpenShift Route only (when route.openshift.io/v1 exists). Long timeouts are required
# for A2A/SSE streaming through the cluster router; defaults are often ~60s and cause
# net::ERR_INCOMPLETE_CHUNKED_ENCODING in the browser.
openshiftRoute:
annotations:
haproxy.router.openshift.io/timeout: 120m
# ==============================================================================
# LLM PROVIDERS CONFIGURATION
# ==============================================================================
Expand Down
11 changes: 9 additions & 2 deletions ui/src/app/a2a/[namespace]/[agentName]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function POST(

if (!backendResponse.ok) {
const errorText = await backendResponse.text();
return new Response(errorText || 'Backend request failed', {
return new Response(errorText || 'Backend request failed', {
status: backendResponse.status,
headers: {
'Content-Type': 'text/plain',
Expand Down Expand Up @@ -90,6 +90,10 @@ export async function POST(
return Promise.resolve();
}

// Any upstream bytes count as activity for proxies; also start keep-alives
// before the first complete SSE frame (otherwise HAProxy may idle-timeout).
resetKeepAliveTimer();

buffer += decoder.decode(value, { stream: true });

// Process complete SSE events (delimited by \n\n)
Expand All @@ -111,7 +115,7 @@ export async function POST(
}).catch(error => {
console.error('A2A Proxy: Error in stream pump:', error);
if (keepAliveTimer) clearTimeout(keepAliveTimer);

if (!isClosed) {
controller.error(error);
isClosed = true;
Expand All @@ -121,6 +125,9 @@ export async function POST(
});
};

// Begin keep-alives immediately so the browser↔UI connection survives long gaps
// until the first (or any) chunk from the controller.
resetKeepAliveTimer();
pump();
}
});
Expand Down
19 changes: 19 additions & 0 deletions ui/src/app/actions/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use server";

import { DEFAULT_STREAM_TIMEOUT_MS } from "@/lib/constants";

export interface UiRuntimeConfig {
streamTimeoutMs: number;
}

/**
* Returns runtime UI configuration sourced from server-side environment
* variables (set by the Helm chart). Read on the server so values reflect the
* deployment at runtime, unlike NEXT_PUBLIC_* vars which are inlined at build.
*/
export async function getUiRuntimeConfig(): Promise<UiRuntimeConfig> {
const raw = process.env.KAGENT_STREAM_TIMEOUT_MS;
const parsed = raw ? Number(raw) : NaN;
const streamTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_STREAM_TIMEOUT_MS;
return { streamTimeoutMs };
}
32 changes: 28 additions & 4 deletions ui/src/components/chat/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type { TokenStats, Session, ChatStatus, ToolDecision } from "@/types";
import StatusDisplay from "./StatusDisplay";
import { createSession, getSessionTasks, checkSessionExists } from "@/app/actions/sessions";
import { waitForSandboxAgentReady } from "@/app/actions/agents";
import { getUiRuntimeConfig } from "@/app/actions/config";
import { DEFAULT_STREAM_TIMEOUT_MS } from "@/lib/constants";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { createMessageHandlers, extractMessagesFromTasks, extractApprovalMessagesFromTasks, extractTokenStatsFromTasks, createMessage, ADKMetadata, ProcessedToolCallData } from "@/lib/messageHandlers";
Expand Down Expand Up @@ -65,6 +67,22 @@ export default function ChatInterface({ selectedAgentName, selectedNamespace, se
const pendingDecisionsRef = useRef<Record<string, ToolDecision>>({});
/** Per-tool rejection reasons collected as the user rejects individual tools. */
const pendingRejectionReasonsRef = useRef<Record<string, string>>({});
// Stream inactivity timeout (ms), configurable via Helm (ui.streamTimeoutSeconds).
const streamTimeoutMsRef = useRef<number>(DEFAULT_STREAM_TIMEOUT_MS);

useEffect(() => {
let cancelled = false;
getUiRuntimeConfig()
.then((config) => {
if (!cancelled) streamTimeoutMsRef.current = config.streamTimeoutMs;
})
.catch(() => {
/* keep default on failure */
});
Comment thread
yanivmn marked this conversation as resolved.
return () => {
cancelled = true;
};
}, []);

const {
isListening,
Expand Down Expand Up @@ -314,18 +332,24 @@ export default function ChatInterface({ selectedAgentName, selectedNamespace, se
const consumeStream = async (stream: AsyncIterable<unknown>) => {
let timeoutTimer: NodeJS.Timeout | null = null;
let streamActive = true;
const STREAM_TIMEOUT_MS = 600000; // 10 minutes

const formatTimeout = (ms: number): string => {
const mins = ms / 60000;
return mins >= 1 ? `${Math.ceil(mins)} minutes` : `${Math.round(ms / 1000)} seconds`;
};

const startTimeout = () => {
if (timeoutTimer) clearTimeout(timeoutTimer);
const streamTimeoutMs = streamTimeoutMsRef.current;
timeoutTimer = setTimeout(() => {
if (streamActive) {
console.error("⏰ Stream timeout - no events received for 10 minutes");
toast.error("⏰ Stream timed out - no events received for 10 minutes");
const label = formatTimeout(streamTimeoutMs);
console.error(`⏰ Stream timeout - no events received for ${label}`);
toast.error(`⏰ Stream timed out - no events received for ${label}`);
streamActive = false;
abortControllerRef.current?.abort();
}
}, STREAM_TIMEOUT_MS);
}, streamTimeoutMs);
};
Comment thread
yanivmn marked this conversation as resolved.
startTimeout();

Expand Down
4 changes: 4 additions & 0 deletions ui/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Model-related constants
export const OLLAMA_DEFAULT_TAG = "latest";
export const OLLAMA_DEFAULT_HOST = "localhost:11434";

// Default client-side stream inactivity timeout (30 minutes) used when Helm
// does not provide an override via ui.streamTimeoutSeconds.
export const DEFAULT_STREAM_TIMEOUT_MS = 1800000;
Loading