-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(webapp): deterministic grounding facts and org-wide tokens for the dashboard agent #4796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e22dc1a
7ec06d9
deba2ca
83cdd09
a9cbea4
5f38261
76673bc
efb6f98
b62a291
7e30696
6f4301a
14a2f68
b51892c
d758f6d
63bcba8
9a8c973
0668fc4
8a78fbd
0da46d3
0f8bd79
c34af0e
f904497
c229222
aef849f
e8af577
1e021c4
44e070e
790e115
10c3d2b
c2952a7
55f0211
64d84db
cb363e7
b5d8144
9d65091
f64c674
9c2082b
62c1940
49601a7
aa84c4b
be828ec
5c3c688
42a89b4
34d0165
ebbf6b7
cc84011
559fc0b
c4d0e06
ca97486
5c65e5f
88d0977
8f6f008
235cfc3
4a4ba67
bd65fa7
2a258e7
4ed9a87
b9285db
e0d1943
7ac6b06
61c1d14
7b5b92e
e3c2692
59528d9
fede1a0
2381a8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@trigger.dev/core": patch | ||
| "@trigger.dev/sdk": patch | ||
| --- | ||
|
|
||
| Chat server sessions can now set a `ttl` on the runs they trigger, so a run that is never picked up expires instead of waiting indefinitely. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| Fix the assistant chat showing a full-screen error when code highlighting fails to load. It now retries automatically and falls back to plain text so the conversation stays usable. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: improvement | ||
| --- | ||
|
|
||
| The AI assistant now gives grounded answers about queues: it can name the exact runs occupying concurrency slots and which limit is blocking, report accurate queue wait times, and help across all projects in your organization. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| The dashboard agent chat no longer waits forever on a stuck response or tool call — it now shows a clear error with a "Try again" option instead of hanging silently. |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Bounded-wait retry can resend during a live turn
Was this helpful? React with 👍 or 👎 to provide feedback.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intended bounded-wait behavior: after the first-event deadline fires, the prior turn is presumed dead and retry is an explicit user action — resending is the recovery path, not a double-send during a live turn. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ import { | |
| } from "@internal/dashboard-agent-contracts"; | ||
| import { useLocation, useNavigate } from "@remix-run/react"; | ||
| import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; | ||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| import { useToast } from "~/components/primitives/Toast"; | ||
| import { AgentQuotaNotice, AgentUpgradeBlock } from "./AgentUpgradeGate"; | ||
| import { DashboardAgentComposer } from "./DashboardAgentComposer"; | ||
|
|
@@ -27,15 +27,25 @@ import { createTranscriptOrder, orderTranscript } from "./message-order"; | |
| import { navigateDestination } from "./navigate-target"; | ||
| import { pendingNavigateIntents, pendingWatchIntents } from "./pending-intents"; | ||
| import type { AgentPageContext } from "./page-context-types"; | ||
| import { inFlightToolName } from "./progress-line"; | ||
| import { retryAction } from "./retry-action"; | ||
| import { | ||
| fetchChatTranscript, | ||
| pollSettledTranscript, | ||
| transcriptLooksUnfinished, | ||
| } from "./settled-transcript"; | ||
| import { toolPendingLabel } from "./tool-labels"; | ||
| import { takeNavigateIntent } from "./turn-navigation"; | ||
| import { sendRequestOutcome } from "./send-request"; | ||
| import { teardownCancelsTurn, unmountTeardown } from "./turn-teardown"; | ||
| import { | ||
| activeToolPendingKey, | ||
| createKeyedDeadline, | ||
| FIRST_EVENT_DEADLINE_MS, | ||
| TOOL_PENDING_DEADLINE_MS, | ||
| turnDeadlineErrorMessage, | ||
| type TurnDeadlineError, | ||
| } from "./turn-deadlines"; | ||
| import { useAgentMessageQuota } from "./useAgentMessageQuota"; | ||
| import { useTriggerUriResolver } from "./useTriggerUriResolver"; | ||
| import { WatchChips, type WatchChip } from "./WatchChips"; | ||
|
|
@@ -80,6 +90,8 @@ export function DashboardAgentChat({ | |
| onTurnSettled, | ||
| onActivityChange, | ||
| onQuotaChange, | ||
| firstEventDeadlineMs = FIRST_EVENT_DEADLINE_MS, | ||
| toolPendingDeadlineMs = TOOL_PENDING_DEADLINE_MS, | ||
| }: { | ||
| chatId: string; | ||
| initialMessages: UIMessage[]; | ||
|
|
@@ -102,13 +114,18 @@ export function DashboardAgentChat({ | |
| pagePaths?: Record<string, string>; | ||
| watchCard?: React.ReactNode; | ||
| appendedMessages?: { messages: UIMessage[]; seq: number }; | ||
| /** Nothing is persisted until the user submits the card. */ | ||
| onWatchIntent?: (spec: WatchSpec) => void; | ||
| /** Nothing is persisted until the user submits the card. `target` is set only when | ||
| * the watch targets another project/environment than this chat's own. */ | ||
| onWatchIntent?: (spec: WatchSpec, target?: { environmentId: string }) => void; | ||
| onCancelWatch: (watchId: string) => void; | ||
| onTurnSettled: () => void; | ||
| onActivityChange?: (chatId: string, activity: TurnActivity | null) => void; | ||
| /** The poll lives here, so this is where the panel learns the cap has lifted. */ | ||
| onQuotaChange?: (quota: MessageQuota) => void; | ||
| /** How long to wait for the first stream event before showing a bounded-wait error. */ | ||
| firstEventDeadlineMs?: number; | ||
| /** How long a single pending tool call can run before showing a bounded-wait error. */ | ||
| toolPendingDeadlineMs?: number; | ||
| }) { | ||
| const [input, setInput] = useState(""); | ||
| // Set when the server refuses a send over the cap, so the block shows at once rather than | ||
|
|
@@ -213,6 +230,53 @@ export function DashboardAgentChat({ | |
|
|
||
| const messages = orderTranscript(rawMessages, orderRef.current); | ||
|
|
||
| // Independent of the SDK's own `error`: both drive the live-error callout, but a | ||
| // deadline firing never touches the server turn or `status`. | ||
| const [deadlineError, setDeadlineError] = useState<TurnDeadlineError | null>(null); | ||
| // Bumped in `retry` to force the first-event effect to re-run when a resend reuses the | ||
| // same `status: "submitted"`. `dismissError` never bumps it. | ||
| const [attempt, setAttempt] = useState(0); | ||
| const firstEventDeadline = useRef( | ||
| createKeyedDeadline<"submitted">({ | ||
| deadlineMs: firstEventDeadlineMs, | ||
| onTimeout: () => setDeadlineError({ kind: "first-event" }), | ||
| onClear: () => | ||
| setDeadlineError((current) => (current?.kind === "first-event" ? null : current)), | ||
| }) | ||
| ).current; | ||
| const toolPendingDeadline = useRef( | ||
| createKeyedDeadline<string>({ | ||
| deadlineMs: toolPendingDeadlineMs, | ||
| onTimeout: (tool) => setDeadlineError({ kind: "tool-pending", tool }), | ||
| onClear: () => | ||
| setDeadlineError((current) => (current?.kind === "tool-pending" ? null : current)), | ||
| }) | ||
| ).current; | ||
| useEffect(() => { | ||
| firstEventDeadline.sync(status === "submitted" ? "submitted" : null); | ||
| // `attempt` forces a re-sync when `status` is unchanged across a retry (see its | ||
| // declaration above). | ||
| }, [status, firstEventDeadline, attempt]); | ||
| useEffect(() => { | ||
| toolPendingDeadline.sync(activeToolPendingKey(status, inFlightToolName(messages))); | ||
| }, [messages, status, toolPendingDeadline]); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| useEffect( | ||
| () => () => { | ||
| firstEventDeadline.dispose(); | ||
| toolPendingDeadline.dispose(); | ||
| }, | ||
| [firstEventDeadline, toolPendingDeadline] | ||
| ); | ||
| // The SDK's own error wins when both are present — it's the more specific failure. | ||
| const effectiveError = useMemo( | ||
| () => | ||
| error ?? | ||
| (deadlineError | ||
| ? new Error(turnDeadlineErrorMessage(deadlineError, toolPendingLabel)) | ||
| : undefined), | ||
| [error, deadlineError] | ||
| ); | ||
|
|
||
| // Read here, not in the panel, so it re-reads as each turn settles. | ||
| const quota = useAgentMessageQuota({ actionPath, chatId, status }); | ||
| useEffect(() => { | ||
|
|
@@ -299,13 +363,36 @@ export function DashboardAgentChat({ | |
| ); | ||
| if (!action) return; | ||
| clearError(); | ||
| setDeadlineError(null); | ||
| // Reset the deadlines' own key, not just the displayed error: a dangling tool part | ||
| // that already fired once would otherwise never re-arm (same key, no change to sync). | ||
| firstEventDeadline.sync(null); | ||
| toolPendingDeadline.sync(null); | ||
| // Forces the first-event effect to re-sync even when `status` stays "submitted" across | ||
| // the retry (a resend re-enters "submitted", the same value the failed turn left it in). | ||
| setAttempt((current) => current + 1); | ||
| turnStartedPathRef.current = renderedPathRef.current; | ||
| if (action.kind === "regenerate") { | ||
| void regenerate(); | ||
| return; | ||
| } | ||
| void sendMessage({ text: action.text, messageId: action.messageId }); | ||
| }, [messages, sendMessage, regenerate, clearError, atMessageCap]); | ||
| }, [ | ||
| messages, | ||
| sendMessage, | ||
| regenerate, | ||
| clearError, | ||
| atMessageCap, | ||
| firstEventDeadline, | ||
| toolPendingDeadline, | ||
| ]); | ||
|
|
||
| const dismissError = useCallback(() => { | ||
| clearError(); | ||
| setDeadlineError(null); | ||
| firstEventDeadline.sync(null); | ||
| toolPendingDeadline.sync(null); | ||
| }, [clearError, firstEventDeadline, toolPendingDeadline]); | ||
|
Comment on lines
+390
to
+395
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Dismissed bounded-wait error never re-arms during the same turn
Was this helpful? React with 👍 or 👎 to provide feedback.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Owner decision — dismiss means 'stop telling me', so it deliberately does not bump the attempt counter (unlike retry, which re-arms). Intended asymmetry. |
||
|
|
||
| const resolveUri = useTriggerUriResolver(actionPath); | ||
|
|
||
|
|
@@ -345,7 +432,10 @@ export function DashboardAgentChat({ | |
| submit(intent.prompt); | ||
| return; | ||
| case "watch": | ||
| onWatchIntent?.(intent.spec); | ||
| onWatchIntent?.( | ||
| intent.spec, | ||
| intent.target ? { environmentId: intent.target.environmentId } : undefined | ||
| ); | ||
| return; | ||
| case "navigate": | ||
| void goTo(intent); | ||
|
|
@@ -383,7 +473,12 @@ export function DashboardAgentChat({ | |
| useEffect(() => { | ||
| const pending = pendingWatchIntents(messages, watchProposedRef.current!); | ||
| const proposed = pending.at(-1); | ||
| if (proposed) onWatchIntent?.(proposed.spec); | ||
| if (proposed) { | ||
| onWatchIntent?.( | ||
| proposed.spec, | ||
| proposed.target ? { environmentId: proposed.target.environmentId } : undefined | ||
| ); | ||
| } | ||
| }, [messages, onWatchIntent]); | ||
|
|
||
| const stop = useCallback(() => { | ||
|
|
@@ -450,10 +545,10 @@ export function DashboardAgentChat({ | |
| <DashboardAgentMessages | ||
| messages={messages} | ||
| activity={activity} | ||
| error={error} | ||
| error={effectiveError} | ||
| onRetry={retry} | ||
| retryDisabledReason={atMessageCap ? MESSAGE_QUOTA_REACHED_REASON : undefined} | ||
| onDismissError={clearError} | ||
| onDismissError={dismissError} | ||
| onIntent={handleIntent} | ||
| pagePaths={pagePaths} | ||
| watches={watches} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.