@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 are present and well-formed
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 (const choice of node.input.choices) {
188
- if (!choice || typeof choice !== 'object')
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 (const opt of node.input.options) {
205
- if (!opt || typeof opt !== 'object')
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",