mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
* feat: render an AI agent result as its answer, not as raw JSON Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: sanitize agent markdown through the shared plugin chain Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: fold agent stream events incrementally per poll Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: separate the agent meta line from the result toggle group Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: add a transcript view of an agent run's conversation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: retire AIAgentLogViewer in favour of the transcript Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: show the partial transcript a max-iterations failure carries Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: move the agent meta line and system prompt below the conversation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: show an agent run as what it did, not as a conversation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: name the agent run breakdown a trace Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): label the save-as-agent form fields per the guidelines Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): reuse the resource form's path and description fields Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): keep the action tags on a max-iterations failure Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): keep offline replay inert and the streamed answer to one turn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ai-agent): reset the streamed answer on providers that skip tool_call Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry the event type narrowing through the stream parser Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: survive a malformed message rather than take the viewer down Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: coerce agent messages once at the parse boundary Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: show an agent run as one scroll ending in its output Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep every streamed turn instead of dropping the narration Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: share the chat divider and drop the unsafe run auto-scroll Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: give the labelled divider a border colour and the standard pretty icon Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: pad the agent run below its badges as well as above Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: follow a streaming run's pane without moving the page Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: share the chat's stick-to-bottom mechanics with the agent run Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the answer's citations and end a turn's reasoning with the turn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: read the agent stream through the windmill-chat sdk parser Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: show an agent run's thinking instead of falling back to raw json Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop a stream the fold cannot use from claiming the run pane Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: identify an agent run by more than the job id the replay withholds Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: drop the tests and comment lines that were not earning their place Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop a streamed turn's text shifting when a tool call closes it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
117 lines
4.2 KiB
Svelte
117 lines
4.2 KiB
Svelte
<script lang="ts">
|
|
import type { Snippet } from 'svelte'
|
|
import { Badge } from '$lib/components/common'
|
|
import GfmMarkdown from './GfmMarkdown.svelte'
|
|
import AgentTrace from './AgentTrace.svelte'
|
|
import ChatCollapsibleCard from './copilot/chat/ChatCollapsibleCard.svelte'
|
|
import LabeledDivider from './LabeledDivider.svelte'
|
|
import WebSearchSourcesDisplay from './copilot/chat/WebSearchSourcesDisplay.svelte'
|
|
import { buildAgentTrace, splitFinalAnswer } from './agentTrace'
|
|
import { runPane } from './agentScroll'
|
|
import { createBottomSticker } from './stickToBottom'
|
|
import { formatTokenCount, summarizeAgentResult, type AgentResult } from './aiAgentResult'
|
|
|
|
interface Props {
|
|
result: AgentResult
|
|
workspaceId?: string
|
|
/** Identifies the run, so a reused viewer lands on the new one's output. */
|
|
runKey?: string
|
|
/**
|
|
* How to render an output that is not text: whatever the result viewer would do
|
|
* with that object on its own, a table for rows or the file viewer for an S3
|
|
* object. Passed in rather than imported so this does not reach back into the
|
|
* viewer rendering it.
|
|
*/
|
|
structuredOutput: Snippet<[unknown]>
|
|
}
|
|
|
|
let { result, workspaceId, runKey, structuredOutput }: Props = $props()
|
|
|
|
let summary = $derived(summarizeAgentResult(result))
|
|
let textOutput = $derived(typeof result.output === 'string' ? result.output : undefined)
|
|
|
|
// The turn that produced `output` is not a trace row: the output block below is
|
|
// that same text, and printing it twice in one scroll reads as the agent having
|
|
// answered itself. Its citations move down with it.
|
|
let answer = $derived(splitFinalAnswer(buildAgentTrace(result.messages), result.output))
|
|
let trace = $derived(answer.trace)
|
|
let reasoning = $derived(result.reasoning?.trim())
|
|
let reasoningExpanded = $state(false)
|
|
|
|
let anchor: HTMLElement | undefined = $state()
|
|
const sticker = createBottomSticker()
|
|
$effect(() => {
|
|
// Also on arrival from a stream: the run finishing adds the output separator,
|
|
// so the end has moved from wherever the stream had the reader parked.
|
|
runKey
|
|
trace.length
|
|
reasoning
|
|
sticker.scrollToEnd(runPane(anchor))
|
|
})
|
|
</script>
|
|
|
|
<div bind:this={anchor} class="flex flex-col w-full py-3">
|
|
{#if trace.length > 0}
|
|
<AgentTrace entries={trace} {workspaceId} />
|
|
{/if}
|
|
{#if reasoning}
|
|
<!-- The whole run's thinking, which the worker hands back joined rather than
|
|
per iteration, so it reads as one block above the answer it led to. -->
|
|
<ChatCollapsibleCard
|
|
label="Thinking"
|
|
expanded={reasoningExpanded}
|
|
onToggle={() => (reasoningExpanded = !reasoningExpanded)}
|
|
contentClass="font-main"
|
|
>
|
|
<GfmMarkdown md={reasoning} prose="xs" noPadding />
|
|
</ChatCollapsibleCard>
|
|
{/if}
|
|
{#if trace.length > 0 || reasoning}
|
|
<LabeledDivider class="my-3">
|
|
<span class="text-2xs text-hint">Output</span>
|
|
</LabeledDivider>
|
|
{/if}
|
|
<div>
|
|
{#if textOutput !== undefined}
|
|
{#if textOutput === ''}
|
|
<span class="text-tertiary text-xs">The agent returned no answer</span>
|
|
{:else}
|
|
<!-- A model writes this, and what it writes is steerable by whatever
|
|
reached its context — a user message, a tool's output. So it is
|
|
untrusted input and goes through the shared sanitizing chain, which
|
|
is also what makes it inert on the public replay page. -->
|
|
<GfmMarkdown md={textOutput} noPadding />
|
|
{/if}
|
|
{:else}
|
|
{@render structuredOutput(result.output)}
|
|
{/if}
|
|
{#if answer.sources}
|
|
<div class="mt-2">
|
|
<WebSearchSourcesDisplay sources={answer.sources} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- What the run cost, as a footnote to what it produced. -->
|
|
<div class="flex items-center gap-2 flex-wrap text-xs mt-3">
|
|
{#if summary.toolCalls > 0}
|
|
<Badge color="blue">
|
|
{summary.toolCalls}
|
|
{summary.toolCalls === 1 ? 'tool call' : 'tool calls'}
|
|
</Badge>
|
|
{/if}
|
|
{#if summary.webSearches > 0}
|
|
<Badge color="blue">
|
|
{summary.webSearches}
|
|
{summary.webSearches === 1 ? 'web search' : 'web searches'}
|
|
</Badge>
|
|
{/if}
|
|
{#if summary.tokens !== undefined}
|
|
<Badge color="gray">{formatTokenCount(summary.tokens)} tokens</Badge>
|
|
{/if}
|
|
{#if summary.cachedTokens}
|
|
<Badge color="gray">{formatTokenCount(summary.cachedTokens)} cached</Badge>
|
|
{/if}
|
|
</div>
|
|
</div>
|