@compilr-dev/sdk 0.10.12 → 0.10.13
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.
|
@@ -145,6 +145,14 @@ function validateNode(node, allNodes, errors) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
function validateQuestion(node, allNodes, errors) {
|
|
148
|
+
// Required: prompt
|
|
149
|
+
if (typeof node.prompt !== 'string' || node.prompt.trim().length === 0) {
|
|
150
|
+
errors.push({
|
|
151
|
+
code: 'INVALID_NODE_TYPE',
|
|
152
|
+
message: `Question node '${node.id}' is missing required 'prompt' string (the question text shown to the user)`,
|
|
153
|
+
nodeId: node.id,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
148
156
|
// Defensive: agent JSON may omit required nested fields. Surface as
|
|
149
157
|
// structured errors rather than letting the validator throw.
|
|
150
158
|
if (!node.input || typeof node.input !== 'object') {
|
|
@@ -174,7 +182,9 @@ function validateQuestion(node, allNodes, errors) {
|
|
|
174
182
|
nodeId: node.id,
|
|
175
183
|
});
|
|
176
184
|
}
|
|
177
|
-
// Validate choices/options arrays
|
|
185
|
+
// Validate choices/options arrays — must be non-empty and each item must
|
|
186
|
+
// have a non-empty 'id' AND 'label'. Without these the user sees an empty
|
|
187
|
+
// row and clicking it produces no answer.
|
|
178
188
|
if (node.input.mode === 'single' || node.input.mode === 'multi') {
|
|
179
189
|
if (!Array.isArray(node.input.choices)) {
|
|
180
190
|
errors.push({
|
|
@@ -183,10 +193,38 @@ function validateQuestion(node, allNodes, errors) {
|
|
|
183
193
|
nodeId: node.id,
|
|
184
194
|
});
|
|
185
195
|
}
|
|
196
|
+
else if (node.input.choices.length === 0) {
|
|
197
|
+
errors.push({
|
|
198
|
+
code: 'INVALID_NODE_TYPE',
|
|
199
|
+
message: `Question '${node.id}' has an empty 'choices' array — must have at least 1 choice (each with 'id' and 'label')`,
|
|
200
|
+
nodeId: node.id,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
186
203
|
else {
|
|
187
|
-
for (
|
|
188
|
-
|
|
204
|
+
for (let i = 0; i < node.input.choices.length; i++) {
|
|
205
|
+
const choice = node.input.choices[i];
|
|
206
|
+
if (!choice || typeof choice !== 'object') {
|
|
207
|
+
errors.push({
|
|
208
|
+
code: 'INVALID_NODE_TYPE',
|
|
209
|
+
message: `Question '${node.id}' choice at index ${String(i)} is null or not an object`,
|
|
210
|
+
nodeId: node.id,
|
|
211
|
+
});
|
|
189
212
|
continue;
|
|
213
|
+
}
|
|
214
|
+
if (typeof choice.id !== 'string' || choice.id.trim().length === 0) {
|
|
215
|
+
errors.push({
|
|
216
|
+
code: 'INVALID_NODE_TYPE',
|
|
217
|
+
message: `Question '${node.id}' choice at index ${String(i)} is missing a non-empty 'id' string`,
|
|
218
|
+
nodeId: node.id,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (typeof choice.label !== 'string' || choice.label.trim().length === 0) {
|
|
222
|
+
errors.push({
|
|
223
|
+
code: 'INVALID_NODE_TYPE',
|
|
224
|
+
message: `Question '${node.id}' choice at index ${String(i)} is missing a non-empty 'label' string (the visible text the user clicks)`,
|
|
225
|
+
nodeId: node.id,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
190
228
|
if (choice.icon !== undefined)
|
|
191
229
|
checkIcon(choice.icon, node.id, errors);
|
|
192
230
|
}
|
|
@@ -200,10 +238,38 @@ function validateQuestion(node, allNodes, errors) {
|
|
|
200
238
|
nodeId: node.id,
|
|
201
239
|
});
|
|
202
240
|
}
|
|
241
|
+
else if (node.input.options.length === 0) {
|
|
242
|
+
errors.push({
|
|
243
|
+
code: 'INVALID_NODE_TYPE',
|
|
244
|
+
message: `Question '${node.id}' has an empty 'options' array — proposal mode needs at least 2 options (each with 'id' and 'label')`,
|
|
245
|
+
nodeId: node.id,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
203
248
|
else {
|
|
204
|
-
for (
|
|
205
|
-
|
|
249
|
+
for (let i = 0; i < node.input.options.length; i++) {
|
|
250
|
+
const opt = node.input.options[i];
|
|
251
|
+
if (!opt || typeof opt !== 'object') {
|
|
252
|
+
errors.push({
|
|
253
|
+
code: 'INVALID_NODE_TYPE',
|
|
254
|
+
message: `Question '${node.id}' option at index ${String(i)} is null or not an object`,
|
|
255
|
+
nodeId: node.id,
|
|
256
|
+
});
|
|
206
257
|
continue;
|
|
258
|
+
}
|
|
259
|
+
if (typeof opt.id !== 'string' || opt.id.trim().length === 0) {
|
|
260
|
+
errors.push({
|
|
261
|
+
code: 'INVALID_NODE_TYPE',
|
|
262
|
+
message: `Question '${node.id}' option at index ${String(i)} is missing a non-empty 'id' string`,
|
|
263
|
+
nodeId: node.id,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (typeof opt.label !== 'string' || opt.label.trim().length === 0) {
|
|
267
|
+
errors.push({
|
|
268
|
+
code: 'INVALID_NODE_TYPE',
|
|
269
|
+
message: `Question '${node.id}' option at index ${String(i)} is missing a non-empty 'label' string (the visible title)`,
|
|
270
|
+
nodeId: node.id,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
207
273
|
if (opt.icon !== undefined)
|
|
208
274
|
checkIcon(opt.icon, node.id, errors);
|
|
209
275
|
}
|
|
@@ -297,6 +363,13 @@ function validateBranch(node, allNodes, errors) {
|
|
|
297
363
|
}
|
|
298
364
|
}
|
|
299
365
|
function validateSummary(node, errors) {
|
|
366
|
+
if (typeof node.title !== 'string' || node.title.trim().length === 0) {
|
|
367
|
+
errors.push({
|
|
368
|
+
code: 'INVALID_NODE_TYPE',
|
|
369
|
+
message: `Summary node '${node.id}' is missing required 'title' string`,
|
|
370
|
+
nodeId: node.id,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
300
373
|
if (node.render && !SUMMARY_RENDERERS.includes(node.render)) {
|
|
301
374
|
errors.push({
|
|
302
375
|
code: 'INVALID_RENDERER',
|
|
@@ -427,7 +500,62 @@ export function createInteractiveFlowTool(handler) {
|
|
|
427
500
|
'Use this when a decision branches into multiple paths that benefit from ' +
|
|
428
501
|
'visual exploration — the user can move forward, go back, and the agent ' +
|
|
429
502
|
'gets back both their answers and the path they took through the tree. ' +
|
|
430
|
-
'Prefer over ask_user when the decision has 2+ branching considerations.'
|
|
503
|
+
'Prefer over ask_user when the decision has 2+ branching considerations.' +
|
|
504
|
+
'\n\n' +
|
|
505
|
+
'INPUT SHAPE — get this right or validation will reject:\n' +
|
|
506
|
+
'{\n' +
|
|
507
|
+
' "flow": {\n' +
|
|
508
|
+
' "title": "<modal header>", // REQUIRED, non-empty\n' +
|
|
509
|
+
' "startNode": "<nodeId>", // REQUIRED, must be a key in nodes\n' +
|
|
510
|
+
' "nodes": { // REQUIRED, map of nodeId → node\n' +
|
|
511
|
+
' "<nodeId>": { ...node },\n' +
|
|
512
|
+
' ...\n' +
|
|
513
|
+
' }\n' +
|
|
514
|
+
' }\n' +
|
|
515
|
+
'}\n\n' +
|
|
516
|
+
'NODE TYPES — each node MUST include its own "id" matching the map key:\n\n' +
|
|
517
|
+
'1. QUESTION (gather input):\n' +
|
|
518
|
+
' { "type": "question", "id": "<id>", "prompt": "<text>",\n' +
|
|
519
|
+
' "input": { "mode": "single", "choices": [\n' +
|
|
520
|
+
' { "id": "yes", "label": "Yes" }, // id + label BOTH required, non-empty\n' +
|
|
521
|
+
' { "id": "no", "label": "No" }\n' +
|
|
522
|
+
' ]\n' +
|
|
523
|
+
' },\n' +
|
|
524
|
+
' "next": "<nodeId>" // OR { "byAnswer": { "yes": "<id>", "no": "<id>" }, "default": "<id>" }\n' +
|
|
525
|
+
' }\n' +
|
|
526
|
+
' input.mode can be: single | multi | text | proposal\n' +
|
|
527
|
+
' - single/multi: choices[] required, each with id + label\n' +
|
|
528
|
+
' - proposal: options[] with id + label + optional pros[] + cons[]\n' +
|
|
529
|
+
' - text: placeholder + multiline optional\n\n' +
|
|
530
|
+
'2. INFO (show, no input):\n' +
|
|
531
|
+
' { "type": "info", "id": "<id>", "title": "<header>", "body": "<markdown>", "next": "<nodeId>" }\n\n' +
|
|
532
|
+
'3. BRANCH (pure routing, no UI):\n' +
|
|
533
|
+
' { "type": "branch", "id": "<id>",\n' +
|
|
534
|
+
' "routes": [ { "when": { "questionId": "<id>", "equals": "<value>" }, "goto": "<nodeId>" } ],\n' +
|
|
535
|
+
' "default": "<nodeId>"\n' +
|
|
536
|
+
' }\n\n' +
|
|
537
|
+
'4. SUMMARY (terminal recap, flow ends here):\n' +
|
|
538
|
+
' { "type": "summary", "id": "<id>", "title": "<header>", "recap": "<optional markdown>" }\n\n' +
|
|
539
|
+
'MINIMAL WORKING EXAMPLE — copy this shape:\n' +
|
|
540
|
+
'{\n' +
|
|
541
|
+
' "flow": {\n' +
|
|
542
|
+
' "title": "Pick a color",\n' +
|
|
543
|
+
' "startNode": "q1",\n' +
|
|
544
|
+
' "nodes": {\n' +
|
|
545
|
+
' "q1": {\n' +
|
|
546
|
+
' "type": "question", "id": "q1", "prompt": "Which color?",\n' +
|
|
547
|
+
' "input": { "mode": "single", "choices": [\n' +
|
|
548
|
+
' { "id": "red", "label": "Red" },\n' +
|
|
549
|
+
' { "id": "blue", "label": "Blue" }\n' +
|
|
550
|
+
' ]},\n' +
|
|
551
|
+
' "next": "done"\n' +
|
|
552
|
+
' },\n' +
|
|
553
|
+
' "done": { "type": "summary", "id": "done", "title": "Got it" }\n' +
|
|
554
|
+
' }\n' +
|
|
555
|
+
' }\n' +
|
|
556
|
+
'}\n\n' +
|
|
557
|
+
'Phase 1a renders only single-mode questions, info, summary, and branch. ' +
|
|
558
|
+
'multi/text/proposal modes accept the input but show a "not yet in Phase 1a" placeholder.',
|
|
431
559
|
inputSchema: INTERACTIVE_FLOW_INPUT_SCHEMA,
|
|
432
560
|
execute: async (input) => {
|
|
433
561
|
try {
|