langflower 0.0.9 → 0.1.0

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 (223) hide show
  1. package/README.md +96 -70
  2. package/docs/public/README.md +2 -2
  3. package/docs/public/configuration.md +4 -0
  4. package/docs/public/extending.md +18 -2
  5. package/docs/public/getting-started.md +7 -0
  6. package/docs/public/how-it-works.md +18 -11
  7. package/docs/public/product.md +35 -25
  8. package/package.json +2 -2
  9. package/ui-dist/chunk-QI4IYILJ.js +4 -0
  10. package/ui-dist/{chunk-7UMVYN3A.js → chunk-UG3GFV4Y.js} +1 -1
  11. package/ui-dist/index.html +2 -2
  12. package/ui-dist/main-ODIDCCNX.js +315 -0
  13. package/ui-dist/styles-2JMBAQC3.css +1 -0
  14. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.js +0 -12
  15. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.d.ts +2 -7
  16. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.js +1 -1
  17. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.d.ts +0 -3
  18. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.js +22 -36
  19. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.d.ts +1 -7
  20. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.js +12 -35
  21. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.d.ts +5 -12
  22. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.js +48 -21
  23. package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.d.ts +11 -0
  24. package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.js +2 -2
  25. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.d.ts +0 -3
  26. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.js +2 -15
  27. package/vendor/common-nodes/dist/ai/features/run-host-services.d.ts +10 -0
  28. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.d.ts +1 -0
  29. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.js +1 -0
  30. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.d.ts +1 -1
  31. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.js +1 -1
  32. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.d.ts +1 -0
  33. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.js +1 -0
  34. package/vendor/common-nodes/dist/ai/nodes/critique/node.d.ts +3 -3
  35. package/vendor/common-nodes/dist/ai/nodes/critique/node.js +10 -19
  36. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.d.ts +3 -3
  37. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.js +10 -11
  38. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.d.ts +3 -3
  39. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.js +7 -10
  40. package/vendor/common-nodes/dist/ai/nodes/review/node.d.ts +3 -3
  41. package/vendor/common-nodes/dist/ai/nodes/review/node.js +12 -19
  42. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.d.ts +5 -5
  43. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.js +251 -96
  44. package/vendor/common-nodes/dist/catalog.js +12 -0
  45. package/vendor/common-nodes/dist/crawl/crawl/node.js +11 -3
  46. package/vendor/common-nodes/dist/crawl/crawl-tools/node.js +5 -1
  47. package/vendor/common-nodes/dist/crawl/extract-links/node.js +7 -1
  48. package/vendor/common-nodes/dist/crawl/fetch-url/node.js +13 -4
  49. package/vendor/common-nodes/dist/crawl/save-page/node.js +11 -3
  50. package/vendor/common-nodes/dist/embeddings/create-embedding.d.ts +39 -0
  51. package/vendor/common-nodes/dist/embeddings/create-embedding.js +154 -0
  52. package/vendor/common-nodes/dist/embeddings/embed-provider/node.d.ts +53 -0
  53. package/vendor/common-nodes/dist/embeddings/embed-provider/node.js +126 -0
  54. package/vendor/common-nodes/dist/embeddings/embed-similarity/node.d.ts +20 -0
  55. package/vendor/common-nodes/dist/embeddings/embed-similarity/node.js +81 -0
  56. package/vendor/common-nodes/dist/embeddings/embed-text/node.d.ts +53 -0
  57. package/vendor/common-nodes/dist/embeddings/embed-text/node.js +91 -0
  58. package/vendor/common-nodes/dist/embeddings/from-embedding.d.ts +8 -0
  59. package/vendor/common-nodes/dist/embeddings/from-embedding.js +24 -0
  60. package/vendor/common-nodes/dist/embeddings/resolve-embedding-provider-model.d.ts +11 -0
  61. package/vendor/common-nodes/dist/embeddings/resolve-embedding-provider-model.js +8 -0
  62. package/vendor/common-nodes/dist/flow/checkpoint/node.js +7 -3
  63. package/vendor/common-nodes/dist/flow/delay/node.d.ts +2 -6
  64. package/vendor/common-nodes/dist/flow/delay/node.js +13 -9
  65. package/vendor/common-nodes/dist/flow/loop/node.js +7 -1
  66. package/vendor/common-nodes/dist/flow/merge/node.js +7 -1
  67. package/vendor/common-nodes/dist/flow/repeat/node.js +5 -1
  68. package/vendor/common-nodes/dist/flow/router/node.js +7 -1
  69. package/vendor/common-nodes/dist/hitl/chat-input/node.d.ts +2 -2
  70. package/vendor/common-nodes/dist/hitl/chat-input/node.js +8 -4
  71. package/vendor/common-nodes/dist/hitl/review-gate/node.js +11 -5
  72. package/vendor/common-nodes/dist/langflower-tools/node.js +5 -1
  73. package/vendor/common-nodes/dist/logic/assert/node.js +7 -1
  74. package/vendor/common-nodes/dist/logic/compare/node.js +7 -1
  75. package/vendor/common-nodes/dist/logic/gate/node.js +7 -1
  76. package/vendor/common-nodes/dist/logic/if/node.js +7 -1
  77. package/vendor/common-nodes/dist/logic/switch/node.js +7 -1
  78. package/vendor/common-nodes/dist/mcp/mcp-http/node.d.ts +4 -4
  79. package/vendor/common-nodes/dist/mcp/mcp-http/node.js +61 -60
  80. package/vendor/common-nodes/dist/mcp/mcp-stdio/node.d.ts +4 -4
  81. package/vendor/common-nodes/dist/mcp/mcp-stdio/node.js +58 -57
  82. package/vendor/common-nodes/dist/memory/memory-tools/node.js +5 -1
  83. package/vendor/common-nodes/dist/output/finish/node.js +13 -1
  84. package/vendor/common-nodes/dist/output/preview/node.js +9 -2
  85. package/vendor/common-nodes/dist/primitives/boolean/node.js +7 -1
  86. package/vendor/common-nodes/dist/primitives/number/node.js +7 -1
  87. package/vendor/common-nodes/dist/primitives/string/node.js +7 -1
  88. package/vendor/common-nodes/dist/primitives/string-multiline/node.d.ts +17 -0
  89. package/vendor/common-nodes/dist/primitives/string-multiline/node.js +31 -0
  90. package/vendor/common-nodes/dist/text/append-file/node.js +11 -3
  91. package/vendor/common-nodes/dist/text/concat/node.js +5 -1
  92. package/vendor/common-nodes/dist/text/read-file/node.js +9 -3
  93. package/vendor/common-nodes/dist/text/split-paced/node.d.ts +21 -0
  94. package/vendor/common-nodes/dist/text/split-paced/node.js +93 -0
  95. package/vendor/common-nodes/dist/text/write-file/node.js +11 -3
  96. package/vendor/common-nodes/dist/tools/collect-agent-tool-handles.d.ts +4 -4
  97. package/vendor/common-nodes/dist/tools/collect-agent-tool-handles.js +3 -11
  98. package/vendor/common-nodes/dist/tools/inventory-tool-round.d.ts +0 -10
  99. package/vendor/common-nodes/dist/tools/inventory-tool-round.js +0 -84
  100. package/vendor/common-nodes/dist/tools/tool-collection/node.d.ts +20 -0
  101. package/vendor/common-nodes/dist/tools/tool-collection/node.js +45 -0
  102. package/vendor/common-nodes/package.json +5 -5
  103. package/vendor/compiler/package.json +1 -1
  104. package/vendor/node-sdk/dist/node-factory/define-embed/embed-handle.d.ts +25 -0
  105. package/vendor/node-sdk/dist/node-factory/define-embed/embed-handle.js +25 -0
  106. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.d.ts +3 -7
  107. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.js +4 -44
  108. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.d.ts +3 -5
  109. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.js +3 -5
  110. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.d.ts +1 -0
  111. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.js +1 -0
  112. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.d.ts +4 -4
  113. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.js +2 -1
  114. package/vendor/node-sdk/dist/node-factory/define-node/define-node.d.ts +2 -0
  115. package/vendor/node-sdk/dist/node-factory/define-node/define-node.js +4 -1
  116. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.d.ts +3 -1
  117. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.js +2 -1
  118. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.d.ts +15 -0
  119. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.js +17 -1
  120. package/vendor/node-sdk/dist/node-factory/define-reactive-node/port-meta.d.ts +17 -4
  121. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/delay-node.js +4 -2
  122. package/vendor/node-sdk/dist/node-factory/define-reactive-node/types.d.ts +6 -7
  123. package/vendor/node-sdk/dist/node-factory/define-reactive-node/ui-schema-inference.d.ts +1 -1
  124. package/vendor/node-sdk/dist/testing/create-node-harness.d.ts +30 -0
  125. package/vendor/node-sdk/dist/testing/create-node-harness.js +68 -0
  126. package/vendor/node-sdk/package.json +5 -5
  127. package/vendor/runtime/dist/port-feed-override.d.ts +13 -0
  128. package/vendor/runtime/dist/port-feed-override.js +21 -0
  129. package/vendor/runtime/dist/port-signal-from-response.d.ts +15 -0
  130. package/vendor/runtime/dist/port-signal-from-response.js +48 -0
  131. package/vendor/runtime/dist/runtime-runner.d.ts +8 -3
  132. package/vendor/runtime/dist/runtime-runner.js +102 -81
  133. package/vendor/runtime/dist/runtime.d.ts +3 -2
  134. package/vendor/runtime/dist/runtime.js +1 -1
  135. package/vendor/runtime/dist/testing/workflows/workflow-events.d.ts +2 -0
  136. package/vendor/runtime/dist/testing/workflows/workflow-events.js +7 -5
  137. package/vendor/runtime/dist/types.d.ts +26 -9
  138. package/vendor/runtime/dist/types.js +3 -0
  139. package/vendor/runtime/package.json +1 -1
  140. package/vendor/server/dist/bootstrap/project-bootstrap.service.d.ts +1 -1
  141. package/vendor/server/dist/bootstrap/seed-skeleton.d.ts +2 -2
  142. package/vendor/server/dist/bootstrap/seed-skeleton.js +17 -2
  143. package/vendor/server/dist/bridge/bind-embed-context.d.ts +7 -0
  144. package/vendor/server/dist/bridge/bind-embed-context.js +16 -0
  145. package/vendor/server/dist/bridge/build-execution-context.d.ts +2 -2
  146. package/vendor/server/dist/bridge/build-execution-context.js +8 -4
  147. package/vendor/server/dist/bridge/emit-bootstrap.js +4 -1
  148. package/vendor/server/dist/bridge/get-live-wired-tools.d.ts +3 -2
  149. package/vendor/server/dist/bridge/get-live-wired-tools.js +8 -5
  150. package/vendor/server/dist/bridge/settings-draft-controller.js +7 -3
  151. package/vendor/server/dist/bridge/wire-config-handlers.js +2 -0
  152. package/vendor/server/dist/bridge/wire-editor-handlers.js +14 -0
  153. package/vendor/server/dist/bridge/wire-runner-handlers.js +2 -2
  154. package/vendor/server/dist/bridge/wire-workflow-handlers.js +23 -9
  155. package/vendor/server/dist/checkpoint/run-checkpoint-session.js +3 -3
  156. package/vendor/server/dist/config/langflower-config.service.d.ts +2 -0
  157. package/vendor/server/dist/config/langflower-config.service.js +30 -0
  158. package/vendor/server/dist/session/build-session-bootstrap.js +1 -0
  159. package/vendor/server/dist/session/langflower-session.d.ts +3 -1
  160. package/vendor/server/dist/session/langflower-session.js +3 -1
  161. package/vendor/server/dist/session/reset-session-execution-feed.d.ts +6 -0
  162. package/vendor/server/dist/session/reset-session-execution-feed.js +8 -0
  163. package/vendor/server/dist/workflow/apply-editor-mutation.js +6 -0
  164. package/vendor/server/dist/workflow/workflow-persisted-inputs.js +8 -6
  165. package/vendor/server/skeleton/instructions.md +15 -3
  166. package/vendor/server/skeleton/nodes/hello-embed/README.md +185 -0
  167. package/vendor/server/skeleton/nodes/hello-embed/hello-embed.nodes.test.ts +181 -0
  168. package/vendor/server/skeleton/nodes/hello-embed/ingest.ts +126 -0
  169. package/vendor/server/skeleton/nodes/hello-embed/lib/chunk-markdown.test.ts +40 -0
  170. package/vendor/server/skeleton/nodes/hello-embed/lib/chunk-markdown.ts +123 -0
  171. package/vendor/server/skeleton/nodes/hello-embed/lib/ingest-search.test.ts +180 -0
  172. package/vendor/server/skeleton/nodes/hello-embed/lib/ingest.ts +128 -0
  173. package/vendor/server/skeleton/nodes/hello-embed/lib/paths.test.ts +20 -0
  174. package/vendor/server/skeleton/nodes/hello-embed/lib/paths.ts +70 -0
  175. package/vendor/server/skeleton/nodes/hello-embed/lib/schema.ts +21 -0
  176. package/vendor/server/skeleton/nodes/hello-embed/lib/search.ts +254 -0
  177. package/vendor/server/skeleton/nodes/hello-embed/lib/text-embedder.ts +14 -0
  178. package/vendor/server/skeleton/nodes/hello-embed/lib/vectors.ts +126 -0
  179. package/vendor/server/skeleton/nodes/hello-embed/lib/walk-markdown.test.ts +62 -0
  180. package/vendor/server/skeleton/nodes/hello-embed/lib/walk-markdown.ts +67 -0
  181. package/vendor/server/skeleton/nodes/hello-embed/package.json +11 -0
  182. package/vendor/server/skeleton/nodes/hello-embed/search-handle.ts +119 -0
  183. package/vendor/server/skeleton/nodes/hello-embed/search.ts +98 -0
  184. package/vendor/server/skeleton/nodes/hello-embed/tsconfig.json +14 -0
  185. package/vendor/server/skeleton/nodes/my-nodes/README.md +13 -3
  186. package/vendor/server/skeleton/nodes/my-nodes/package.json +1 -1
  187. package/vendor/server/skeleton/schemas/langflower-config.schema.json +4 -0
  188. package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +125 -38
  189. package/vendor/server/skeleton/skills/langflower-helper/architecture.md +17 -6
  190. package/vendor/server/skeleton/skills/langflower-helper/layout.md +24 -17
  191. package/vendor/server/skeleton/skills/langflower-node-writer/SKILL.md +8 -1
  192. package/vendor/server/skeleton/skills/langflower-workflow-writer/SKILL.md +12 -8
  193. package/vendor/server/skeleton/workflows/kb-create.json +15 -43
  194. package/vendor/server/skeleton/workflows/kb-ingest.json +88 -0
  195. package/vendor/server/skeleton/workflows/kb-manual-search.json +105 -0
  196. package/vendor/server/skeleton/workflows/kb-navigate.json +8 -22
  197. package/vendor/server/skeleton/workflows/kb-rag.json +192 -0
  198. package/vendor/server/skeleton/workflows/kb-tool.json +111 -0
  199. package/vendor/server/skeleton/workflows/simple-coder.json +18 -46
  200. package/vendor/server/skeleton/workflows/starter.json +8 -22
  201. package/vendor/shared/dist/execution/derive-run-settle-outcome.js +2 -2
  202. package/vendor/shared/dist/execution/derive-run-settle-outcome.test.js +11 -8
  203. package/vendor/shared/dist/langflower-bus-config.d.ts +22 -6
  204. package/vendor/shared/dist/langflower-bus-config.js +23 -7
  205. package/vendor/shared/dist/langflower-config/merge-langflower-config-layers.test.js +8 -0
  206. package/vendor/shared/dist/langflower-config/settings-draft.d.ts +5 -0
  207. package/vendor/shared/dist/langflower-config/settings-draft.js +15 -7
  208. package/vendor/shared/dist/langflower-ws-waits.d.ts +3 -1
  209. package/vendor/shared/dist/langflower-ws-waits.js +2 -2
  210. package/vendor/shared/dist/langflower.d.ts +1 -1
  211. package/vendor/shared/dist/langflower.js +1 -1
  212. package/vendor/shared/dist/types/langflower-bootstrap.d.ts +6 -0
  213. package/vendor/shared/dist/types/langflower-config.d.ts +17 -0
  214. package/vendor/shared/dist/types/langflower-palette.d.ts +2 -1
  215. package/vendor/tools/dist/harness-types.d.ts +1 -1
  216. package/vendor/tools/dist/mcp/build-mcp-handle.d.ts +3 -21
  217. package/vendor/tools/dist/mcp/build-mcp-handle.js +2 -11
  218. package/vendor/tools/dist/mcp/create-system-mcp-handles.d.ts +9 -6
  219. package/vendor/tools/dist/mcp/create-system-mcp-handles.js +21 -17
  220. package/vendor/tools/package.json +1 -0
  221. package/ui-dist/chunk-MDGXX73Z.js +0 -4
  222. package/ui-dist/main-2JJGCNTD.js +0 -313
  223. package/ui-dist/styles-EOXODOI7.css +0 -1
@@ -1,13 +1,14 @@
1
1
  import { defineLlmNode, isSteerControlPayload, toLlmRecoveryPortValue, } from '@langflower/node-sdk/llm';
2
- import { concat, EMPTY, filter, mergeMap, Observable, of, } from 'rxjs';
2
+ import { TOOL_HANDLE_WIRE_TYPE, } from '@langflower/node-sdk';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { catchError, concat, concatMap, filter, firstValueFrom, map, mergeMap, Observable, of, Subject, take, tap, TimeoutError, } from 'rxjs';
5
+ import { statefulObservable } from '@rx-evo/stateful-observable';
3
6
  import { llmPanelUiSchema } from '../../features/ui-schema/llm-panel-ui-schema.js';
4
7
  import { llmCompactionUiSchema } from '../../features/ui-schema/llm-compaction-ui-schema.js';
5
8
  import { llmRecoveryUiSchema } from '../../features/ui-schema/llm-recovery-ui-schema.js';
6
9
  import { AGENT_MAX_ITERATIONS_CAP, DEFAULT_AGENT_MAX_ITERATIONS, normalizeMaxIterations, } from '../../features/prompt/normalize-max-iterations.js';
7
- import { appendToolInventory, createLlmSessionCycle$, demuxByKind, } from '../../features/llm-session/llm-session-shell.js';
10
+ import { appendToolInventory, demuxByKind, } from '../../features/llm-session/llm-session-shell.js';
8
11
  import { runAgentLoop, } from '../../features/llm-loop/run-agent-loop.js';
9
- import { waitForSubagentResult } from '../../features/wait-for-subagent-result.js';
10
- import { flattenSubAgentRegistrations, isSubAgentSpawnPayload, SUBAGENT_REGISTRATION_WIRE_TYPE, SUBAGENT_RESULT_WIRE_TYPE, SUBAGENT_SPAWN_WIRE_TYPE, } from '../../features/sub-agent-protocol.js';
11
12
  import { collectAgentToolHandles } from '../../../tools/collect-agent-tool-handles.js';
12
13
  import { parseLlmRolePreset, resolveEffectiveSkillId, } from '../../features/llm-role-preset.js';
13
14
  import { resolveChatProviderModel } from '../../features/prompt/resolve-chat-provider-model.js';
@@ -24,23 +25,59 @@ const normalizeSkillIds = (raw) => {
24
25
  .map((entry) => String(entry ?? '').trim())
25
26
  .filter((entry) => entry.length > 0);
26
27
  };
27
- const acceptSpawn = (raw, nodeId, skillIds) => {
28
- if (!isSubAgentSpawnPayload(raw)) {
29
- return null;
28
+ const formatSpecialistHandleName = (displayName) => `${displayName}(subagent)`;
29
+ const slugToolId = (name, nodeId) => {
30
+ const slug = (raw) => raw
31
+ .replace(/[^a-zA-Z0-9_-]+/g, '_')
32
+ .replace(/^_+|_+$/g, '')
33
+ .slice(0, 64);
34
+ const fromName = slug(name.trim());
35
+ if (fromName.length > 0) {
36
+ return fromName;
30
37
  }
31
- if (raw.nodeId !== nodeId) {
32
- return null;
38
+ const fromNode = slug(nodeId);
39
+ return fromNode.length > 0 ? fromNode : 'subagent';
40
+ };
41
+ const formatSpawnUserContent = (task, skillId) => {
42
+ if (skillId.length === 0) {
43
+ return task;
44
+ }
45
+ return `[skill:${skillId}]\n${task}`;
46
+ };
47
+ const buildSpecialistDescription = (name, description, skillIds) => {
48
+ const parts = [
49
+ `${name} is a canvas Sub-Agent — a separate specialist agent, not a regular tool (not files, shell, or MCP).`,
50
+ 'Call this tool to delegate a task; the specialist runs its own model and tools and returns a text result.',
51
+ 'Do not try to do this specialist work yourself when this tool is the right owner.',
52
+ ];
53
+ if (description.trim().length > 0) {
54
+ parts.push(description.trim());
33
55
  }
34
- if (raw.skillId.length > 0 && !skillIds.includes(raw.skillId)) {
35
- return null;
56
+ if (skillIds.length > 0) {
57
+ parts.push(`Skills: ${skillIds.join(', ')}.`);
36
58
  }
37
- return raw;
59
+ parts.push('Required argument: task — the assignment for this Sub-Agent.');
60
+ return parts.join(' ');
38
61
  };
39
- const formatSpawnUserContent = (spawn) => {
40
- if (spawn.skillId.length === 0) {
41
- return spawn.task;
62
+ const buildSpecialistInputSchema = (skillIds) => {
63
+ const properties = {
64
+ task: {
65
+ type: 'string',
66
+ description: 'Task to delegate to this Sub-Agent specialist',
67
+ },
68
+ };
69
+ if (skillIds.length > 0) {
70
+ properties.skillId = {
71
+ type: 'string',
72
+ enum: [...skillIds],
73
+ description: 'Optional skill announced by this Sub-Agent Inspector selector',
74
+ };
42
75
  }
43
- return `[skill:${spawn.skillId}]\n${spawn.task}`;
76
+ return {
77
+ type: 'object',
78
+ properties,
79
+ required: ['task'],
80
+ };
44
81
  };
45
82
  const resolveTurnFactory = (context) => {
46
83
  if (context.scriptedTurns !== undefined) {
@@ -48,33 +85,78 @@ const resolveTurnFactory = (context) => {
48
85
  }
49
86
  return context.factory;
50
87
  };
51
- const requireChatConfig = (providerId, model, scripted) => {
88
+ const chatConfigError = (providerId, model, scripted) => {
52
89
  if (scripted) {
53
- return;
90
+ return undefined;
54
91
  }
55
92
  if (providerId.trim().length === 0) {
56
- throw new Error('Provider is required for Sub-Agent chat');
93
+ return 'Error: Provider is required for Sub-Agent chat';
57
94
  }
58
95
  if (model.trim().length === 0) {
59
- throw new Error('Model is required for Sub-Agent chat');
96
+ return 'Error: Model is required for Sub-Agent chat';
97
+ }
98
+ return undefined;
99
+ };
100
+ const EMPTY_INVOKE_RESULT = 'Error: Sub-Agent returned no content';
101
+ const finalizeInvokeText = (responseText, lastDraft, lastReasoning) => {
102
+ if (responseText.trim().length > 0) {
103
+ return responseText;
104
+ }
105
+ if (lastDraft.trim().length > 0) {
106
+ return lastDraft;
107
+ }
108
+ if (lastReasoning.trim().length > 0) {
109
+ return lastReasoning;
110
+ }
111
+ return EMPTY_INVOKE_RESULT;
112
+ };
113
+ const seedInvokeHistory = (history, context) => {
114
+ const system = {
115
+ role: 'system',
116
+ content: appendToolInventory(context.effectiveSystemPrompt, context.tools),
117
+ };
118
+ if (history.length === 0) {
119
+ return [system];
120
+ }
121
+ if (history[0]?.role === 'system') {
122
+ return [system, ...history.slice(1)];
60
123
  }
124
+ return [system, ...history];
61
125
  };
62
- const runSubAgentTurn = (context, spawn, history, subagentResult$) => {
126
+ const reduceInvokeHistory = (history, chunk) => {
127
+ if (chunk.kind === 'historySync') {
128
+ return [...chunk.messages];
129
+ }
130
+ if (chunk.kind === 'invokeDone' && chunk.text.trim().length > 0) {
131
+ return [...history, { role: 'assistant', content: chunk.text }];
132
+ }
133
+ return history;
134
+ };
135
+ const runSubAgentTurn = (context, turn, history) => {
136
+ const fail = (text) => of({ kind: 'response', text }, {
137
+ kind: 'invokeDone',
138
+ requestId: turn.requestId,
139
+ text,
140
+ });
63
141
  const scripted = context.scriptedTurns !== undefined;
64
142
  const factory = resolveTurnFactory(context);
65
143
  if (factory === undefined) {
66
- return new Observable((subscriber) => {
67
- subscriber.error(new Error('Sub-Agent chat is only available during server workflow runs'));
68
- });
144
+ return fail('Error: Sub-Agent chat is only available during server workflow runs');
69
145
  }
70
- requireChatConfig(context.providerId, context.model, scripted);
146
+ const configError = chatConfigError(context.providerId, context.model, scripted);
147
+ if (configError !== undefined) {
148
+ return fail(configError);
149
+ }
150
+ const userContent = formatSpawnUserContent(turn.task, turn.skillId);
151
+ let lastDraft = '';
152
+ let lastReasoning = '';
71
153
  return concat(of({
72
154
  kind: 'historySync',
73
155
  messages: [
74
156
  ...history,
75
157
  {
76
158
  role: 'user',
77
- content: formatSpawnUserContent(spawn),
159
+ content: userContent,
78
160
  },
79
161
  ],
80
162
  }), runAgentLoop({
@@ -85,50 +167,106 @@ const runSubAgentTurn = (context, spawn, history, subagentResult$) => {
85
167
  model: scripted
86
168
  ? context.model.trim() || 'scripted'
87
169
  : context.model,
88
- messages: [
89
- ...history,
90
- { role: 'user', content: formatSpawnUserContent(spawn) },
91
- ],
170
+ messages: [...history, { role: 'user', content: userContent }],
92
171
  tools: context.tools,
93
172
  toolCtx: context.toolCtx,
94
173
  maxIterations: context.maxIterations,
95
174
  compaction: context.compaction,
96
175
  recovery: context.recovery,
97
- subagentRegistrations: context.subagentRegistrations,
98
176
  ...(context.requestPermission !== undefined
99
177
  ? { requestPermission: context.requestPermission }
100
178
  : {}),
101
179
  ...(context.steerControl$ !== undefined
102
180
  ? { steerControl$: context.steerControl$ }
103
181
  : {}),
104
- ...(context.subagentRegistrations.length > 0
105
- ? {
106
- waitForSubagentResult: (callId, signal) => waitForSubagentResult(subagentResult$, callId, signal, context.recovery.subagentTimeoutMs),
107
- }
108
- : {}),
109
182
  }).pipe(mergeMap((chunk) => {
183
+ if (chunk.kind === 'reasoning' && chunk.text.length > 0) {
184
+ lastReasoning += chunk.text;
185
+ }
186
+ if (chunk.kind === 'draftResponse' && chunk.text.length > 0) {
187
+ lastDraft += chunk.text;
188
+ }
110
189
  if (chunk.kind !== 'response') {
111
190
  return of(chunk);
112
191
  }
113
- return of(chunk, {
114
- kind: 'subagentResult',
115
- payload: {
116
- callId: spawn.callId,
117
- result: chunk.text,
118
- },
192
+ const text = finalizeInvokeText(chunk.text, lastDraft, lastReasoning);
193
+ return of({ kind: 'response', text }, {
194
+ kind: 'invokeDone',
195
+ requestId: turn.requestId,
196
+ text,
119
197
  });
198
+ }), catchError((error) => {
199
+ const text = error instanceof Error
200
+ ? `Error: ${error.message}`
201
+ : 'Error: Sub-Agent turn failed.';
202
+ return fail(text);
120
203
  })));
121
204
  };
205
+ /** Wall-clock until the inner completes — not `timeout({ first })`, which
206
+ * is satisfied by `historySync` and would never abort a hung generator. */
207
+ const withInvokeDeadline = (source, timeoutMs) => new Observable((subscriber) => {
208
+ const handle = setTimeout(() => {
209
+ subscriber.error(new TimeoutError());
210
+ }, timeoutMs);
211
+ const inner = source.subscribe({
212
+ next: (value) => {
213
+ subscriber.next(value);
214
+ },
215
+ error: (err) => {
216
+ clearTimeout(handle);
217
+ subscriber.error(err);
218
+ },
219
+ complete: () => {
220
+ clearTimeout(handle);
221
+ subscriber.complete();
222
+ },
223
+ });
224
+ return () => {
225
+ clearTimeout(handle);
226
+ inner.unsubscribe();
227
+ };
228
+ });
229
+ const parseInvokeTurn = (raw, skillIds) => {
230
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
231
+ return {
232
+ ok: false,
233
+ text: 'Error: Sub-Agent invoke payload is invalid.',
234
+ };
235
+ }
236
+ const record = raw;
237
+ const requestId = typeof record.requestId === 'string' && record.requestId.length > 0
238
+ ? record.requestId
239
+ : randomUUID();
240
+ const task = typeof record.task === 'string' ? record.task.trim() : '';
241
+ const skillId = typeof record.skillId === 'string' ? record.skillId.trim() : '';
242
+ if (task.length === 0) {
243
+ return {
244
+ ok: false,
245
+ text: 'Error: Sub-Agent requires a non-empty task.',
246
+ };
247
+ }
248
+ if (skillId.length > 0 && !skillIds.includes(skillId)) {
249
+ return {
250
+ ok: false,
251
+ text: `Error: Skill «${skillId}» is not announced by this Sub-Agent.`,
252
+ };
253
+ }
254
+ return { ok: true, turn: { requestId, task, skillId } };
255
+ };
122
256
  /**
123
- * Sub-Agent: ordinary OpenAI-compatible agent plus `registration` announce.
124
- * Parent spawn arrives on `task`; final text returns on `result` with callId.
257
+ * Sub-Agent: ordinary OpenAI-compatible agent that announces one ToolHandle
258
+ * on `subagent-registration`. Parent invoke runs this node's in-node loop.
125
259
  * @see docs/ADR.md ADR-021
126
260
  */
127
261
  export const subAgentNode = defineLlmNode({
128
262
  type: 'common-sub-agent',
129
263
  displayName: 'Sub-Agent',
130
264
  category: 'AI',
131
- description: 'OpenAI-compatible specialist agent that announces via registration and answers parent spawns on task → result.',
265
+ description: `
266
+ A specialist agent on the canvas. Wire it into a parent agent's **tools** so the parent can delegate a task.
267
+
268
+ The specialist streams in its own work-log card. Set name, role, and skills on this node.
269
+ `.trim(),
132
270
  uiSchema: [
133
271
  {
134
272
  field: 'name',
@@ -153,47 +291,31 @@ export const subAgentNode = defineLlmNode({
153
291
  ...llmRecoveryUiSchema,
154
292
  ],
155
293
  bind(ctx, { makeInput, configureOutput, combineInputs }, inventory) {
156
- const { tools, mcp, subagentRegistration, subagentResult, steerControl, } = inventory;
294
+ const { tools, steerControl } = inventory;
157
295
  const systemPrompt = makeInput('systemPrompt', {
158
296
  name: 'systemPrompt',
159
297
  wireType: 'string',
160
298
  inline: 'text-multiline',
161
299
  defaultValue: '',
162
300
  });
163
- const task = makeInput('task', {
164
- name: 'task',
165
- wireType: SUBAGENT_SPAWN_WIRE_TYPE,
166
- required: true,
167
- });
168
- const registration$ = combineInputs([ctx], ([ec]) => {
169
- const skillIds = normalizeSkillIds(ec.params.skillIds);
170
- const name = String(ec.params.name ?? 'Sub-Agent').trim();
171
- const description = String(ec.params.description ?? '');
172
- return {
173
- targetNodeId: ec.nodeId,
174
- name: name.length > 0 ? name : 'Sub-Agent',
175
- description,
176
- skills: skillIds.map((skillId) => ({
177
- skillId,
178
- description: skillId,
179
- })),
180
- };
181
- });
182
- const context$ = combineInputs([systemPrompt, tools, subagentRegistration, mcp, ctx], ([systemPromptValue, toolList, subagentList, mcpList, ec]) => {
301
+ const invoke$ = new Subject();
302
+ let latestContext;
303
+ let history = [];
304
+ let invokeTail = Promise.resolve();
305
+ const context$ = combineInputs([systemPrompt, tools, ctx], ([systemPromptValue, toolList, ec]) => {
183
306
  const rolePreset = parseLlmRolePreset(ec.params.rolePreset);
184
307
  const skillId = resolveEffectiveSkillId(rolePreset, ec.params.skillId);
185
308
  const hostServices = getRunHostServices(ec);
186
309
  const skillMarkdown = hostServices?.skillMarkdown ?? '';
187
310
  const agentsMarkdown = hostServices?.agentsMarkdown ?? '';
311
+ const name = String(ec.params.name ?? 'Sub-Agent').trim();
312
+ const inspectorDescription = String(ec.params.description ?? '');
188
313
  return {
189
314
  prompt: '',
190
315
  tools: collectAgentToolHandles({
191
316
  toolHandles: ec.toolHandles,
192
317
  toolsPort: toolList,
193
- mcpHandles: ec.mcpHandles,
194
- mcpPort: mcpList,
195
318
  }),
196
- subagentRegistrations: flattenSubAgentRegistrations(subagentList),
197
319
  ...resolveChatProviderModel(ec.params, hostServices),
198
320
  skillId,
199
321
  rolePreset,
@@ -234,26 +356,67 @@ export const subAgentNode = defineLlmNode({
234
356
  factory: hostServices?.createChatCompletionStream,
235
357
  scriptedTurns: parseScriptedTurns(ec.params['scriptedToolTurns']),
236
358
  nodeId: ec.nodeId,
359
+ displayName: name.length > 0 ? name : 'Sub-Agent',
360
+ inspectorDescription,
237
361
  skillIds: normalizeSkillIds(ec.params.skillIds),
238
362
  };
239
363
  });
240
- const cycle$ = createLlmSessionCycle$(context$, task.value$, (context) => ({
241
- history: [
364
+ // Loader input is invoke start, not the chunk Subject: wrapping
365
+ // chunks$ would re-pending on every token (demux fans loading).
366
+ const cycle$ = statefulObservable({
367
+ input: invoke$,
368
+ mapOperator: concatMap,
369
+ loader: ({ context, turn }) => {
370
+ history = seedInvokeHistory(history, context);
371
+ const timeoutMs = context.recovery.subagentTimeoutMs;
372
+ const failTimedOut = (text) => of({ kind: 'response', text }, {
373
+ kind: 'invokeDone',
374
+ requestId: turn.requestId,
375
+ text,
376
+ });
377
+ const inner$ = runSubAgentTurn(context, turn, history).pipe(tap((chunk) => {
378
+ history = reduceInvokeHistory(history, chunk);
379
+ }));
380
+ if (timeoutMs <= 0) {
381
+ return inner$;
382
+ }
383
+ return withInvokeDeadline(inner$, timeoutMs).pipe(catchError((error) => failTimedOut(error instanceof TimeoutError
384
+ ? `Error: Sub-Agent timed out after ${timeoutMs}ms.`
385
+ : error instanceof Error
386
+ ? `Error: ${error.message}`
387
+ : 'Error: Sub-Agent turn failed.')));
388
+ },
389
+ });
390
+ const enqueueInvoke = (announced, args, _toolCtx) => {
391
+ const context = latestContext ?? announced;
392
+ const parsed = parseInvokeTurn({ ...args, requestId: randomUUID() }, announced.skillIds);
393
+ if (!parsed.ok) {
394
+ return Promise.resolve(parsed.text);
395
+ }
396
+ const run = () => {
397
+ const result = firstValueFrom(cycle$.value$.pipe(filter((chunk) => chunk.kind === 'invokeDone' &&
398
+ chunk.requestId === parsed.turn.requestId), map((chunk) => chunk.text), take(1)), { defaultValue: EMPTY_INVOKE_RESULT });
399
+ invoke$.next({ context, turn: parsed.turn });
400
+ return result;
401
+ };
402
+ const next = invokeTail.then(run, run);
403
+ invokeTail = next.then(() => undefined, () => undefined);
404
+ return next;
405
+ };
406
+ const specialistTools$ = context$.pipeValue(tap((context) => {
407
+ latestContext = context;
408
+ }), map((context) => {
409
+ const handleName = formatSpecialistHandleName(context.displayName);
410
+ return [
242
411
  {
243
- role: 'system',
244
- content: appendToolInventory(context.effectiveSystemPrompt, context.tools),
412
+ toolId: slugToolId(handleName, context.nodeId),
413
+ name: handleName,
414
+ description: buildSpecialistDescription(handleName, context.inspectorDescription, context.skillIds),
415
+ inputSchema: buildSpecialistInputSchema(context.skillIds),
416
+ invoke: (args, toolCtx) => enqueueInvoke(context, args, toolCtx),
245
417
  },
246
- ],
247
- trackAssistantHistory: true,
248
- appendUserFeedbackToHistory: false,
249
- session: undefined,
250
- }), (context, turnPayload, history, _session) => {
251
- const spawn = acceptSpawn(turnPayload, context.nodeId, context.skillIds);
252
- if (spawn === null) {
253
- return EMPTY;
254
- }
255
- return runSubAgentTurn(context, spawn, history, subagentResult.value$);
256
- }, { primeTurn0: false });
418
+ ];
419
+ }));
257
420
  const reasoning$ = cycle$.pipeValue(demuxByKind('reasoning', (chunk) => chunk
258
421
  .text));
259
422
  const draftResponse$ = cycle$.pipeValue(demuxByKind('draftResponse', (chunk) => chunk
@@ -265,14 +428,12 @@ export const subAgentNode = defineLlmNode({
265
428
  }));
266
429
  const response$ = cycle$.pipeValue(demuxByKind('response', (chunk) => chunk
267
430
  .text));
268
- const result$ = cycle$.pipeValue(demuxByKind('subagentResult', (chunk) => chunk.payload));
269
- const nestedSubagent$ = cycle$.pipeValue(demuxByKind('subagentSpawn', (chunk) => chunk
270
- .payload));
271
431
  return {
272
- inputs: [systemPrompt, task],
432
+ inputs: [systemPrompt],
273
433
  outputs: [
274
- configureOutput('registration', registration$, {
275
- wireType: SUBAGENT_REGISTRATION_WIRE_TYPE,
434
+ configureOutput('subagent-registration', specialistTools$, {
435
+ name: 'subagent-registration',
436
+ wireType: TOOL_HANDLE_WIRE_TYPE,
276
437
  }),
277
438
  configureOutput('reasoning', reasoning$, {
278
439
  wireType: 'string',
@@ -286,16 +447,10 @@ export const subAgentNode = defineLlmNode({
286
447
  wireType: 'string',
287
448
  feed: { role: 'result' },
288
449
  }),
289
- configureOutput('result', result$, {
290
- wireType: SUBAGENT_RESULT_WIRE_TYPE,
291
- // Protocol payload for parent wiring — not work-log content.
292
- feed: { role: 'none' },
293
- }),
294
450
  ],
295
451
  inventoryOutputs: {
296
452
  toolLog$,
297
453
  recovery$,
298
- subagent$: nestedSubagent$,
299
454
  },
300
455
  };
301
456
  },
@@ -23,9 +23,11 @@ import { previewNode } from './output/preview/node.js';
23
23
  import { booleanNode } from './primitives/boolean/node.js';
24
24
  import { numberNode } from './primitives/number/node.js';
25
25
  import { stringNode } from './primitives/string/node.js';
26
+ import { stringMultilineNode } from './primitives/string-multiline/node.js';
26
27
  import { appendFileNode } from './text/append-file/node.js';
27
28
  import { concatNode } from './text/concat/node.js';
28
29
  import { readFileNode } from './text/read-file/node.js';
30
+ import { splitPacedNode } from './text/split-paced/node.js';
29
31
  import { writeFileNode } from './text/write-file/node.js';
30
32
  import { crawlNode } from './crawl/crawl/node.js';
31
33
  import { crawlToolsNode } from './crawl/crawl-tools/node.js';
@@ -34,10 +36,15 @@ import { fetchUrlNode } from './crawl/fetch-url/node.js';
34
36
  import { savePageNode } from './crawl/save-page/node.js';
35
37
  import { memoryToolsNode } from './memory/memory-tools/node.js';
36
38
  import { langflowerToolsNode } from './langflower-tools/node.js';
39
+ import { toolCollectionNode } from './tools/tool-collection/node.js';
40
+ import { embedTextNode } from './embeddings/embed-text/node.js';
41
+ import { embedSimilarityNode } from './embeddings/embed-similarity/node.js';
42
+ import { embedProviderNode } from './embeddings/embed-provider/node.js';
37
43
  const COMMON_REACTIVE_NODES = [
38
44
  booleanNode,
39
45
  numberNode,
40
46
  stringNode,
47
+ stringMultilineNode,
41
48
  mergeNode,
42
49
  delayNode,
43
50
  checkpointNode,
@@ -47,6 +54,7 @@ const COMMON_REACTIVE_NODES = [
47
54
  previewNode,
48
55
  finishNode,
49
56
  concatNode,
57
+ splitPacedNode,
50
58
  readFileNode,
51
59
  writeFileNode,
52
60
  appendFileNode,
@@ -66,11 +74,15 @@ const COMMON_REACTIVE_NODES = [
66
74
  switchNode,
67
75
  memoryToolsNode,
68
76
  langflowerToolsNode,
77
+ toolCollectionNode,
69
78
  fetchUrlNode,
70
79
  extractLinksNode,
71
80
  savePageNode,
72
81
  crawlNode,
73
82
  crawlToolsNode,
83
+ embedTextNode,
84
+ embedSimilarityNode,
85
+ embedProviderNode,
74
86
  ];
75
87
  const COMMON_REACTIVE_NODE_CATALOG = Object.fromEntries(COMMON_REACTIVE_NODES.map((node) => [node.type, node]));
76
88
  export function getCommonReactiveNodeCatalog() {
@@ -1,4 +1,4 @@
1
- import { defineReactiveNode } from '@langflower/node-sdk';
1
+ import { defineReactiveNode, withLoading } from '@langflower/node-sdk';
2
2
  import { createCrawlContext } from '@langflower/tools/create-crawl-context';
3
3
  import { createWebFetch } from '@langflower/tools/create-web-fetch';
4
4
  import { runBfsCrawl } from '@langflower/tools/run-bfs-crawl';
@@ -24,7 +24,13 @@ export const crawlNode = defineReactiveNode({
24
24
  displayName: 'Crawl',
25
25
  category: 'Crawl',
26
26
  paletteSecondary: true,
27
- description: 'BFS crawl from a seed URL with depth/page limits; saves pages into the crawl run.',
27
+ description: `
28
+ Start from a URL and follow links up to a depth and page cap. Pages are saved as they are visited.
29
+
30
+ Typical uses:
31
+ - Research a site without clicking by hand
32
+ - Collect a small corpus for an LLM
33
+ `.trim(),
28
34
  uiSchema: [
29
35
  {
30
36
  field: 'maxDepth',
@@ -67,7 +73,9 @@ export const crawlNode = defineReactiveNode({
67
73
  const pages$ = combineInputs([startUrl, ctx], ([rawUrl, ec]) => ({
68
74
  startUrl: String(rawUrl ?? '').trim(),
69
75
  ec,
70
- })).pipeValue(mergeMap(({ startUrl: seed, ec }) => {
76
+ }))
77
+ .pipe(withLoading())
78
+ .pipeValue(mergeMap(({ startUrl: seed, ec }) => {
71
79
  if (seed.length === 0) {
72
80
  return throwError(() => new Error('Crawl requires a non-empty startUrl.'));
73
81
  }
@@ -8,6 +8,10 @@ export const crawlToolsNode = defineToolRegistrations({
8
8
  type: 'common-crawl-tools',
9
9
  displayName: 'Crawl Tools',
10
10
  category: 'Tools',
11
- description: 'Emits crawl_fetch / crawl_extract_links / crawl_save_page / crawl_bfs with imported handlers for agent inventory.',
11
+ description: `
12
+ Give an agent fetch, extract-links, save-page, and crawl tools.
13
+
14
+ Wire **tools** into an LLM so it can research the web from chat.
15
+ `.trim(),
12
16
  tools: CRAWL_TOOL_CONFIGS,
13
17
  });
@@ -11,7 +11,13 @@ export const extractLinksNode = defineReactiveNode({
11
11
  displayName: 'Extract Links',
12
12
  category: 'Crawl',
13
13
  paletteSecondary: true,
14
- description: 'Resolves `href` values from HTML into an absolute URL list.',
14
+ description: `
15
+ Pull absolute URLs from HTML.
16
+
17
+ Typical uses:
18
+ - List outbound links after Fetch URL
19
+ - Feed those URLs into Crawl or another fetch
20
+ `.trim(),
15
21
  uiSchema: [],
16
22
  bind(_ctx, { makeInput, configureOutput, combineInputs }) {
17
23
  const html = makeInput('html', {
@@ -1,4 +1,4 @@
1
- import { defineReactiveNode } from '@langflower/node-sdk';
1
+ import { defineReactiveNode, withLoading } from '@langflower/node-sdk';
2
2
  import { createWebFetch } from '@langflower/tools/create-web-fetch';
3
3
  import { from, map, mergeMap, throwError } from 'rxjs';
4
4
  import { htmlToText } from '@langflower/tools/html';
@@ -18,7 +18,13 @@ export const fetchUrlNode = defineReactiveNode({
18
18
  displayName: 'Fetch URL',
19
19
  category: 'Crawl',
20
20
  paletteSecondary: true,
21
- description: 'Fetches a URL through the SSRF-guarded harness and emits HTML + plain text.',
21
+ description: `
22
+ Download a page and get HTML plus readable text.
23
+
24
+ Typical uses:
25
+ - Pull an article before Extract Links or an LLM
26
+ - Seed a Crawl
27
+ `.trim(),
22
28
  uiSchema: [
23
29
  {
24
30
  field: 'timeoutMs',
@@ -43,7 +49,9 @@ export const fetchUrlNode = defineReactiveNode({
43
49
  const fetched$ = combineInputs([url, ctx], ([rawUrl, ec]) => ({
44
50
  rawUrl: String(rawUrl ?? '').trim(),
45
51
  ec,
46
- })).pipeValue(mergeMap(({ rawUrl, ec }) => {
52
+ }))
53
+ .pipe(withLoading())
54
+ .pipeValue(mergeMap(({ rawUrl, ec }) => {
47
55
  if (rawUrl.length === 0) {
48
56
  return throwError(() => new Error('Fetch URL requires a non-empty url.'));
49
57
  }
@@ -59,7 +67,8 @@ export const fetchUrlNode = defineReactiveNode({
59
67
  maxBytes,
60
68
  })).pipe(map((result) => {
61
69
  if (!result.ok && result.body.length === 0) {
62
- throw new Error(result.error ?? `Fetch failed for ${rawUrl}`);
70
+ throw new Error(result.error ??
71
+ `Fetch failed for ${rawUrl}`);
63
72
  }
64
73
  return {
65
74
  html: result.body,
@@ -1,4 +1,4 @@
1
- import { defineReactiveNode } from '@langflower/node-sdk';
1
+ import { defineReactiveNode, withLoading } from '@langflower/node-sdk';
2
2
  import { createCrawlContext } from '@langflower/tools/create-crawl-context';
3
3
  import { from, mergeMap, throwError } from 'rxjs';
4
4
  import { extractHtmlTitle } from '@langflower/tools/html';
@@ -12,7 +12,13 @@ export const savePageNode = defineReactiveNode({
12
12
  displayName: 'Save Page',
13
13
  category: 'Crawl',
14
14
  paletteSecondary: true,
15
- description: 'Saves url/html/text into the run crawl directory and emits saved metadata.',
15
+ description: `
16
+ Store a fetched page in this run's crawl folder and emit where it was saved.
17
+
18
+ Typical uses:
19
+ - Keep HTML/text for later review
20
+ - Persist a page before the next crawl hop
21
+ `.trim(),
16
22
  uiSchema: [],
17
23
  bind(ctx, { makeInput, configureOutput, combineInputs }) {
18
24
  const url = makeInput('url', {
@@ -38,7 +44,9 @@ export const savePageNode = defineReactiveNode({
38
44
  html: String(rawHtml ?? ''),
39
45
  text: String(rawText ?? ''),
40
46
  ec,
41
- })).pipeValue(mergeMap(({ url: pageUrl, html: pageHtml, text: pageText, ec }) => {
47
+ }))
48
+ .pipe(withLoading())
49
+ .pipeValue(mergeMap(({ url: pageUrl, html: pageHtml, text: pageText, ec }) => {
42
50
  if (pageUrl.length === 0) {
43
51
  return throwError(() => new Error('Save Page requires a non-empty url.'));
44
52
  }