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>
185 lines
7.7 KiB
TypeScript
185 lines
7.7 KiB
TypeScript
import { deepEqual } from 'fast-equals'
|
|
import type { AgentTool } from './agentToolUtils'
|
|
|
|
// Tools resolved from a linked agent's `ai_agent` resource: the step stores `tools: []`, so the graph
|
|
// and the tool editor read the resolved set from here. Keyed by scope then module id — the scope
|
|
// keeps flows shown at the same time (an editor and an embedded preview) from aliasing each other.
|
|
let byScope = $state<Record<string, Record<string, AgentTool[]>>>({})
|
|
|
|
// Which agent each entry resolved from, so a write to one agent can be reflected on every step that
|
|
// links it rather than only the one that triggered the write. Plain, not reactive: it is read
|
|
// imperatively when reconciling a deploy, never rendered.
|
|
const agentRefByEntry = new Map<string, string>()
|
|
|
|
/** A step may name its agent bare or as `$res:<path>`; both are the same agent, and the index has
|
|
* to answer for a lookup written either way. */
|
|
function normalizeAgentRef(agentRef: string): string {
|
|
return agentRef.replace(/^\$res:/, '').replace(/^res:\/\//, '')
|
|
}
|
|
|
|
function entryKey(scope: string, moduleId: string): string {
|
|
return `${scope}\u0000${moduleId}`
|
|
}
|
|
|
|
function forgetScopeRefs(scope: string) {
|
|
const prefix = `${scope}\u0000`
|
|
for (const key of agentRefByEntry.keys()) {
|
|
if (key.startsWith(prefix)) agentRefByEntry.delete(key)
|
|
}
|
|
}
|
|
|
|
// A long-lived tab would otherwise keep every flow it ever visited (raw tool script contents
|
|
// included). Past this cap the least recently used scope is evicted, skipping any a mounted view
|
|
// still holds.
|
|
const MAX_SCOPES = 32
|
|
let scopeOrder: string[] = []
|
|
|
|
// Recency on read, so a scope still being displayed isn't evicted by unrelated publishes (a run
|
|
// viewer opens one bucket per nested job). Only reorders the plain recency list — evicting here
|
|
// would mutate reactive state during a render.
|
|
function noteScopeRead(scope: string) {
|
|
if (byScope[scope] === undefined) return
|
|
const last = scopeOrder[scopeOrder.length - 1]
|
|
if (last === scope) return
|
|
scopeOrder = [...scopeOrder.filter((s) => s !== scope), scope]
|
|
}
|
|
|
|
// Scopes a mounted view is currently relying on. A run viewer keeps one per nested job, hidden ones
|
|
// included, so the cap alone would evict buckets still in use — and restoring one would evict
|
|
// another, forever. Retained scopes are never evicted; the cap yields to correctness.
|
|
const retainedScopes = new Map<string, number>()
|
|
// A just-renamed scope, protected from eviction until a reader retains it: holders release the old
|
|
// key before retaining the new one, so the migrated bucket is unretained in between.
|
|
let pendingMigration: string | undefined = undefined
|
|
|
|
export function retainLinkedToolsScope(scope: string) {
|
|
retainedScopes.set(scope, (retainedScopes.get(scope) ?? 0) + 1)
|
|
if (scope === pendingMigration) {
|
|
pendingMigration = undefined
|
|
}
|
|
}
|
|
|
|
export function releaseLinkedToolsScope(scope: string) {
|
|
const next = (retainedScopes.get(scope) ?? 0) - 1
|
|
if (next > 0) {
|
|
retainedScopes.set(scope, next)
|
|
} else {
|
|
retainedScopes.delete(scope)
|
|
}
|
|
// Scopes skipped while retained are otherwise never reconsidered, leaving the store over the cap
|
|
// for the tab's life once enough views have been closed.
|
|
evictOverCap()
|
|
}
|
|
|
|
// `protect` is the scope just written: it is the newest, so when every older one is retained it
|
|
// would otherwise be the only eligible victim and get dropped the moment it was published.
|
|
function evictOverCap(protect?: string) {
|
|
while (scopeOrder.length > MAX_SCOPES) {
|
|
const victim = scopeOrder.find(
|
|
(s) => s !== protect && s !== pendingMigration && !retainedScopes.has(s)
|
|
)
|
|
if (victim === undefined) {
|
|
break
|
|
}
|
|
scopeOrder = scopeOrder.filter((s) => s !== victim)
|
|
delete byScope[victim]
|
|
forgetScopeRefs(victim)
|
|
}
|
|
}
|
|
|
|
function touchScope(scope: string) {
|
|
scopeOrder = [...scopeOrder.filter((s) => s !== scope), scope]
|
|
evictOverCap(scope)
|
|
}
|
|
|
|
/**
|
|
* Scope key for the store: workspace + flow path. Flow paths repeat across workspaces, so a late
|
|
* async resolution from a previous workspace must land in its own bucket instead of overwriting
|
|
* the tools of an identically-named flow in the current one. Every publisher and reader must
|
|
* derive the workspace the same way (operating workspace, falling back to the nav workspace).
|
|
*/
|
|
export function linkedToolsScope(
|
|
workspace: string | undefined,
|
|
flowPath: string | undefined
|
|
): string {
|
|
return `${workspace ?? ''}:${flowPath ?? ''}`
|
|
}
|
|
// Bumped on every mutation. Non-reactive graph recomputations (which read the map inside untrack)
|
|
// track this to re-run when a link resolves after the initial render, e.g. right after linking.
|
|
let version = $state(0)
|
|
|
|
export function setLinkedAgentTools(
|
|
scope: string,
|
|
moduleId: string,
|
|
tools: AgentTool[],
|
|
/** The agent these tools came from. Taken here rather than recorded separately so the index and
|
|
* the tools cannot drift: every writer has the ref in hand, and one that forgot to file it left
|
|
* a step invisible to `linkedModulesForAgent`. */
|
|
agentRef: string
|
|
) {
|
|
agentRefByEntry.set(entryKey(scope, moduleId), normalizeAgentRef(agentRef))
|
|
// Publishers re-run and hand us a fresh-but-equal array each time; only mutate on a real change,
|
|
// else the version bump would retrigger the graph recompute in a loop.
|
|
if (deepEqual(byScope[scope]?.[moduleId], tools)) return
|
|
byScope[scope] = { ...(byScope[scope] ?? {}), [moduleId]: tools }
|
|
touchScope(scope)
|
|
version++
|
|
}
|
|
|
|
/** Every module in this scope resolved from `agentRef`. A saved agent can be linked by more than
|
|
* one step, and all of them show tools that a write to it has just changed. */
|
|
export function linkedModulesForAgent(scope: string, agentRef: string): string[] {
|
|
const prefix = `${scope}\u0000`
|
|
const wanted = normalizeAgentRef(agentRef)
|
|
const out: string[] = []
|
|
for (const [key, ref] of agentRefByEntry) {
|
|
if (key.startsWith(prefix) && ref === wanted) out.push(key.slice(prefix.length))
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Move one scope's resolutions into another: used when a rename moves readers to a new scope,
|
|
* and to sweep republished data (keyed by the flow doc's path) into the live-edited scope. The
|
|
* source bucket carries the newer resolution in both cases, so it wins the merge. */
|
|
export function migrateLinkedAgentToolsScope(oldScope: string, newScope: string) {
|
|
if (oldScope === newScope || byScope[oldScope] === undefined) return
|
|
byScope[newScope] = { ...(byScope[newScope] ?? {}), ...(byScope[oldScope] ?? {}) }
|
|
for (const moduleId of Object.keys(byScope[oldScope] ?? {})) {
|
|
const ref = agentRefByEntry.get(entryKey(oldScope, moduleId))
|
|
if (ref !== undefined) agentRefByEntry.set(entryKey(newScope, moduleId), ref)
|
|
}
|
|
forgetScopeRefs(oldScope)
|
|
delete byScope[oldScope]
|
|
pendingMigration = newScope
|
|
// Mark the new key most-recently-used but don't evict here: the next publish or release enforces
|
|
// the cap, by which point readers have re-retained under the new key.
|
|
scopeOrder = [...scopeOrder.filter((s) => s !== oldScope && s !== newScope), newScope]
|
|
version++
|
|
}
|
|
|
|
export function clearLinkedAgentTools(scope: string, moduleId: string) {
|
|
agentRefByEntry.delete(entryKey(scope, moduleId))
|
|
if (byScope[scope]?.[moduleId] === undefined) return
|
|
const rest = { ...byScope[scope] }
|
|
delete rest[moduleId]
|
|
byScope[scope] = rest
|
|
version++
|
|
}
|
|
|
|
export function getLinkedAgentTools(scope: string, moduleId: string): AgentTool[] {
|
|
noteScopeRead(scope)
|
|
return byScope[scope]?.[moduleId] ?? []
|
|
}
|
|
|
|
/** Reactive snapshot of one scope's module→tools map — read this where a computation must react to
|
|
* resolution (the graph passes it to computeAIToolNodes, which indexes it by module id). */
|
|
export function linkedAgentToolsForScope(scope: string): Record<string, AgentTool[]> {
|
|
noteScopeRead(scope)
|
|
return byScope[scope] ?? {}
|
|
}
|
|
|
|
/** Reactive counter that changes on any link resolution. Track it to trigger a recompute. */
|
|
export function linkedAgentToolsVersion(): number {
|
|
return version
|
|
}
|