Files
windmill/frontend/src/lib/components/JobStatus.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

95 lines
3.1 KiB
Svelte

<script lang="ts">
import { displayDate, msToReadableTime } from '$lib/utils'
import type { CompletedJob, QueuedJob } from '$lib/gen'
import Badge from './common/badge/Badge.svelte'
import { isSuspended } from './agentRequest'
import { forLater } from '$lib/forLater'
import { Circle } from 'lucide-svelte'
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
import QueuePosition from './QueuePosition.svelte'
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
const SMALL_ICON_SIZE = 12
interface Props {
job: QueuedJob | CompletedJob | undefined
large?: boolean
}
let { job, large = false }: Props = $props()
</script>
{#if job && 'success' in job && job.success}
<Badge {large} color="green">
Successfully ran in {msToReadableTime(job.duration_ms)}
{job.is_skipped ? '(Skipped)' : ''}
{#if job.self_wait_time_ms || job.aggregate_wait_time_ms}
<WaitTimeWarning
self_wait_time_ms={job.self_wait_time_ms}
aggregate_wait_time_ms={job.aggregate_wait_time_ms}
variant="alert"
/>
{/if}
</Badge>
{:else if job && 'success' in job && job.resolved}
<Badge {large} color="orange" title={job.resolution_note}>
<!-- `resolved_by` is absent both for an automatic resolution and for a manual one
outside EE, so the automatic wording comes from `resolved_automatically`. -->
{job.canceled ? 'Canceled' : 'Failed'} after {msToReadableTime(job.duration_ms)}, resolved{job.resolved_automatically
? ' automatically'
: job.resolved_by
? ` by ${job.resolved_by}`
: ''}
{#if job.self_wait_time_ms || job.aggregate_wait_time_ms}
<WaitTimeWarning
self_wait_time_ms={job.self_wait_time_ms}
aggregate_wait_time_ms={job.aggregate_wait_time_ms}
variant="alert"
/>
{/if}
</Badge>
{:else if job && 'success' in job}
<Badge {large} color="red">
{job.canceled ? 'Canceled' : 'Failed'} after {msToReadableTime(job.duration_ms)}
{#if job.self_wait_time_ms || job.aggregate_wait_time_ms}
<WaitTimeWarning
self_wait_time_ms={job.self_wait_time_ms}
aggregate_wait_time_ms={job.aggregate_wait_time_ms}
variant="alert"
/>
{/if}
</Badge>
{:else if job && 'running' in job && job.running && isSuspended(job)}
<div>
<Badge {large} color="violet">
Suspended
{#if job.flow_status}
({(job.flow_status?.step ?? 0) + 1} of {job.raw_flow?.modules?.length ?? '?'})
{/if}
</Badge>
</div>
{:else if job && 'running' in job && job.running}
<div>
<Badge {large} color="yellow">
Running
{#if job.flow_status}
({(job.flow_status?.step ?? 0) + 1} of {job.raw_flow?.modules?.length ?? '?'})
{/if}
</Badge>
</div>
{:else if job && 'running' in job && 'scheduled_for' in job && job.scheduled_for && forLater(job.scheduled_for)}
<div>
<Badge color="blue" {large}>
Scheduled for {displayDate(job.scheduled_for)}
</Badge>
</div>
{:else if job && 'running' in job}
<div class="flex flex-row gap-1 items-center">
<Badge color="orange" {large}>Queued</Badge>
<NoWorkerWithTagWarning tag={job.tag} />
<QueuePosition jobId={job.id} workspaceId={job.workspace_id} minimal />
</div>
{:else}
<Circle size={SMALL_ICON_SIZE} class="text-gray-200" />
{/if}