mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 08:01: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>
187 lines
6.2 KiB
TypeScript
187 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
agentConfigToInputTransforms,
|
|
agentEditorRefusal,
|
|
flowLocalInputs,
|
|
inputTransformsToAgentConfig,
|
|
nonStaticBrainKeys,
|
|
summarizeAgentBrain,
|
|
toolInputOverrides,
|
|
transformValuedBrainKeys
|
|
} from './agentResourceUtils'
|
|
|
|
describe('transformValuedBrainKeys', () => {
|
|
// The tag is the whole test: `{type:'ai'}` carries no payload key to look for.
|
|
it('flags every transform variant, whatever it carries', () => {
|
|
expect(
|
|
transformValuedBrainKeys({
|
|
system_prompt: { type: 'ai' },
|
|
temperature: { type: 'javascript', expr: 'flow_input.t' },
|
|
max_iterations: { type: 'static', value: 3 }
|
|
})
|
|
).toEqual(['system_prompt', 'temperature', 'max_iterations'])
|
|
})
|
|
|
|
it('leaves the brain values that are legitimately objects', () => {
|
|
expect(
|
|
transformValuedBrainKeys({
|
|
output_schema: { type: 'object', properties: {} },
|
|
memory: { kind: 'auto', context_length: 20 },
|
|
provider: { kind: 'openai', model: 'gpt-4o' },
|
|
system_prompt: 'hi'
|
|
})
|
|
).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('agentEditorRefusal', () => {
|
|
it('opens an agent', () => {
|
|
expect(agentEditorRefusal('u/admin/agent', 'ai_agent')).toBeUndefined()
|
|
})
|
|
|
|
// A path with no deployed row answers with its own draft, and the generic resource editor writes
|
|
// no type: assuming `ai_agent` there would create that path as an agent on deploy.
|
|
it('refuses an unknown type rather than assuming one', () => {
|
|
expect(agentEditorRefusal('u/admin/db', undefined)).toContain('no deployed agent')
|
|
})
|
|
})
|
|
|
|
describe('summarizeAgentBrain', () => {
|
|
it('returns only set fields, in brain-key order, formatted', () => {
|
|
const rows = summarizeAgentBrain({
|
|
provider: { kind: 'openai', model: 'gpt-4o', resource: '$res:f/x/openai' } as any,
|
|
system_prompt: 'You are helpful',
|
|
temperature: 0.7,
|
|
streaming: true,
|
|
max_iterations: 10
|
|
})
|
|
expect(rows).toEqual([
|
|
{ label: 'Provider', value: 'openai · gpt-4o' },
|
|
{ label: 'System message', value: 'You are helpful' },
|
|
{ label: 'Stream the response', value: 'on' },
|
|
{ label: 'Temperature', value: '0.7' },
|
|
{ label: 'Max iterations', value: '10' }
|
|
])
|
|
})
|
|
|
|
it('skips empty/undefined fields', () => {
|
|
expect(summarizeAgentBrain({ system_prompt: '', provider: undefined as any })).toEqual([])
|
|
expect(summarizeAgentBrain(undefined)).toEqual([])
|
|
})
|
|
|
|
it('summarizes structured fields compactly', () => {
|
|
// memory is serialized with a `kind` tag (serde tag = "kind")
|
|
const rows = summarizeAgentBrain({
|
|
memory: { kind: 'auto', context_length: 20 } as any,
|
|
output_schema: { type: 'object' } as any
|
|
})
|
|
expect(rows).toEqual([
|
|
{ label: 'Memory', value: 'auto' },
|
|
{ label: 'Output schema', value: 'configured' }
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('inputTransformsToAgentConfig', () => {
|
|
it('captures only static brain values and carries tools', () => {
|
|
const config = inputTransformsToAgentConfig(
|
|
{
|
|
provider: { type: 'static', value: { kind: 'openai' } },
|
|
system_prompt: { type: 'static', value: 'hi' },
|
|
temperature: { type: 'javascript', expr: 'flow_input.t' }, // non-static → dropped
|
|
max_iterations: { type: 'static', value: undefined }, // undefined → dropped
|
|
user_message: { type: 'static', value: 'hello' } // not a brain key → dropped
|
|
} as any,
|
|
[{ id: 't1' }] as any
|
|
)
|
|
expect(config).toEqual({
|
|
tools: [{ id: 't1' }],
|
|
provider: { kind: 'openai' },
|
|
system_prompt: 'hi'
|
|
})
|
|
})
|
|
|
|
it('defaults tools to []', () => {
|
|
expect(inputTransformsToAgentConfig({}, undefined)).toEqual({ tools: [] })
|
|
})
|
|
})
|
|
|
|
describe('agentConfigToInputTransforms', () => {
|
|
it('wraps brain values as static transforms and round-trips through the config', () => {
|
|
const config = { provider: { kind: 'openai' }, system_prompt: 'hi', tools: [{ id: 't1' }] }
|
|
const its = agentConfigToInputTransforms(config as any)
|
|
expect(its).toEqual({
|
|
provider: { type: 'static', value: { kind: 'openai' } },
|
|
system_prompt: { type: 'static', value: 'hi' }
|
|
})
|
|
const back = inputTransformsToAgentConfig(its, config.tools as any)
|
|
expect(back).toEqual(config)
|
|
})
|
|
})
|
|
|
|
describe('nonStaticBrainKeys', () => {
|
|
it('lists brain keys with a non-static transform, in brain-key order', () => {
|
|
expect(
|
|
nonStaticBrainKeys({
|
|
provider: { type: 'static', value: {} },
|
|
temperature: { type: 'connected' },
|
|
system_prompt: { type: 'javascript', expr: 'x' }
|
|
} as any)
|
|
).toEqual(['system_prompt', 'temperature'])
|
|
})
|
|
|
|
it('flags a non-static provider (the save-blocking case) but not a static one', () => {
|
|
// provider is required on the resource; a non-static one gets dropped and must block saving
|
|
expect(nonStaticBrainKeys({ provider: { type: 'javascript', expr: 'x' } } as any)).toContain(
|
|
'provider'
|
|
)
|
|
expect(nonStaticBrainKeys({ provider: { type: 'static', value: {} } } as any)).not.toContain(
|
|
'provider'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('flowLocalInputs', () => {
|
|
it('keeps only user_message/user_attachments, dropping brain transforms', () => {
|
|
expect(
|
|
flowLocalInputs({
|
|
provider: { type: 'static', value: {} },
|
|
user_message: { type: 'static', value: 'hi' },
|
|
user_attachments: { type: 'static', value: [] }
|
|
} as any)
|
|
).toEqual({
|
|
user_message: { type: 'static', value: 'hi' },
|
|
user_attachments: { type: 'static', value: [] }
|
|
})
|
|
})
|
|
|
|
it('handles undefined', () => {
|
|
expect(flowLocalInputs(undefined)).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe('toolInputOverrides', () => {
|
|
const base = {
|
|
tenant: { type: 'javascript', expr: 'flow_input.tenant' },
|
|
query: { type: 'static', value: 'x' }
|
|
} as any
|
|
|
|
it('returns nothing when inputs equal the resource base (opening a tool is a no-op)', () => {
|
|
expect(toolInputOverrides(base, base)).toEqual({})
|
|
})
|
|
|
|
it('returns only the edited keys, so a revert to base drops back to nothing', () => {
|
|
const edited = { ...base, query: { type: 'static', value: 'y' } }
|
|
expect(toolInputOverrides(edited, base)).toEqual({ query: { type: 'static', value: 'y' } })
|
|
// reverting query back to the base value yields an empty override set again
|
|
expect(toolInputOverrides(base, base)).toEqual({})
|
|
})
|
|
|
|
it('includes keys absent from the base', () => {
|
|
expect(toolInputOverrides({ extra: { type: 'static', value: 1 } } as any, base)).toEqual({
|
|
extra: { type: 'static', value: 1 }
|
|
})
|
|
})
|
|
})
|