@goodandready/dsh-image-gen 0.11.40 → 0.11.42

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/lib/lineage.js ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Image Lineage & Version Branching (#439)
3
+ * Traverses parent-child relationships across revisions, provides DAG queries,
4
+ * branching support, and sanitized parameter replay payloads.
5
+ */
6
+
7
+ /**
8
+ * Sanitizes parameters to remove credentials and internal server paths.
9
+ */
10
+ export function sanitizeLineageParams(params = {}) {
11
+ if (!params || typeof params !== 'object') return {}
12
+ const sanitized = {}
13
+ for (const [key, val] of Object.entries(params)) {
14
+ const lowerKey = key.toLowerCase()
15
+ // Exclude API keys and secrets
16
+ if (
17
+ lowerKey.includes('key') ||
18
+ lowerKey.includes('secret') ||
19
+ lowerKey.includes('token') ||
20
+ lowerKey.includes('auth') ||
21
+ lowerKey.includes('password')
22
+ ) {
23
+ continue
24
+ }
25
+
26
+ if (typeof val === 'string') {
27
+ // Clean host absolute paths (e.g. /mnt/external/... -> filename)
28
+ if (val.startsWith('/mnt/') || val.startsWith('/home/') || val.includes(':\\')) {
29
+ sanitized[key] = val.split(/[/\\]/).pop()
30
+ } else {
31
+ sanitized[key] = val
32
+ }
33
+ } else if (typeof val === 'object' && val !== null) {
34
+ sanitized[key] = sanitizeLineageParams(val)
35
+ } else {
36
+ sanitized[key] = val
37
+ }
38
+ }
39
+ return sanitized
40
+ }
41
+
42
+ /**
43
+ * Normalizes history entry into standard lineage node.
44
+ */
45
+ export function normalizeLineageNode(entry, idx = 0) {
46
+ if (!entry) return null
47
+ const revisionId = entry.revisionId || entry.id || entry.attachmentId || `rev_${entry.timestamp || idx}`
48
+ const parentId = entry.parentId || entry.parentImageId || null
49
+ const branchId = entry.branchId || 'main'
50
+ const operation = entry.operation || (parentId ? 'edit' : 'generate')
51
+
52
+ return {
53
+ revisionId: String(revisionId),
54
+ parentId: parentId ? String(parentId) : null,
55
+ branchId: String(branchId),
56
+ operation,
57
+ timestamp: entry.timestamp || new Date().toISOString(),
58
+ prompt: entry.prompt || '',
59
+ seed: entry.seed !== undefined ? entry.seed : null,
60
+ provider: entry.provider || 'unknown',
61
+ model: entry.model || 'unknown',
62
+ path: entry.path || entry.url || '',
63
+ appliedProfile: entry.appliedProfile || null,
64
+ params: sanitizeLineageParams(entry.params || {
65
+ size: entry.size || entry.image_size,
66
+ aspectRatio: entry.aspectRatio || entry.aspect_ratio,
67
+ format: entry.format,
68
+ }),
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Builds Lineage DAG map from array of history entries.
74
+ */
75
+ export function buildLineageDAG(historyEntries = []) {
76
+ const nodes = new Map()
77
+ const childrenMap = new Map()
78
+
79
+ for (let i = 0; i < historyEntries.length; i++) {
80
+ const node = normalizeLineageNode(historyEntries[i], i)
81
+ if (node) {
82
+ nodes.set(node.revisionId, node)
83
+ // Also index by path or attachmentId for backward compatibility
84
+ if (historyEntries[i].path) {
85
+ nodes.set(historyEntries[i].path, node)
86
+ }
87
+ if (historyEntries[i].attachmentId) {
88
+ nodes.set(historyEntries[i].attachmentId, node)
89
+ }
90
+ }
91
+ }
92
+
93
+ // Build children relationships
94
+ for (const node of new Set(nodes.values())) {
95
+ if (node.parentId) {
96
+ const parent = nodes.get(node.parentId)
97
+ const parentKey = parent ? parent.revisionId : node.parentId
98
+ if (!childrenMap.has(parentKey)) {
99
+ childrenMap.set(parentKey, [])
100
+ }
101
+ childrenMap.get(parentKey).push(node)
102
+ }
103
+ }
104
+
105
+ return { nodes, childrenMap }
106
+ }
107
+
108
+ /**
109
+ * Retrieves the full ancestry chain and child branches for a revision.
110
+ */
111
+ export function getImageLineage(targetId, historyEntries = []) {
112
+ if (!targetId) return null
113
+ const { nodes, childrenMap } = buildLineageDAG(historyEntries)
114
+
115
+ const targetNode = nodes.get(String(targetId))
116
+ if (!targetNode) return null
117
+
118
+ // 1. Ancestry chain (upward traversal from target to root)
119
+ const ancestry = []
120
+ let curr = targetNode
121
+ const visited = new Set()
122
+
123
+ while (curr && curr.parentId && !visited.has(curr.revisionId)) {
124
+ visited.add(curr.revisionId)
125
+ const parent = nodes.get(curr.parentId)
126
+ if (parent) {
127
+ ancestry.unshift(parent)
128
+ curr = parent
129
+ } else {
130
+ // Parent exists outside current history limit
131
+ ancestry.unshift({
132
+ revisionId: curr.parentId,
133
+ parentId: null,
134
+ branchId: 'unknown',
135
+ operation: 'unknown',
136
+ prompt: '[Historical ancestor not in current window]',
137
+ missing: true,
138
+ })
139
+ break
140
+ }
141
+ }
142
+
143
+ // 2. Direct children and branches
144
+ const children = childrenMap.get(targetNode.revisionId) || []
145
+ const branches = [...new Set(children.map((c) => c.branchId))]
146
+
147
+ return {
148
+ target: targetNode,
149
+ ancestry,
150
+ children,
151
+ branches,
152
+ depth: ancestry.length,
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Produces a sanitized replay payload for re-running generation from a specific revision.
158
+ */
159
+ export function getReplayPayload(revisionId, historyEntries = []) {
160
+ const lineage = getImageLineage(revisionId, historyEntries)
161
+ if (!lineage) {
162
+ return { ok: false, error: `Revision "${revisionId}" not found in history.` }
163
+ }
164
+
165
+ const node = lineage.target
166
+ const toolName = node.operation === 'edit' ? 'edit_image' : 'generate_image'
167
+
168
+ const replayParams = {
169
+ prompt: node.prompt,
170
+ ...(node.seed !== null ? { seed: node.seed } : {}),
171
+ ...node.params,
172
+ }
173
+
174
+ if (node.parentId && node.operation === 'edit') {
175
+ replayParams.image = node.parentId
176
+ }
177
+
178
+ return {
179
+ ok: true,
180
+ revisionId: node.revisionId,
181
+ tool: toolName,
182
+ provider: node.provider,
183
+ model: node.model,
184
+ params: replayParams,
185
+ reproducibilityNotes:
186
+ 'Cloud models may exhibit minor pixel variations across runs with identical seed; ComfyUI/local models provide deterministic outputs when models and workflow match.',
187
+ }
188
+ }
@@ -22,6 +22,10 @@ import { registerDiagramTools } from './tools/diagram.js'
22
22
  import { registerRecipeTools } from './tools/recipes.js'
23
23
  import { registerCapabilityTools } from './tools/capabilities.js'
24
24
  import { registerProjectProfileTools } from './tools/project-profiles.js'
25
+ import { registerInpaintStagingTools } from './tools/inpaint-staging.js'
26
+ import { registerBatchQueueTools } from './tools/batch-queue.js'
27
+ import { registerVariantTools } from './tools/variant-selection.js'
28
+ import { registerLineageTools } from './tools/lineage.js'
25
29
  import {
26
30
  createLiveProgressSession,
27
31
  publishLiveProgress,
@@ -157,6 +161,18 @@ export function registerAllTools(ctx, deps) {
157
161
  if (Boolean(sets.profiles)) {
158
162
  registerProjectProfileTools(targetCtx, deps)
159
163
  }
164
+ if (Boolean(sets.staging)) {
165
+ registerInpaintStagingTools(targetCtx, deps)
166
+ }
167
+ if (Boolean(sets.queue)) {
168
+ registerBatchQueueTools(targetCtx, deps)
169
+ }
170
+ if (Boolean(sets.variants)) {
171
+ registerVariantTools(targetCtx, deps)
172
+ }
173
+ if (Boolean(sets.lineage)) {
174
+ registerLineageTools(targetCtx, deps)
175
+ }
160
176
  }
161
177
 
162
178
  function dispose() {
@@ -0,0 +1,134 @@
1
+ import {
2
+ enqueueBatch,
3
+ getBatchQueue,
4
+ getBatchProgress,
5
+ cancelBatch,
6
+ retryFailedBatchItems,
7
+ } from '../batch-queue.js'
8
+
9
+ /**
10
+ * Registers batch queue management tools (#437).
11
+ */
12
+ export function registerBatchQueueTools(ctx, deps = {}) {
13
+ ctx.tools.register({
14
+ name: 'enqueue_batch_jobs',
15
+ description: 'Enqueues a series of generation or processing jobs with budget pre-flight check and concurrency control.',
16
+ parameters: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ properties: {
20
+ name: {
21
+ type: 'string',
22
+ description: 'Descriptive title for the batch.',
23
+ },
24
+ items: {
25
+ type: 'array',
26
+ items: {
27
+ type: 'object',
28
+ additionalProperties: true,
29
+ properties: {
30
+ tool: { type: 'string' },
31
+ params: { type: 'object', additionalProperties: true },
32
+ estimatedCost: { type: 'number' },
33
+ },
34
+ },
35
+ description: 'Array of jobs to execute.',
36
+ },
37
+ concurrency: {
38
+ type: 'number',
39
+ description: 'Maximum concurrent active workers (default 2, max 10).',
40
+ },
41
+ maxBudgetUSD: {
42
+ type: 'number',
43
+ description: 'Maximum total allowed budget in USD for this entire batch.',
44
+ },
45
+ },
46
+ required: ['items'],
47
+ },
48
+ output: {
49
+ render(args, value) {
50
+ return [
51
+ {
52
+ type: 'text',
53
+ text: `Enqueued Batch: [${value?.batchId}] "${value?.name}"\n` +
54
+ `Total Jobs: ${value?.items?.length} | Estimated Cost: $${value?.totalEstimatedCost?.toFixed(3)}\n` +
55
+ `Status: ${value?.status} | Concurrency: ${value?.concurrency}`,
56
+ },
57
+ ]
58
+ },
59
+ },
60
+ async execute(args) {
61
+ const storagePath = deps.queueStoragePath || null
62
+ return enqueueBatch(
63
+ {
64
+ name: args.name,
65
+ items: args.items,
66
+ concurrency: args.concurrency || 2,
67
+ maxBudget: args.maxBudgetUSD || Infinity,
68
+ },
69
+ storagePath
70
+ )
71
+ },
72
+ })
73
+
74
+ ctx.tools.register({
75
+ name: 'manage_batch_queue',
76
+ description: 'Manages batch queues: status inquiry, cancellation, or retrying failed items.',
77
+ parameters: {
78
+ type: 'object',
79
+ additionalProperties: false,
80
+ properties: {
81
+ batchId: {
82
+ type: 'string',
83
+ description: 'Target batch queue identifier.',
84
+ },
85
+ action: {
86
+ type: 'string',
87
+ enum: ['status', 'cancel', 'retry_failed'],
88
+ description: 'Action to perform: check status, cancel, or retry failed items.',
89
+ },
90
+ itemId: {
91
+ type: 'string',
92
+ description: 'Optional single item ID to cancel.',
93
+ },
94
+ },
95
+ required: ['batchId', 'action'],
96
+ },
97
+ output: {
98
+ render(args, value) {
99
+ if (args.action === 'status') {
100
+ const p = value?.progress || {}
101
+ return [
102
+ {
103
+ type: 'text',
104
+ text: `Batch [${args.batchId}] Status: ${value?.status} (${p.percent}%)\n` +
105
+ `Total: ${p.total} | Completed: ${p.completed} | Failed: ${p.failed} | Queued: ${p.queued} | Running: ${p.running} | Interrupted: ${p.interrupted || 0}`,
106
+ },
107
+ ]
108
+ }
109
+ if (args.action === 'cancel') {
110
+ return [{ type: 'text', text: `Cancelled batch [${args.batchId}]. Queued jobs discarded.` }]
111
+ }
112
+ if (args.action === 'retry_failed') {
113
+ return [{ type: 'text', text: `Reset ${value?.resetCount || 0} failed items for retry in batch [${args.batchId}].` }]
114
+ }
115
+ return [{ type: 'text', text: JSON.stringify(value) }]
116
+ },
117
+ },
118
+ async execute(args) {
119
+ const storagePath = deps.queueStoragePath || null
120
+ if (args.action === 'status') {
121
+ const batch = getBatchQueue(args.batchId, storagePath)
122
+ if (!batch) return { ok: false, error: `Batch "${args.batchId}" not found` }
123
+ return { ok: true, batchId: batch.batchId, status: batch.status, progress: getBatchProgress(batch), items: batch.items }
124
+ }
125
+ if (args.action === 'cancel') {
126
+ return cancelBatch(args.batchId, args.itemId, storagePath)
127
+ }
128
+ if (args.action === 'retry_failed') {
129
+ return retryFailedBatchItems(args.batchId, args.itemId ? [args.itemId] : null, storagePath)
130
+ }
131
+ throw new Error(`Unknown action: ${args.action}`)
132
+ },
133
+ })
134
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ createStagingSession,
3
+ reviewStagedEdit,
4
+ getStagingSession,
5
+ } from '../inpaint-staging.js'
6
+
7
+ /**
8
+ * Registers staging & review workflow tools (#436).
9
+ */
10
+ export function registerInpaintStagingTools(ctx, deps = {}) {
11
+ ctx.tools.register({
12
+ name: 'stage_image_edit',
13
+ description: 'Stages a localized inpaint or image modification in preview state for user review before committing.',
14
+ parameters: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ image: {
19
+ type: 'string',
20
+ description: 'Original image path or URL to edit.',
21
+ },
22
+ prompt: {
23
+ type: 'string',
24
+ description: 'Edit instruction or prompt for the localized modification.',
25
+ },
26
+ mask: {
27
+ type: 'string',
28
+ description: 'Optional inpainting mask path, base64, or SVG string.',
29
+ },
30
+ bbox: {
31
+ type: 'array',
32
+ items: { type: 'number' },
33
+ description: 'Optional bounding box [x1, y1, x2, y2] (normalized 0-1000).',
34
+ },
35
+ previewImage: {
36
+ type: 'string',
37
+ description: 'Initial preview image path if already produced.',
38
+ },
39
+ },
40
+ required: ['image', 'prompt'],
41
+ },
42
+ output: {
43
+ render(args, value) {
44
+ return [
45
+ {
46
+ type: 'text',
47
+ text: `Staging Session Created: [${value?.stageId}]\n` +
48
+ `Status: ${value?.status} (preview only, original preserved)\n` +
49
+ `Instruction: "${value?.prompt}"\n` +
50
+ `Preview: ${value?.previewImage || 'Generating in background...'}`,
51
+ },
52
+ ]
53
+ },
54
+ },
55
+ async execute(args) {
56
+ const storagePath = deps.stagingStoragePath || null
57
+ return createStagingSession(
58
+ {
59
+ originalImage: args.image,
60
+ prompt: args.prompt,
61
+ mask: args.mask,
62
+ bbox: args.bbox,
63
+ previewImage: args.previewImage,
64
+ provider: deps.cfg?.provider || 'fal',
65
+ },
66
+ storagePath
67
+ )
68
+ },
69
+ })
70
+
71
+ ctx.tools.register({
72
+ name: 'review_staged_edit',
73
+ description: 'Reviews a staged image edit: accept (commits change), reject (restores original), retry (new prompt), or cancel.',
74
+ parameters: {
75
+ type: 'object',
76
+ additionalProperties: false,
77
+ properties: {
78
+ stageId: {
79
+ type: 'string',
80
+ description: 'ID of the staging session to review.',
81
+ },
82
+ action: {
83
+ type: 'string',
84
+ enum: ['accept', 'reject', 'retry', 'cancel'],
85
+ description: 'Review decision: accept, reject, retry, or cancel.',
86
+ },
87
+ prompt: {
88
+ type: 'string',
89
+ description: 'New instruction if retrying with updated prompt.',
90
+ },
91
+ reason: {
92
+ type: 'string',
93
+ description: 'Optional reason when rejecting or cancelling.',
94
+ },
95
+ },
96
+ required: ['stageId', 'action'],
97
+ },
98
+ output: {
99
+ render(args, value) {
100
+ if (!value?.ok) {
101
+ return [{ type: 'text', text: `Review failed: ${value?.error || 'Unknown error'}` }]
102
+ }
103
+ return [
104
+ {
105
+ type: 'text',
106
+ text: `Review Result: ${value.message}\nStatus: ${value.status}\nStage ID: ${args.stageId}`,
107
+ },
108
+ ]
109
+ },
110
+ },
111
+ async execute(args) {
112
+ const storagePath = deps.stagingStoragePath || null
113
+ return reviewStagedEdit(args.stageId, args.action, { prompt: args.prompt, reason: args.reason }, storagePath)
114
+ },
115
+ })
116
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ getImageLineage,
3
+ getReplayPayload,
4
+ } from '../lineage.js'
5
+ import { readHistory } from '../history.js'
6
+
7
+ /**
8
+ * Registers lineage inspection and replay tools (#439).
9
+ */
10
+ export function registerLineageTools(ctx, deps = {}) {
11
+ ctx.tools.register({
12
+ name: 'inspect_image_lineage',
13
+ description: 'Inspects generation and edit history ancestry DAG for an image revision, showing parents, branches, and prompts.',
14
+ parameters: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ revisionId: {
19
+ type: 'string',
20
+ description: 'Revision ID, attachment ID, or image file path to inspect.',
21
+ },
22
+ },
23
+ required: ['revisionId'],
24
+ },
25
+ output: {
26
+ render(args, value) {
27
+ if (!value?.target) return [{ type: 'text', text: `No lineage found for "${args.revisionId}".` }]
28
+ const t = value.target
29
+ const ancestryLines = (value.ancestry || []).map((a, i) => ` [Parent ${i + 1}] (${a.operation}) ${a.prompt || a.revisionId}`).join('\n')
30
+ const childrenLines = (value.children || []).map((c) => ` [Child] (${c.operation}) ${c.prompt || c.revisionId}`).join('\n')
31
+
32
+ return [
33
+ {
34
+ type: 'text',
35
+ text: `Lineage for Revision: [${t.revisionId}]\n` +
36
+ `Branch: ${t.branchId} | Operation: ${t.operation}\n` +
37
+ `Prompt: "${t.prompt}"\n` +
38
+ (ancestryLines ? `Ancestry (${value.depth}):\n${ancestryLines}\n` : 'Root generation (no parents)\n') +
39
+ (childrenLines ? `Children:\n${childrenLines}` : 'Leaf revision (no children yet)'),
40
+ },
41
+ ]
42
+ },
43
+ },
44
+ async execute(args) {
45
+ const entries = typeof deps.readHistory === 'function' ? await deps.readHistory() : await readHistory()
46
+ return getImageLineage(args.revisionId, entries)
47
+ },
48
+ })
49
+
50
+ ctx.tools.register({
51
+ name: 'get_image_replay_payload',
52
+ description: 'Extracts sanitized parameter payload from a past image revision to replay or branch generation without credential leaks.',
53
+ parameters: {
54
+ type: 'object',
55
+ additionalProperties: false,
56
+ properties: {
57
+ revisionId: {
58
+ type: 'string',
59
+ description: 'Revision ID to extract replay configuration from.',
60
+ },
61
+ },
62
+ required: ['revisionId'],
63
+ },
64
+ output: {
65
+ render(args, value) {
66
+ if (!value?.ok) return [{ type: 'text', text: `Replay extraction failed: ${value?.error}` }]
67
+ return [
68
+ {
69
+ type: 'text',
70
+ text: `Replay Payload for [${value.revisionId}]:\n` +
71
+ `Tool: ${value.tool} (${value.provider}/${value.model})\n` +
72
+ `Parameters: ${JSON.stringify(value.params, null, 2)}\n` +
73
+ `Notes: ${value.reproducibilityNotes}`,
74
+ },
75
+ ]
76
+ },
77
+ },
78
+ async execute(args) {
79
+ const entries = typeof deps.readHistory === 'function' ? await deps.readHistory() : await readHistory()
80
+ return getReplayPayload(args.revisionId, entries)
81
+ },
82
+ })
83
+ }
@@ -0,0 +1,132 @@
1
+ import {
2
+ createVariantSession,
3
+ selectVariantWinner,
4
+ getVariantSession,
5
+ } from '../variant-selection.js'
6
+
7
+ /**
8
+ * Registers variant comparison and winner selection tools (#438).
9
+ */
10
+ export function registerVariantTools(ctx, deps = {}) {
11
+ ctx.tools.register({
12
+ name: 'evaluate_image_variants',
13
+ description: 'Sets up a side-by-side comparison session for multiple image options with structured criteria.',
14
+ parameters: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ name: {
19
+ type: 'string',
20
+ description: 'Title for the evaluation session.',
21
+ },
22
+ variants: {
23
+ type: 'array',
24
+ items: {
25
+ type: 'object',
26
+ additionalProperties: true,
27
+ properties: {
28
+ path: { type: 'string' },
29
+ label: { type: 'string' },
30
+ seed: { type: 'number' },
31
+ prompt: { type: 'string' },
32
+ },
33
+ },
34
+ description: 'Array of at least 2 image variants to evaluate.',
35
+ },
36
+ },
37
+ required: ['variants'],
38
+ },
39
+ output: {
40
+ render(args, value) {
41
+ return [
42
+ {
43
+ type: 'text',
44
+ text: `Variant Session Created: [${value?.selectionId}] "${value?.name}"\n` +
45
+ `Variants (${value?.variants?.length}):\n` +
46
+ (value?.variants || [])
47
+ .map((v) => ` #${v.index}: ${v.label} (seed: ${v.seed || 'none'}) - ${v.path}`)
48
+ .join('\n'),
49
+ },
50
+ ]
51
+ },
52
+ },
53
+ async execute(args) {
54
+ const storagePath = deps.variantStoragePath || null
55
+ return createVariantSession(
56
+ {
57
+ name: args.name,
58
+ variants: args.variants,
59
+ },
60
+ storagePath
61
+ )
62
+ },
63
+ })
64
+
65
+ ctx.tools.register({
66
+ name: 'select_variant_winner',
67
+ description: 'Designates the winning variant, tags discards/refinements, and prepares continuation payload for next pipeline step.',
68
+ parameters: {
69
+ type: 'object',
70
+ additionalProperties: false,
71
+ properties: {
72
+ selectionId: {
73
+ type: 'string',
74
+ description: 'Evaluation session ID.',
75
+ },
76
+ winnerIndex: {
77
+ type: 'number',
78
+ description: 'Index of winning variant (0-based).',
79
+ },
80
+ winnerPath: {
81
+ type: 'string',
82
+ description: 'Path of winning variant (alternative to winnerIndex).',
83
+ },
84
+ refineIndexes: {
85
+ type: 'array',
86
+ items: { type: 'number' },
87
+ description: 'Optional indexes of variants kept for refinement.',
88
+ },
89
+ nextAction: {
90
+ type: 'string',
91
+ enum: ['none', 'upscale', 'edit', 'remix', 'vectorize'],
92
+ description: 'Next action to execute with the winner.',
93
+ },
94
+ actionParams: {
95
+ type: 'object',
96
+ additionalProperties: true,
97
+ description: 'Parameters for the next action.',
98
+ },
99
+ },
100
+ required: ['selectionId'],
101
+ },
102
+ output: {
103
+ render(args, value) {
104
+ if (!value?.ok) return [{ type: 'text', text: `Selection failed: ${value?.error}` }]
105
+ const w = value.winner
106
+ return [
107
+ {
108
+ type: 'text',
109
+ text: `Selected Winner: Option #${w.index} (${w.label})\n` +
110
+ `Path: ${w.path}\n` +
111
+ `Next Action: ${value.continuationPayload?.action || 'None'}\n` +
112
+ (value.continuationPayload ? `Continuation Ready: ${JSON.stringify(value.continuationPayload.params)}` : ''),
113
+ },
114
+ ]
115
+ },
116
+ },
117
+ async execute(args) {
118
+ const storagePath = deps.variantStoragePath || null
119
+ return selectVariantWinner(
120
+ args.selectionId,
121
+ {
122
+ winnerIndex: args.winnerIndex,
123
+ winnerPath: args.winnerPath,
124
+ refineIndexes: args.refineIndexes,
125
+ nextAction: args.nextAction,
126
+ actionParams: args.actionParams,
127
+ },
128
+ storagePath
129
+ )
130
+ },
131
+ })
132
+ }