mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 00:03:09 +00:00
* feat(ai-agent): built-in approval tool that parks the agent An AI agent step can now be given a built-in "Request approval" tool. The model calls `request_approval` with a message; the agent stores its conversation in the new `ai_agent_checkpoint` table, releases its worker by parking its own queue row like a workflow-as-code parent, and resumes with the answer (approved, approver, comment) or a timeout once someone answers from the run page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): tie approval answers to their request and hold the turn - A turn that asks for approval runs none of its other calls; they are refused so the model repeats them after the answer. - The run page sends the request's call id. An answer naming another request is ignored and the agent parks again for the time left; `suspend` is zeroed when the answer is read, so answers sent while the agent runs are refused. - The checkpoint is read whatever the current roster holds, the answer's outcome is written by position, and a cancel that beats the park completes the job as cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): say why calls beside an invalid approval request were refused Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): one approval form per request, none on a finished run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): answer approval requests from a flow's chat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): show chat approvals for agents that do not stream Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): approval as a per-tool setting, built-in tool asks a question Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): say when a nested agent leaves out tools that require approval Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): show chat approvals and questions on the tool's own card Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): question choices, and approval buttons shared with approval steps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): show a waiting agent as suspended, and refused calls as not run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): each added chat card shows its own call, answered marker keyed by job Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ai-agent): approve and reject a held call through resume_suspended Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): require approval in a tool settings tab, plain descriptions on built-in tool panes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): sentence case for the web search tool label Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-agent): built-in tool drawers explain themselves in the header subtitle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): clearer question tool subtitle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai-agent): badge the tool settings tab like run settings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ai-agent): regenerate system prompts and MCP session files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
4.9 KiB
Svelte
158 lines
4.9 KiB
Svelte
<!--
|
|
@component
|
|
A saved agent's configuration, read-only: the model it calls, the instructions it is given and
|
|
the tools it can use. Laid out as the AI session's assistant settings are, and its Tools section
|
|
is that one, since both answer the same question about what an assistant can see and use.
|
|
-->
|
|
<script lang="ts">
|
|
import { Boxes, Cpu, ScrollText } from 'lucide-svelte'
|
|
import Modal2 from '$lib/components/common/modal/Modal2.svelte'
|
|
import SidebarNavigation from '$lib/components/common/sidebar/SidebarNavigation.svelte'
|
|
import AssistantToolsSection from '$lib/components/copilot/chat/AssistantToolsSection.svelte'
|
|
import type { ToolSummary } from '$lib/components/copilot/chat/agentContext'
|
|
import {
|
|
AGENT_BRAIN_LABELS,
|
|
summarizeAgentBrain,
|
|
type AIAgentConfig
|
|
} from '../agentResourceUtils'
|
|
import {
|
|
BUILTIN_TOOLS,
|
|
builtinToolType,
|
|
toolDisplayName,
|
|
type AgentTool
|
|
} from '../agentToolUtils'
|
|
|
|
type Section = 'model' | 'instructions' | 'tools'
|
|
|
|
interface Props {
|
|
config: AIAgentConfig
|
|
/** The arguments a tool takes, where they are known. */
|
|
toolSchema?: (id: string) => any
|
|
}
|
|
|
|
let { config, toolSchema = undefined }: Props = $props()
|
|
|
|
let isOpen = $state(false)
|
|
let section = $state<Section>('model')
|
|
let toolsBusy = $state(false)
|
|
|
|
export function open(target: Section = section) {
|
|
section = target
|
|
isOpen = true
|
|
}
|
|
|
|
const SYSTEM_PROMPT_LABEL = AGENT_BRAIN_LABELS['system_prompt']
|
|
|
|
let settings = $derived(
|
|
summarizeAgentBrain(config).filter((r) => r.label !== SYSTEM_PROMPT_LABEL)
|
|
)
|
|
let systemPrompt = $derived(
|
|
typeof config.system_prompt === 'string' && config.system_prompt !== ''
|
|
? config.system_prompt
|
|
: undefined
|
|
)
|
|
let tools = $derived<ToolSummary[]>(
|
|
(Array.isArray(config.tools) ? (config.tools as AgentTool[]) : [])
|
|
.map((tool) => ({
|
|
name: toolDisplayName(tool) ?? tool?.id ?? '',
|
|
description: toolKind(tool),
|
|
parameters: { required: [], ...(toolSchema?.(tool?.id) ?? {}) }
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
// The section keys and deduplicates its rows by name, and two MCP servers or web searches
|
|
// can share one, so a repeat is told apart by its position.
|
|
.map((tool, i, all) => {
|
|
const nth = all.slice(0, i).filter((t) => t.name === tool.name).length
|
|
return nth === 0 ? tool : { ...tool, name: `${tool.name} (${nth + 1})` }
|
|
})
|
|
)
|
|
|
|
let sections = $derived([
|
|
{ id: 'model', label: 'Model', icon: Cpu },
|
|
{ id: 'instructions', label: 'Instructions', icon: ScrollText },
|
|
{ id: 'tools', label: 'Tools', icon: Boxes, count: tools.length }
|
|
])
|
|
|
|
function toolKind(tool: AgentTool): string {
|
|
const value = tool?.value as Record<string, any> | undefined
|
|
if (value?.tool_type === 'mcp') return 'MCP server'
|
|
const builtin = builtinToolType(tool)
|
|
if (builtin) return BUILTIN_TOOLS[builtin].label
|
|
switch (value?.type) {
|
|
case 'script':
|
|
return `Script ${value.path ?? ''}`.trim()
|
|
case 'flow':
|
|
return `Flow ${value.path ?? ''}`.trim()
|
|
case 'rawscript':
|
|
return 'Inline script'
|
|
case 'aiagent':
|
|
return 'Agent'
|
|
default:
|
|
return 'Tool'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Modal2
|
|
bind:isOpen
|
|
title="Agent configuration"
|
|
fixedWidth="md"
|
|
fixedHeight="lg"
|
|
closeOnOutsideClick={!toolsBusy}
|
|
closeOnEscape={!toolsBusy}
|
|
>
|
|
{#snippet headerLeft()}
|
|
<p class="pl-3 pt-1 text-xs text-secondary truncate">
|
|
What the agent runs with. Edit the agent to change it.
|
|
</p>
|
|
{/snippet}
|
|
|
|
<div class="w-full flex min-h-0 gap-4">
|
|
<div class="w-52 shrink-0 flex flex-col border-r border-border-light pr-3">
|
|
<SidebarNavigation
|
|
groups={[{ items: sections }]}
|
|
selectedId={section}
|
|
onNavigate={(id) => (section = id as Section)}
|
|
/>
|
|
</div>
|
|
|
|
<div class="grow min-w-0 flex flex-col min-h-0">
|
|
{#if section === 'model'}
|
|
<dl class="flex flex-col gap-3 overflow-y-auto pr-2">
|
|
{#each settings as row (row.label)}
|
|
<div class="flex flex-col gap-0.5">
|
|
<dt class="text-2xs text-secondary">{row.label}</dt>
|
|
<dd class="text-xs text-primary break-words">{row.value}</dd>
|
|
</div>
|
|
{:else}
|
|
<span class="text-xs text-secondary">Not configured</span>
|
|
{/each}
|
|
</dl>
|
|
{:else if section === 'instructions'}
|
|
<div class="overflow-y-auto pr-2">
|
|
{#if systemPrompt}
|
|
<p
|
|
class="text-xs text-primary whitespace-pre-wrap break-words rounded-md bg-surface-secondary p-3"
|
|
>
|
|
{systemPrompt}
|
|
</p>
|
|
{:else}
|
|
<span class="text-xs text-secondary">No system message</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<!-- Mounted throughout, as the assistant settings keep it: it owns its scrolling and
|
|
the detail page it opens. -->
|
|
<div class="{section === 'tools' ? 'flex' : 'hidden'} grow min-h-0 flex-col overflow-hidden">
|
|
<AssistantToolsSection
|
|
{tools}
|
|
providerTools={[]}
|
|
description="What the agent can call: scripts, flows, other agents, MCP servers and web search."
|
|
active={section === 'tools'}
|
|
bind:blocksClose={toolsBusy}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal2>
|