mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-22 00:03:35 +00:00
* feat(frontend): group the AI agent step form and edit saved agents in a modal Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: edit a saved AI agent through its own resource draft Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: drop the agent fork-for-edit session now that edits live in a draft Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: edit ai_agent resources from the resources page with the agent editor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: send a standalone agent's brain from the module when testing a step Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep the agent draft faithful to the resource it deploys to Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: add the sqlx cache entry for the eval subject rename Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: share the module insert between the graph and the agent editor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): open evals inside the agent editor, actions in its header Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): add tools from the agent editor and lighten its test pane Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): open an ai_agent deep link in the agent editor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): drop the failed result badge on a step that never ran Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): head the agent editor's levels with a back control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): drop connect and fill inputs from the agent editor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): lighten the agent editor's run panel Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): stop a nested agent tool's config reading as AI-filled Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): offer only AI or static on an agent tool's inputs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): give a saved agent's tool editor a static-only surface Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): open an agent tool in a drawer beside the agent Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(frontend): hide unset agent config in the run form Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(frontend): share the input forms' pickers and s3 lookup Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(frontend): drop a dead agent-editor export and fix two stale comments Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): reach an ai_agent's resource-level settings and copilot Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): open an ai_agent's resource view as JSON, not the generic form Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): address review findings on the agent editor's draft and streaming Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): close the agent editor on a version restore, as the resource editor does Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): stop the provider picker auto-writing a kind, and clear review nits Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(frontend): drop the fork-for-edit leftovers from the agent card Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): mount the agent editor in the dev flow editor and guard the deep-link race Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): deploy the agent config that was submitted, and refuse one no run could use Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(frontend): build the agent editor's rows from the design-system button Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): keep a draft-only agent's draft, and let a blank MCP summary deploy Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): guard read-only agents, incomplete MCP tools and duplicate editor mounts Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read-only agent editor, linked-card refresh, atomic eval rename * fix: eval rename needs the privileged pool, per-workspace write access * fix(frontend): drop the agent editor target when its mount goes away * refactor: drop the agent rename work from this PR, unban the bindable defaults * fix(frontend): refuse a renaming deploy and drop the copilot from static-only fields * fix(frontend): mirror the worker's streaming rule and scope agent writes to their target * fix(frontend): read runtime streaming as off and reset the drawer's json view * fix(frontend): read an unsettled output_type as non-streaming too * fix(frontend): let the showing modal claim an agent opened from inside it * fix(frontend): keep in-flight edits, tool replacements and every linked step in sync * fix(frontend): keep attachments in the run form and bind the agent ref to its tools * fix(frontend): preview the agent as authored and re-evaluate step args on run * fix(frontend): scope agent-editor ownership to the flow's workspace * fix(frontend): drop the tool drill-in where there is no graph to select on * fix(frontend): require a provider kind and keep one resource editor open at a time * fix(frontend): keep legacy nulls, static-only text literal, and the handover anchor * test(worker): pin the agent streaming default * fix(frontend): let an AI-fillable input be switched to static * fix(frontend): report agent editor background failures instead of floating them * fix(frontend): keep the version pane's path alive while the editor closes * fix(frontend): clear the anchor-keep flag at the start of each drawer session * fix(frontend): preview the agent without its synthetic path, refresh the baseline on external writes * refactor(frontend): drop the unverifiable baseline refresh, state the synthetic-path rule * fix(frontend): keep the synthetic path out of agent tool test runs too * refactor(frontend): host the agent editor under the agent's own path * fix(frontend): mark an agent editor's host explicitly instead of inferring it from the path * fix(frontend): discard linked-agent responses from before a deploy * fix(frontend): keep a flow mount from claiming an agent editor's nested target * feat(frontend): keep an agent used as a tool inside the agent being edited * fix(frontend): reserve the agent editor's root module id * docs(frontend): record why the agent editor previews under the agent's path * fix(frontend): refuse to open or deploy a resource that is not an agent * docs(frontend): put the scope-migration comment on the function it describes * fix(frontend): refuse an agent path whose resource type is not proven * fix(frontend): recheck the resource type before deploying, and keep expressions off static-only inputs * fix(frontend): lazy-load the agent editor and slide its levels like the evals pane * refactor: drop unreachable non-list tools check from agent deploy Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): clear text-only agent fields on image output, reserve the root id Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): keep the agent editor usable for a non-list tools value Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): stop the parked eval run list from taking arrow keys Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): report a non-list tools value on deploy instead of throwing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): keep temperature editable for image output Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): skip non-object tool entries when rendering an agent Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): guard tool entry reads instead of copying the tool array Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): key tool rows by position so duplicate ids render Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
344 lines
11 KiB
Svelte
344 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { moduleSlot, savedModuleById } from '../moduleSlot'
|
|
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
|
|
import { type FlowModule } from '$lib/gen'
|
|
import { getContext } from 'svelte'
|
|
|
|
import { stepSettingDefaults } from '$lib/components/flows/flowStepSettings'
|
|
import { emptyString } from '$lib/utils'
|
|
import type { FlowEditorContext } from '../types'
|
|
import FlowLoop from './FlowLoop.svelte'
|
|
import FlowModuleComponent from './FlowModuleComponent.svelte'
|
|
import FlowBranchAllWrapper from './FlowBranchAllWrapper.svelte'
|
|
import FlowBranchOneWrapper from './FlowBranchOneWrapper.svelte'
|
|
import {
|
|
createInlineScriptModule,
|
|
pickFlow,
|
|
pickScript
|
|
} from '$lib/components/flows/flowStateUtils.svelte'
|
|
import FlowInputs from './FlowInputs.svelte'
|
|
import { Alert } from '$lib/components/common'
|
|
import FlowInputsFlow from './FlowInputsFlow.svelte'
|
|
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
|
|
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
|
|
import FlowWhileLoop from './FlowWhileLoop.svelte'
|
|
import FlowCard from '../common/FlowCard.svelte'
|
|
import type { TriggerContext } from '$lib/components/triggers'
|
|
import { formatCron } from '$lib/utils'
|
|
import AgentToolWrapper from './AgentToolWrapper.svelte'
|
|
const { selectionManager, flowStateStore, opWorkspace } =
|
|
getContext<FlowEditorContext>('FlowEditorContext')
|
|
const selectedId = $derived(selectionManager.getSelectedId())
|
|
|
|
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
|
|
|
let scriptKind: 'script' | 'trigger' | 'approval' | 'preprocessor' = $state('script')
|
|
let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = $state('script')
|
|
|
|
// These pointers are used to easily access previewArgs of parent module, and previous module
|
|
|
|
interface Props {
|
|
flowModule: FlowModule
|
|
noEditor?: boolean
|
|
enableAi?: boolean
|
|
savedModule?: FlowModule | undefined
|
|
// Pointer to parent module, only defined within Branches or Loops.
|
|
parentModule?: FlowModule | undefined
|
|
// Pointer to previous module, for easy access to testing results
|
|
previousModule?: FlowModule | undefined
|
|
forceTestTab?: Record<string, boolean>
|
|
highlightArg?: Record<string, string | undefined>
|
|
isAgentTool?: boolean
|
|
/** Lets an agent step add a tool through the same path as the graph's `+ Tool`. */
|
|
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
|
|
}
|
|
|
|
let {
|
|
flowModule = $bindable(),
|
|
noEditor = false,
|
|
enableAi = false,
|
|
savedModule = undefined,
|
|
parentModule = $bindable(),
|
|
previousModule = undefined,
|
|
forceTestTab,
|
|
highlightArg,
|
|
isAgentTool = false,
|
|
flowModuleSchemaMap = undefined
|
|
}: Props = $props()
|
|
|
|
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
|
const primaryIndex = triggersState.triggers.findIndex((t) => t.isPrimary)
|
|
if (primaryIndex === -1) {
|
|
const primaryCfg = {
|
|
summary: 'Scheduled poll of flow',
|
|
args: {},
|
|
schedule: formatCron('0 */15 * * *'),
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
enabled: true,
|
|
is_flow: true
|
|
}
|
|
triggersState.addDraftTrigger(triggersCount, 'schedule', undefined, primaryCfg)
|
|
} else if (triggersState.triggers[primaryIndex].draftConfig) {
|
|
//If there is a primary schedule draft update it
|
|
const newCfg = { ...triggersState.triggers[primaryIndex].draftConfig }
|
|
let updated = false
|
|
if (!newCfg.schedule) {
|
|
newCfg.schedule = formatCron('0 */15 * * *')
|
|
updated = true
|
|
}
|
|
if (!newCfg.enabled) {
|
|
newCfg.enabled = true
|
|
updated = true
|
|
}
|
|
if (updated) {
|
|
triggersState.triggers[primaryIndex].draftConfig = newCfg
|
|
}
|
|
}
|
|
|
|
module.stop_after_if = stepSettingDefaults('early-stop', 'trigger')
|
|
}
|
|
async function createModuleFromScript(
|
|
path: string,
|
|
summary: string,
|
|
kind: string,
|
|
hash: string | undefined
|
|
) {
|
|
const [module, state] = await pickScript(
|
|
path,
|
|
summary,
|
|
flowModule.id,
|
|
hash,
|
|
undefined,
|
|
opWorkspace?.()
|
|
)
|
|
|
|
if (kind == 'approval') {
|
|
module.suspend = stepSettingDefaults('suspend')
|
|
}
|
|
|
|
if (kind == 'trigger') {
|
|
initializePrimaryScheduleForTriggerScript(module)
|
|
}
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}
|
|
</script>
|
|
|
|
{#if flowModule.id === selectedId}
|
|
{#if flowModule.value.type === 'forloopflow'}
|
|
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} {enableAi} />
|
|
{:else if flowModule.value.type === 'whileloopflow'}
|
|
<FlowWhileLoop {noEditor} bind:mod={flowModule} {previousModule} {parentModule} />
|
|
{:else if flowModule.value.type === 'branchone'}
|
|
<FlowBranchesOneWrapper {noEditor} {previousModule} {parentModule} {enableAi} bind:flowModule />
|
|
{:else if flowModule.value.type === 'branchall'}
|
|
<FlowBranchesAllWrapper {noEditor} {previousModule} {parentModule} bind:flowModule />
|
|
{:else if flowModule.value.type === 'identity'}
|
|
{#if selectedId == 'failure'}
|
|
<div class="p-4">
|
|
<Alert type="info" title="Error handlers are triggered upon non recovered errors">
|
|
If defined, the error handler will take the error as input.
|
|
</Alert>
|
|
</div>
|
|
{:else if selectedId == 'preprocessor'}
|
|
<div class="p-4">
|
|
<Alert
|
|
type="info"
|
|
title="Preprocessor is called when the flow is triggered by API or email"
|
|
>
|
|
It prepares arguments for the flow. Besides request arguments, the preprocessor receives a
|
|
`wm_trigger` argument with trigger details.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if flowModule.value.flow}
|
|
<FlowInputsFlow
|
|
on:pick={async ({ detail }) => {
|
|
const { path, summary } = detail
|
|
const [module, state] = await pickFlow(path, summary, flowModule.id, opWorkspace?.())
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}}
|
|
/>
|
|
{:else}
|
|
<FlowInputs
|
|
{noEditor}
|
|
summary={flowModule.summary}
|
|
shouldDisableTriggerScripts={parentModule !== undefined ||
|
|
previousModule !== undefined ||
|
|
selectedId == 'failure' ||
|
|
selectedId == 'preprocessor'}
|
|
on:pick={async ({ detail }) => {
|
|
const { path, summary, kind, hash } = detail
|
|
// The picked script's summary is a default: anything already typed on the step
|
|
// was the user's choice and outranks it.
|
|
createModuleFromScript(
|
|
path,
|
|
emptyString(flowModule.summary) ? summary : flowModule.summary,
|
|
kind,
|
|
hash
|
|
)
|
|
}}
|
|
on:new={async ({ detail }) => {
|
|
const { language, kind, subkind, summary } = detail
|
|
|
|
const [module, state] = await createInlineScriptModule(
|
|
language,
|
|
kind,
|
|
subkind,
|
|
flowModule.id,
|
|
summary
|
|
)
|
|
scriptKind = kind
|
|
scriptTemplate = subkind
|
|
|
|
if (kind == 'trigger') {
|
|
initializePrimaryScheduleForTriggerScript(module)
|
|
}
|
|
|
|
if (kind == 'approval') {
|
|
module.suspend = stepSettingDefaults('suspend')
|
|
}
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}}
|
|
failureModule={selectedId === 'failure'}
|
|
preprocessorModule={selectedId === 'preprocessor'}
|
|
/>
|
|
{/if}
|
|
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent'}
|
|
<FlowModuleComponent
|
|
{noEditor}
|
|
bind:flowModule
|
|
{parentModule}
|
|
{previousModule}
|
|
failureModule={selectedId === 'failure'}
|
|
preprocessorModule={selectedId === 'preprocessor'}
|
|
{scriptKind}
|
|
{scriptTemplate}
|
|
{enableAi}
|
|
{savedModule}
|
|
forceTestTab={forceTestTab?.[flowModule.id]}
|
|
highlightArg={highlightArg?.[flowModule.id]}
|
|
{isAgentTool}
|
|
{flowModuleSchemaMap}
|
|
/>
|
|
{/if}
|
|
{:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'}
|
|
{#each flowModule.value.modules as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(
|
|
() => (flowModule.value as { modules: FlowModule[] }).modules,
|
|
child.id,
|
|
child
|
|
)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.modules[index - 1]}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{:else if flowModule.value.type === 'branchone'}
|
|
{#if selectedId === `${flowModule?.id}-branch-default`}
|
|
<div class="h-full flex flex-col">
|
|
<FlowCard {noEditor} title="Default branch">
|
|
<div class="p-4">
|
|
<p class="text-xs text-tertiary">
|
|
Nothing to configure — this branch runs when none of the predicates match.
|
|
</p>
|
|
</div>
|
|
</FlowCard>
|
|
</div>
|
|
{:else}
|
|
{#each flowModule.value.default as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(
|
|
() => (flowModule.value as { default: FlowModule[] }).default,
|
|
child.id,
|
|
child
|
|
)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.default[index - 1]}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{#each flowModule.value.branches as branch, branchIndex (branch)}
|
|
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
|
<FlowBranchOneWrapper
|
|
{noEditor}
|
|
{branch}
|
|
parentModule={flowModule}
|
|
{previousModule}
|
|
{enableAi}
|
|
/>
|
|
{:else}
|
|
{#each branch.modules as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(() => branch.modules, child.id, child)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else if flowModule.value.type === 'branchall'}
|
|
{#each flowModule.value.branches as branch, branchIndex (branch)}
|
|
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
|
<FlowBranchAllWrapper {noEditor} {branch} />
|
|
{:else}
|
|
{#each branch.modules as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(() => branch.modules, child.id, child)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
|
|
{enableAi}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else if flowModule.value.type === 'aiagent'}
|
|
{#each flowModule.value.tools ?? [] as tool, toolIndex (toolIndex)}
|
|
{#if selectedId === tool.id}
|
|
<AgentToolWrapper
|
|
{noEditor}
|
|
bind:tool={flowModule.value.tools![toolIndex]}
|
|
parentModule={flowModule}
|
|
{previousModule}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
siblingToolNames={flowModule.value.tools!.map((t) => t.summary ?? '')}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|