@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.
- package/dist/bindings-v2.d.ts +19 -0
- package/dist/bindings-v2.js +29 -0
- package/dist/compile-cli.js +6 -55
- package/dist/core/context-index.d.ts +18 -0
- package/dist/core/context-index.js +19 -0
- package/dist/decompile-cli.js +2 -1
- package/dist/decompile.d.ts +5 -3
- package/dist/decompile.js +145 -12
- package/dist/merge-cli.js +97 -0
- package/dist/merge-sidecars.d.ts +12 -1
- package/dist/merge-sidecars.js +139 -9
- package/dist/merge.d.ts +1 -0
- package/dist/merge.js +37 -3
- package/dist/project-resource-users.d.ts +9 -0
- package/dist/project-resource-users.js +47 -0
- package/dist/serialize.js +8 -19
- package/package.json +2 -2
package/dist/bindings-v2.d.ts
CHANGED
|
@@ -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;
|
package/dist/bindings-v2.js
CHANGED
|
@@ -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;
|
package/dist/compile-cli.js
CHANGED
|
@@ -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,
|
|
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
|
|
229
|
+
const agentFiles = agentDirs
|
|
229
230
|
.map((dir) => readInlineAgentFiles(dir))
|
|
230
|
-
.filter((files) => files !== undefined)
|
|
231
|
-
|
|
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
|
|
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
|
+
};
|
package/dist/decompile-cli.js
CHANGED
|
@@ -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
|
}
|
package/dist/decompile.d.ts
CHANGED
|
@@ -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
|
|
148
|
-
* folder
|
|
149
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/merge-sidecars.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/merge-sidecars.js
CHANGED
|
@@ -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
|
|
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:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 ??
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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.
|
|
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": "
|
|
95
|
+
"gitref": "09ce362aa97a2af0513e0c5883ccaa032df52a4a"
|
|
96
96
|
}
|