forge-openclaw-plugin 0.2.26 → 0.2.27

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.
Files changed (108) hide show
  1. package/README.md +59 -3
  2. package/dist/assets/{board-ta0rUHOf.js → board-C6jCchjI.js} +2 -2
  3. package/dist/assets/{board-ta0rUHOf.js.map → board-C6jCchjI.js.map} +1 -1
  4. package/dist/assets/index-DVvS8iiU.css +1 -0
  5. package/dist/assets/index-zYB-9Dfo.js +85 -0
  6. package/dist/assets/index-zYB-9Dfo.js.map +1 -0
  7. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
  8. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
  9. package/dist/assets/{motion-fBKPB6yw.js → motion-DFHrH2rd.js} +2 -2
  10. package/dist/assets/{motion-fBKPB6yw.js.map → motion-DFHrH2rd.js.map} +1 -1
  11. package/dist/assets/{table-C-IGTQni.js → table-ZL7Di_u3.js} +2 -2
  12. package/dist/assets/{table-C-IGTQni.js.map → table-ZL7Di_u3.js.map} +1 -1
  13. package/dist/assets/{ui-DInOpaYF.js → ui-CKNPpz7q.js} +2 -2
  14. package/dist/assets/{ui-DInOpaYF.js.map → ui-CKNPpz7q.js.map} +1 -1
  15. package/dist/assets/vendor-DoNZuFhn.js +1247 -0
  16. package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
  17. package/dist/index.html +7 -7
  18. package/dist/openclaw/local-runtime.js +16 -0
  19. package/dist/openclaw/routes.d.ts +27 -0
  20. package/dist/openclaw/routes.js +16 -12
  21. package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  22. package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
  23. package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
  24. package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
  25. package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
  26. package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
  27. package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  28. package/dist/server/server/src/app.js +1684 -117
  29. package/dist/server/server/src/connectors/box-registry.js +44 -9
  30. package/dist/server/server/src/data-management-types.js +107 -0
  31. package/dist/server/server/src/db.js +68 -4
  32. package/dist/server/server/src/demo-data.js +2 -2
  33. package/dist/server/server/src/health.js +702 -18
  34. package/dist/server/server/src/managers/platform/llm-manager.js +7 -4
  35. package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
  36. package/dist/server/server/src/managers/platform/secrets-manager.js +18 -1
  37. package/dist/server/server/src/managers/runtime.js +9 -0
  38. package/dist/server/server/src/movement.js +1971 -112
  39. package/dist/server/server/src/openapi.js +489 -1
  40. package/dist/server/server/src/psyche-types.js +9 -1
  41. package/dist/server/server/src/repositories/activity-events.js +8 -0
  42. package/dist/server/server/src/repositories/ai-connectors.js +522 -74
  43. package/dist/server/server/src/repositories/habits.js +37 -1
  44. package/dist/server/server/src/repositories/model-settings.js +13 -3
  45. package/dist/server/server/src/repositories/notes.js +3 -0
  46. package/dist/server/server/src/repositories/settings.js +380 -18
  47. package/dist/server/server/src/repositories/tasks.js +170 -10
  48. package/dist/server/server/src/runtime-data-root.js +82 -0
  49. package/dist/server/server/src/screen-time.js +802 -0
  50. package/dist/server/server/src/services/data-management.js +788 -0
  51. package/dist/server/server/src/services/entity-crud.js +205 -2
  52. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  53. package/dist/server/server/src/services/life-force-model.js +197 -0
  54. package/dist/server/server/src/services/life-force.js +1270 -0
  55. package/dist/server/server/src/services/psyche-observation-calendar.js +383 -16
  56. package/dist/server/server/src/types.js +286 -13
  57. package/dist/server/server/src/web.js +228 -13
  58. package/dist/server/src/components/customization/utility-widgets.js +136 -27
  59. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  60. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  61. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  62. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  63. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +63 -8
  64. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  65. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +62 -54
  66. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +18 -8
  67. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +56 -38
  68. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  69. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  70. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +35 -30
  71. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  72. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  73. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  74. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +3 -1
  75. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +39 -3
  76. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  77. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  78. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +47 -32
  79. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  80. package/dist/server/src/lib/api.js +280 -21
  81. package/dist/server/src/lib/data-management-types.js +1 -0
  82. package/dist/server/src/lib/entity-visuals.js +279 -0
  83. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  84. package/dist/server/src/lib/knowledge-graph.js +470 -0
  85. package/dist/server/src/lib/schemas.js +4 -0
  86. package/dist/server/src/lib/snapshot-normalizer.js +43 -1
  87. package/dist/server/src/lib/workbench/contracts.js +229 -0
  88. package/dist/server/src/lib/workbench/nodes.js +200 -0
  89. package/dist/server/src/lib/workbench/registry.js +52 -5
  90. package/dist/server/src/lib/workbench/runtime.js +254 -38
  91. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  92. package/openclaw.plugin.json +1 -1
  93. package/package.json +1 -1
  94. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  95. package/server/migrations/038_data_management_settings.sql +11 -0
  96. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  97. package/server/migrations/040_screen_time_domain.sql +89 -0
  98. package/server/migrations/041_companion_source_states.sql +21 -0
  99. package/server/migrations/042_movement_boxes.sql +47 -0
  100. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  101. package/skills/forge-openclaw/SKILL.md +24 -11
  102. package/skills/forge-openclaw/entity_conversation_playbooks.md +210 -34
  103. package/skills/forge-openclaw/psyche_entity_playbooks.md +113 -17
  104. package/dist/assets/index-Ro0ZF_az.css +0 -1
  105. package/dist/assets/index-ytlpSj23.js +0 -79
  106. package/dist/assets/index-ytlpSj23.js.map +0 -1
  107. package/dist/assets/vendor-lE3tZJcC.js +0 -876
  108. package/dist/assets/vendor-lE3tZJcC.js.map +0 -1
@@ -8,6 +8,7 @@ import { createAiConnectorSchema, aiConnectorConversationSchema, aiConnectorRunR
8
8
  import { FORGE_DEFAULT_AGENT_ID, getAiModelConnectionById, listAiModelConnections, readModelConnectionCredential } from "./model-settings.js";
9
9
  import { getAiProcessorById, listAiProcessorLinks, listAiProcessors } from "./ai-processors.js";
10
10
  import { buildConnectorOutputCatalogEntry, executeForgeBoxTool, resolveForgeBoxSnapshot } from "../connectors/box-registry.js";
11
+ import { normalizeWorkbenchPortDefinition } from "../../../src/lib/workbench/nodes.js";
11
12
  const execFile = promisify(execFileCallback);
12
13
  const MAX_TOOL_STEPS = 6;
13
14
  const MAX_RUN_HISTORY = 20;
@@ -164,7 +165,7 @@ function buildDefaultGraph(kind, title) {
164
165
  data: {
165
166
  label: "Output",
166
167
  description: "Published connector output.",
167
- outputKey: "primary",
168
+ outputKey: "answer",
168
169
  enabledToolKeys: []
169
170
  }
170
171
  }
@@ -190,7 +191,7 @@ function ensurePublishedOutputs(connectorId, graph) {
190
191
  buildConnectorOutputCatalogEntry({
191
192
  connectorId,
192
193
  title: "Connector",
193
- outputId: "primary"
194
+ outputId: "answer"
194
195
  })
195
196
  ].map((entry) => ({
196
197
  id: entry.id.replace(/^connector-output:/, ""),
@@ -213,6 +214,7 @@ function mapRun(row) {
213
214
  mode: row.mode,
214
215
  status: row.status,
215
216
  userInput: row.user_input,
217
+ inputs: parseJson(row.inputs_json, {}),
216
218
  context: parseJson(row.context_json, {}),
217
219
  conversationId: row.conversation_id,
218
220
  result: parseJson(row.result_json, null),
@@ -233,6 +235,8 @@ function mapConversation(row) {
233
235
  });
234
236
  }
235
237
  function mapConnector(row) {
238
+ const rawGraph = parseJson(row.graph_json, { nodes: [], edges: [] });
239
+ const normalizedGraph = normalizeConnectorGraph(rawGraph);
236
240
  return aiConnectorSchema.parse({
237
241
  id: row.id,
238
242
  slug: row.slug,
@@ -241,7 +245,8 @@ function mapConnector(row) {
241
245
  kind: row.kind,
242
246
  homeSurfaceId: row.home_surface_id,
243
247
  endpointEnabled: row.endpoint_enabled === 1,
244
- graph: parseJson(row.graph_json, { nodes: [], edges: [] }),
248
+ graph: normalizedGraph,
249
+ publicInputs: parseJson(row.public_inputs_json, []),
245
250
  publishedOutputs: parseJson(row.published_outputs_json, []),
246
251
  lastRun: parseJson(row.last_run_json, null),
247
252
  legacyProcessorId: row.legacy_processor_id,
@@ -255,6 +260,37 @@ export function listAiConnectorRuns(connectorId) {
255
260
  .all(connectorId, MAX_RUN_HISTORY);
256
261
  return rows.map(mapRun);
257
262
  }
263
+ export function getAiConnectorRunById(connectorId, runId) {
264
+ const row = getDatabase()
265
+ .prepare(`SELECT * FROM ai_connector_runs WHERE connector_id = ? AND id = ?`)
266
+ .get(connectorId, runId);
267
+ return row ? mapRun(row) : null;
268
+ }
269
+ export function getAiConnectorRunNodeResults(connectorId, runId) {
270
+ const run = getAiConnectorRunById(connectorId, runId);
271
+ return run?.result?.nodeResults ?? null;
272
+ }
273
+ export function getAiConnectorRunNodeResult(connectorId, runId, nodeId) {
274
+ const results = getAiConnectorRunNodeResults(connectorId, runId);
275
+ if (!results) {
276
+ return null;
277
+ }
278
+ return results.find((entry) => entry.nodeId === nodeId) ?? null;
279
+ }
280
+ export function getLatestAiConnectorNodeOutput(connectorId, nodeId) {
281
+ const run = listAiConnectorRuns(connectorId).find((entry) => entry.status === "completed" && entry.result);
282
+ if (!run?.result) {
283
+ return null;
284
+ }
285
+ const nodeResult = run.result.nodeResults.find((entry) => entry.nodeId === nodeId) ?? null;
286
+ if (!nodeResult) {
287
+ return null;
288
+ }
289
+ return {
290
+ run,
291
+ nodeResult
292
+ };
293
+ }
258
294
  export function getAiConnectorConversationById(conversationId) {
259
295
  const row = getDatabase()
260
296
  .prepare(`SELECT * FROM ai_connector_conversations WHERE id = ?`)
@@ -288,20 +324,21 @@ function updateConnectorLastRun(connectorId, run) {
288
324
  function insertRun(input) {
289
325
  getDatabase()
290
326
  .prepare(`INSERT INTO ai_connector_runs (
291
- id, connector_id, mode, status, user_input, context_json, conversation_id, result_json, error, created_at, completed_at
292
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
327
+ id, connector_id, mode, status, user_input, inputs_json, context_json, conversation_id, result_json, error, created_at, completed_at
328
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
293
329
  ON CONFLICT(id) DO UPDATE SET
294
330
  connector_id = excluded.connector_id,
295
331
  mode = excluded.mode,
296
332
  status = excluded.status,
297
333
  user_input = excluded.user_input,
334
+ inputs_json = excluded.inputs_json,
298
335
  context_json = excluded.context_json,
299
336
  conversation_id = excluded.conversation_id,
300
337
  result_json = excluded.result_json,
301
338
  error = excluded.error,
302
339
  created_at = excluded.created_at,
303
340
  completed_at = excluded.completed_at`)
304
- .run(input.id, input.connectorId, input.mode, input.status, input.userInput, JSON.stringify(input.context), input.conversationId, input.result ? JSON.stringify(input.result) : null, input.error, input.createdAt, input.completedAt);
341
+ .run(input.id, input.connectorId, input.mode, input.status, input.userInput, JSON.stringify(input.inputs), JSON.stringify(input.context), input.conversationId, input.result ? JSON.stringify(input.result) : null, input.error, input.createdAt, input.completedAt);
305
342
  updateConnectorLastRun(input.connectorId, input);
306
343
  return input;
307
344
  }
@@ -316,7 +353,33 @@ function resolveAllowedPath(inputPath) {
316
353
  }
317
354
  function tryParseStructuredAgentResponse(value) {
318
355
  try {
319
- return JSON.parse(value);
356
+ const parsed = JSON.parse(value);
357
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
358
+ return null;
359
+ }
360
+ const action = parsed.action;
361
+ if (action === "final") {
362
+ const text = parsed.text;
363
+ return {
364
+ action,
365
+ text: typeof text === "string" ? text : value
366
+ };
367
+ }
368
+ if (action === "tool") {
369
+ const tool = parsed.tool;
370
+ const args = parsed.args;
371
+ if (typeof tool !== "string" || tool.trim().length === 0) {
372
+ return null;
373
+ }
374
+ return {
375
+ action,
376
+ tool,
377
+ args: args && typeof args === "object" && !Array.isArray(args)
378
+ ? args
379
+ : {}
380
+ };
381
+ }
382
+ return null;
320
383
  }
321
384
  catch {
322
385
  return null;
@@ -351,30 +414,95 @@ function coerceText(value) {
351
414
  return "";
352
415
  }
353
416
  }
354
- function buildOutputMap(primaryText, primaryJson, outputKeys = []) {
355
- const outputMap = {
356
- primary: {
357
- text: primaryText,
358
- json: primaryJson
417
+ function coerceJsonObject(value) {
418
+ return value && typeof value === "object" && !Array.isArray(value)
419
+ ? value
420
+ : null;
421
+ }
422
+ function validatePortValueType(port, value) {
423
+ switch (port.kind) {
424
+ case "text":
425
+ case "markdown":
426
+ case "summary":
427
+ return typeof value === "string";
428
+ case "number":
429
+ return typeof value === "number" && Number.isFinite(value);
430
+ case "boolean":
431
+ return typeof value === "boolean";
432
+ case "array":
433
+ case "entity_list":
434
+ case "record_list":
435
+ return Array.isArray(value);
436
+ case "object":
437
+ case "json":
438
+ case "record":
439
+ case "context":
440
+ case "filters":
441
+ case "metrics":
442
+ case "timeline":
443
+ case "selection":
444
+ case "entity":
445
+ return Boolean(value) && typeof value === "object";
446
+ default:
447
+ return true;
448
+ }
449
+ }
450
+ function normalizePublicInputBindings(connector, publicInput) {
451
+ if (publicInput.bindings.length > 0) {
452
+ return publicInput.bindings;
453
+ }
454
+ return connector.graph.nodes.flatMap((node) => {
455
+ const inputs = defaultInputsForNode(node);
456
+ const params = node.data.params ?? [];
457
+ const matches = [];
458
+ if (inputs.some((entry) => entry.key === publicInput.key)) {
459
+ matches.push({
460
+ nodeId: node.id,
461
+ targetKey: publicInput.key,
462
+ targetKind: "input"
463
+ });
359
464
  }
360
- };
361
- for (const key of outputKeys) {
362
- if (!primaryJson || !(key in primaryJson)) {
363
- continue;
465
+ if (params.some((entry) => entry.key === publicInput.key)) {
466
+ matches.push({
467
+ nodeId: node.id,
468
+ targetKey: publicInput.key,
469
+ targetKind: "param"
470
+ });
364
471
  }
365
- const value = primaryJson[key];
366
- outputMap[key] = {
472
+ return matches;
473
+ });
474
+ }
475
+ function buildPublicInputValue(publicInput, value) {
476
+ return {
477
+ sourceNodeId: `flow_input:${publicInput.key}`,
478
+ sourceHandle: publicInput.key,
479
+ targetHandle: publicInput.key,
480
+ text: coerceText(value),
481
+ json: coerceJsonObject(value)
482
+ };
483
+ }
484
+ function buildOutputMap(primaryText, primaryJson, outputs = []) {
485
+ const outputMap = {};
486
+ const declaredOutputs = outputs.length > 0 ? outputs : [{ key: "summary" }];
487
+ declaredOutputs.forEach((output, index) => {
488
+ const value = primaryJson && output.key in primaryJson
489
+ ? primaryJson[output.key]
490
+ : index === 0 || output.key === "summary"
491
+ ? primaryText
492
+ : null;
493
+ outputMap[output.key] = {
367
494
  text: coerceText(value),
368
495
  json: value && typeof value === "object" && !Array.isArray(value)
369
496
  ? value
370
497
  : null
371
498
  };
372
- }
499
+ });
373
500
  return outputMap;
374
501
  }
375
502
  function readOutputSelection(value, handle) {
376
- if (!handle || handle === "primary") {
377
- return { text: value.text, json: value.json };
503
+ if (!handle) {
504
+ const lead = Object.values(value.outputMap)[0];
505
+ return lead ?? { text: value.text, json: value.json };
378
506
  }
379
507
  const selected = value.outputMap[handle];
380
508
  if (selected) {
@@ -391,6 +519,148 @@ function readOutputSelection(value, handle) {
391
519
  }
392
520
  return { text: value.text, json: value.json };
393
521
  }
522
+ function defaultOutputsForNode(node) {
523
+ if (node.data.outputs?.length) {
524
+ return node.data.outputs.map((port) => normalizeWorkbenchPortDefinition(port));
525
+ }
526
+ switch (node.type) {
527
+ case "user_input":
528
+ return [{ key: "message", label: "Message", kind: "text" }];
529
+ case "value":
530
+ return [{ key: "value", label: "Value", kind: "record" }];
531
+ case "merge":
532
+ return [{ key: "merged", label: "Merged context", kind: "context" }];
533
+ case "template":
534
+ return [{ key: "rendered", label: "Rendered output", kind: "markdown" }];
535
+ case "pick_key":
536
+ return [{ key: "selected", label: "Selected value", kind: "record" }];
537
+ case "functor":
538
+ case "chat":
539
+ return [{ key: "answer", label: "Answer", kind: "markdown" }];
540
+ case "output":
541
+ return [{ key: node.data.outputKey?.trim() || "result", label: "Published result", kind: "record" }];
542
+ case "box":
543
+ case "box_input":
544
+ return [{ key: "summary", label: "Summary", kind: "summary" }];
545
+ }
546
+ }
547
+ function defaultInputsForNode(node) {
548
+ if (node.data.inputs?.length) {
549
+ return node.data.inputs.map((port) => normalizeWorkbenchPortDefinition(port));
550
+ }
551
+ switch (node.type) {
552
+ case "functor":
553
+ case "chat":
554
+ return [{ key: "input", label: "Flow input", kind: "context" }];
555
+ case "merge":
556
+ return [
557
+ { key: "left", label: "Left input", kind: "context" },
558
+ { key: "right", label: "Right input", kind: "context" }
559
+ ];
560
+ case "template":
561
+ return [{ key: "input", label: "Template input", kind: "context" }];
562
+ case "pick_key":
563
+ return [{ key: "object", label: "Source object", kind: "object" }];
564
+ case "output":
565
+ return [{ key: "result", label: "Published result", kind: "record" }];
566
+ default:
567
+ return [];
568
+ }
569
+ }
570
+ function normalizeConnectorNodeContracts(node) {
571
+ const normalizePorts = (ports, direction) => ports.map((port) => {
572
+ const normalized = normalizeWorkbenchPortDefinition(port);
573
+ if (normalized.key !== "primary") {
574
+ return normalized;
575
+ }
576
+ const nextKey = direction === "output"
577
+ ? node.type === "functor" || node.type === "chat"
578
+ ? "answer"
579
+ : node.type === "box" || node.type === "box_input"
580
+ ? "summary"
581
+ : node.type === "value"
582
+ ? "value"
583
+ : node.type === "merge"
584
+ ? "merged"
585
+ : node.type === "template"
586
+ ? "rendered"
587
+ : node.type === "pick_key"
588
+ ? "selected"
589
+ : "result"
590
+ : node.type === "functor" || node.type === "chat"
591
+ ? "input"
592
+ : node.type === "output"
593
+ ? "result"
594
+ : normalized.key;
595
+ return normalizeWorkbenchPortDefinition({
596
+ ...normalized,
597
+ key: nextKey,
598
+ kind: nextKey === normalized.key ? normalized.kind : undefined
599
+ });
600
+ });
601
+ const normalizedInputs = normalizePorts(defaultInputsForNode(node).length > 0 ? defaultInputsForNode(node) : node.data.inputs ?? [], "input");
602
+ const normalizedOutputs = defaultOutputsForNode({
603
+ ...node,
604
+ data: {
605
+ ...node.data,
606
+ outputs: normalizePorts(node.data.outputs ?? [], "output")
607
+ }
608
+ });
609
+ const normalizedOutputKey = (() => {
610
+ const current = node.data.outputKey?.trim();
611
+ if (!current || current === "primary") {
612
+ return normalizedOutputs[0]?.key ?? "";
613
+ }
614
+ if (normalizedOutputs.some((output) => output.key === current)) {
615
+ return current;
616
+ }
617
+ return normalizedOutputs[0]?.key ?? current;
618
+ })();
619
+ return {
620
+ ...node,
621
+ data: {
622
+ ...node.data,
623
+ inputs: normalizedInputs,
624
+ outputs: normalizedOutputs,
625
+ outputKey: normalizedOutputKey
626
+ }
627
+ };
628
+ }
629
+ function canonicalEdgeHandle(handle, ports, preferred) {
630
+ if (ports.length === 0) {
631
+ return null;
632
+ }
633
+ if (!handle || handle === "primary") {
634
+ if (preferred && ports.some((port) => port.key === preferred)) {
635
+ return preferred;
636
+ }
637
+ return ports[0]?.key ?? null;
638
+ }
639
+ if (ports.some((port) => port.key === handle)) {
640
+ return handle;
641
+ }
642
+ if (preferred && ports.some((port) => port.key === preferred)) {
643
+ return preferred;
644
+ }
645
+ return ports[0]?.key ?? null;
646
+ }
647
+ function normalizeConnectorGraph(graph) {
648
+ const normalizedNodes = graph.nodes.map((node) => normalizeConnectorNodeContracts(node));
649
+ const nodeMap = new Map(normalizedNodes.map((node) => [node.id, node]));
650
+ const normalizedEdges = graph.edges.map((edge) => {
651
+ const sourceNode = nodeMap.get(edge.source);
652
+ const targetNode = nodeMap.get(edge.target);
653
+ return {
654
+ ...edge,
655
+ sourceHandle: canonicalEdgeHandle(edge.sourceHandle, sourceNode?.data.outputs ?? [], sourceNode?.data.outputs?.[0]?.key),
656
+ targetHandle: canonicalEdgeHandle(edge.targetHandle, targetNode?.data.inputs ?? [], targetNode?.data.inputs?.[0]?.key)
657
+ };
658
+ });
659
+ return {
660
+ nodes: normalizedNodes,
661
+ edges: normalizedEdges
662
+ };
663
+ }
394
664
  async function executeMachineTool(tool, args) {
395
665
  if (tool === "machine_read_file") {
396
666
  const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
@@ -429,9 +699,15 @@ function getConversationBasePrompt(input) {
429
699
  return [
430
700
  input.node.data.prompt?.trim() || "",
431
701
  input.userInput ? `User input:\n${input.userInput}` : "",
702
+ input.conversation && input.node.type === "chat" && input.conversation.transcript.length > 0
703
+ ? `Conversation history:\n${input.conversation.transcript
704
+ .slice(-8)
705
+ .map((entry) => `${entry.role}: ${entry.text}`)
706
+ .join("\n")}`
707
+ : "",
432
708
  input.upstream.length > 0
433
709
  ? `Linked inputs:\n${input.upstream
434
- .map((entry, index) => `Input ${index + 1}:\n${entry.text}${entry.json ? `\nJSON: ${JSON.stringify(entry.json)}` : ""}`)
710
+ .map((entry, index) => `Input ${entry.targetHandle || index + 1}:\n${entry.text}${entry.json ? `\nJSON: ${JSON.stringify(entry.json)}` : ""}`)
435
711
  .join("\n\n")}`
436
712
  : "",
437
713
  input.transcript.length > 0 ? `Tool transcript:\n${input.transcript.join("\n\n")}` : ""
@@ -514,7 +790,7 @@ function resolveConnectorModelProfile(node, secrets) {
514
790
  : credential?.kind === "oauth"
515
791
  ? credential.access
516
792
  : null;
517
- if (!explicitApiKey) {
793
+ if (!explicitApiKey && fallbackConnection.provider !== "mock") {
518
794
  throw new Error("The selected connector model connection is missing a credential.");
519
795
  }
520
796
  const profile = {
@@ -532,7 +808,7 @@ function resolveConnectorModelProfile(node, secrets) {
532
808
  };
533
809
  return {
534
810
  profile,
535
- apiKey: explicitApiKey
811
+ apiKey: explicitApiKey ?? "mock"
536
812
  };
537
813
  }
538
814
  async function runModelNode(input) {
@@ -555,7 +831,7 @@ async function runModelNode(input) {
555
831
  'For a final answer return {"action":"final","text":"..."}',
556
832
  'For a tool call return {"action":"tool","tool":"tool_key","args":{...}}',
557
833
  `Available tools: ${activeTools
558
- .map((tool) => `${tool.key} (${tool.description})`)
834
+ .map((tool) => `${tool.key} (${tool.description})${tool.argsSchema ? ` args=${JSON.stringify(tool.argsSchema)}` : ""}`)
559
835
  .join("; ")}.`
560
836
  ].join(" ")
561
837
  : "Return only the final answer text."
@@ -567,7 +843,8 @@ async function runModelNode(input) {
567
843
  node: input.node,
568
844
  userInput: input.userInput,
569
845
  upstream: input.upstream,
570
- transcript
846
+ transcript,
847
+ conversation: input.conversation
571
848
  });
572
849
  let rawText = "";
573
850
  if (conversationAware && isOpenAiFamily(profile)) {
@@ -593,7 +870,8 @@ async function runModelNode(input) {
593
870
  text: rawText.trim(),
594
871
  json: tryParseJsonObject(rawText.trim()),
595
872
  conversationId,
596
- logs: transcript
873
+ logs: transcript,
874
+ availableTools: []
597
875
  };
598
876
  }
599
877
  const structured = tryParseStructuredAgentResponse(rawText.trim());
@@ -602,7 +880,8 @@ async function runModelNode(input) {
602
880
  text: structured?.text?.trim() || rawText.trim(),
603
881
  json: tryParseJsonObject(structured?.text?.trim() || rawText.trim()),
604
882
  conversationId,
605
- logs: transcript
883
+ logs: transcript,
884
+ availableTools: activeTools.map((tool) => tool.key)
606
885
  };
607
886
  }
608
887
  const toolResult = structured.tool.startsWith("machine_")
@@ -619,10 +898,14 @@ async function runModelNode(input) {
619
898
  text: "Connector stopped after reaching the maximum tool step count.",
620
899
  json: null,
621
900
  conversationId,
622
- logs: transcript
901
+ logs: transcript,
902
+ availableTools: activeTools.map((tool) => tool.key)
623
903
  };
624
904
  }
625
905
  function validateConnectorGraph(graph) {
906
+ if (graph.nodes.length === 0) {
907
+ throw new Error("Connector graph has no nodes yet.");
908
+ }
626
909
  const nodeIds = new Set(graph.nodes.map((node) => node.id));
627
910
  for (const edge of graph.edges) {
628
911
  if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
@@ -654,8 +937,44 @@ function validateConnectorGraph(graph) {
654
937
  for (const node of graph.nodes) {
655
938
  visit(node.id);
656
939
  }
940
+ const incomingCounts = new Map();
941
+ const outgoingCounts = new Map();
942
+ for (const edge of graph.edges) {
943
+ incomingCounts.set(edge.target, (incomingCounts.get(edge.target) ?? 0) + 1);
944
+ outgoingCounts.set(edge.source, (outgoingCounts.get(edge.source) ?? 0) + 1);
945
+ }
946
+ const outputNodes = graph.nodes.filter((node) => node.type === "output");
947
+ if (outputNodes.length === 0) {
948
+ throw new Error("Connector graph is missing an output node.");
949
+ }
950
+ const disconnectedOutput = outputNodes.find((node) => (incomingCounts.get(node.id) ?? 0) === 0);
951
+ if (disconnectedOutput) {
952
+ throw new Error(`Output node "${disconnectedOutput.data.label || disconnectedOutput.id}" has no incoming connection.`);
953
+ }
954
+ const aiNodeMissingPrompt = graph.nodes.find((node) => (node.type === "functor" || node.type === "chat") &&
955
+ !(node.data.promptTemplate?.trim() || node.data.prompt?.trim()));
956
+ if (aiNodeMissingPrompt) {
957
+ throw new Error(`AI node "${aiNodeMissingPrompt.data.label || aiNodeMissingPrompt.id}" is missing a prompt.`);
958
+ }
959
+ const mergeNodeMissingInputs = graph.nodes.find((node) => node.type === "merge" && (incomingCounts.get(node.id) ?? 0) < 2);
960
+ if (mergeNodeMissingInputs) {
961
+ throw new Error(`Merge node "${mergeNodeMissingInputs.data.label || mergeNodeMissingInputs.id}" must receive both left and right inputs.`);
962
+ }
963
+ const templateNodeMissingTemplate = graph.nodes.find((node) => node.type === "template" && !(node.data.template ?? "").trim());
964
+ if (templateNodeMissingTemplate) {
965
+ throw new Error(`Template node "${templateNodeMissingTemplate.data.label || templateNodeMissingTemplate.id}" is missing its template string.`);
966
+ }
967
+ const pickKeyNodeMissingSelection = graph.nodes.find((node) => node.type === "pick_key" && !(node.data.selectedKey ?? "").trim());
968
+ if (pickKeyNodeMissingSelection) {
969
+ throw new Error(`Pick-key node "${pickKeyNodeMissingSelection.data.label || pickKeyNodeMissingSelection.id}" is missing the key it should select.`);
970
+ }
971
+ const isolatedNode = graph.nodes.find((node) => node.type !== "output" &&
972
+ (outgoingCounts.get(node.id) ?? 0) === 0);
973
+ if (isolatedNode) {
974
+ throw new Error(`Node "${isolatedNode.data.label || isolatedNode.id}" is not connected to anything downstream.`);
975
+ }
657
976
  }
658
- function buildOutputResult(connector, resolvedNodeValues) {
977
+ function buildOutputResult(connector, resolvedNodeValues, nodeResults) {
659
978
  const outputs = Object.fromEntries(connector.publishedOutputs.map((output) => {
660
979
  const nodeValue = resolvedNodeValues.get(output.nodeId);
661
980
  return [
@@ -670,7 +989,8 @@ function buildOutputResult(connector, resolvedNodeValues) {
670
989
  const first = connector.publishedOutputs[0];
671
990
  return aiConnectorRunResultSchema.parse({
672
991
  primaryText: first ? outputs[first.id]?.text ?? "" : "",
673
- outputs
992
+ outputs,
993
+ nodeResults
674
994
  });
675
995
  }
676
996
  function parseValueLiteral(valueType, valueLiteral) {
@@ -717,11 +1037,50 @@ async function executeConnector(connector, rawInput, services) {
717
1037
  }
718
1038
  const values = new Map();
719
1039
  const debugNodes = [];
1040
+ const nodeResults = [];
720
1041
  const debugErrors = [];
721
1042
  const outputNodes = connector.graph.nodes.filter((node) => node.type === "output");
722
1043
  const activeConversation = parsedInput.conversationId
723
1044
  ? getAiConnectorConversationById(parsedInput.conversationId)
724
1045
  : getAiConnectorConversationForConnector(connector.id);
1046
+ const publicInputValues = new Map();
1047
+ const nodePublicInputs = new Map();
1048
+ const nodePublicParams = new Map();
1049
+ for (const publicInput of connector.publicInputs) {
1050
+ const hasProvided = Object.prototype.hasOwnProperty.call(parsedInput.inputs, publicInput.key);
1051
+ const resolvedValue = hasProvided
1052
+ ? parsedInput.inputs[publicInput.key]
1053
+ : publicInput.defaultValue;
1054
+ if (resolvedValue === undefined) {
1055
+ if (publicInput.required) {
1056
+ throw new Error(`Flow input "${publicInput.label}" is required.`);
1057
+ }
1058
+ continue;
1059
+ }
1060
+ if (!validatePortValueType(publicInput, resolvedValue)) {
1061
+ throw new Error(`Flow input "${publicInput.label}" must match the ${publicInput.kind} type.`);
1062
+ }
1063
+ const bindings = normalizePublicInputBindings(connector, publicInput);
1064
+ if (bindings.length === 0) {
1065
+ throw new Error(`Flow input "${publicInput.label}" is not bound to any node input or parameter yet.`);
1066
+ }
1067
+ publicInputValues.set(publicInput.key, resolvedValue);
1068
+ for (const binding of bindings) {
1069
+ if (binding.targetKind === "param") {
1070
+ const current = nodePublicParams.get(binding.nodeId) ?? {};
1071
+ current[binding.targetKey] = resolvedValue;
1072
+ nodePublicParams.set(binding.nodeId, current);
1073
+ continue;
1074
+ }
1075
+ const current = nodePublicInputs.get(binding.nodeId) ?? [];
1076
+ const publicBindingValue = buildPublicInputValue(publicInput, resolvedValue);
1077
+ current.push({
1078
+ ...publicBindingValue,
1079
+ targetHandle: binding.targetKey
1080
+ });
1081
+ nodePublicInputs.set(binding.nodeId, current);
1082
+ }
1083
+ }
725
1084
  const evaluateNode = async (nodeId) => {
726
1085
  const existing = values.get(nodeId);
727
1086
  if (existing) {
@@ -731,8 +1090,9 @@ async function executeConnector(connector, rawInput, services) {
731
1090
  if (!node) {
732
1091
  throw new Error(`Missing connector node ${nodeId}.`);
733
1092
  }
1093
+ const startedAt = Date.now();
734
1094
  const upstreamEdges = incoming.get(nodeId) ?? [];
735
- const upstream = await Promise.all(upstreamEdges.map(async (edge) => {
1095
+ const graphUpstream = await Promise.all(upstreamEdges.map(async (edge) => {
736
1096
  const upstreamValue = await evaluateNode(edge.source);
737
1097
  const selected = readOutputSelection(upstreamValue, edge.sourceHandle);
738
1098
  return {
@@ -741,16 +1101,55 @@ async function executeConnector(connector, rawInput, services) {
741
1101
  selected
742
1102
  };
743
1103
  }));
1104
+ const publicInputs = (nodePublicInputs.get(nodeId) ?? []).map((entry) => ({
1105
+ edge: {
1106
+ id: `${nodeId}_${entry.targetHandle}`,
1107
+ source: entry.sourceNodeId,
1108
+ target: nodeId,
1109
+ sourceHandle: entry.sourceHandle,
1110
+ targetHandle: entry.targetHandle,
1111
+ label: null
1112
+ },
1113
+ sourceValue: {
1114
+ text: entry.text,
1115
+ json: entry.json,
1116
+ tools: [],
1117
+ conversationId: null,
1118
+ outputMap: {
1119
+ [entry.sourceHandle ?? entry.targetHandle]: {
1120
+ text: entry.text,
1121
+ json: entry.json
1122
+ }
1123
+ },
1124
+ logs: []
1125
+ },
1126
+ selected: {
1127
+ text: entry.text,
1128
+ json: entry.json
1129
+ }
1130
+ }));
1131
+ const upstream = [...graphUpstream, ...publicInputs];
1132
+ const resolvedInputsForDebug = upstream.map((entry) => ({
1133
+ sourceNodeId: entry.edge.source,
1134
+ sourceHandle: entry.edge.sourceHandle ?? null,
1135
+ targetHandle: entry.edge.targetHandle ?? null,
1136
+ text: entry.selected.text,
1137
+ json: entry.selected.json
1138
+ }));
744
1139
  let resolved;
1140
+ let nodeToolKeys = [];
745
1141
  if (node.type === "box" || node.type === "box_input") {
746
1142
  const boxId = node.data.boxId?.trim() || "";
747
1143
  const resolvedInputs = Object.fromEntries(upstream.map(({ edge, selected }, index) => [
748
1144
  edge.targetHandle ?? edge.sourceHandle ?? `input_${index + 1}`,
749
1145
  selected.json ?? selected.text
750
1146
  ]));
751
- const resolvedParams = node.data.paramValues && typeof node.data.paramValues === "object"
752
- ? node.data.paramValues
753
- : {};
1147
+ const resolvedParams = {
1148
+ ...(node.data.paramValues && typeof node.data.paramValues === "object"
1149
+ ? node.data.paramValues
1150
+ : {}),
1151
+ ...(nodePublicParams.get(nodeId) ?? {})
1152
+ };
754
1153
  const providedSnapshot = boxId ? parsedInput.boxSnapshots[boxId] : null;
755
1154
  const snapshot = providedSnapshot && typeof providedSnapshot === "object"
756
1155
  ? {
@@ -783,10 +1182,7 @@ async function executeConnector(connector, rawInput, services) {
783
1182
  contentJson: null,
784
1183
  tools: []
785
1184
  };
786
- const outputKeys = [
787
- ...(node.data.outputs ?? []).map((port) => port.key),
788
- ...Object.keys(snapshot.contentJson ?? {})
789
- ];
1185
+ const outputDefs = defaultOutputsForNode(node);
790
1186
  resolved = {
791
1187
  text: snapshot.contentText,
792
1188
  json: snapshot.contentJson,
@@ -794,12 +1190,14 @@ async function executeConnector(connector, rawInput, services) {
794
1190
  boxId: snapshot.boxId,
795
1191
  key: tool.key,
796
1192
  label: tool.label,
797
- description: tool.description
1193
+ description: tool.description,
1194
+ argsSchema: tool.argsSchema
798
1195
  })),
799
1196
  conversationId: null,
800
- outputMap: buildOutputMap(snapshot.contentText, snapshot.contentJson, outputKeys),
1197
+ outputMap: buildOutputMap(snapshot.contentText, snapshot.contentJson, outputDefs),
801
1198
  logs: []
802
1199
  };
1200
+ nodeToolKeys = resolved.tools.map((tool) => tool.key);
803
1201
  }
804
1202
  else if (node.type === "value") {
805
1203
  const parsedValue = parseValueLiteral(node.data.valueType ?? "string", node.data.valueLiteral ?? "");
@@ -816,17 +1214,27 @@ async function executeConnector(connector, rawInput, services) {
816
1214
  json: jsonValue,
817
1215
  tools: [],
818
1216
  conversationId: null,
819
- outputMap: buildOutputMap(textValue, jsonValue, ["primary"]),
1217
+ outputMap: buildOutputMap(textValue, jsonValue, defaultOutputsForNode(node)),
820
1218
  logs: []
821
1219
  };
822
1220
  }
823
1221
  else if (node.type === "user_input") {
1222
+ const inputJson = Object.keys(parsedInput.context).length > 0
1223
+ ? {
1224
+ message: parsedInput.userInput || "",
1225
+ inputs: Object.fromEntries(publicInputValues),
1226
+ context: parsedInput.context
1227
+ }
1228
+ : {
1229
+ message: parsedInput.userInput || "",
1230
+ inputs: Object.fromEntries(publicInputValues)
1231
+ };
824
1232
  resolved = {
825
1233
  text: parsedInput.userInput || "",
826
- json: Object.keys(parsedInput.context).length > 0 ? parsedInput.context : null,
1234
+ json: inputJson,
827
1235
  tools: [],
828
1236
  conversationId: activeConversation?.id ?? null,
829
- outputMap: buildOutputMap(parsedInput.userInput || "", Object.keys(parsedInput.context).length > 0 ? parsedInput.context : null, Object.keys(parsedInput.context ?? {})),
1237
+ outputMap: buildOutputMap(parsedInput.userInput || "", inputJson, defaultOutputsForNode(node)),
830
1238
  logs: []
831
1239
  };
832
1240
  }
@@ -840,11 +1248,23 @@ async function executeConnector(connector, rawInput, services) {
840
1248
  .filter((entry) => Boolean(entry) && typeof entry === "object"));
841
1249
  resolved = {
842
1250
  text: mergedText,
843
- json: Object.keys(mergedJson).length > 0 ? mergedJson : null,
1251
+ json: Object.keys(mergedJson).length > 0
1252
+ ? {
1253
+ merged: mergedJson
1254
+ }
1255
+ : {
1256
+ merged: mergedText
1257
+ },
844
1258
  tools: upstream.flatMap((entry) => entry.sourceValue.tools),
845
1259
  conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
846
1260
  .conversationId ?? null,
847
- outputMap: buildOutputMap(mergedText, Object.keys(mergedJson).length > 0 ? mergedJson : null, Object.keys(mergedJson)),
1261
+ outputMap: buildOutputMap(mergedText, Object.keys(mergedJson).length > 0
1262
+ ? {
1263
+ merged: mergedJson
1264
+ }
1265
+ : {
1266
+ merged: mergedText
1267
+ }, defaultOutputsForNode(node)),
848
1268
  logs: []
849
1269
  };
850
1270
  }
@@ -855,11 +1275,17 @@ async function executeConnector(connector, rawInput, services) {
855
1275
  .replaceAll("{{json}}", primary.json ? JSON.stringify(primary.json) : "");
856
1276
  resolved = {
857
1277
  text: rendered,
858
- json: tryParseJsonObject(rendered),
1278
+ json: {
1279
+ rendered,
1280
+ ...(tryParseJsonObject(rendered) ?? {})
1281
+ },
859
1282
  tools: [],
860
1283
  conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
861
1284
  .conversationId ?? null,
862
- outputMap: buildOutputMap(rendered, tryParseJsonObject(rendered)),
1285
+ outputMap: buildOutputMap(rendered, {
1286
+ rendered,
1287
+ ...(tryParseJsonObject(rendered) ?? {})
1288
+ }, defaultOutputsForNode(node)),
863
1289
  logs: []
864
1290
  };
865
1291
  }
@@ -874,26 +1300,37 @@ async function executeConnector(connector, rawInput, services) {
874
1300
  : null;
875
1301
  resolved = {
876
1302
  text: coerceText(selectedValue),
877
- json: selectedJson,
1303
+ json: selectedJson ?? {
1304
+ selected: selectedValue
1305
+ },
878
1306
  tools: [],
879
1307
  conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
880
1308
  .conversationId ?? null,
881
- outputMap: buildOutputMap(coerceText(selectedValue), selectedJson),
1309
+ outputMap: buildOutputMap(coerceText(selectedValue), selectedJson ?? {
1310
+ selected: selectedValue
1311
+ }, defaultOutputsForNode(node)),
882
1312
  logs: []
883
1313
  };
884
1314
  }
885
1315
  else if (node.type === "output") {
886
- const mergedText = upstream
887
- .map((entry) => entry.selected.text)
1316
+ const outputHandle = node.data.outputKey?.trim() || null;
1317
+ const publishedSelections = upstream.map((entry) => readOutputSelection(entry.sourceValue, outputHandle ?? entry.edge.sourceHandle));
1318
+ const mergedText = publishedSelections
1319
+ .map((entry) => entry.text)
888
1320
  .filter(Boolean)
889
1321
  .join("\n\n");
1322
+ const leadSelection = publishedSelections[0] ?? { text: mergedText, json: null };
1323
+ const publishedKey = outputHandle || "result";
1324
+ const publishedJson = leadSelection.json ?? {
1325
+ [publishedKey]: leadSelection.text
1326
+ };
890
1327
  resolved = {
891
1328
  text: mergedText,
892
- json: upstream[0]?.selected.json ?? null,
1329
+ json: publishedJson,
893
1330
  tools: [],
894
1331
  conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
895
1332
  .conversationId ?? null,
896
- outputMap: buildOutputMap(mergedText, upstream[0]?.selected.json ?? null, Object.keys(upstream[0]?.selected.json ?? {})),
1333
+ outputMap: buildOutputMap(mergedText, publishedJson, defaultOutputsForNode(node)),
897
1334
  logs: []
898
1335
  };
899
1336
  }
@@ -908,41 +1345,50 @@ async function executeConnector(connector, rawInput, services) {
908
1345
  tools: entry.sourceValue.tools,
909
1346
  conversationId: entry.sourceValue.conversationId,
910
1347
  outputMap: entry.sourceValue.outputMap,
911
- logs: entry.sourceValue.logs
1348
+ logs: entry.sourceValue.logs,
1349
+ targetHandle: entry.edge.targetHandle ?? null
912
1350
  })),
913
1351
  services,
914
1352
  conversation: activeConversation
915
1353
  });
916
- const outputKeys = (node.data.outputs ?? []).map((port) => port.key);
1354
+ const outputDefs = defaultOutputsForNode(node);
917
1355
  resolved = {
918
1356
  text: modelResult.text,
919
1357
  json: modelResult.json,
920
1358
  tools: [],
921
1359
  conversationId: modelResult.conversationId,
922
- outputMap: buildOutputMap(modelResult.text, modelResult.json, outputKeys),
1360
+ outputMap: buildOutputMap(modelResult.text, modelResult.json, outputDefs),
923
1361
  logs: modelResult.logs
924
1362
  };
1363
+ nodeToolKeys = modelResult.availableTools;
925
1364
  }
926
1365
  values.set(nodeId, resolved);
927
1366
  debugNodes.push({
928
1367
  nodeId: node.id,
929
1368
  nodeType: node.type,
930
1369
  label: node.data.label,
931
- input: upstream.map((entry) => ({
932
- sourceNodeId: entry.edge.source,
933
- sourceHandle: entry.edge.sourceHandle ?? null,
934
- targetHandle: entry.edge.targetHandle ?? null,
935
- text: entry.selected.text,
936
- json: entry.selected.json
937
- })),
1370
+ input: resolvedInputsForDebug,
938
1371
  output: {
939
1372
  text: resolved.text,
940
1373
  json: resolved.json
941
1374
  },
942
- tools: resolved.tools.map((tool) => tool.key),
1375
+ tools: nodeToolKeys,
943
1376
  logs: resolved.logs,
944
1377
  error: null
945
1378
  });
1379
+ nodeResults.push({
1380
+ nodeId: node.id,
1381
+ nodeType: node.type,
1382
+ label: node.data.label,
1383
+ input: resolvedInputsForDebug,
1384
+ primaryText: resolved.text,
1385
+ payload: resolved.json,
1386
+ outputMap: resolved.outputMap,
1387
+ tools: nodeToolKeys,
1388
+ logs: resolved.logs,
1389
+ error: null,
1390
+ timingMs: Date.now() - startedAt
1391
+ });
946
1392
  return resolved;
947
1393
  };
948
1394
  try {
@@ -955,7 +1401,7 @@ async function executeConnector(connector, rawInput, services) {
955
1401
  throw error;
956
1402
  }
957
1403
  const result = aiConnectorRunResultSchema.parse({
958
- ...buildOutputResult(connector, values),
1404
+ ...buildOutputResult(connector, values, nodeResults),
959
1405
  debugTrace: parsedInput.debug
960
1406
  ? {
961
1407
  nodes: debugNodes,
@@ -1055,7 +1501,7 @@ function migrateLegacyProcessor(processorId) {
1055
1501
  data: {
1056
1502
  label: "Output",
1057
1503
  description: "Imported legacy output.",
1058
- outputKey: "primary",
1504
+ outputKey: "answer",
1059
1505
  enabledToolKeys: []
1060
1506
  }
1061
1507
  };
@@ -1115,13 +1561,13 @@ export function createAiConnector(input) {
1115
1561
  const now = new Date().toISOString();
1116
1562
  const id = `aic_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
1117
1563
  const slug = buildConnectorSlug(parsed.title, id);
1118
- const graph = parsed.graph.nodes.length > 0 ? parsed.graph : buildDefaultGraph(parsed.kind, parsed.title);
1564
+ const graph = normalizeConnectorGraph(parsed.graph.nodes.length > 0 ? parsed.graph : buildDefaultGraph(parsed.kind, parsed.title));
1119
1565
  const publishedOutputs = ensurePublishedOutputs(id, graph);
1120
1566
  getDatabase()
1121
1567
  .prepare(`INSERT INTO ai_connectors (
1122
- id, slug, title, description, kind, home_surface_id, endpoint_enabled, graph_json, published_outputs_json, last_run_json, legacy_processor_id, created_at, updated_at
1123
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1124
- .run(id, slug, parsed.title, parsed.description, parsed.kind, parsed.homeSurfaceId, parsed.endpointEnabled ? 1 : 0, JSON.stringify(graph), JSON.stringify(publishedOutputs), null, input.legacyProcessorId ?? null, now, now);
1568
+ id, slug, title, description, kind, home_surface_id, endpoint_enabled, graph_json, public_inputs_json, published_outputs_json, last_run_json, legacy_processor_id, created_at, updated_at
1569
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1570
+ .run(id, slug, parsed.title, parsed.description, parsed.kind, parsed.homeSurfaceId, parsed.endpointEnabled ? 1 : 0, JSON.stringify(graph), JSON.stringify(parsed.publicInputs), JSON.stringify(publishedOutputs), null, input.legacyProcessorId ?? null, now, now);
1125
1571
  return getAiConnectorById(id);
1126
1572
  }
1127
1573
  export function updateAiConnector(connectorId, patch) {
@@ -1130,7 +1576,7 @@ export function updateAiConnector(connectorId, patch) {
1130
1576
  return null;
1131
1577
  }
1132
1578
  const parsed = updateAiConnectorSchema.parse(patch);
1133
- const nextGraph = parsed.graph ?? current.graph;
1579
+ const nextGraph = normalizeConnectorGraph(parsed.graph ?? current.graph);
1134
1580
  validateConnectorGraph(nextGraph);
1135
1581
  const nextTitle = parsed.title ?? current.title;
1136
1582
  const next = {
@@ -1141,14 +1587,15 @@ export function updateAiConnector(connectorId, patch) {
1141
1587
  ? buildConnectorSlug(parsed.title, current.id)
1142
1588
  : current.slug,
1143
1589
  graph: nextGraph,
1590
+ publicInputs: parsed.publicInputs ?? current.publicInputs,
1144
1591
  publishedOutputs: ensurePublishedOutputs(current.id, nextGraph)
1145
1592
  };
1146
1593
  const now = new Date().toISOString();
1147
1594
  getDatabase()
1148
1595
  .prepare(`UPDATE ai_connectors
1149
- SET slug = ?, title = ?, description = ?, kind = ?, home_surface_id = ?, endpoint_enabled = ?, graph_json = ?, published_outputs_json = ?, updated_at = ?
1596
+ SET slug = ?, title = ?, description = ?, kind = ?, home_surface_id = ?, endpoint_enabled = ?, graph_json = ?, public_inputs_json = ?, published_outputs_json = ?, updated_at = ?
1150
1597
  WHERE id = ?`)
1151
- .run(next.slug, next.title, next.description, next.kind, next.homeSurfaceId, next.endpointEnabled ? 1 : 0, JSON.stringify(next.graph), JSON.stringify(next.publishedOutputs), now, connectorId);
1598
+ .run(next.slug, next.title, next.description, next.kind, next.homeSurfaceId, next.endpointEnabled ? 1 : 0, JSON.stringify(next.graph), JSON.stringify(next.publicInputs), JSON.stringify(next.publishedOutputs), now, connectorId);
1152
1599
  return getAiConnectorById(connectorId);
1153
1600
  }
1154
1601
  export function deleteAiConnector(connectorId) {
@@ -1170,6 +1617,7 @@ export async function runAiConnector(connectorId, input, services, mode = "run")
1170
1617
  mode,
1171
1618
  status: "running",
1172
1619
  userInput: input.userInput ?? "",
1620
+ inputs: input.inputs ?? {},
1173
1621
  context: input.context ?? {},
1174
1622
  conversationId: input.conversationId ?? null,
1175
1623
  result: null,