mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 08:03:11 +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>
161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
clearLinkedAgentTools,
|
|
getLinkedAgentTools,
|
|
linkedModulesForAgent,
|
|
linkedToolsScope,
|
|
migrateLinkedAgentToolsScope,
|
|
releaseLinkedToolsScope,
|
|
retainLinkedToolsScope,
|
|
setLinkedAgentTools
|
|
} from './linkedAgentToolsStore.svelte'
|
|
import type { AgentTool } from './agentToolUtils'
|
|
|
|
// The cap in the store. Filling past it is the only way to observe eviction.
|
|
const MAX_SCOPES = 32
|
|
|
|
function tool(id: string): AgentTool {
|
|
return { id, value: { tool_type: 'flowmodule', type: 'script', path: `u/t/${id}` } } as AgentTool
|
|
}
|
|
|
|
// Unique per test so the module-level store doesn't leak between them.
|
|
let seq = 0
|
|
function freshScope(name = 'flow') {
|
|
return linkedToolsScope(`ws${seq++}`, name)
|
|
}
|
|
|
|
function fillPastCap(exclude: string) {
|
|
for (let i = 0; i <= MAX_SCOPES; i++) {
|
|
const filler = linkedToolsScope('filler', `${exclude}-${seq}-${i}`)
|
|
setLinkedAgentTools(filler, 'm', [tool(`f${i}`)], 'u/admin/a')
|
|
}
|
|
}
|
|
|
|
describe('linkedAgentToolsStore', () => {
|
|
it('keeps each scope module map separate', () => {
|
|
const a = freshScope()
|
|
const b = freshScope()
|
|
setLinkedAgentTools(a, 'step', [tool('x')], 'u/admin/a')
|
|
setLinkedAgentTools(b, 'step', [tool('y')], 'u/admin/a')
|
|
expect(getLinkedAgentTools(a, 'step').map((t) => t.id)).toEqual(['x'])
|
|
expect(getLinkedAgentTools(b, 'step').map((t) => t.id)).toEqual(['y'])
|
|
})
|
|
|
|
it('evicts an unretained scope once past the cap', () => {
|
|
const victim = freshScope()
|
|
setLinkedAgentTools(victim, 'step', [tool('x')], 'u/admin/a')
|
|
fillPastCap(victim)
|
|
expect(getLinkedAgentTools(victim, 'step')).toEqual([])
|
|
})
|
|
|
|
// A run viewer holds one scope per mounted nested job; those must not evict what is on screen.
|
|
it('never evicts a retained scope', () => {
|
|
const held = freshScope()
|
|
setLinkedAgentTools(held, 'step', [tool('x')], 'u/admin/a')
|
|
retainLinkedToolsScope(held)
|
|
fillPastCap(held)
|
|
expect(getLinkedAgentTools(held, 'step').map((t) => t.id)).toEqual(['x'])
|
|
releaseLinkedToolsScope(held)
|
|
})
|
|
|
|
// Retained scopes can push the store past the cap, since eviction skips them. Releasing one has
|
|
// to prune, or closing views would leave it over the cap for the tab's life.
|
|
it('prunes on release once retained scopes have filled the cap', () => {
|
|
const held: string[] = []
|
|
for (let i = 0; i <= MAX_SCOPES; i++) {
|
|
const scope = linkedToolsScope('retained', `${seq}-${i}`)
|
|
setLinkedAgentTools(scope, 'step', [tool(`t${i}`)], 'u/admin/a')
|
|
retainLinkedToolsScope(scope)
|
|
held.push(scope)
|
|
}
|
|
// All retained, so nothing was evicted and the store sits over the cap.
|
|
expect(getLinkedAgentTools(held[0], 'step').map((t) => t.id)).toEqual(['t0'])
|
|
|
|
releaseLinkedToolsScope(held[0])
|
|
expect(getLinkedAgentTools(held[0], 'step')).toEqual([])
|
|
expect(getLinkedAgentTools(held[held.length - 1], 'step')).toHaveLength(1)
|
|
|
|
for (const scope of held.slice(1)) {
|
|
releaseLinkedToolsScope(scope)
|
|
}
|
|
})
|
|
|
|
// A rename moves readers to a new scope; they only retain the new key afterwards, so the
|
|
// migration itself must not let the fresh bucket be evicted.
|
|
it('carries tools across a rename without evicting the new scope', () => {
|
|
const from = freshScope('old')
|
|
const to = freshScope('new')
|
|
setLinkedAgentTools(from, 'step', [tool('x')], 'u/admin/a')
|
|
retainLinkedToolsScope(from)
|
|
migrateLinkedAgentToolsScope(from, to)
|
|
expect(getLinkedAgentTools(to, 'step').map((t) => t.id)).toEqual(['x'])
|
|
expect(getLinkedAgentTools(from, 'step')).toEqual([])
|
|
releaseLinkedToolsScope(from)
|
|
})
|
|
|
|
// Readers release the old scope before retaining the new one. Over the cap, with everything else
|
|
// retained, the migrated bucket would be the only evictable entry in that window.
|
|
it('keeps migrated tools when the old scope is released before the new one is retained', () => {
|
|
const held: string[] = []
|
|
for (let i = 0; i < MAX_SCOPES; i++) {
|
|
const scope = linkedToolsScope('rename-fill', `${seq}-${i}`)
|
|
setLinkedAgentTools(scope, 'step', [tool(`t${i}`)], 'u/admin/a')
|
|
retainLinkedToolsScope(scope)
|
|
held.push(scope)
|
|
}
|
|
const from = freshScope('before')
|
|
const to = freshScope('after')
|
|
setLinkedAgentTools(from, 'step', [tool('x')], 'u/admin/a')
|
|
retainLinkedToolsScope(from)
|
|
|
|
migrateLinkedAgentToolsScope(from, to)
|
|
releaseLinkedToolsScope(from)
|
|
retainLinkedToolsScope(to)
|
|
|
|
expect(getLinkedAgentTools(to, 'step').map((t) => t.id)).toEqual(['x'])
|
|
|
|
releaseLinkedToolsScope(to)
|
|
for (const scope of held) {
|
|
releaseLinkedToolsScope(scope)
|
|
}
|
|
})
|
|
|
|
it('clears one module without disturbing its siblings', () => {
|
|
const scope = freshScope()
|
|
setLinkedAgentTools(scope, 'a', [tool('x')], 'u/admin/a')
|
|
setLinkedAgentTools(scope, 'b', [tool('y')], 'u/admin/a')
|
|
clearLinkedAgentTools(scope, 'a')
|
|
expect(getLinkedAgentTools(scope, 'a')).toEqual([])
|
|
expect(getLinkedAgentTools(scope, 'b').map((t) => t.id)).toEqual(['y'])
|
|
})
|
|
|
|
// A deploy has to refresh every step showing the agent it wrote, so this index is what makes the
|
|
// difference between one step's nodes updating and all of them.
|
|
describe('linkedModulesForAgent', () => {
|
|
it('finds every module linked to one agent, and no others', () => {
|
|
const scope = freshScope()
|
|
setLinkedAgentTools(scope, 'a', [tool('x')], 'u/admin/one')
|
|
setLinkedAgentTools(scope, 'b', [tool('y')], 'u/admin/one')
|
|
setLinkedAgentTools(scope, 'c', [tool('z')], 'u/admin/other')
|
|
expect(linkedModulesForAgent(scope, 'u/admin/one').sort()).toEqual(['a', 'b'])
|
|
expect(linkedModulesForAgent(scope, 'u/admin/other')).toEqual(['c'])
|
|
})
|
|
|
|
it('reads a $res-prefixed link and a bare path as the same agent', () => {
|
|
const scope = freshScope()
|
|
setLinkedAgentTools(scope, 'a', [tool('x')], '$res:u/admin/one')
|
|
expect(linkedModulesForAgent(scope, 'u/admin/one')).toEqual(['a'])
|
|
})
|
|
|
|
it('drops a module that was cleared, and stays scoped', () => {
|
|
const scope = freshScope()
|
|
const other = freshScope()
|
|
setLinkedAgentTools(scope, 'a', [tool('x')], 'u/admin/one')
|
|
setLinkedAgentTools(other, 'a', [tool('x')], 'u/admin/one')
|
|
clearLinkedAgentTools(scope, 'a')
|
|
expect(linkedModulesForAgent(scope, 'u/admin/one')).toEqual([])
|
|
expect(linkedModulesForAgent(other, 'u/admin/one')).toEqual(['a'])
|
|
})
|
|
})
|
|
})
|