Files
windmill/frontend/src/lib/components/flows/agentResourceUtils.ts
T
hugocasaandClaude Opus 5 f037c73d10 feat(frontend): group the agent form and edit saved agents as drafts (#10880)
* 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>
2026-09-04 16:32:17 +02:00

203 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { deepEqual } from 'fast-equals'
import type { InputTransform } from '$lib/gen'
import { AGENT_FIELDS } from './agentFormFields'
// The brain fields stored flat in an `ai_agent` resource value. The flow-local inputs
// (user_message/user_attachments) are intentionally excluded — they are supplied per-flow.
export const AGENT_BRAIN_KEYS = [
'provider',
'output_type',
'system_prompt',
'streaming',
'memory',
'output_schema',
'max_completion_tokens',
'temperature',
'max_iterations'
] as const
export const AGENT_FLOW_LOCAL_KEYS = ['user_message', 'user_attachments'] as const
export type AgentTool = Record<string, any>
/**
* Why this resource cannot be edited as an agent, if it cannot. A path with no deployed row answers
* with its own draft, whose type the generic resource editor never writes, so an unknown type is
* refused rather than assumed: deploying it would create that path as an agent.
*/
export function agentEditorRefusal(
path: string,
resourceType: string | undefined
): string | undefined {
if (resourceType === 'ai_agent') return undefined
return resourceType
? `${path} is a ${resourceType} resource, not an agent.`
: `${path} has no deployed agent to edit.`
}
/** Brain keys whose step transform is non-static and would be dropped by a save-as-agent snapshot. */
export function nonStaticBrainKeys(
inputTransforms: Record<string, InputTransform> | undefined
): string[] {
return AGENT_BRAIN_KEYS.filter((key) => {
const t = inputTransforms?.[key] as any
return t && t.type !== 'static'
})
}
/**
* Keep only the flow-local inputs on a step's input_transforms. Used when linking: the brain comes
* from the resource, so the step must not carry stale `provider`/`system_prompt`/… transforms — at
* runtime they'd still be resolved (an unnecessary, possibly failing provider `$res:` fetch) yet
* never used, since the linked branch takes the brain from the resource.
*/
export function flowLocalInputs(
inputTransforms: Record<string, InputTransform> | undefined
): Record<string, InputTransform> {
const out: Record<string, InputTransform> = {}
for (const key of AGENT_FLOW_LOCAL_KEYS) {
if (inputTransforms?.[key]) {
out[key] = inputTransforms[key]
}
}
return out
}
/**
* The host-flow overrides to store on a linked step for one tool: the subset of the tool's edited
* input_transforms that diverges from the resource tool's own transforms. Storing only the diff (not
* the full merged map) keeps unchanged inputs inheriting from the resource, makes merely opening a
* tool a no-op (its inputs still equal base overrides), and lets reverting an edit persist.
*/
export function toolInputOverrides(
inputs: Record<string, InputTransform> | undefined,
base: Record<string, InputTransform> | undefined
): Record<string, InputTransform> {
const overrides: Record<string, InputTransform> = {}
for (const [key, value] of Object.entries(inputs ?? {})) {
if (!deepEqual(value, base?.[key])) {
overrides[key] = value
}
}
return overrides
}
export interface AIAgentConfig {
provider?: unknown
output_type?: string
system_prompt?: string
streaming?: boolean
memory?: unknown
output_schema?: unknown
max_completion_tokens?: number
temperature?: number
max_iterations?: number
tools?: AgentTool[]
}
/** Extract the static brain values from a step's input_transforms into a flat agent config. */
export function inputTransformsToAgentConfig(
inputTransforms: Record<string, InputTransform> | undefined,
tools: AgentTool[] | undefined
): AIAgentConfig {
const config: AIAgentConfig = { tools: tools ?? [] }
for (const key of AGENT_BRAIN_KEYS) {
const t = inputTransforms?.[key] as any
// `null` as well as `undefined`: a placeholder transform is `{"type":"static"}`, and it comes
// back from the API — and from a schema backfill — with an explicit null. Writing it through
// would put `memory: null` in the saved agent and show as a change against a config that
// simply omits the key.
if (t && t.type === 'static' && t.value !== undefined && t.value !== null) {
;(config as any)[key] = t.value
}
}
return config
}
/**
* Reduce the AI agent schema to only the flow-local inputs. Used when a step is linked to a saved
* agent: the brain fields come from the resource, so only user_message/user_attachments stay editable.
*/
export function flowLocalAgentSchema(schema: any): any {
if (!schema?.properties) {
return schema
}
const properties: Record<string, unknown> = {}
for (const key of AGENT_FLOW_LOCAL_KEYS) {
if (schema.properties[key]) {
properties[key] = schema.properties[key]
}
}
return {
...schema,
properties,
order: (schema.order ?? Object.keys(properties)).filter((k: string) => k in properties),
required: (schema.required ?? []).filter((k: string) => k in properties)
}
}
/** Read off the form's own registry, so a linked agent's summary cannot name a field differently
* from the form that edits it. */
export const AGENT_BRAIN_LABELS: Record<string, string> = Object.fromEntries(
AGENT_FIELDS.map((f) => [f.key, f.label])
)
/**
* Brain keys set to an input transform rather than to a value, which a saved agent's plain JSON
* cannot hold. Matched on the tag so the whole `InputTransform` union is covered: a payload-key
* test would miss `{"type":"ai"}`, which carries none.
*/
export function transformValuedBrainKeys(args: Record<string, any> | undefined): string[] {
return AGENT_BRAIN_KEYS.filter((key) => {
const v = args?.[key]
return (
v !== null &&
typeof v === 'object' &&
(v.type === 'javascript' || v.type === 'ai' || v.type === 'static')
)
})
}
/** Flatten a saved agent's brain config into human-readable label/value rows for a read-only
* display on a linked step. Only set fields are returned, in the canonical brain-key order. */
export function summarizeAgentBrain(
config: AIAgentConfig | undefined
): { label: string; value: string }[] {
const rows: { label: string; value: string }[] = []
for (const key of AGENT_BRAIN_KEYS) {
const v = (config as any)?.[key]
if (v === undefined || v === null || v === '') continue
let value: string
if (key === 'provider') {
value = [v.kind, v.model].filter(Boolean).join(' · ') || 'configured'
} else if (key === 'memory') {
// Memory configs are serialized with a `kind` tag (serde tag = "kind").
value = typeof v === 'object' ? (v.kind ?? v.type ?? 'configured') : String(v)
} else if (key === 'output_schema') {
value = 'configured'
} else if (typeof v === 'boolean') {
value = v ? 'on' : 'off'
} else if (typeof v === 'object') {
value = JSON.stringify(v)
} else {
value = String(v)
}
rows.push({ label: AGENT_BRAIN_LABELS[key] ?? key, value })
}
return rows
}
/** Inverse: wrap brain config values as static input_transforms (used when unlinking a step). */
export function agentConfigToInputTransforms(
config: AIAgentConfig
): Record<string, InputTransform> {
const it: Record<string, InputTransform> = {}
for (const key of AGENT_BRAIN_KEYS) {
const v = (config as any)[key]
if (v !== undefined) {
it[key] = { type: 'static', value: v } as InputTransform
}
}
return it
}