mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +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>
203 lines
7.0 KiB
TypeScript
203 lines
7.0 KiB
TypeScript
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
|
||
}
|