@uipath/maestro-builder-sdk 6.30.2 → 6.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -97,5 +97,24 @@ export declare function inlineAgentResources(files: InlineAgentFiles): BindingsV
97
97
  * connection row's `resource` as `connection`.
98
98
  */
99
99
  export declare function mergeInlineAgentResources(existing: BindingsV2Resource[], fresh: BindingsV2Resource[]): BindingsV2Resource[];
100
+ /**
101
+ * A project's resource rows with its inline agents' rows re-derived, the way
102
+ * compile writes them into `bindings_v2.json` (#962), and used by merge when it
103
+ * carries an agent's resource change (#1221).
104
+ *
105
+ * Each agent's rows come from its directory as it now stands
106
+ * ({@link inlineAgentResources}). The rows a REMOVED resource declared go with
107
+ * it: refresh never retracts a row, so an index the flow dropped would stay
108
+ * declared and a deploy could still fail on it. They are generated by the same
109
+ * function, so they match exactly, and a row a remaining resource still
110
+ * declares is kept. Connection rows are not touched here: compile re-derives
111
+ * them from the flow ({@link connectionResources}).
112
+ *
113
+ * @param resources - The project's current rows.
114
+ * @param agents - Every inline agent's files, as they now stand.
115
+ * @param removedResources - The `resource.json` bodies of the resources removed.
116
+ * @returns The rows to write.
117
+ */
118
+ export declare function refreshInlineAgentRows(resources: BindingsV2Resource[], agents: InlineAgentFiles[], removedResources: unknown[]): BindingsV2Resource[];
100
119
  /** Read one inline agent's directory the way refresh does; `undefined` when it has no readable `agent.json`. */
101
120
  export declare function readInlineAgentFiles(agentDir: string): InlineAgentFiles | undefined;
@@ -507,6 +507,35 @@ export function mergeInlineAgentResources(existing, fresh) {
507
507
  }
508
508
  return out;
509
509
  }
510
+ /**
511
+ * A project's resource rows with its inline agents' rows re-derived, the way
512
+ * compile writes them into `bindings_v2.json` (#962), and used by merge when it
513
+ * carries an agent's resource change (#1221).
514
+ *
515
+ * Each agent's rows come from its directory as it now stands
516
+ * ({@link inlineAgentResources}). The rows a REMOVED resource declared go with
517
+ * it: refresh never retracts a row, so an index the flow dropped would stay
518
+ * declared and a deploy could still fail on it. They are generated by the same
519
+ * function, so they match exactly, and a row a remaining resource still
520
+ * declares is kept. Connection rows are not touched here: compile re-derives
521
+ * them from the flow ({@link connectionResources}).
522
+ *
523
+ * @param resources - The project's current rows.
524
+ * @param agents - Every inline agent's files, as they now stand.
525
+ * @param removedResources - The `resource.json` bodies of the resources removed.
526
+ * @returns The rows to write.
527
+ */
528
+ export function refreshInlineAgentRows(resources, agents, removedResources) {
529
+ const agentRows = agents.map((files) => inlineAgentResources(files));
530
+ const live = new Set(agentRows.flat().map((r) => `${r.resource}\u0000${r.key}`));
531
+ const stale = new Set(inlineAgentResources({ agent: {}, resources: removedResources, features: [] })
532
+ .filter((r) => r.resource !== 'connection' && !live.has(`${r.resource}\u0000${r.key}`))
533
+ .map((r) => `${r.resource}\u0000${r.key}`));
534
+ let out = resources.filter((r) => !stale.has(`${r.resource}\u0000${r.key}`));
535
+ for (const rows of agentRows)
536
+ out = mergeInlineAgentResources(out, rows);
537
+ return out;
538
+ }
510
539
  /** Read one inline agent's directory the way refresh does; `undefined` when it has no readable `agent.json`. */
511
540
  export function readInlineAgentFiles(agentDir) {
512
541
  let agent;
@@ -27,10 +27,11 @@ import { check } from './check.js';
27
27
  import { flowCheckOpts, openCliLibrary } from './cli-library.js';
28
28
  import { Bindings } from './bindings.js';
29
29
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
30
- import { connectionResources, inlineAgentResources, mergeBindingsV2, mergeInlineAgentResources, readBindingsV2, readInlineAgentFiles, } from './bindings-v2.js';
30
+ import { connectionResources, inlineAgentResources, mergeBindingsV2, readBindingsV2, refreshInlineAgentRows, readInlineAgentFiles, } from './bindings-v2.js';
31
31
  import { ensureTypeScriptRuntime } from './node-runtime.js';
32
32
  import { defaultBindingsFile } from './workdir.js';
33
33
  import { isWithin } from './path-within.js';
34
+ import { resourceSourcesOfOtherFlows } from './project-resource-users.js';
34
35
  import { BindingRefusal } from './core/binding-messages.js';
35
36
  import { ByoaRefusal } from './core/byoa.js';
36
37
  export async function run(argv) {
@@ -225,26 +226,15 @@ export async function run(argv) {
225
226
  // requires every one of them, so leaving them to a refresh the SDK route
226
227
  // never runs made every agent with a deployed-resource tool, a context index
227
228
  // or an app escalation fail validate with `AgentValidationDrift`.
228
- const agentRows = agentDirs
229
+ const agentFiles = agentDirs
229
230
  .map((dir) => readInlineAgentFiles(dir))
230
- .filter((files) => files !== undefined)
231
- .map((files) => inlineAgentResources(files));
232
- // The rows a PRUNED resource declared go with it: refresh never retracts a
233
- // row, so an index the flow dropped would stay declared and a deploy could
234
- // still fail on it. Generated by the same function, so they match exactly,
235
- // and kept when a resource that still exists declares the same row.
236
- // Connection rows are skipped: every one is re-derived from the flow below.
237
- const live = new Set(agentRows.flat().map((r) => `${r.resource}\u0000${r.key}`));
238
- const stale = new Set(inlineAgentResources({ agent: {}, resources: removedResources, features: [] })
239
- .filter((r) => r.resource !== 'connection' && !live.has(`${r.resource}\u0000${r.key}`))
240
- .map((r) => `${r.resource}\u0000${r.key}`));
231
+ .filter((files) => files !== undefined);
232
+ const agentRows = agentFiles.map((files) => inlineAgentResources(files));
241
233
  // Nothing to declare and nothing already declared — do not create an empty
242
234
  // file for a flow that has no resources at all.
243
235
  if (connections.length > 0 || existing !== undefined || agentRows.some((rows) => rows.length > 0)) {
244
236
  const merged = mergeBindingsV2(existing, connections);
245
- merged.resources = merged.resources.filter((r) => !stale.has(`${r.resource}\u0000${r.key}`));
246
- for (const rows of agentRows)
247
- merged.resources = mergeInlineAgentResources(merged.resources, rows);
237
+ merged.resources = refreshInlineAgentRows(merged.resources, agentFiles, removedResources);
248
238
  writeJsonFile(sidecarPath, merged);
249
239
  const derived = new Set([...connections, ...agentRows.flat()].map((r) => `${r.resource}\u0000${r.key}`));
250
240
  const agentCount = merged.resources.filter((r) => r.resource !== 'connection' && derived.has(`${r.resource}\u0000${r.key}`)).length;
@@ -260,42 +250,3 @@ function serializeWarningDiagnostic(w) {
260
250
  return { level: 'warning', code: w.code, message: w.message };
261
251
  }
262
252
  runWhenInvokedDirectly(import.meta.url, 'compile', run);
263
- /**
264
- * The `inputs.source` uuids the OTHER `.flow` files in `dir` reference, each
265
- * mapped to the first file that does. `dir` is the namespace sidecar paths are
266
- * relative to, so these are the agent resources another flow still uses. The
267
- * flow being written (`self`) is left out: what it no longer references is what
268
- * pruning is for. Subflow bodies are included; an unreadable file contributes
269
- * nothing.
270
- */
271
- function resourceSourcesOfOtherFlows(dir, self) {
272
- const users = new Map();
273
- let entries;
274
- try {
275
- entries = readdirSync(dir).filter((f) => f.endsWith('.flow') && resolve(dir, f) !== self);
276
- }
277
- catch {
278
- return users;
279
- }
280
- for (const f of entries) {
281
- let flow;
282
- try {
283
- flow = JSON.parse(readFileSync(join(dir, f), 'utf8'));
284
- }
285
- catch {
286
- continue;
287
- }
288
- const walk = (scope) => {
289
- const s = scope;
290
- for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
291
- const src = n?.inputs?.source;
292
- if (typeof src === 'string' && !users.has(src))
293
- users.set(src, f);
294
- }
295
- if (s?.subflows && typeof s.subflows === 'object')
296
- Object.values(s.subflows).forEach(walk);
297
- };
298
- walk(flow);
299
- }
300
- return users;
301
- }
@@ -32,3 +32,21 @@ export declare function parseContextIndexType(type: string): {
32
32
  slug: string;
33
33
  id: string;
34
34
  } | undefined;
35
+ /**
36
+ * The retrieval settings an index gets when the author sets none: what
37
+ * `contextResourceJson` (serialize.ts) writes into `resource.json` `settings`, and
38
+ * what decompile leaves out of the source because compile would write it anyway.
39
+ */
40
+ export declare const CONTEXT_INDEX_DEFAULTS: {
41
+ readonly retrievalMode: "semantic";
42
+ readonly threshold: 0;
43
+ readonly resultCount: 3;
44
+ };
45
+ /**
46
+ * The file extension each retrieval mode accepts by default. The platform's
47
+ * manifest defaults every mode's NODE to `All`, but its resource schema pins
48
+ * `structured`/`batchtransform` to `csv` and `deeprag` to `pdf`/`txt` — so a
49
+ * flat `All` default would emit a resource the platform refuses to parse for
50
+ * every mode but `semantic`.
51
+ */
52
+ export declare const CONTEXT_INDEX_DEFAULT_FILE_EXTENSION: Readonly<Record<string, string>>;
@@ -36,3 +36,22 @@ export function parseContextIndexType(type) {
36
36
  const dot = tail.lastIndexOf('.');
37
37
  return dot < 0 ? { slug: tail, id: '' } : { slug: tail.slice(0, dot), id: tail.slice(dot + 1) };
38
38
  }
39
+ /**
40
+ * The retrieval settings an index gets when the author sets none: what
41
+ * `contextResourceJson` (serialize.ts) writes into `resource.json` `settings`, and
42
+ * what decompile leaves out of the source because compile would write it anyway.
43
+ */
44
+ export const CONTEXT_INDEX_DEFAULTS = { retrievalMode: 'semantic', threshold: 0, resultCount: 3 };
45
+ /**
46
+ * The file extension each retrieval mode accepts by default. The platform's
47
+ * manifest defaults every mode's NODE to `All`, but its resource schema pins
48
+ * `structured`/`batchtransform` to `csv` and `deeprag` to `pdf`/`txt` — so a
49
+ * flat `All` default would emit a resource the platform refuses to parse for
50
+ * every mode but `semantic`.
51
+ */
52
+ export const CONTEXT_INDEX_DEFAULT_FILE_EXTENSION = {
53
+ semantic: 'All',
54
+ structured: 'csv',
55
+ deeprag: 'pdf',
56
+ batchtransform: 'csv',
57
+ };
@@ -150,7 +150,8 @@ if (!existsSync(BASELINE)) {
150
150
  if (existsSync(BASELINE)) args.push('--baseline', BASELINE);
151
151
  run('merge', args);
152
152
  console.log('\\n>>> Wrote ' + ${JSON.stringify(base + '.merged.flow')}
153
- + '. Replace the original with it, together with every <source>/ file merge reports writing beside it.');
153
+ + '. Replace the original with it, together with every <source>/ file merge reports writing beside it,'
154
+ + ' and delete every directory merge lists under "remove".');
154
155
  }
155
156
  `;
156
157
  }
@@ -144,9 +144,11 @@ export interface DecompileOptions {
144
144
  * (`{{input.start__output__notes}}`), so the recovered source declares its
145
145
  * inputs under those names — the same flow, not the same spelling. Without a
146
146
  * reader, a shell agent decompiles with empty prompts and a warning.
147
- * A context index placed by the canvas or the CLI keeps its real name and
148
- * folder only in `<agent source>/resources/<inputs.source>/resource.json`,
149
- * which is read the same way.
147
+ * A context index placed by the canvas or the CLI keeps its real name,
148
+ * folder and retrieval `settings` (mode, query, threshold, result count, file
149
+ * extension) only in `<agent source>/resources/<inputs.source>/resource.json`,
150
+ * which is read the same way. A setting `context: [...]` cannot express is
151
+ * reported through `warn` (an error when `bestEffort` is false).
150
152
  */
151
153
  readSidecar?: (relativePath: string) => string | undefined;
152
154
  }
package/dist/decompile.js CHANGED
@@ -36,7 +36,8 @@ import { FLOW_NODE_TYPES } from './flow-node-registry.js';
36
36
  import { SUBFLOW_START_LABEL } from './core/subflow-start.js';
37
37
  import { commentText } from './core/comment-text.js';
38
38
  import { tsLiteral, tsNumber, tsText } from './core/ts-literal.js';
39
- import { contextIndexSlug, parseContextIndexType } from './core/context-index.js';
39
+ import { ownEntry } from './core/own-entry.js';
40
+ import { CONTEXT_INDEX_DEFAULTS, CONTEXT_INDEX_DEFAULT_FILE_EXTENSION, contextIndexSlug, parseContextIndexType, } from './core/context-index.js';
40
41
  import { wireExprBody } from './core/wire-expr.js';
41
42
  import { hasExits } from './core/step-ports.js';
42
43
  import { parseIxpProjectType } from './core/actions.js';
@@ -914,7 +915,146 @@ function contextIndexIdentity(agent, c, rPlain) {
914
915
  + `will write a different node type. Set \`name\` to the index's real name`
915
916
  + (sidecar === undefined ? ', or keep the resources/ folder beside the .flow so decompile can read it.' : '.'));
916
917
  }
917
- return { name, id, folderPath: rPlain(c, 'folderPath') ?? sidecarText('folderPath') };
918
+ return { name, id, folderPath: rPlain(c, 'folderPath') ?? sidecarText('folderPath'), sidecar };
919
+ }
920
+ /** The retrieval modes `ContextIndexRef.retrievalMode` can hold. */
921
+ const CONTEXT_RETRIEVAL_MODES = new Set(['semantic', 'structured', 'deeprag', 'batchtransform']);
922
+ /** The `settings` keys of a context `resource.json` that {@link contextIndexSettings} reads or checks. */
923
+ const CONTEXT_SETTINGS_KEYS = new Set([
924
+ 'retrievalMode', 'query', 'folderPathPrefix', 'threshold', 'resultCount', 'fileExtension',
925
+ 'citationMode', 'webSearchGrounding', 'outputColumns',
926
+ ]);
927
+ /**
928
+ * A context index's retrieval settings, as the `context: [...]` entry's option
929
+ * rows (`retrievalMode`, `query`, `threshold`, `resultCount`, `fileExtension`),
930
+ * each left out when it is what compile writes anyway.
931
+ *
932
+ * The inverse of `contextResourceJson` (serialize.ts). Read from the sidecar's
933
+ * `settings` first: a node the canvas or the CLI placed keeps its inputs minimal
934
+ * (`inputs.source`), so `resource.json` is the only copy, and it is what the
935
+ * runtime reads. The node's own inputs (the SDK writes both) answer when there
936
+ * is no sidecar. Before #1183 neither was read, so a no-edit round trip reset
937
+ * every setting to the SDK default without a word.
938
+ *
939
+ * The query is variant-tagged in the sidecar and mode-tagged on the node:
940
+ *
941
+ * - `dynamic` / `prompt` → a plain string (the guidance the model reads);
942
+ * - `static` / `text-builder` whose text is a `=js:` expression → that
943
+ * expression, which compile writes back as `text-builder` / `static`;
944
+ * - `static` with `''` is the canvas's spelling of "no query", as the SDK's
945
+ * `dynamic` with `''` is: left out.
946
+ *
947
+ * A setting the SDK cannot express (a `static` query of plain text, an
948
+ * `argument` query, deeprag citations off, batchtransform web grounding off or
949
+ * output columns, a folder-path prefix, an unknown mode or key) is reported:
950
+ * a warning, or an error under `--strict`. Compile would write the SDK's
951
+ * value instead, so it is never dropped silently.
952
+ */
953
+ function contextIndexSettings(agent, c, sidecar) {
954
+ const settings = sidecar?.settings && typeof sidecar.settings === 'object' && !Array.isArray(sidecar.settings)
955
+ ? sidecar.settings : undefined;
956
+ const ni = (c.inputs ?? {});
957
+ const where = `context index "${c.id}" on agent "${agent.id}"`;
958
+ const lossy = (what) => {
959
+ const message = `${where}: ${what}, which \`context: [...]\` cannot express, so compile writes the SDK's `
960
+ + `value instead. Edit that setting in the canvas after merging, or keep the original resource.json.`;
961
+ if (runStrict)
962
+ throw new Error(`decompile: ${message}`);
963
+ warnOnce(message);
964
+ };
965
+ const textOf = (v) => (typeof v === 'string' ? v : undefined);
966
+ const numOf = (v) => {
967
+ const raw = unwrapPlain(v);
968
+ return typeof raw === 'number' && Number.isFinite(raw) ? raw : undefined;
969
+ };
970
+ const valueOf = (v) => (v && typeof v === 'object' ? v.value : undefined);
971
+ // Retrieval mode.
972
+ const rawMode = textOf(settings?.retrievalMode) ?? textOf(unwrapSource(ni.retrievalMode));
973
+ let mode = CONTEXT_INDEX_DEFAULTS.retrievalMode;
974
+ if (rawMode !== undefined && rawMode !== '') {
975
+ if (CONTEXT_RETRIEVAL_MODES.has(rawMode))
976
+ mode = rawMode;
977
+ else
978
+ lossy(`its retrieval mode is ${JSON.stringify(rawMode)}`);
979
+ }
980
+ const sidecarQuery = (() => {
981
+ const q = settings?.query;
982
+ if (!q || typeof q !== 'object')
983
+ return undefined;
984
+ if (q.variant === 'dynamic')
985
+ return { kind: 'prompt', text: textOf(q.description) ?? '' };
986
+ if (q.variant === 'static')
987
+ return { kind: 'text', text: textOf(q.value) ?? '' };
988
+ if (q.variant === 'argument')
989
+ return { kind: 'argument', text: textOf(q.value) ?? '' };
990
+ lossy(`its query has variant ${JSON.stringify(q.variant)}`);
991
+ return undefined;
992
+ })();
993
+ const nodeQuery = (() => {
994
+ const q = ni.query;
995
+ if (typeof q === 'string')
996
+ return q === '' ? undefined : { kind: q.startsWith('=js:') ? 'text' : 'prompt', text: q };
997
+ if (!q || typeof q !== 'object')
998
+ return undefined;
999
+ const t = q;
1000
+ if (t.mode === 'text-builder')
1001
+ return { kind: 'text', text: textOf(t.textValue) ?? '' };
1002
+ if (t.mode === 'variable')
1003
+ return { kind: 'argument', text: textOf(t.argumentPath) ?? '' };
1004
+ return { kind: 'prompt', text: textOf(t.promptValue) ?? '' };
1005
+ })();
1006
+ const query = sidecarQuery ?? nodeQuery;
1007
+ let querySrc;
1008
+ if (query?.kind === 'prompt' && query.text !== '')
1009
+ querySrc = tsLiteral(query.text);
1010
+ else if (query?.kind === 'text' && query.text.startsWith('=js:'))
1011
+ querySrc = tsLiteral(query.text);
1012
+ else if (query?.kind === 'text' && query.text !== '')
1013
+ lossy(`its query is static text (${JSON.stringify(query.text)})`);
1014
+ else if (query?.kind === 'argument')
1015
+ lossy(`its query is the agent argument ${JSON.stringify(query.text)}`);
1016
+ // Numbers and the file extension.
1017
+ const threshold = numOf(settings?.threshold) ?? numOf(ni.threshold);
1018
+ const resultCount = numOf(settings?.resultCount) ?? numOf(ni.resultCount);
1019
+ const fileExtension = textOf(valueOf(settings?.fileExtension)) ?? textOf(unwrapSource(ni.fileExtension));
1020
+ // The node's flat `All` is the platform's node default for every mode; only
1021
+ // the resource's per-mode default is what compile would write anyway.
1022
+ const defaultExtension = ownEntry(CONTEXT_INDEX_DEFAULT_FILE_EXTENSION, mode) ?? 'All';
1023
+ const extensionSrc = fileExtension === undefined || fileExtension === '' || fileExtension === defaultExtension
1024
+ || (settings?.fileExtension === undefined && fileExtension === 'All')
1025
+ ? undefined : tsLiteral(fileExtension);
1026
+ // What compile writes as constants; anything else has no field.
1027
+ if (mode === 'deeprag') {
1028
+ const citation = textOf(valueOf(settings?.citationMode)) ?? textOf(unwrapSource(ni.citations));
1029
+ if (citation !== undefined && !['Inline', 'enabled'].includes(citation))
1030
+ lossy(`its citation mode is ${JSON.stringify(citation)}`);
1031
+ }
1032
+ if (mode === 'batchtransform') {
1033
+ const grounding = textOf(valueOf(settings?.webSearchGrounding)) ?? textOf(valueOf(ni.webSearchGrounding));
1034
+ if (grounding !== undefined && !['Enabled', 'enabled'].includes(grounding))
1035
+ lossy(`its web search grounding is ${JSON.stringify(grounding)}`);
1036
+ const columns = settings?.outputColumns ?? ni.outputColumns;
1037
+ if (Array.isArray(columns) && columns.length > 0)
1038
+ lossy(`it has ${columns.length} output column(s)`);
1039
+ }
1040
+ // The folder-path prefix: variant-tagged in the sidecar, mode-tagged on the node.
1041
+ const field = (v, k) => (v && typeof v === 'object' ? textOf(v[k]) : undefined);
1042
+ const prefixText = settings?.folderPathPrefix !== undefined
1043
+ ? field(settings.folderPathPrefix, 'value') ?? field(settings.folderPathPrefix, 'description') ?? ''
1044
+ : field(ni.folderPathPrefix, 'textValue') ?? field(ni.folderPathPrefix, 'promptValue') ?? '';
1045
+ if (prefixText !== '')
1046
+ lossy(`its folder-path prefix is ${JSON.stringify(prefixText)}`);
1047
+ for (const k of Object.keys(settings ?? {})) {
1048
+ if (!CONTEXT_SETTINGS_KEYS.has(k))
1049
+ lossy(`its resource.json settings carry ${JSON.stringify(k)}`);
1050
+ }
1051
+ return [
1052
+ ['query', querySrc],
1053
+ ['retrievalMode', mode !== CONTEXT_INDEX_DEFAULTS.retrievalMode ? tsLiteral(mode) : undefined],
1054
+ ['threshold', threshold !== undefined && threshold !== CONTEXT_INDEX_DEFAULTS.threshold ? tsNumber(threshold) : undefined],
1055
+ ['resultCount', resultCount !== undefined && resultCount !== CONTEXT_INDEX_DEFAULTS.resultCount ? tsNumber(resultCount) : undefined],
1056
+ ['fileExtension', extensionSrc],
1057
+ ];
918
1058
  }
919
1059
  /**
920
1060
  * A context resource's `resource.json` sidecar, parsed; `undefined` when it is
@@ -1136,21 +1276,14 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1136
1276
  return opts([['kind', tsLiteral('builtin')], ['tool', tsLiteral('summarize')]]); // unreachable in practice
1137
1277
  });
1138
1278
  const contextSrc = resource('context').map((c) => {
1139
- // `query` is a MODE TRIPLE, not a string: the author's value lands in
1140
- // `promptValue` (prompt mode) or `textValue` (text-builder). Reading the
1141
- // field itself finds an object and silently recovers nothing.
1142
- const q = rInputs(c).query;
1143
- const queryText = typeof q?.promptValue === 'string' && q.promptValue !== '' ? q.promptValue
1144
- : typeof q?.textValue === 'string' && q.textValue !== '' ? q.textValue
1145
- : typeof q === 'string' && q !== '' ? q
1146
- : undefined;
1147
- const { name, id, folderPath } = contextIndexIdentity(node, c, rPlain);
1279
+ const { name, id, folderPath, sidecar } = contextIndexIdentity(node, c, rPlain);
1148
1280
  return opts([
1149
1281
  ['name', tsLiteral(name)],
1150
1282
  ['id', tsLiteral(id)],
1151
1283
  ['folderPath', folderPath !== undefined ? tsLiteral(folderPath) : undefined],
1152
1284
  ['folderKey', rPlain(c, 'folderKey') !== undefined ? tsLiteral(rPlain(c, 'folderKey')) : undefined],
1153
- ['query', queryText !== undefined ? tsLiteral(queryText) : undefined],
1285
+ // `query` and the retrieval settings, from resource.json or the node (#1183).
1286
+ ...contextIndexSettings(node, c, sidecar),
1154
1287
  ]);
1155
1288
  });
1156
1289
  const escalationSrc = resource('escalation').map((e) => {
package/dist/merge-cli.js CHANGED
@@ -30,14 +30,24 @@
30
30
  * compiled the side itself (`--baseline-from`, an edited `.flow.ts`), which keeps
31
31
  * the sidecars in memory; what merge cannot carry is warned, and refused under
32
32
  * `--strict`. See merge-sidecars.ts.
33
+ *
34
+ * Agent resources (#1221): a context or tool the edit adds or changes is written
35
+ * beside the merged output too. One the edit REMOVES is listed, one
36
+ * `flow-merge: remove <dir>` line each (and `removed` in `--json`), because
37
+ * copying the candidate over the project cannot delete a file: delete those
38
+ * directories from the project. When the original sits in a Flow project
39
+ * (`project.uiproj`), the project's `bindings_v2.json` is re-derived for the
40
+ * changed agents, as compile does, and written beside the merged output.
33
41
  */
34
42
  import { reportResult } from './cli-result.js';
35
43
  import { writeJsonFile } from './core/json-file.js';
36
44
  import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
45
+ import { readBindingsV2, readInlineAgentFiles, refreshInlineAgentRows } from './bindings-v2.js';
37
46
  import { basename, dirname, join, resolve } from 'node:path';
38
47
  import { MergeRefusedError, mergeFlowWithSidecars } from './merge.js';
39
48
  import { sidecarSetFromList } from './merge-sidecars.js';
40
49
  import { isWithin } from './path-within.js';
50
+ import { resourceSourcesOfOtherFlows } from './project-resource-users.js';
41
51
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
42
52
  import { ensureTypeScriptRuntime } from './node-runtime.js';
43
53
  import { flowCheckOpts, openCliLibrary } from './cli-library.js';
@@ -78,6 +88,71 @@ export function sidecarsBeside(flowPath) {
78
88
  return files;
79
89
  };
80
90
  }
91
+ /**
92
+ * The project's `bindings_v2.json` after merge's resource changes, or `undefined`
93
+ * when the original is not in a Flow project (`project.uiproj`), no agent
94
+ * resource changed, the file already holds the resulting rows, or the change
95
+ * declares no different row (the rows derived
96
+ * after the change equal those derived from the project as it is, so a changed
97
+ * `resultCount` does not create or rewrite the file).
98
+ *
99
+ * Only the agents whose resources changed are re-derived, each from its
100
+ * directory as it will stand once the candidate is installed: the original's
101
+ * files, with merge's writes over them and its removals taken out.
102
+ */
103
+ function projectBindings(projectDir, writes, removals) {
104
+ if (!existsSync(join(projectDir, 'project.uiproj')))
105
+ return undefined;
106
+ const isResource = (path) => /\/resources\/[^/]+\/resource\.json$/.test(path);
107
+ const agentOf = (path) => path.split('/')[0];
108
+ const changedAgents = new Set([
109
+ ...writes.filter((w) => isResource(w.path)).map((w) => agentOf(w.path)),
110
+ ...removals.map(agentOf),
111
+ ]);
112
+ if (changedAgents.size === 0)
113
+ return undefined;
114
+ const removedBodies = [];
115
+ const agents = [];
116
+ const agentsBefore = [];
117
+ const asResources = (files) => [...files.values()].filter((r) => !!r && typeof r === 'object' && '$resourceType' in r);
118
+ for (const source of changedAgents) {
119
+ const onDisk = readInlineAgentFiles(join(projectDir, source));
120
+ const files = new Map();
121
+ const resourcesDir = join(projectDir, source, 'resources');
122
+ if (existsSync(resourcesDir)) {
123
+ for (const entry of readdirSync(resourcesDir).sort()) {
124
+ try {
125
+ files.set(`${source}/resources/${entry}/resource.json`, JSON.parse(readFileSync(join(resourcesDir, entry, 'resource.json'), 'utf8')));
126
+ }
127
+ catch {
128
+ // Refresh skips an unreadable file the same way.
129
+ }
130
+ }
131
+ }
132
+ agentsBefore.push({ agent: onDisk?.agent ?? {}, resources: asResources(files), features: onDisk?.features ?? [] });
133
+ for (const dir of removals.filter((r) => agentOf(r) === source)) {
134
+ const path = `${dir}/resource.json`;
135
+ if (files.has(path))
136
+ removedBodies.push(files.get(path));
137
+ files.delete(path);
138
+ }
139
+ let agent = onDisk?.agent ?? {};
140
+ for (const w of writes.filter((x) => agentOf(x.path) === source)) {
141
+ if (w.path === `${source}/agent.json`)
142
+ agent = w.json;
143
+ else if (isResource(w.path))
144
+ files.set(w.path, w.json);
145
+ }
146
+ agents.push({ agent, resources: asResources(files), features: onDisk?.features ?? [] });
147
+ }
148
+ const existing = readBindingsV2(join(projectDir, 'bindings_v2.json'));
149
+ const before = refreshInlineAgentRows(existing?.resources ?? [], agentsBefore, []);
150
+ const after = refreshInlineAgentRows(existing?.resources ?? [], agents, removedBodies);
151
+ // Nothing to write when the change declares nothing new, or the file already says it.
152
+ if (JSON.stringify(after) === JSON.stringify(before) || JSON.stringify(after) === JSON.stringify(existing?.resources ?? []))
153
+ return undefined;
154
+ return { version: '2.0', ...existing, resources: after };
155
+ }
81
156
  export async function run(argv) {
82
157
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
83
158
  console.error(USAGE);
@@ -205,11 +280,33 @@ export async function run(argv) {
205
280
  writeJsonFile(target, sidecar.json);
206
281
  console.log(`flow-merge: wrote ${sidecar.path} (carries ${sidecar.carried.join(', ')})`);
207
282
  }
283
+ const originalDir = dirOf(originalPath);
284
+ // A resource another `.flow` in the project still names stays (compile's prune
285
+ // has the same guard): a decompiled canvas agent keeps its `source` directory,
286
+ // so two flows can share it.
287
+ const users = resourceSourcesOfOtherFlows(originalDir, resolve(originalPath));
288
+ const removals = merged.removals.filter((rel) => {
289
+ const user = users.get(basename(rel));
290
+ if (user !== undefined)
291
+ console.log(`flow-merge: kept ${rel}: ${user} still references it`);
292
+ return user === undefined;
293
+ });
294
+ const removed = removals.map((rel) => join(originalDir, rel));
295
+ for (const dir of removed) {
296
+ console.log(`flow-merge: remove ${dir} from the project: the edit removed this agent resource, and copying the candidate cannot delete it`);
297
+ }
298
+ const bindingsV2 = projectBindings(originalDir, merged.sidecars, removals);
299
+ if (bindingsV2 !== undefined) {
300
+ writeJsonFile(join(outDir, 'bindings_v2.json'), bindingsV2);
301
+ console.log('flow-merge: wrote bindings_v2.json (the changed agents\' resource rows, re-derived as compile does)');
302
+ }
208
303
  reportResult({
209
304
  path: resolve(out),
210
305
  nodes: (flow.nodes ?? []).length,
211
306
  edges: (flow.edges ?? []).length,
212
307
  ...(targets.length > 0 ? { sidecars: targets.map((t) => t.target) } : {}),
308
+ ...(removed.length > 0 ? { removed } : {}),
309
+ ...(bindingsV2 !== undefined ? { bindings: join(resolve(outDir), 'bindings_v2.json') } : {}),
213
310
  });
214
311
  return 0;
215
312
  }
@@ -19,6 +19,16 @@ export interface MergeSidecarSources {
19
19
  /** Why {@link baseline} is absent, phrased to finish "merge cannot tell an edit from the round trip: …". */
20
20
  baselineMissing?: string;
21
21
  }
22
+ /** The three flows, for matching resource directories by the node that names them. */
23
+ export interface MergeSidecarFlows {
24
+ original: FlowLike;
25
+ baseline?: FlowLike;
26
+ edited: FlowLike;
27
+ }
28
+ type FlowLike = {
29
+ nodes?: AnyNode[];
30
+ subflows?: Record<string, unknown>;
31
+ } | undefined;
22
32
  /** One file the merged flow needs written beside it, relative to it. */
23
33
  export interface SidecarWrite {
24
34
  path: string;
@@ -64,8 +74,9 @@ export declare function mergeJson3(original: unknown, baseline: unknown, edited:
64
74
  export declare function mergeAgentSidecars(merged: {
65
75
  nodes?: AnyNode[];
66
76
  subflows?: Record<string, unknown>;
67
- }, sources: MergeSidecarSources): {
77
+ }, sources: MergeSidecarSources, flows?: MergeSidecarFlows): {
68
78
  writes: SidecarWrite[];
79
+ removals: string[];
69
80
  findings: MergeFinding[];
70
81
  };
71
82
  export {};
@@ -30,7 +30,7 @@
30
30
  * - no baseline sidecar to compare with (no `--baseline`, or the baseline and the
31
31
  * edited compile were written to one directory, so the second overwrote the
32
32
  * first's `<source>/`), and the edited `agent.json` differs from the original's;
33
- * - a change to the agent's resource files (a context or tool added or removed);
33
+ * - without a baseline sidecar, a change to the agent's resource files;
34
34
  * - an `agent.json` edit on a node that carries its own prompts (an embedded
35
35
  * node: decompile, like Studio Web, reads the node's prompts first, so the
36
36
  * edited file would not be what runs).
@@ -38,6 +38,30 @@
38
38
  * An agent the merged flow adds (no sidecar in the original) gets the edited
39
39
  * compile's files whole.
40
40
  *
41
+ * ## Resources (#1221)
42
+ *
43
+ * A context or tool the author adds, removes or changes is carried too. The
44
+ * three sides name the same resource's directory differently: the canvas by its
45
+ * own id (`resources/d84e2c17-…/`), compile by a uuid derived from the step and
46
+ * the resource (`resources/e3ed6f35-…/`). So resources are matched by IDENTITY,
47
+ * not directory: the id of the flow node whose `inputs.source` names the
48
+ * directory (node ids survive the round trip by construction), or, for a
49
+ * resource no node names, its content without `id`. Then, per resource:
50
+ *
51
+ * - added (in the edited compile, not the baseline): the edited file, written
52
+ * under the directory the merged node names;
53
+ * - changed: the author's delta applied onto the ORIGINAL file, written back
54
+ * under the original directory (the merged node keeps the original `source`,
55
+ * see `mergeFlow`);
56
+ * - removed (in the baseline, not the edited compile): the original directory
57
+ * goes into `removals`. Copying the candidate over the project never deletes
58
+ * a file, so the caller has to delete these.
59
+ *
60
+ * A resource only the original has (one compile does not emit) is left alone.
61
+ * A changed or removed resource that pairs with no original one (the canvas
62
+ * named its node, and its file has fields compile does not write) is reported,
63
+ * and refused under `strict`: guessing would delete or duplicate the wrong one.
64
+ *
41
65
  * Pure: the caller reads the files ({@link SidecarSet}) and writes the result.
42
66
  */
43
67
  import { jsonClone } from './core/json-file.js';
@@ -140,6 +164,100 @@ function resourceChanges(from, to) {
140
164
  ];
141
165
  return out.length > 0 ? out.join('; ') : undefined;
142
166
  }
167
+ /** `<source>/resources/<dir>/resource.json` -> `<dir>`; `undefined` for any other path. */
168
+ function resourceDir(source, path) {
169
+ const prefix = `${source}/resources/`;
170
+ if (!path.startsWith(prefix) || !path.endsWith('/resource.json'))
171
+ return undefined;
172
+ const dir = path.slice(prefix.length, -'/resource.json'.length);
173
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(dir) ? dir : undefined;
174
+ }
175
+ /** Every node of a flow, root and subflows, by the `inputs.source` it names. */
176
+ function nodeIdsBySource(flow) {
177
+ const out = new Map();
178
+ const walk = (scope) => {
179
+ const sc = scope;
180
+ for (const n of Array.isArray(sc?.nodes) ? sc.nodes : []) {
181
+ const src = n?.inputs?.source;
182
+ if (typeof src === 'string' && !out.has(src))
183
+ out.set(src, n.id);
184
+ }
185
+ if (sc?.subflows && typeof sc.subflows === 'object')
186
+ Object.values(sc.subflows).forEach(walk);
187
+ };
188
+ walk(flow);
189
+ return out;
190
+ }
191
+ /** A resource file without the `id` its writer derives from the directory. */
192
+ function withoutId(json) {
193
+ if (!isObject(json))
194
+ return json;
195
+ const { id: _id, ...rest } = json;
196
+ return rest;
197
+ }
198
+ /**
199
+ * One side's resources of the agent at `source`, keyed by identity: `node:<id>`
200
+ * for a resource directory a node of that side's flow names in `inputs.source`,
201
+ * else `content:<the file without id>`.
202
+ */
203
+ function resourcesByIdentity(source, files, flow) {
204
+ const byDir = nodeIdsBySource(flow);
205
+ const out = new Map();
206
+ for (const [path, json] of Object.entries(files)) {
207
+ const dir = resourceDir(source, path);
208
+ if (dir === undefined)
209
+ continue;
210
+ const nodeId = byDir.get(dir);
211
+ out.set(nodeId !== undefined ? `node:${nodeId}` : `content:${stable(withoutId(json))}`, { dir, json });
212
+ }
213
+ return out;
214
+ }
215
+ /**
216
+ * The resource files of the agent at `source` the merged flow needs, and the
217
+ * original resource directories it no longer has (see the module comment).
218
+ */
219
+ function carryResources(source, files, flows) {
220
+ const o = resourcesByIdentity(source, files.original, flows.original);
221
+ const b = resourcesByIdentity(source, files.baseline, flows.baseline);
222
+ const e = resourcesByIdentity(source, files.edited, flows.edited);
223
+ const writes = [];
224
+ const removals = [];
225
+ /** Baseline resources the edit changed or removed that pair with no original resource. */
226
+ const unmatched = [];
227
+ const pathOf = (dir) => `${source}/resources/${dir}/resource.json`;
228
+ for (const key of new Set([...b.keys(), ...e.keys()])) {
229
+ const [ok, bk, ek] = [o.get(key), b.get(key), e.get(key)];
230
+ if (ek !== undefined && bk === undefined) {
231
+ // Added by the author. Its directory is the one the edited (and so the
232
+ // merged) node names; an original resource of the same identity the round
233
+ // trip dropped is updated in place instead.
234
+ if (ok !== undefined)
235
+ continue;
236
+ writes.push({ path: pathOf(ek.dir), json: jsonClone(ek.json), carried: ['added resource'] });
237
+ }
238
+ else if (ek === undefined && bk !== undefined) {
239
+ if (ok !== undefined)
240
+ removals.push(`${source}/resources/${ok.dir}`);
241
+ else
242
+ unmatched.push(pathOf(bk.dir));
243
+ }
244
+ else if (ek !== undefined && bk !== undefined && stable(withoutId(bk.json)) !== stable(withoutId(ek.json))) {
245
+ const changed = [];
246
+ if (ok === undefined) {
247
+ // Writing the edited file would add a second resource beside the
248
+ // original one it was meant to change.
249
+ unmatched.push(pathOf(bk.dir));
250
+ continue;
251
+ }
252
+ // The derived `id` is not the author's: give both compiles the original's,
253
+ // so only the content delta lands (and the original's key order stays).
254
+ const withOrigId = (json) => (isObject(json) && isObject(ok.json) && 'id' in ok.json ? { ...json, id: ok.json.id } : json);
255
+ const json = mergeJson3(ok.json, withOrigId(bk.json), withOrigId(ek.json), changed);
256
+ writes.push({ path: pathOf(ok.dir), json, carried: changed });
257
+ }
258
+ }
259
+ return { writes, removals, unmatched };
260
+ }
143
261
  /** Whether a node carries its prompts itself, so its `agent.json` is not what runs (see decompile's `sidecarAgentInputs`). */
144
262
  function carriesOwnPrompts(node) {
145
263
  const i = node?.inputs ?? {};
@@ -151,8 +269,9 @@ const finding = (message) => ({ kind: 'agent-sidecar', message, refuses: true })
151
269
  * not carry. `merged` is `mergeFlow`'s result. An agent is new when the
152
270
  * original side has no `<source>/agent.json` for it.
153
271
  */
154
- export function mergeAgentSidecars(merged, sources) {
272
+ export function mergeAgentSidecars(merged, sources, flows) {
155
273
  const writes = [];
274
+ const removals = [];
156
275
  const findings = [];
157
276
  for (const [source, node] of agentSources(merged)) {
158
277
  const edited = sources.edited(source);
@@ -202,13 +321,24 @@ export function mergeAgentSidecars(merged, sources) {
202
321
  writes.push({ path: agentPath, json: next, carried: changed });
203
322
  }
204
323
  }
205
- // Resources: report a changed set; the canvas names them by its own ids, so
206
- // the edited files cannot be mapped onto the original's directories.
207
- const res = resourceChanges(base, edited);
208
- if (res) {
209
- findings.push(finding(`agent "${node.id}": the edit changes its resource files (${res}). Merge does not carry resource files; `
210
- + 'the original\'s stay as they are. Make that change on the canvas.'));
324
+ // Resources: carried by identity (#1221), when the three flows are known.
325
+ if (flows === undefined) {
326
+ const res = resourceChanges(base, edited);
327
+ if (res) {
328
+ findings.push(finding(`agent "${node.id}": the edit changes its resource files (${res}). Merge does not carry resource files; `
329
+ + 'the original\'s stay as they are. Make that change on the canvas.'));
330
+ }
331
+ continue;
332
+ }
333
+ const carriedResources = carryResources(source, { original: orig, baseline: base, edited }, flows);
334
+ writes.push(...carriedResources.writes);
335
+ removals.push(...carriedResources.removals);
336
+ for (const path of carriedResources.unmatched) {
337
+ findings.push(finding(`agent "${node.id}": the edit changes or removes the resource ${path}, but merge cannot tell which of the `
338
+ + 'original\'s resource directories it is (no original node has the id of the node that names it, and no '
339
+ + 'original resource file has its content), so the change is NOT carried and the original\'s resource files '
340
+ + 'stay as they are. Make that change on the canvas.'));
211
341
  }
212
342
  }
213
- return { writes, findings };
343
+ return { writes, removals, findings };
214
344
  }
package/dist/merge.d.ts CHANGED
@@ -176,4 +176,5 @@ export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?:
176
176
  export declare function mergeFlowWithSidecars(original: AnyFlow, edited: AnyFlow, sidecars: MergeSidecarSources, options?: MergeOptions): {
177
177
  flow: AnyFlow;
178
178
  sidecars: SidecarWrite[];
179
+ removals: string[];
179
180
  };
package/dist/merge.js CHANGED
@@ -140,6 +140,38 @@ function withOriginalLayout(edited, orig, originalLayout) {
140
140
  }
141
141
  return edited;
142
142
  }
143
+ /**
144
+ * Give an edited node the original's `inputs.source` when only the round trip
145
+ * changed it (the baseline's equals the edited one's).
146
+ *
147
+ * `source` names the directory of a node's sidecar files: an agent's
148
+ * `<source>/agent.json`, an agent resource's `<agent>/resources/<source>/`.
149
+ * Decompile keeps an agent's, but compile derives a resource's from the step
150
+ * and the resource, so the canvas's `resources/d84e2c17-…/` comes back as
151
+ * `resources/e3ed6f35-…/`. An edited resource node taken whole would then name
152
+ * a directory the project does not have. The author never writes a resource's
153
+ * `source`, so keeping the original's is the delta rule (#1221); merge-sidecars
154
+ * writes the carried file back under that same directory.
155
+ */
156
+ function withOriginalSource(node, orig, base) {
157
+ const o = orig?.inputs?.source;
158
+ const e = node.inputs?.source;
159
+ if (typeof o !== 'string' || typeof e !== 'string' || o === e || base?.inputs?.source !== e)
160
+ return node;
161
+ return { ...node, inputs: { ...node.inputs, source: o } };
162
+ }
163
+ /** {@link withOriginalSource} over every subflow node the original subflow of the same key also has. */
164
+ function keepOriginalSourcesInSubflows(original, subflows, baseline) {
165
+ for (const [key, sf] of Object.entries(subflows)) {
166
+ const nodes = sf?.nodes;
167
+ if (!Array.isArray(nodes))
168
+ continue;
169
+ const origNodes = byId(original.subflows?.[key]?.nodes);
170
+ const baseNodes = byId(baseline.subflows?.[key]?.nodes);
171
+ for (let i = 0; i < nodes.length; i++)
172
+ nodes[i] = withOriginalSource(nodes[i], origNodes.get(nodes[i].id), baseNodes.get(nodes[i].id));
173
+ }
174
+ }
143
175
  /** The original's `layout.nodes` map, when it has one. */
144
176
  function layoutNodesOf(f) {
145
177
  const nodes = f.layout?.nodes;
@@ -160,9 +192,9 @@ export function mergeFlow(original, edited, options = {}) {
160
192
  */
161
193
  export function mergeFlowWithSidecars(original, edited, sidecars, options = {}) {
162
194
  const { result, findings } = mergeFlowFindings(original, edited, options);
163
- const carried = mergeAgentSidecars(result, sidecars);
195
+ const carried = mergeAgentSidecars(result, sidecars, { original, baseline: options.baseline, edited });
164
196
  reportOrRefuse([...findings, ...carried.findings], options);
165
- return { flow: result, sidecars: carried.writes };
197
+ return { flow: result, sidecars: carried.writes, removals: carried.removals };
166
198
  }
167
199
  /** Warn every finding; under `strict`, throw over the refusing ones (and still warn the rest). */
168
200
  function reportOrRefuse(findings, options) {
@@ -189,7 +221,7 @@ function mergeFlowFindings(original, edited, options) {
189
221
  }
190
222
  else {
191
223
  // Added or edited → take the edited node, re-attaching original layout.
192
- finalNodes.push(withOriginalLayout(jsonClone(e), orig, layoutNodesOf(original)));
224
+ finalNodes.push(withOriginalSource(withOriginalLayout(jsonClone(e), orig, layoutNodesOf(original)), orig, baseNodes.get(e.id)));
193
225
  if (orig && orig.type === e.type && orig.typeVersion !== e.typeVersion) {
194
226
  options.warn?.(`node ${JSON.stringify(e.id)} moved ${e.type}@${String(orig.typeVersion)} -> ${String(e.typeVersion)} `
195
227
  + `(the edited source does not pin ${String(orig.typeVersion)}; keep the { version } pin, `
@@ -203,6 +235,8 @@ function mergeFlowFindings(original, edited, options) {
203
235
  // ── Variables & subflows: the edited flow is authoritative (the user's graph). ──
204
236
  result.variables = jsonClone(edited.variables);
205
237
  result.subflows = jsonClone(edited.subflows ?? {});
238
+ if (options.baseline)
239
+ keepOriginalSourcesInSubflows(original, result.subflows, options.baseline);
206
240
  const subflowScopes = Object.values(result.subflows);
207
241
  // ── Edges: the edited graph's edges define the wiring, under the edited
208
242
  // file's own ids. compile draws every edge id — root AND subflows — from
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `inputs.source` uuids the OTHER `.flow` files in `dir` reference, each
3
+ * mapped to the first file that does. `dir` is the namespace sidecar paths are
4
+ * relative to, so these are the agent resources another flow still uses. The
5
+ * flow being written (`self`) is left out: what it no longer references is what
6
+ * pruning is for. Subflow bodies are included; an unreadable file contributes
7
+ * nothing.
8
+ */
9
+ export declare function resourceSourcesOfOtherFlows(dir: string, self: string): Map<string, string>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * project-resource-users — which other `.flow` files in a Flow project still
3
+ * use a sidecar directory. Compile's prune and merge's resource removal both
4
+ * must leave such a directory alone: a decompiled canvas agent keeps its
5
+ * `source` directory, so two flows in one project can share it (#1151, #1184).
6
+ */
7
+ import { readdirSync, readFileSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ /**
10
+ * The `inputs.source` uuids the OTHER `.flow` files in `dir` reference, each
11
+ * mapped to the first file that does. `dir` is the namespace sidecar paths are
12
+ * relative to, so these are the agent resources another flow still uses. The
13
+ * flow being written (`self`) is left out: what it no longer references is what
14
+ * pruning is for. Subflow bodies are included; an unreadable file contributes
15
+ * nothing.
16
+ */
17
+ export function resourceSourcesOfOtherFlows(dir, self) {
18
+ const users = new Map();
19
+ let entries;
20
+ try {
21
+ entries = readdirSync(dir).filter((f) => f.endsWith('.flow') && resolve(dir, f) !== self);
22
+ }
23
+ catch {
24
+ return users;
25
+ }
26
+ for (const f of entries) {
27
+ let flow;
28
+ try {
29
+ flow = JSON.parse(readFileSync(join(dir, f), 'utf8'));
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ const walk = (scope) => {
35
+ const s = scope;
36
+ for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
37
+ const src = n?.inputs?.source;
38
+ if (typeof src === 'string' && !users.has(src))
39
+ users.set(src, f);
40
+ }
41
+ if (s?.subflows && typeof s.subflows === 'object')
42
+ Object.values(s.subflows).forEach(walk);
43
+ };
44
+ walk(flow);
45
+ }
46
+ return users;
47
+ }
package/dist/serialize.js CHANGED
@@ -10,7 +10,7 @@ import ixpDefinition from './ixp-definition.json' with { type: 'json' };
10
10
  import { BUNDLED_DEFINITIONS, bundledVersions } from './flow-definitions.js';
11
11
  import { Expr, toExpr, SCHEDULE_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf } from './flow-sdk.js';
12
12
  import { FlowAction, FlowTrigger } from './core/node-classes.js';
13
- import { contextIndexType } from './core/context-index.js';
13
+ import { CONTEXT_INDEX_DEFAULTS, CONTEXT_INDEX_DEFAULT_FILE_EXTENSION, contextIndexType } from './core/context-index.js';
14
14
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
15
15
  import { rawNodeInputProblem, registeredRawType } from './raw-node-advice.js';
16
16
  import { planContributionBindings } from './core/contribution-bindings.js';
@@ -855,19 +855,6 @@ function renderContextQuery(query, rename) {
855
855
  }
856
856
  return { ...base, promptValue: asText };
857
857
  }
858
- /**
859
- * The file extension each retrieval mode accepts by default. The platform's
860
- * manifest defaults every mode's NODE to `All`, but its resource schema pins
861
- * `structured`/`batchtransform` to `csv` and `deeprag` to `pdf`/`txt` — so a
862
- * flat `All` default would emit a resource the platform refuses to parse for
863
- * every mode but `semantic`.
864
- */
865
- const DEFAULT_FILE_EXTENSION = {
866
- semantic: 'All',
867
- structured: 'csv',
868
- deeprag: 'pdf',
869
- batchtransform: 'csv',
870
- };
871
858
  /**
872
859
  * The `resources/<id>/resource.json` an inline agent's context index needs.
873
860
  *
@@ -891,7 +878,7 @@ const DEFAULT_FILE_EXTENSION = {
891
878
  * every other binding with it.
892
879
  */
893
880
  function contextResourceJson(cx, spec) {
894
- const mode = cx.retrievalMode ?? 'semantic';
881
+ const mode = cx.retrievalMode ?? CONTEXT_INDEX_DEFAULTS.retrievalMode;
895
882
  return {
896
883
  $resourceType: 'context',
897
884
  id: spec.id,
@@ -920,10 +907,12 @@ function contextResourceJson(cx, spec) {
920
907
  query: spec.query.mode === 'text-builder'
921
908
  ? { variant: 'static', value: String(spec.query.textValue ?? '') }
922
909
  : { variant: 'dynamic', description: String(spec.query.promptValue ?? '') },
923
- folderPathPrefix: { variant: 'static' },
924
- threshold: cx.threshold ?? 0,
925
- resultCount: cx.resultCount ?? 3,
926
- fileExtension: { value: cx.fileExtension ?? ownEntry(DEFAULT_FILE_EXTENSION, mode) ?? 'All' },
910
+ // `value: ''`, as the canvas writes it (flow-workbench `mapContextQueryToWire`
911
+ // always sets the value), so a canvas index's resource round-trips unchanged.
912
+ folderPathPrefix: { variant: 'static', value: '' },
913
+ threshold: cx.threshold ?? CONTEXT_INDEX_DEFAULTS.threshold,
914
+ resultCount: cx.resultCount ?? CONTEXT_INDEX_DEFAULTS.resultCount,
915
+ fileExtension: { value: cx.fileExtension ?? ownEntry(CONTEXT_INDEX_DEFAULT_FILE_EXTENSION, mode) ?? 'All' },
927
916
  // Mode-specific keys the schema REQUIRES of those two modes and rejects
928
917
  // nowhere else, carrying the node's own constants through the enum's
929
918
  // spelling (the node says `enabled`; the resource says `Enabled`/`Inline`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.30.2",
3
+ "version": "6.30.4",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "dcf5c8c2813ef37768cf148cd82cc27791621715"
95
+ "gitref": "09ce362aa97a2af0513e0c5883ccaa032df52a4a"
96
96
  }