Compare commits

...
7 changed files with 46 additions and 30 deletions
@@ -59,6 +59,10 @@
let toolNameError = $derived(
isAgentTool ? getToolNameError(summary ?? '', undefined, siblingToolNames) : undefined
)
// An agent tool's summary is the function name the model is given, not free text, so the field
// asks for a name instead. That holds for every kind a tool can be, which is why the input
// below is one shared snippet rather than one per branch.
let summaryPlaceholder = $derived(isAgentTool ? 'Tool name' : 'Summary')
const dispatch = createEventDispatcher()
const customUi: FlowBuilderWhitelabelCustomUi | undefined = getContext('customUi')
@@ -191,6 +195,14 @@
})
</script>
{#snippet summaryInput()}
<input
bind:value={summary}
placeholder={summaryPlaceholder}
class={twMerge('w-full grow', toolNameError && '!border-red-400')}
/>
{/snippet}
<div class="flex flex-col gap-1 px-4 py-2">
<div
class="overflow-x-auto scrollbar-hidden flex items-center justify-between flex-nowrap w-full"
@@ -210,7 +222,7 @@
code={flowModuleValue.content}
class="w-full"
elementProps={{
placeholder: isAgentTool ? 'Tool name' : 'Summary'
placeholder: summaryPlaceholder
}}
hideError={isAgentTool}
{siblingToolNames}
@@ -225,22 +237,15 @@
<DropdownV2 size="sm" placement="bottom-end" items={scriptItems} />
{/if}
<div class="flex min-w-[8rem] flex-1 flex-col">
<input
bind:value={summary}
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
class={twMerge('w-full grow', toolNameError && '!border-red-400')}
/>
{#if toolNameError && !isAgentTool}
<p class="text-3xs text-red-400 leading-tight mt-0.5">{toolNameError}</p>
{/if}
<div class="flex min-w-[8rem] flex-1">
{@render summaryInput()}
</div>
{:else if flowModuleValue.type === 'flow'}
<Badge color="indigo" capitalize>flow</Badge>
<input bind:value={summary} placeholder="Summary" class="w-full grow" />
{@render summaryInput()}
{:else if flowModuleValue.type === 'aiagent'}
<Badge color="indigo">AI Agent</Badge>
<input bind:value={summary} placeholder="Summary" class="w-full grow" />
{@render summaryInput()}
{/if}
</div>
</span>
@@ -463,7 +463,6 @@
parentModule={agentModule as FlowModule}
{enableAi}
staticOnly
noToolNavigation
forceTestTab={readOnly ? undefined : { [tool.id]: true }}
siblingToolNames={tools.filter((t) => t?.id !== tool?.id).map((t) => t?.summary ?? '')}
/>
@@ -18,8 +18,6 @@
/** See `FlowModuleComponent`: set when the tool belongs to a saved agent rather than to a
* step of this flow. */
staticOnly?: boolean
/** See `FlowModuleComponent`: set where there is no graph to select a nested tool on. */
noToolNavigation?: boolean
}
let {
@@ -31,8 +29,7 @@
forceTestTab,
highlightArg,
siblingToolNames = undefined,
staticOnly = false,
noToolNavigation = false
staticOnly = false
}: Props = $props()
</script>
@@ -64,7 +61,6 @@
highlightArg={highlightArg?.[tool.id]}
isAgentTool={true}
{staticOnly}
{noToolNavigation}
bind:toolDescription={tool.description}
{siblingToolNames}
/>
@@ -115,6 +115,9 @@
/** A linked agent's memory, once its config has loaded: whether it keeps managed memory decides
* which history inputs the step offers. */
linkedMemory?: { memory: unknown } | undefined
/** Set when this agent's config lives in a resource, so `tools` arrives with it rather than
* with the step. */
agentLinked?: boolean
}
let {
@@ -145,7 +148,8 @@
onAddTool = undefined,
onDeleteTool = undefined,
toolPickerPortal = undefined,
linkedMemory = undefined
linkedMemory = undefined,
agentLinked = false
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
@@ -226,10 +230,16 @@
})
})
// No editor adds tools to an agent used as a tool, so an empty roster is all an inline one will
// ever have. Not a linked one: its tools arrive with the resource, so empty here means "not
// loaded" as often as "none", and `enabled_tools` is the step's to set either way.
let toolsHidden = $derived(isAgentTool && !agentLinked && tools.length === 0)
let scopedFields = $derived(
AGENT_FIELDS.filter(
(spec) =>
agentFieldAppliesTo(spec, schemaProperties) && (!filter || filter.includes(spec.key))
agentFieldAppliesTo(spec, schemaProperties) &&
(!filter || filter.includes(spec.key)) &&
!(toolsHidden && spec.group === 'tools')
)
)
@@ -126,10 +126,6 @@
staticOnly?: boolean
/** Lets the agent's Tools section add a tool through the graph's own insert path. */
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
/** Drop the tool roster's drill-in. Selecting a tool means selecting its graph node, so a
* surface without a graph — the agent editor, which addresses one tool at a time — would
* offer a row whose click lands nowhere. */
noToolNavigation?: boolean
toolDescription?: string | undefined
siblingToolNames?: string[]
}
@@ -150,7 +146,6 @@
isAgentTool = false,
staticOnly = false,
flowModuleSchemaMap = undefined,
noToolNavigation = false,
toolDescription = $bindable(undefined),
siblingToolNames = undefined
}: Props = $props()
@@ -204,6 +199,14 @@
let visibleSelected = $derived(selected === 'chat' && !canShowChatTab ? 'inputs' : selected)
let runSettings: FlowRunSettings | undefined = $state()
let agentLinked = $derived(flowModule.value.type === 'aiagent' && Boolean(flowModule.value.agent))
// A tool row drills in by selecting that tool's graph node, and the graph draws tool nodes only
// for a step's own agent (`computeAIToolNodes`). An agent used as a tool has none, so its rows
// would offer a click that lands nowhere.
let onSelectToolInGraph = $derived(
isAgentTool
? undefined
: (toolId: string) => selectionManager.selectId(toolId, { openPanel: true })
)
let validCode = $state(true)
let width = $state(1200)
let testJob: Job | undefined = $state(undefined)
@@ -1240,15 +1243,14 @@
workspace={opWs}
visibilityKey={agentFieldsKey}
linkedMemory={agentLinked ? linkedAgentMemory : undefined}
{agentLinked}
tools={agentLinked
? getLinkedAgentTools(
linkedToolsScope(opWs, $pathStore),
linkedToolsModuleId
)
: (flowModule.value.tools ?? [])}
onSelectTool={noToolNavigation
? undefined
: (toolId) => selectionManager.selectId(toolId, { openPanel: true })}
onSelectTool={onSelectToolInGraph}
onAddTool={flowModuleSchemaMap
? (detail) =>
flowModuleSchemaMap?.addToolToAgent(flowModule.id, detail)
@@ -232,6 +232,7 @@
/>
<TopLevelNode
label="AI Agent"
chevron={false}
onSelect={() => {
dispatch('pickAiAgentTool')
dispatch('close')
@@ -22,11 +22,14 @@
/** Highlight with the neutral hover surface instead of the accent, for transient
* (hover/keyboard) selection rather than the persistent category selection. */
neutral?: boolean
/** Overrides the label's default chevron, for an entry that inserts directly here instead of
* opening a sub-menu. */
chevron?: boolean
onSelect: () => void
onHover?: () => void
}
let { label, selected, returnIcon, neutral = false, onSelect, onHover }: Props = $props()
let { label, selected, returnIcon, neutral = false, chevron, onSelect, onHover }: Props = $props()
interface IconConfig {
icon: ComponentType
@@ -73,7 +76,7 @@
>
<span class="grow min-w-0 flex items-center gap-2">
{#if config}
{@render iconWithText(config.icon, config.showChevron, config.iconClass ?? '')}
{@render iconWithText(config.icon, chevron ?? config.showChevron, config.iconClass ?? '')}
{/if}
</span>
{#if returnIcon && selected}