Files
windmill/frontend/src/lib/components/flows/agentToolTree.ts
T
GuilhemandClaude Opus 5 449b1a6933 fix: ground the chat's AI agent provider in the workspace's models (#10774)
* fix: ground the chat's AI agent provider in the workspace's models

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: never reject an AI agent model the catalog could not confirm

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: benchmark AI agent provider grounding in ai_evals

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: only reject an AI agent model an exhaustive listing rules out

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep instance-level AI settings out of the workspace provider catalog

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep untrusted model ids out of the chat's context

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: carry completeness on the model listing itself

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: correct two comments left behind by the catalog rework

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: bound the model listing and verify the default against it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: honour a workspace default a filtered listing names

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: recognise a workspace default past the prompt's model cap

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep an aliasing provider's unlisted model ids permissive

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:42:02 +02:00

213 lines
5.3 KiB
TypeScript

import type { FlowModule } from '$lib/gen'
import {
getToolNameError,
isFlowModuleTool,
type AgentTool,
type FlowModuleTool
} from './agentToolUtils'
import { forEachAiAgentModule } from './aiAgentModules'
type FlowNodeLike = Pick<FlowModule, 'id' | 'value'>
export type AgentToolOwner = {
agentId: string
tools: AgentTool[]
toolIndex: number
tool: AgentTool
depth: number
}
export type RemovedAgentTool = {
tool: AgentTool
removedIds: string[]
}
export function findAgentToolOwner(
modules: FlowModule[],
toolId: string
): AgentToolOwner | undefined {
return findAgentToolOwnerInModules(modules, toolId, 0)
}
export function removeAgentToolOwner(owner: AgentToolOwner): RemovedAgentTool | undefined {
const candidate = owner.tools[owner.toolIndex]
if (!candidate || candidate.id !== owner.tool.id) {
return undefined
}
owner.tools.splice(owner.toolIndex, 1)
return {
tool: candidate,
removedIds: collectAgentToolIds(candidate)
}
}
export function collectFlowNodeIds(module: FlowModule): string[] {
return collectFlowNodeIdsFromNode(module)
}
export function collectAgentToolIds(tool: AgentTool): string[] {
return collectFlowNodeIdsFromNode(tool as FlowModuleTool)
}
function findAgentToolOwnerInModules(
modules: FlowModule[],
toolId: string,
depth: number
): AgentToolOwner | undefined {
for (const module of modules) {
const owner = findAgentToolOwnerInNode(module, toolId, depth)
if (owner) {
return owner
}
}
return undefined
}
function findAgentToolOwnerInNode(
node: FlowNodeLike,
toolId: string,
depth: number
): AgentToolOwner | undefined {
if (node.value.type === 'forloopflow' || node.value.type === 'whileloopflow') {
return findAgentToolOwnerInModules(node.value.modules, toolId, depth)
}
if (node.value.type === 'branchall') {
for (const branch of node.value.branches) {
const owner = findAgentToolOwnerInModules(branch.modules, toolId, depth)
if (owner) {
return owner
}
}
return undefined
}
if (node.value.type === 'branchone') {
const defaultOwner = findAgentToolOwnerInModules(node.value.default, toolId, depth)
if (defaultOwner) {
return defaultOwner
}
for (const branch of node.value.branches) {
const owner = findAgentToolOwnerInModules(branch.modules, toolId, depth)
if (owner) {
return owner
}
}
return undefined
}
if (node.value.type !== 'aiagent') {
return undefined
}
// Absent for a linked agent, whose tools live in the resource rather than on the module.
const tools = node.value.tools ?? []
const toolIndex = tools.findIndex((tool) => tool.id === toolId)
if (toolIndex !== -1) {
return {
agentId: node.id,
tools,
toolIndex,
tool: tools[toolIndex],
depth: depth + 1
}
}
for (const tool of tools) {
if (!isFlowModuleTool(tool)) {
continue
}
const owner = findAgentToolOwnerInNode(tool as FlowNodeLike, toolId, depth + 1)
if (owner) {
return owner
}
}
return undefined
}
function collectFlowNodeIdsFromNode(node: FlowNodeLike): string[] {
const ids = [node.id]
if (node.value.type === 'forloopflow' || node.value.type === 'whileloopflow') {
for (const module of node.value.modules) {
ids.push(...collectFlowNodeIds(module))
}
return ids
}
if (node.value.type === 'branchall') {
for (const branch of node.value.branches) {
for (const module of branch.modules) {
ids.push(...collectFlowNodeIds(module))
}
}
return ids
}
if (node.value.type === 'branchone') {
for (const module of node.value.default) {
ids.push(...collectFlowNodeIds(module))
}
for (const branch of node.value.branches) {
for (const module of branch.modules) {
ids.push(...collectFlowNodeIds(module))
}
}
return ids
}
if (node.value.type === 'aiagent') {
for (const tool of node.value.tools ?? []) {
ids.push(...collectAgentToolIds(tool))
}
}
return ids
}
/** Ids of AI agent modules that neither link to a saved agent nor set a provider — they pass schema
* validation (a linked step legitimately has no provider of its own) but fail on every run. */
export function collectProviderlessAgentIds(modules: unknown): string[] {
const ids: string[] = []
forEachAiAgentModule(modules, (mod, v) => {
if (!v.agent && !v.input_transforms?.provider) {
ids.push(mod.id)
}
})
return ids
}
export type InvalidAgentToolName = {
agentId: string
toolId: string
name: string
error: string
}
/** Agent tools whose name (their `summary`) the worker rejects — the flow saves fine but every run
* of the agent step fails, so writers must catch this before the flow is stored. */
export function collectInvalidAgentToolNames(modules: unknown): InvalidAgentToolName[] {
const invalid: InvalidAgentToolName[] = []
forEachAiAgentModule(modules, (mod, v) => {
const tools: AgentTool[] = Array.isArray(v.tools) ? v.tools : []
// Only a flowmodule tool's summary is a callable name: the worker never reads an mcp or
// websearch summary, so a blank one there must stay writable (both default to '').
const named = tools.filter(
(tool) => tool.value?.tool_type !== 'mcp' && tool.value?.tool_type !== 'websearch'
)
const siblingNames = named.map((tool) => tool.summary ?? '')
for (const tool of named) {
const name = tool.summary ?? ''
const error = getToolNameError(name, tool.value?.tool_type, siblingNames)
if (error) {
invalid.push({ agentId: mod.id, toolId: tool.id, name, error })
}
}
})
return invalid
}