Files
windmill/frontend/src/lib/components/flows/content/AgentConfigModal.svelte
T
hugocasaandClaude Opus 5.5 a1bc9ec0c2 feat: tool approval and a question tool for ai agent steps (#11826)
* 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>
2026-10-26 19:22:45 +01:00

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>