Skip to content

Commit d8ed040

Browse files
Merge branch 'main' into feat/forwarded-token-as-actor
2 parents cd2a52d + 79cab39 commit d8ed040

8 files changed

Lines changed: 94 additions & 10 deletions

File tree

helm/kagent/files/nginx.conf

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ http {
4747
proxy_set_header X-Forwarded-Proto $scheme;
4848
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
4949
proxy_set_header Origin $scheme://$host;
50-
proxy_read_timeout 600s;
51-
proxy_send_timeout 600s;
50+
proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }};
51+
proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }};
5252
proxy_buffering off;
5353
}
5454

@@ -63,6 +63,10 @@ http {
6363
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
6464
proxy_set_header Origin $scheme://$host;
6565
proxy_cache_bypass $http_upgrade;
66+
# Increased timeouts for streaming endpoints
67+
proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }};
68+
proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }};
69+
proxy_buffering off;
6670
}
6771

6872
location /health {
@@ -80,8 +84,8 @@ http {
8084
proxy_set_header X-Forwarded-Proto $scheme;
8185
proxy_set_header X-Forwarded-Host $server_name;
8286
proxy_cache_bypass $http_upgrade;
83-
proxy_read_timeout 3600s;
84-
proxy_send_timeout 3600s;
87+
proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }};
88+
proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }};
8589
proxy_buffering off;
8690
}
8791
}

helm/kagent/templates/openshift-route.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ kind: Route
66
metadata:
77
name: {{ include "kagent.fullname" . }}-ui
88
namespace: {{ include "kagent.namespace" . }}
9+
{{- with .Values.ui.openshiftRoute.annotations }}
10+
annotations:
11+
{{- toYaml . | nindent 6 }}
12+
{{- end }}
913
spec:
1014
to:
1115
kind: Service

helm/kagent/templates/ui-deployment.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ spec:
6464
- name: SSO_REDIRECT_PATH
6565
value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }}
6666
{{- end }}
67+
{{- if .Values.ui.streamTimeoutSeconds }}
68+
- name: KAGENT_STREAM_TIMEOUT_MS
69+
value: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }}
70+
{{- end }}
6771
{{- with .Values.ui.additionalForwardedHeaders }}
6872
- name: KAGENT_ADDITIONAL_FORWARDED_HEADERS
6973
value: {{ join "," . | quote }}

helm/kagent/values.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,17 @@ ui:
351351
# Path to redirect users to when they click "Sign in with SSO" on the login page
352352
# Default: /oauth2/start (oauth2-proxy's authentication start endpoint)
353353
ssoRedirectPath: "/oauth2/start"
354+
# -- Client-side chat stream inactivity timeout (seconds). The browser aborts a
355+
# streaming response if no event is received within this window. Should be >=
356+
# ui.nginx.proxyReadTimeout so nginx isn't the silent limit. Default 1800 (30m).
357+
streamTimeoutSeconds: 1800
358+
# -- Nginx proxy timeout configuration for the UI sidecar (values are passed
359+
# directly to the corresponding nginx directives, e.g. "1800s").
360+
nginx:
361+
# -- proxy_read_timeout: max time between two successive reads from the upstream.
362+
proxyReadTimeout: 1800s
363+
# -- proxy_send_timeout: max time between two successive writes to the upstream.
364+
proxySendTimeout: 1800s
354365
env: {} # Additional configuration key-value pairs for the ui ConfigMap
355366
# -- Additional request headers (beyond Authorization) the UI proxy will forward
356367
# to the backend. Names are case-insensitive. Hop-by-hop headers (Connection,
@@ -419,6 +430,13 @@ ui:
419430
# oauth2-proxy instead of the chart's edge-terminated Route.
420431
route:
421432
enabled: true
433+
434+
# OpenShift Route only (when route.openshift.io/v1 exists). Long timeouts are required
435+
# for A2A/SSE streaming through the cluster router; defaults are often ~60s and cause
436+
# net::ERR_INCOMPLETE_CHUNKED_ENCODING in the browser.
437+
openshiftRoute:
438+
annotations:
439+
haproxy.router.openshift.io/timeout: 120m
422440
# ==============================================================================
423441
# LLM PROVIDERS CONFIGURATION
424442
# ==============================================================================

ui/src/app/a2a/[namespace]/[agentName]/route.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function POST(
3232

3333
if (!backendResponse.ok) {
3434
const errorText = await backendResponse.text();
35-
return new Response(errorText || 'Backend request failed', {
35+
return new Response(errorText || 'Backend request failed', {
3636
status: backendResponse.status,
3737
headers: {
3838
'Content-Type': 'text/plain',
@@ -90,6 +90,10 @@ export async function POST(
9090
return Promise.resolve();
9191
}
9292

93+
// Any upstream bytes count as activity for proxies; also start keep-alives
94+
// before the first complete SSE frame (otherwise HAProxy may idle-timeout).
95+
resetKeepAliveTimer();
96+
9397
buffer += decoder.decode(value, { stream: true });
9498

9599
// Process complete SSE events (delimited by \n\n)
@@ -111,7 +115,7 @@ export async function POST(
111115
}).catch(error => {
112116
console.error('A2A Proxy: Error in stream pump:', error);
113117
if (keepAliveTimer) clearTimeout(keepAliveTimer);
114-
118+
115119
if (!isClosed) {
116120
controller.error(error);
117121
isClosed = true;
@@ -121,6 +125,9 @@ export async function POST(
121125
});
122126
};
123127

128+
// Begin keep-alives immediately so the browser↔UI connection survives long gaps
129+
// until the first (or any) chunk from the controller.
130+
resetKeepAliveTimer();
124131
pump();
125132
}
126133
});

ui/src/app/actions/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use server";
2+
3+
import { DEFAULT_STREAM_TIMEOUT_MS } from "@/lib/constants";
4+
5+
export interface UiRuntimeConfig {
6+
streamTimeoutMs: number;
7+
}
8+
9+
/**
10+
* Returns runtime UI configuration sourced from server-side environment
11+
* variables (set by the Helm chart). Read on the server so values reflect the
12+
* deployment at runtime, unlike NEXT_PUBLIC_* vars which are inlined at build.
13+
*/
14+
export async function getUiRuntimeConfig(): Promise<UiRuntimeConfig> {
15+
const raw = process.env.KAGENT_STREAM_TIMEOUT_MS;
16+
const parsed = raw ? Number(raw) : NaN;
17+
const streamTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_STREAM_TIMEOUT_MS;
18+
return { streamTimeoutMs };
19+
}

ui/src/components/chat/ChatInterface.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { createSession, getSessionTasks, checkSessionExists } from "@/app/action
2222
import { deriveSessionTitle, isPlaceholderSessionTitle } from "@/lib/sessionTitle";
2323
import { normalizeSessionTimestamps } from "@/lib/sessionTimestamps";
2424
import { getAgentWithResolvedKind, waitForSandboxAgentReady } from "@/app/actions/agents";
25+
import { getUiRuntimeConfig } from "@/app/actions/config";
26+
import { DEFAULT_STREAM_TIMEOUT_MS } from "@/lib/constants";
2527
import { toast } from "sonner";
2628
import { useRouter } from "next/navigation";
2729
import { createMessageHandlers, extractMessagesFromTasks, extractApprovalMessagesFromTasks, extractTokenStatsFromTasks, createMessage, ADKMetadata, ProcessedToolCallData } from "@/lib/messageHandlers";
@@ -71,6 +73,22 @@ export default function ChatInterface({ selectedAgentName, selectedNamespace, se
7173
const pendingDecisionsRef = useRef<Record<string, ToolDecision>>({});
7274
/** Per-tool rejection reasons collected as the user rejects individual tools. */
7375
const pendingRejectionReasonsRef = useRef<Record<string, string>>({});
76+
// Stream inactivity timeout (ms), configurable via Helm (ui.streamTimeoutSeconds).
77+
const streamTimeoutMsRef = useRef<number>(DEFAULT_STREAM_TIMEOUT_MS);
78+
79+
useEffect(() => {
80+
let cancelled = false;
81+
getUiRuntimeConfig()
82+
.then((config) => {
83+
if (!cancelled) streamTimeoutMsRef.current = config.streamTimeoutMs;
84+
})
85+
.catch(() => {
86+
/* keep default on failure */
87+
});
88+
return () => {
89+
cancelled = true;
90+
};
91+
}, []);
7492

7593
const {
7694
isListening,
@@ -382,18 +400,24 @@ export default function ChatInterface({ selectedAgentName, selectedNamespace, se
382400
const consumeStream = async (stream: AsyncIterable<unknown>) => {
383401
let timeoutTimer: NodeJS.Timeout | null = null;
384402
let streamActive = true;
385-
const STREAM_TIMEOUT_MS = 600000; // 10 minutes
403+
404+
const formatTimeout = (ms: number): string => {
405+
const mins = ms / 60000;
406+
return mins >= 1 ? `${Math.ceil(mins)} minutes` : `${Math.round(ms / 1000)} seconds`;
407+
};
386408

387409
const startTimeout = () => {
388410
if (timeoutTimer) clearTimeout(timeoutTimer);
411+
const streamTimeoutMs = streamTimeoutMsRef.current;
389412
timeoutTimer = setTimeout(() => {
390413
if (streamActive) {
391-
console.error("⏰ Stream timeout - no events received for 10 minutes");
392-
toast.error("⏰ Stream timed out - no events received for 10 minutes");
414+
const label = formatTimeout(streamTimeoutMs);
415+
console.error(`⏰ Stream timeout - no events received for ${label}`);
416+
toast.error(`⏰ Stream timed out - no events received for ${label}`);
393417
streamActive = false;
394418
abortControllerRef.current?.abort();
395419
}
396-
}, STREAM_TIMEOUT_MS);
420+
}, streamTimeoutMs);
397421
};
398422
startTimeout();
399423

ui/src/lib/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
// Model-related constants
22
export const OLLAMA_DEFAULT_TAG = "latest";
33
export const OLLAMA_DEFAULT_HOST = "localhost:11434";
4+
5+
// Default client-side stream inactivity timeout (30 minutes) used when Helm
6+
// does not provide an override via ui.streamTimeoutSeconds.
7+
export const DEFAULT_STREAM_TIMEOUT_MS = 1800000;

0 commit comments

Comments
 (0)