diff --git a/web/src/pages/agents/template-retrieval-binding.ts b/web/src/pages/agents/template-retrieval-binding.ts index 8df0925fd0..872d06bb74 100644 --- a/web/src/pages/agents/template-retrieval-binding.ts +++ b/web/src/pages/agents/template-retrieval-binding.ts @@ -6,7 +6,10 @@ import { Operator, RetrievalFrom } from '@/pages/agent/constant'; * Retrieval params live in two views of a DSL: the canvas graph * (`graph.nodes[].data.form` / `...tools[].params`) and the components the * runtime executes (`components[].obj.params` / `...tools[].params`). Both are - * written on save, so binding helpers must keep the two views in sync. + * written on save, so binding helpers must keep the two views in sync. Counting + * helpers, however, follow the components view only — the topology the engine + * runs — so a single retrieval step mirrored in both views is never counted + * twice. */ interface RetrievalParams { retrieval_from?: string; @@ -15,56 +18,80 @@ interface RetrievalParams { kb_ids?: string[]; } -function walkRetrievalParams( +function pushRetrievalParams( + locations: Array<{ params: RetrievalParams }>, + params: unknown, +) { + if ( + params && + typeof params === 'object' && + typeof (params as RetrievalParams).retrieval_from === 'string' + ) { + locations.push({ params: params as RetrievalParams }); + } +} + +/** Retrieval steps reachable from the canvas graph view. */ +function walkGraphRetrievalParams( dsl: DSL | Record, ): Array<{ params: RetrievalParams }> { const locations: Array<{ params: RetrievalParams }> = []; - const pushParams = (params: unknown) => { - if ( - params && - typeof params === 'object' && - typeof (params as RetrievalParams).retrieval_from === 'string' - ) { - locations.push({ params: params as RetrievalParams }); - } - }; - const graph = (dsl as Record)?.graph; if (Array.isArray(graph?.nodes)) { for (const node of graph.nodes) { const label = node?.data?.label; const form = node?.data?.form; if (label === Operator.Retrieval) { - pushParams(form); + pushRetrievalParams(locations, form); } else if (label === Operator.Agent) { for (const tool of form?.tools ?? []) { if (tool?.component_name === Operator.Retrieval) { - pushParams(tool?.params); + pushRetrievalParams(locations, tool?.params); } } } } } + return locations; +} +/** Retrieval steps the runtime actually executes (topology of record). */ +function walkComponentsRetrievalParams( + dsl: DSL | Record, +): Array<{ params: RetrievalParams }> { + const locations: Array<{ params: RetrievalParams }> = []; const components = (dsl as Record)?.components; if (components && typeof components === 'object') { for (const component of Object.values(components)) { const obj = (component as Record)?.obj; if (obj?.component_name === Operator.Retrieval) { - pushParams(obj?.params); + pushRetrievalParams(locations, obj?.params); } else if (obj?.component_name === Operator.Agent) { for (const tool of obj?.params?.tools ?? []) { if (tool?.component_name === Operator.Retrieval) { - pushParams(tool?.params); + pushRetrievalParams(locations, tool?.params); } } } } } - return locations; } +/** + * Every retrieval location across both views of the DSL. Binding must write + * both the canvas graph and the executed components so a later save (which + * rebuilds components from the graph) cannot silently unbind a step. + */ +function walkRetrievalParams( + dsl: DSL | Record, +): Array<{ params: RetrievalParams }> { + return [ + ...walkGraphRetrievalParams(dsl), + ...walkComponentsRetrievalParams(dsl), + ]; +} + export interface RetrievalBindingCount { datasetCount: number; memoryCount: number; @@ -73,7 +100,10 @@ export interface RetrievalBindingCount { /** * Counts retrieval steps that explicitly source from a dataset/memory but do * not carry any binding yet. Such steps fail at runtime with a - * `dataset_ids`/`memory_ids is required` error. + * `dataset_ids`/`memory_ids is required` error. The count follows the + * `components` view — the topology the runtime executes — and only falls back + * to the graph view when the DSL has no components block, so a retrieval step + * mirrored in both views is reported once. */ export function countUnboundRetrieval( dsl: DSL | Record | undefined, @@ -83,7 +113,13 @@ export function countUnboundRetrieval( if (!dsl) { return { datasetCount, memoryCount }; } - for (const { params } of walkRetrievalParams(dsl)) { + const dslObject = dsl as Record; + const hasComponents = + !!dslObject.components && typeof dslObject.components === 'object'; + const locations = hasComponents + ? walkComponentsRetrievalParams(dslObject) + : walkGraphRetrievalParams(dslObject); + for (const { params } of locations) { if (params.retrieval_from === RetrievalFrom.Dataset) { const ids = params.dataset_ids ?? params.kb_ids; if (!Array.isArray(ids) || ids.length === 0) {