@uipath/maestro-builder-sdk 6.30.0 → 6.30.1

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.
@@ -105,11 +105,16 @@ function pipelineScript(originalPath, tsPath, pipelinePath) {
105
105
  // Generated by flow-decompile — the brownfield read/modify/write pipeline for
106
106
  // ${commentText(basename(originalPath))}. Run it, edit ${commentText(basename(tsPath))}, run it again.
107
107
  ${pipelineFrontDoor('flow')}
108
+ import { mkdirSync } from 'node:fs';
108
109
 
109
110
  const ORIGINAL = ${JSON.stringify(originalPath)};
110
111
  const TS = ${JSON.stringify(tsPath)};
111
- const BASELINE = ${JSON.stringify(base + '.baseline.flow')};
112
- const EDITED = ${JSON.stringify(base + '.edited.flow')};
112
+ // The baseline and the edited compile each get a directory of their own: a
113
+ // compile writes an inline agent's \`<source>/agent.json\` beside its output, and
114
+ // merge reads both copies to carry a prompt or settings edit (#1207). In one
115
+ // directory the edited compile would overwrite the baseline's.
116
+ const BASELINE = ${JSON.stringify(join(base + '.baseline', basename(base) + '.flow'))};
117
+ const EDITED = ${JSON.stringify(join(base + '.edited', basename(base) + '.flow'))};
113
118
  const MERGED = ${JSON.stringify(base + '.merged.flow')};
114
119
 
115
120
  // Ensure the decompiled source exists (a fresh clone may have only the .flow).
@@ -118,10 +123,12 @@ if (!existsSync(TS)) run('decompile', [ORIGINAL, '-o', TS, '--no-pipeline']);
118
123
  if (!existsSync(BASELINE)) {
119
124
  // ── Step 1: capture the baseline = compile of the PRISTINE decompiled code,
120
125
  // BEFORE any edit. Run this once right after decompiling, then edit. ──
126
+ mkdirSync(dirname(BASELINE), { recursive: true });
121
127
  run('compile', [TS, '-o', BASELINE, '--no-check']);
122
128
  console.log('\\n>>> Baseline captured. Now EDIT ' + ${JSON.stringify(basename(tsPath))} + ', then re-run this script to compile + merge.');
123
129
  } else {
124
130
  // ── Step 2: compile your edits, then merge them back into the original. ──
131
+ mkdirSync(dirname(EDITED), { recursive: true });
125
132
  run('compile', [TS, '-o', EDITED, '--no-check']);
126
133
 
127
134
  // ── DIFF GATE (seam) ──────────────────────────────────────────────────────
@@ -142,7 +149,8 @@ if (!existsSync(BASELINE)) {
142
149
  const args = namedOriginal ? [EDITED, '--original', ORIGINAL, '-o', MERGED] : [ORIGINAL, EDITED, '-o', MERGED];
143
150
  if (existsSync(BASELINE)) args.push('--baseline', BASELINE);
144
151
  run('merge', args);
145
- console.log('\\n>>> Wrote ' + ${JSON.stringify(base + '.merged.flow')});
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.');
146
154
  }
147
155
  `;
148
156
  }
@@ -1,3 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { type SidecarSet } from './merge-sidecars.js';
2
3
  import { type CliExitCode } from './cli-run.js';
4
+ /**
5
+ * The agent sidecars beside the flow at `flowPath`: `<source>/agent.json` and each
6
+ * `<source>/resources/<id>/resource.json`, parsed. An unreadable or invalid file
7
+ * reads as absent.
8
+ */
9
+ export declare function sidecarsBeside(flowPath: string): SidecarSet;
3
10
  export declare function run(argv: string[]): Promise<CliExitCode>;
package/dist/merge-cli.js CHANGED
@@ -18,12 +18,26 @@
18
18
  * With `--baseline`, every wire-identity difference between the baseline and
19
19
  * the original (what the no-edit round trip changed) is printed as a warning;
20
20
  * `--strict` refuses instead (exit 1, nothing written). See merge.ts.
21
+ *
22
+ * Agent sidecars (#1207): an inline agent's prompts and settings live in
23
+ * `<source>/agent.json` beside each flow, not in the node. Merge reads that file
24
+ * beside the original, the baseline and the edited flow, applies the author's
25
+ * change (baseline -> edited) onto the original's copy, and writes the result
26
+ * beside the merged output, one `flow-merge: wrote <source>/agent.json` line per
27
+ * file. Copy those files along with the merged `.flow`. The baseline's sidecars
28
+ * are only known when the baseline and the edited flow are in different
29
+ * directories (each compile writes `<source>/` beside its output), or when merge
30
+ * compiled the side itself (`--baseline-from`, an edited `.flow.ts`), which keeps
31
+ * the sidecars in memory; what merge cannot carry is warned, and refused under
32
+ * `--strict`. See merge-sidecars.ts.
21
33
  */
22
34
  import { reportResult } from './cli-result.js';
23
35
  import { writeJsonFile } from './core/json-file.js';
24
- import { readFileSync } from 'node:fs';
25
- import { basename, resolve } from 'node:path';
26
- import { MergeRefusedError, mergeFlow } from './merge.js';
36
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
37
+ import { basename, dirname, join, resolve } from 'node:path';
38
+ import { MergeRefusedError, mergeFlowWithSidecars } from './merge.js';
39
+ import { sidecarSetFromList } from './merge-sidecars.js';
40
+ import { isWithin } from './path-within.js';
27
41
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
28
42
  import { ensureTypeScriptRuntime } from './node-runtime.js';
29
43
  import { flowCheckOpts, openCliLibrary } from './cli-library.js';
@@ -32,6 +46,38 @@ import { defaultBindingsFile } from './workdir.js';
32
46
  import { baselineFromOriginal, checkEdited, classifyDrafts, compileSourceDraftTolerant } from './merge-compile.js';
33
47
  const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
34
48
  + '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
49
+ /**
50
+ * The agent sidecars beside the flow at `flowPath`: `<source>/agent.json` and each
51
+ * `<source>/resources/<id>/resource.json`, parsed. An unreadable or invalid file
52
+ * reads as absent.
53
+ */
54
+ export function sidecarsBeside(flowPath) {
55
+ const dir = dirname(resolve(flowPath));
56
+ const read = (rel) => {
57
+ try {
58
+ return JSON.parse(readFileSync(join(dir, rel), 'utf8'));
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ };
64
+ return (source) => {
65
+ const agent = read(`${source}/agent.json`);
66
+ if (agent === undefined)
67
+ return undefined;
68
+ const files = { [`${source}/agent.json`]: agent };
69
+ const resourcesDir = join(dir, source, 'resources');
70
+ if (existsSync(resourcesDir)) {
71
+ for (const entry of readdirSync(resourcesDir)) {
72
+ const rel = `${source}/resources/${entry}/resource.json`;
73
+ const json = read(rel);
74
+ if (json !== undefined)
75
+ files[rel] = json;
76
+ }
77
+ }
78
+ return files;
79
+ };
80
+ }
35
81
  export async function run(argv) {
36
82
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
37
83
  console.error(USAGE);
@@ -62,6 +108,7 @@ export async function run(argv) {
62
108
  let drafts = [];
63
109
  let diagnostics = [];
64
110
  let edited;
111
+ let editedSidecars;
65
112
  if (editedIsSource) {
66
113
  // The edited source gets what `compile` gives it: check() with the same
67
114
  // options, and the serializer's warnings (CONNECTION_STUB, ...) printed.
@@ -70,14 +117,18 @@ export async function run(argv) {
70
117
  warn: (w) => console.error(` ⚠ [${w.code}] ${w.message}`),
71
118
  });
72
119
  edited = compiled.flow;
120
+ editedSidecars = compiled.sidecars;
73
121
  drafts = compiled.drafts;
74
122
  diagnostics = checkEdited(compiled.built, edited, flowCheckOpts(library, bindings));
75
123
  }
76
124
  else {
77
125
  edited = JSON.parse(readFileSync(editedPath, 'utf8'));
78
126
  }
79
- const baseline = baselineFrom
80
- ? (await baselineFromOriginal(baselineFrom, compileOpts, (m) => console.error(`flow-merge: decompile warning: ${m}`))).flow
127
+ const baselineCompile = baselineFrom
128
+ ? await baselineFromOriginal(baselineFrom, compileOpts, (m) => console.error(`flow-merge: decompile warning: ${m}`))
129
+ : undefined;
130
+ const baselineSidecars = baselineCompile?.sidecars;
131
+ const baseline = baselineCompile ? baselineCompile.flow
81
132
  : baselinePath ? JSON.parse(readFileSync(baselinePath, 'utf8')) : undefined;
82
133
  const strict = argv.includes('--strict');
83
134
  if (strict && !baseline) {
@@ -107,9 +158,25 @@ export async function run(argv) {
107
158
  console.error(`flow-merge: ${errors.length} error(s) in nodes merge takes from the edited flow; finish them or revert the edit. Nothing written.`);
108
159
  return 1;
109
160
  }
161
+ // Each compile writes `<source>/` beside its own output, so a baseline in the
162
+ // edited flow's directory had its sidecars overwritten by the edited compile.
163
+ // A side merge compiled itself (--baseline-from, an edited .flow.ts) carries
164
+ // its sidecars in memory instead.
165
+ const dirOf = (p) => dirname(resolve(p));
166
+ const baselineShared = baselinePath !== undefined && !editedIsSource && dirOf(baselinePath) === dirOf(editedPath);
167
+ const sources = {
168
+ original: sidecarsBeside(originalPath),
169
+ edited: editedSidecars ? sidecarSetFromList(editedSidecars) : sidecarsBeside(editedPath),
170
+ baseline: baselineSidecars ? sidecarSetFromList(baselineSidecars)
171
+ : baselinePath !== undefined && !baselineShared ? sidecarsBeside(baselinePath) : undefined,
172
+ baselineMissing: baselinePath === undefined
173
+ ? 'no --baseline or --baseline-from was given'
174
+ : `the baseline and the edited flow are in one directory (${dirOf(editedPath)}), so the edited compile `
175
+ + 'overwrote the baseline\'s agent sidecars',
176
+ };
110
177
  let merged;
111
178
  try {
112
- merged = mergeFlow(original, edited, {
179
+ merged = mergeFlowWithSidecars(original, edited, sources, {
113
180
  baseline,
114
181
  strict,
115
182
  warn: (m) => console.error(`flow-merge: warning: ${m}`),
@@ -122,9 +189,28 @@ export async function run(argv) {
122
189
  return 1;
123
190
  }
124
191
  const out = flagValue(argv, '-o') ?? 'merged.flow';
125
- writeJsonFile(out, merged);
126
- console.log(`flow-merge: wrote ${basename(out)} (${(merged.nodes ?? []).length} nodes, ${(merged.edges ?? []).length} edges)`);
127
- reportResult({ path: resolve(out), nodes: (merged.nodes ?? []).length, edges: (merged.edges ?? []).length });
192
+ const outDir = dirOf(out);
193
+ const targets = merged.sidecars.map((s) => ({ sidecar: s, target: resolve(outDir, s.path) }));
194
+ const escaping = targets.find(({ target }) => !isWithin(outDir, target));
195
+ if (escaping) {
196
+ console.error(`flow-merge: refusing to write a sidecar outside the merged flow's directory: ${escaping.sidecar.path}`);
197
+ return 1;
198
+ }
199
+ const flow = merged.flow;
200
+ mkdirSync(outDir, { recursive: true });
201
+ writeJsonFile(out, flow);
202
+ console.log(`flow-merge: wrote ${basename(out)} (${(flow.nodes ?? []).length} nodes, ${(flow.edges ?? []).length} edges)`);
203
+ for (const { sidecar, target } of targets) {
204
+ mkdirSync(dirname(target), { recursive: true });
205
+ writeJsonFile(target, sidecar.json);
206
+ console.log(`flow-merge: wrote ${sidecar.path} (carries ${sidecar.carried.join(', ')})`);
207
+ }
208
+ reportResult({
209
+ path: resolve(out),
210
+ nodes: (flow.nodes ?? []).length,
211
+ edges: (flow.edges ?? []).length,
212
+ ...(targets.length > 0 ? { sidecars: targets.map((t) => t.target) } : {}),
213
+ });
128
214
  return 0;
129
215
  }
130
216
  runWhenInvokedDirectly(import.meta.url, 'flow-merge', run);
@@ -1,10 +1,17 @@
1
1
  import { type DraftFinding, type SerializeOpts } from './serialize.js';
2
2
  import { type CheckOpts, type Diagnostic } from './check.js';
3
3
  import type { BuiltFlow } from './flow-sdk.js';
4
- /** A compile's flow and the draft refusals it reported instead of throwing. */
4
+ /**
5
+ * A compile's flow, the draft refusals it reported instead of throwing, and the
6
+ * agent sidecars (`<source>/agent.json`, resource files) it produced, none written.
7
+ */
5
8
  export interface DraftTolerantCompile {
6
9
  flow: any;
7
10
  drafts: DraftFinding[];
11
+ sidecars: Array<{
12
+ path: string;
13
+ json: unknown;
14
+ }>;
8
15
  }
9
16
  /** Compile `built` with draft tolerance. Every other refusal still throws. */
10
17
  export declare function compileDraftTolerant(built: BuiltFlow, opts?: Omit<SerializeOpts, 'draft'>): DraftTolerantCompile;
@@ -18,8 +18,9 @@ import { PACKAGE_NAME } from './package-name.js';
18
18
  /** Compile `built` with draft tolerance. Every other refusal still throws. */
19
19
  export function compileDraftTolerant(built, opts = {}) {
20
20
  const drafts = [];
21
- const flow = serialize(built, { ...opts, draft: (f) => drafts.push(f) });
22
- return { flow, drafts };
21
+ const sidecars = [];
22
+ const flow = serialize(built, { ...opts, draft: (f) => drafts.push(f), sidecars });
23
+ return { flow, drafts, sidecars };
23
24
  }
24
25
  /**
25
26
  * Load a `.flow.ts` source and compile it with draft tolerance.
@@ -33,7 +33,7 @@ interface RtFlow {
33
33
  /** One difference, or one inconsistency in the merged file. */
34
34
  export interface MergeFinding {
35
35
  /** Which comparison found it. */
36
- kind: 'node-removed' | 'node-added' | 'node-type' | 'implicit-end' | 'edge-handle-renamed' | 'edge-rewired' | 'edge-removed' | 'edge-added' | 'handle-ids' | 'binding-removed' | 'binding-added' | 'undeclared-handle' | 'duplicate-binding';
36
+ kind: 'node-removed' | 'node-added' | 'node-type' | 'implicit-end' | 'edge-handle-renamed' | 'edge-rewired' | 'edge-removed' | 'edge-added' | 'handle-ids' | 'binding-removed' | 'binding-added' | 'undeclared-handle' | 'duplicate-binding' | 'agent-sidecar';
37
37
  /** The finding, phrased for the author, naming the node, edge or row. */
38
38
  message: string;
39
39
  /**
@@ -373,7 +373,8 @@ export class MergeRefusedError extends Error {
373
373
  /** Every refusing finding, in report order. */
374
374
  findings;
375
375
  constructor(findings) {
376
- super(`refused (--strict): the merged file would differ from the original where the author made no edit:\n`
376
+ super(`refused (--strict): the merged file would differ from the original where the author made no edit, `
377
+ + `or would leave out an edit the author made:\n`
377
378
  + findings.map((f) => ` - ${f.message}`).join('\n'));
378
379
  this.name = 'MergeRefusedError';
379
380
  this.findings = findings;
@@ -0,0 +1,71 @@
1
+ import type { MergeFinding } from './merge-roundtrip.js';
2
+ /**
3
+ * The sidecar files of one side of the merge, per agent `source`: every file
4
+ * under `<source>/` that merge understands, keyed by its path relative to the
5
+ * flow (`<source>/agent.json`, `<source>/resources/<id>/resource.json`), parsed.
6
+ * `undefined` when that side has no `<source>/agent.json`.
7
+ */
8
+ export type SidecarSet = (source: string) => Record<string, unknown> | undefined;
9
+ /** The three sides' sidecars, as {@link mergeAgentSidecars} reads them. */
10
+ export interface MergeSidecarSources {
11
+ original: SidecarSet;
12
+ edited: SidecarSet;
13
+ /**
14
+ * The baseline compile's sidecars. Leave it out when they are not known, and
15
+ * say why in {@link baselineMissing}: merge then cannot tell an edit from the
16
+ * round trip's own changes, and reports instead of carrying.
17
+ */
18
+ baseline?: SidecarSet;
19
+ /** Why {@link baseline} is absent, phrased to finish "merge cannot tell an edit from the round trip: …". */
20
+ baselineMissing?: string;
21
+ }
22
+ /** One file the merged flow needs written beside it, relative to it. */
23
+ export interface SidecarWrite {
24
+ path: string;
25
+ json: unknown;
26
+ /** What it carries, for the report: the changed `agent.json` paths, or `new agent`. */
27
+ carried: string[];
28
+ }
29
+ /** The `{ path, json }` list `serializeWithSidecars` returns, as a {@link SidecarSet}. */
30
+ export declare function sidecarSetFromList(list: ReadonlyArray<{
31
+ path: string;
32
+ json: unknown;
33
+ }>): SidecarSet;
34
+ interface AnyNode {
35
+ id: string;
36
+ inputs?: Record<string, unknown>;
37
+ }
38
+ /**
39
+ * The agent `source` directories a flow's nodes point at, root and subflows,
40
+ * each with the first node that does. A `source` is a single directory name
41
+ * (the agent's uuid); anything else (an expression, a path) is not a sidecar
42
+ * directory and is skipped.
43
+ */
44
+ export declare function agentSources(flow: {
45
+ nodes?: AnyNode[];
46
+ subflows?: Record<string, unknown>;
47
+ } | undefined): Map<string, AnyNode>;
48
+ /**
49
+ * Three-way merge of one JSON value: `original` with the change from `baseline`
50
+ * to `edited` applied. Objects merge key by key; arrays merge index by index
51
+ * when all three have the same length (a prompt edit changes `messages[0]` and
52
+ * leaves `messages[1]` as the original has it), and are replaced whole otherwise.
53
+ * Every replaced path is pushed to `changed`.
54
+ *
55
+ * For example original `{a: 1, b: 2}`, baseline `{a: 1}`, edited `{a: 3}` gives
56
+ * `{a: 3, b: 2}` and `changed = ["a"]`.
57
+ */
58
+ export declare function mergeJson3(original: unknown, baseline: unknown, edited: unknown, changed: string[], path?: string): unknown;
59
+ /**
60
+ * The sidecar files the merged flow needs written beside it, and what merge could
61
+ * not carry. `merged` is `mergeFlow`'s result. An agent is new when the
62
+ * original side has no `<source>/agent.json` for it.
63
+ */
64
+ export declare function mergeAgentSidecars(merged: {
65
+ nodes?: AnyNode[];
66
+ subflows?: Record<string, unknown>;
67
+ }, sources: MergeSidecarSources): {
68
+ writes: SidecarWrite[];
69
+ findings: MergeFinding[];
70
+ };
71
+ export {};
@@ -0,0 +1,214 @@
1
+ /**
2
+ * merge-sidecars — carry an agent's sidecar edits through a brownfield merge (#1207).
3
+ *
4
+ * An inline agent is TWO artifacts: the node in the `.flow`, and the agent's own
5
+ * configuration in `<source>/agent.json` beside it (plus a
6
+ * `<source>/resources/<id>/resource.json` per attached resource). Since #927 the
7
+ * node is a shell: the prompts, the model and the run settings live ONLY in the
8
+ * sidecar. So an edit to a prompt leaves the agent node byte-equal in the baseline
9
+ * and the edited compile, `mergeFlow` keeps the original node (correctly: the node
10
+ * did not change), and the only artifact that changed is a file merge used not to
11
+ * read. The edit was lost without a word.
12
+ *
13
+ * This module is merge's view of those files. For every agent the merged flow
14
+ * keeps, it computes what the author changed in `agent.json` (baseline -> edited)
15
+ * and applies exactly that onto the ORIGINAL's `agent.json`:
16
+ *
17
+ * original { model: "gpt-5.4", maxIterations: 25, system: "Summarize..." }
18
+ * baseline { model: "gpt-5.4", system: "Summarize..." } <- round trip drops maxIterations
19
+ * edited { model: "gpt-5.4", system: "EDITED..." }
20
+ * merged { model: "gpt-5.4", maxIterations: 25, system: "EDITED..." }
21
+ *
22
+ * Taking `edited`'s file whole would carry the round trip's own changes too
23
+ * (`maxIterations` dropped above; on a canvas flow, a dropped `description`, a
24
+ * default model filled in). A delta carries the edit and nothing else, the same
25
+ * rule `mergeFlow` applies to nodes.
26
+ *
27
+ * What it cannot carry, it reports as a {@link MergeFinding} that `strict`
28
+ * refuses, so no sidecar edit can disappear silently:
29
+ *
30
+ * - no baseline sidecar to compare with (no `--baseline`, or the baseline and the
31
+ * edited compile were written to one directory, so the second overwrote the
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);
34
+ * - an `agent.json` edit on a node that carries its own prompts (an embedded
35
+ * node: decompile, like Studio Web, reads the node's prompts first, so the
36
+ * edited file would not be what runs).
37
+ *
38
+ * An agent the merged flow adds (no sidecar in the original) gets the edited
39
+ * compile's files whole.
40
+ *
41
+ * Pure: the caller reads the files ({@link SidecarSet}) and writes the result.
42
+ */
43
+ import { jsonClone } from './core/json-file.js';
44
+ /** The `{ path, json }` list `serializeWithSidecars` returns, as a {@link SidecarSet}. */
45
+ export function sidecarSetFromList(list) {
46
+ return (source) => {
47
+ const files = {};
48
+ for (const s of list)
49
+ if (s.path.startsWith(`${source}/`))
50
+ files[s.path] = s.json;
51
+ return files[`${source}/agent.json`] === undefined ? undefined : files;
52
+ };
53
+ }
54
+ /**
55
+ * The agent `source` directories a flow's nodes point at, root and subflows,
56
+ * each with the first node that does. A `source` is a single directory name
57
+ * (the agent's uuid); anything else (an expression, a path) is not a sidecar
58
+ * directory and is skipped.
59
+ */
60
+ export function agentSources(flow) {
61
+ const out = new Map();
62
+ const walk = (scope) => {
63
+ const s = scope;
64
+ for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
65
+ const src = n?.inputs?.source;
66
+ if (typeof src === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(src) && !out.has(src))
67
+ out.set(src, n);
68
+ }
69
+ if (s?.subflows && typeof s.subflows === 'object')
70
+ Object.values(s.subflows).forEach(walk);
71
+ };
72
+ walk(flow);
73
+ return out;
74
+ }
75
+ /** JSON with object keys sorted, so key order is not a difference. */
76
+ function stable(v) {
77
+ return JSON.stringify(v, (_k, val) => (val && typeof val === 'object' && !Array.isArray(val)
78
+ ? Object.fromEntries(Object.keys(val).sort().map((k) => [k, val[k]]))
79
+ : val));
80
+ }
81
+ const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
82
+ /**
83
+ * Three-way merge of one JSON value: `original` with the change from `baseline`
84
+ * to `edited` applied. Objects merge key by key; arrays merge index by index
85
+ * when all three have the same length (a prompt edit changes `messages[0]` and
86
+ * leaves `messages[1]` as the original has it), and are replaced whole otherwise.
87
+ * Every replaced path is pushed to `changed`.
88
+ *
89
+ * For example original `{a: 1, b: 2}`, baseline `{a: 1}`, edited `{a: 3}` gives
90
+ * `{a: 3, b: 2}` and `changed = ["a"]`.
91
+ */
92
+ export function mergeJson3(original, baseline, edited, changed, path = '') {
93
+ if (stable(baseline) === stable(edited))
94
+ return jsonClone(original);
95
+ if (isObject(original) && isObject(baseline) && isObject(edited)) {
96
+ const out = { ...jsonClone(original) };
97
+ for (const k of new Set([...Object.keys(baseline), ...Object.keys(edited)])) {
98
+ if (stable(baseline[k]) === stable(edited[k]))
99
+ continue;
100
+ const at = path ? `${path}.${k}` : k;
101
+ if (edited[k] === undefined) {
102
+ delete out[k];
103
+ changed.push(at);
104
+ }
105
+ else {
106
+ out[k] = mergeJson3(original[k], baseline[k], edited[k], changed, at);
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ if (Array.isArray(original) && Array.isArray(baseline) && Array.isArray(edited)
112
+ && original.length === baseline.length && baseline.length === edited.length) {
113
+ return original.map((o, i) => mergeJson3(o, baseline[i], edited[i], changed, `${path}[${i}]`));
114
+ }
115
+ changed.push(path || '(whole file)');
116
+ return jsonClone(edited);
117
+ }
118
+ /** A resource file's identity: its content without the `id` (and directory) the writer derives. */
119
+ function resourceIdentities(files) {
120
+ const out = new Map();
121
+ for (const [path, json] of Object.entries(files)) {
122
+ if (!/\/resources\/[^/]+\/resource\.json$/.test(path))
123
+ continue;
124
+ const { id: _id, ...rest } = (isObject(json) ? json : {});
125
+ out.set(stable(rest), path);
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * The resource files `to` adds or removes relative to `from`, by identity
131
+ * (content without `id`), as `added <path>; removed <path>`; `undefined` when
132
+ * the sets are equal.
133
+ */
134
+ function resourceChanges(from, to) {
135
+ const a = resourceIdentities(from);
136
+ const b = resourceIdentities(to);
137
+ const out = [
138
+ ...[...b.keys()].filter((k) => !a.has(k)).map((k) => `added ${b.get(k)}`),
139
+ ...[...a.keys()].filter((k) => !b.has(k)).map((k) => `removed ${a.get(k)}`),
140
+ ];
141
+ return out.length > 0 ? out.join('; ') : undefined;
142
+ }
143
+ /** Whether a node carries its prompts itself, so its `agent.json` is not what runs (see decompile's `sidecarAgentInputs`). */
144
+ function carriesOwnPrompts(node) {
145
+ const i = node?.inputs ?? {};
146
+ return typeof i.systemPrompt === 'string' || typeof i.userPrompt === 'string';
147
+ }
148
+ const finding = (message) => ({ kind: 'agent-sidecar', message, refuses: true });
149
+ /**
150
+ * The sidecar files the merged flow needs written beside it, and what merge could
151
+ * not carry. `merged` is `mergeFlow`'s result. An agent is new when the
152
+ * original side has no `<source>/agent.json` for it.
153
+ */
154
+ export function mergeAgentSidecars(merged, sources) {
155
+ const writes = [];
156
+ const findings = [];
157
+ for (const [source, node] of agentSources(merged)) {
158
+ const edited = sources.edited(source);
159
+ if (edited === undefined)
160
+ continue; // the edited compile wrote no sidecar here: nothing to carry
161
+ const orig = sources.original(source);
162
+ const agentPath = `${source}/agent.json`;
163
+ if (orig === undefined) {
164
+ // An agent the edit added (or one whose original sidecar is missing):
165
+ // the edited compile's files are the only ones there are.
166
+ for (const [path, json] of Object.entries(edited))
167
+ writes.push({ path, json: jsonClone(json), carried: ['new agent'] });
168
+ continue;
169
+ }
170
+ const base = sources.baseline?.(source);
171
+ if (base === undefined) {
172
+ if (stable(orig[agentPath]) !== stable(edited[agentPath])) {
173
+ const why = sources.baselineMissing
174
+ ?? `the baseline compile wrote no ${agentPath}`;
175
+ findings.push(finding(`agent "${node.id}": the edited ${agentPath} differs from the original's, and merge cannot tell an edit `
176
+ + `from the round trip's own changes: ${why}. The change is NOT carried; the original's ${agentPath} stays. `
177
+ + 'Compile the baseline and the edited source into two different directories and pass --baseline.'));
178
+ }
179
+ // A context or tool added or removed changes no agent.json, so compare the
180
+ // resource files too, against the original's this time.
181
+ const res = resourceChanges(orig, edited);
182
+ if (res) {
183
+ const why = sources.baselineMissing ?? 'the baseline compile wrote no sidecars';
184
+ findings.push(finding(`agent "${node.id}": the edited resource files differ from the original's (${res}), and merge cannot tell `
185
+ + `an edit from the round trip's own changes: ${why}. Merge does not carry resource files; the original's `
186
+ + 'stay as they are.'));
187
+ }
188
+ continue;
189
+ }
190
+ // agent.json: the author's delta, applied onto the original.
191
+ const changed = [];
192
+ const next = mergeJson3(orig[agentPath], base[agentPath], edited[agentPath], changed);
193
+ if (changed.length > 0) {
194
+ // `node` is the merged node: the original's when merge kept it, the
195
+ // edited shell when the author changed the node too.
196
+ if (carriesOwnPrompts(node)) {
197
+ findings.push(finding(`agent "${node.id}": its ${agentPath} was edited (${changed.join(', ')}), but the original node carries its `
198
+ + 'own prompts, which are read before agent.json, so the edit would not reach the agent. The change is NOT '
199
+ + 'carried. Make the change on the canvas, or save the agent with its prompts in agent.json only.'));
200
+ }
201
+ else if (stable(next) !== stable(orig[agentPath])) {
202
+ writes.push({ path: agentPath, json: next, carried: changed });
203
+ }
204
+ }
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.'));
211
+ }
212
+ }
213
+ return { writes, findings };
214
+ }
package/dist/merge.d.ts CHANGED
@@ -74,7 +74,9 @@
74
74
  * checked for an edge that leaves a handle its kept source node does not
75
75
  * declare. See merge-roundtrip.ts.
76
76
  */
77
+ import { type MergeSidecarSources, type SidecarWrite } from './merge-sidecars.js';
77
78
  export { MergeRefusedError, type MergeFinding } from './merge-roundtrip.js';
79
+ export type { MergeSidecarSources, SidecarSet, SidecarWrite } from './merge-sidecars.js';
78
80
  interface AnyNode {
79
81
  id: string;
80
82
  type: string;
@@ -143,8 +145,9 @@ export interface MergeOptions {
143
145
  warn?: (message: string) => void;
144
146
  /**
145
147
  * Refuse instead of warn: when the merged file differs from the original
146
- * where the author made no edit, throw {@link MergeRefusedError} listing every
147
- * such finding, and return nothing. The other findings still go to `warn`.
148
+ * where the author made no edit, or (with sidecars, `mergeFlowWithSidecars`)
149
+ * would leave out an agent sidecar edit, throw {@link MergeRefusedError}
150
+ * listing every such finding, and return nothing. The other findings still go to `warn`.
148
151
  * Two findings never refuse, because the merged file does what the original
149
152
  * does: an End the compile appends to a tail the original leaves open, and a
150
153
  * binding row the round trip drops (merge keeps the original's). Meant to be
@@ -162,3 +165,15 @@ export interface MergeOptions {
162
165
  */
163
166
  export declare function nodesTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
164
167
  export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?: MergeOptions): AnyFlow;
168
+ /**
169
+ * `mergeFlow`, plus the agent sidecar files the merged flow needs written beside
170
+ * it (#1207). An inline agent's prompts and settings live only in
171
+ * `<source>/agent.json`, so an edit to them changes no node; this carries the
172
+ * author's change to that file onto the original's copy. What it cannot carry
173
+ * becomes a finding: warned, and refused under `strict`, with the flow's own
174
+ * findings. See merge-sidecars.ts.
175
+ */
176
+ export declare function mergeFlowWithSidecars(original: AnyFlow, edited: AnyFlow, sidecars: MergeSidecarSources, options?: MergeOptions): {
177
+ flow: AnyFlow;
178
+ sidecars: SidecarWrite[];
179
+ };
package/dist/merge.js CHANGED
@@ -79,6 +79,7 @@ import { jsonClone } from './core/json-file.js';
79
79
  import { DEFINITIONLESS_NODE_TYPES } from './core/definitionless-node-types.js';
80
80
  import { MergeRefusedError, mergedFileFindings, roundTripFindings } from './merge-roundtrip.js';
81
81
  import { placeAddedNodes } from './node-placement.js';
82
+ import { mergeAgentSidecars } from './merge-sidecars.js';
82
83
  export { MergeRefusedError } from './merge-roundtrip.js';
83
84
  function byId(nodes) {
84
85
  return new Map((nodes ?? []).map((n) => [n.id, n]));
@@ -145,6 +146,34 @@ function layoutNodesOf(f) {
145
146
  return nodes && typeof nodes === 'object' && !Array.isArray(nodes) ? nodes : undefined;
146
147
  }
147
148
  export function mergeFlow(original, edited, options = {}) {
149
+ const { result, findings } = mergeFlowFindings(original, edited, options);
150
+ reportOrRefuse(findings, options);
151
+ return result;
152
+ }
153
+ /**
154
+ * `mergeFlow`, plus the agent sidecar files the merged flow needs written beside
155
+ * it (#1207). An inline agent's prompts and settings live only in
156
+ * `<source>/agent.json`, so an edit to them changes no node; this carries the
157
+ * author's change to that file onto the original's copy. What it cannot carry
158
+ * becomes a finding: warned, and refused under `strict`, with the flow's own
159
+ * findings. See merge-sidecars.ts.
160
+ */
161
+ export function mergeFlowWithSidecars(original, edited, sidecars, options = {}) {
162
+ const { result, findings } = mergeFlowFindings(original, edited, options);
163
+ const carried = mergeAgentSidecars(result, sidecars);
164
+ reportOrRefuse([...findings, ...carried.findings], options);
165
+ return { flow: result, sidecars: carried.writes };
166
+ }
167
+ /** Warn every finding; under `strict`, throw over the refusing ones (and still warn the rest). */
168
+ function reportOrRefuse(findings, options) {
169
+ const refusing = options.strict ? findings.filter((f) => f.refuses) : [];
170
+ for (const f of findings)
171
+ if (!refusing.includes(f))
172
+ options.warn?.(f.message);
173
+ if (refusing.length > 0)
174
+ throw new MergeRefusedError(refusing);
175
+ }
176
+ function mergeFlowFindings(original, edited, options) {
148
177
  const result = jsonClone(original);
149
178
  const origNodes = byId(original.nodes);
150
179
  const baseNodes = byId(options.baseline?.nodes);
@@ -219,19 +248,12 @@ export function mergeFlow(original, edited, options = {}) {
219
248
  result.version = edited.version;
220
249
  }
221
250
  // ── What the no-edit round trip changed, and what mixing sources broke. ──
222
- // Under strict, the findings that do not refuse are still warned, so a
223
- // refused run shows them too.
251
+ // The caller warns them, or refuses under strict (`reportOrRefuse`).
224
252
  const findings = [
225
253
  ...(options.baseline ? roundTripFindings(original, options.baseline, result) : []),
226
254
  ...mergedFileFindings(result, original),
227
255
  ];
228
- const refusing = options.strict ? findings.filter((f) => f.refuses) : [];
229
- for (const f of findings)
230
- if (!refusing.includes(f))
231
- options.warn?.(f.message);
232
- if (refusing.length > 0)
233
- throw new MergeRefusedError(refusing);
234
- return result;
256
+ return { result, findings };
235
257
  }
236
258
  /**
237
259
  * The root edges with their edited ids, except that an edge with no id, or with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.30.0",
3
+ "version": "6.30.1",
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": "5b38f3fae80cbcb874c6d92afb49690b77656caf"
95
+ "gitref": "07dd8e31058cf19c3db3a504b6e2bbb56cf980d2"
96
96
  }