langflower 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,125 +3,63 @@
3
3
  > **Disclaimer:** Langflower is currently in internal testing. Anyone can
4
4
  > download and try it, but some functions may not be stable.
5
5
 
6
- ## Key concept: everything is a node
7
-
8
- Langflower is not built around the LLM as a first-class citizen. The core
9
- abstraction is a **reactive node**: several typed inputs, several typed
10
- outputs. Each port acts independently a node can receive on one input
11
- and emit on one output at any time, without waiting for the rest. The
12
- runtime wires those nodes into a workflow.
13
-
14
- That is enough for complex processing chains — including loops and
15
- conditions.
16
-
17
- The trick is the same rule applied to models. Because ports fire on their
18
- own, an LLM agent is just another reactive node: prompt and tools in;
19
- response out; streaming tool log, reasoning, and draft as extra outputs
20
- that can update while the run continues. All LLM-specific logic stays as
21
- that node's internal state. It does not leak through the rest of the app.
22
-
23
- ## Open possibilities
24
-
25
- Because every unit of work is a node, Langflower is not pinned to one
26
- product shape — coding harness, chat harness, or any other fixed loop.
27
- Workflows stay flexible. With the right wiring they fit the job.
28
-
29
- Need a chat? Wire user input, an agent, and HITL feedback. Agent-to-agent
30
- dialogue is two agents connected together. Coding is the same graph plus
31
- file-ops tools. Tired of “I’m done” while the code still does not compile?
32
- Write a custom review-gate node. The agent never gets a path around it.
33
-
34
- ## Hard harness
35
-
36
- The sequence is the workflow topology. That is how Langflower orchestrates
37
- complex work: QA, review, and code checks sit on the graph, so agents
38
- cannot skip them. A well-configured workflow forces high-quality output —
39
- the model does not get to declare itself finished.
6
+ Langflower is a local visual workflow for a project folder: a node graph
7
+ that runs agents, tools, checks, and human gates on your machine. You
8
+ author the pipeline on a canvas in the browser you already have. The LLM
9
+ is a node in that graph, not the product — the topology you wire decides
10
+ what happens next. Data, custom nodes, and workflows stay in that folder.
40
11
 
41
12
  **Not another chat harness. A local node graph.**
42
13
 
43
- ### Keep everything local
44
-
45
- Langflower runs on your machine and does not expose your project as a
46
- hosted product. Files stay where they are. You can reproduce what mature
47
- cloud tools offer at home, or on a closed network with internal LLM
48
- providers.
49
-
50
- ### Scoped to a folder
51
-
52
- You start from a folder. Data, custom nodes, and workflows are scoped to
53
- that workspace. Open the folder, and the graph, files, and run belong
54
- together.
55
-
56
- ### Extensible even now
57
-
58
- This is still an early version, but Langflower already supports custom
59
- nodes and custom node packs. Share and reuse workflows and nodes the same
60
- way you share the rest of the project.
14
+ The unit of work is a **reactive node**: typed inputs, typed outputs.
15
+ Each port fires on its own — a node can emit on one output without
16
+ waiting for the rest, so the node chooses the path. That is enough for
17
+ cycles and conditional branches, not only a straight chain. An LLM agent
18
+ is the same kind of node prompt and tools in, response out. Coding,
19
+ chat, agent-to-agent dialogue, and custom gates are the same idea: pick
20
+ the graph, not a fixed loop.
61
21
 
62
22
  ![Langflower starter workflow](https://raw.githubusercontent.com/earthdmitriy/langflower/master/docs/img/starter.png)
63
23
 
64
- ## How it works
65
-
66
- 1. Start Langflower with a project folder.
67
- 2. Open a workflow on the canvas, or create one for the task.
68
- 3. Run it and watch agents, tools, checks, and file changes move through
69
- visible stages.
70
- 4. Approve, reject, or add feedback when the workflow asks for a human
71
- decision.
72
- 5. Find the resulting files and data in the same workspace.
24
+ The default **starter** workflow: an onboarding helper plus a Writer
25
+ sub-agent for workflows and custom nodes.
73
26
 
74
- ## Under the hood
27
+ ![Langflower dev workflow](https://raw.githubusercontent.com/earthdmitriy/langflower/master/docs/img/lf_dev.png)
75
28
 
76
- The core is a reactive runtime that wires nodes together. The **node SDK**
77
- is the public contract: any node that follows it can run on that runtime.
78
- **Common nodes** is the built-in catalog. It uses the same SDK as custom
79
- node packs.
80
-
81
- The **server** composes those pieces, compiles user-defined nodes, and owns
82
- the live run. The **UI** is a thin browser client. It listens to WebSocket
83
- events from the server; it does not own the workflow. Close the tab while a
84
- long run continues. Reopen it, and the server catches the UI up, so the
85
- canvas stays in sync.
86
-
87
- For a short builder-oriented picture, see
88
- [How it works](https://github.com/earthdmitriy/langflower/blob/master/docs/public/how-it-works.md).
29
+ **Langflower dev** the workflow used to develop Langflower itself.
30
+ Memory tools, ts-scan (MCP), and custom **LF Dev Tools** merge in a tool
31
+ collection and fan out to the agents. The prompt passes **lf-review-gate**
32
+ first (format, typecheck, tests) so Plan starts from a green tree. A
33
+ human review gate accepts the plan, then Coder works; the result must
34
+ pass lf-review-gate again, then a human review gate.
89
35
 
90
36
  ## Why Langflower
91
37
 
92
- - **You stay in control.** Workflows can request approval before sensitive
93
- file edits or shell commands run.
94
- - **Use the browser you already have.** Other harnesses pack a web UI into
95
- a built-in browser such as Electron. Langflower uses your existing
96
- browser, so you can close the tab, free those resources, and let the
97
- server keep the run; reopen it and the UI catches up.
98
- - **Extend it when the defaults are not enough.** Langflower uses common
99
- agent primitives — **MCP** and **skills** — and lets you define **custom
100
- nodes** on top: processing in the graph, or custom tools for agents via
101
- the same **ToolHandle** contract as built-ins.
102
-
103
- ## How it compares
104
-
105
- Versus chat-style harnesses (often an Electron shell around a model loop):
106
- Langflower is a local node graph. The LLM is a node, not the product.
107
- Order comes from topology, not from the model deciding it is done.
108
- The UI is your existing browser, not a bundled one.
109
-
110
- Versus cloud graph tools: the same idea of wiring nodes, but aimed at a
111
- folder on your machine — home or a closed network with internal
112
- providers — not at hosting a service or cloning ETL in the cloud.
113
-
114
- ## What it lacks
115
-
116
- - **Chat sessions.** Node-internal state is the current architecture, so
117
- serializable chat-session mechanics are hard. Maybe later.
118
- - **Image and video.** No asset management for multimodal models. Not yet.
119
- - **No built-in IDE or git UI.** We are not reinventing those wheels. Use
120
- the editor and git tools you already have.
38
+ - **The graph is the harness.** QA, review, build, and tests sit on the
39
+ topology, so the agent cannot skip them or declare itself finished. A
40
+ review-gate has no path around it.
41
+ - **Safe tools, not a general shell.** Wrap format, build, and unit tests
42
+ as custom nodes that expose a **ToolHandle**. You do not need a general
43
+ bash tool that could accidentally wipe all data from your disk. If a
44
+ command must stay open-ended, the workflow can still ask for approval
45
+ first.
46
+ - **Your browser, not a bundled one.** Close the tab and free those
47
+ resources; the server keeps the run. Reopen it, and the UI catches up.
48
+
49
+ Versus chat-style harnesses (OpenCode-like): order comes from the graph,
50
+ not from the model deciding it is done. Versus cloud graph tools
51
+ (Langflow, n8n): the same idea of wiring nodes, aimed at a folder on
52
+ your machine home or a closed network with internal providers not at
53
+ hosting a service.
54
+
55
+ **MCP** and **skills** work as usual. Custom nodes sit on top of the same
56
+ **ToolHandle** contract as built-ins.
121
57
 
122
58
  ## Quick start
123
59
 
124
- Requires **Node.js ≥ 22**.
60
+ Requires **Node.js ≥ 22**. Live agent runs need an OpenAI-compatible
61
+ provider in Settings (API keys via `{env:VAR_NAME}`). Simple nodes and
62
+ the Fake LLM work without one.
125
63
 
126
64
  One-shot OS installers (Node LTS if needed + global `langflower`):
127
65
  [install/](install/) (`windows.ps1`, `linux.sh`, `macos.sh`).
@@ -146,6 +84,37 @@ instances from different folders at once.
146
84
 
147
85
  Full walkthrough: [Getting started](https://github.com/earthdmitriy/langflower/blob/master/docs/public/getting-started.md).
148
86
 
87
+ ## How it works
88
+
89
+ 1. Start Langflower with a project folder.
90
+ 2. Open a workflow on the canvas, or create one for the task.
91
+ 3. Run it and watch agents, tools, checks, and file changes move through
92
+ visible stages.
93
+ 4. Approve, reject, or add feedback when the workflow asks for a human
94
+ decision.
95
+ 5. Find the resulting files and data in the same workspace.
96
+
97
+ ## What it lacks
98
+
99
+ - **Chat sessions.** You cannot save a chat and reopen it tomorrow.
100
+ Agent state lives inside the node for now. Maybe later.
101
+ - **Image and video.** No asset management for multimodal models. Not yet.
102
+ - **No built-in IDE or git UI.** We are not reinventing those wheels. Use
103
+ the editor and git tools you already have.
104
+
105
+ ## Under the hood
106
+
107
+ The core is a reactive runtime. The **node SDK** is the public contract:
108
+ any node that follows it can run on that runtime. **Common nodes** is the
109
+ built-in catalog. It uses the same SDK as custom node packs.
110
+
111
+ The **server** compiles user-defined nodes and owns the live run. The
112
+ **UI** is a thin browser client over WebSocket; it does not own the
113
+ workflow.
114
+
115
+ For a short builder-oriented picture, see
116
+ [How it works](https://github.com/earthdmitriy/langflower/blob/master/docs/public/how-it-works.md).
117
+
149
118
  Maintainers (monorepo only): [docs/RELEASE.md](https://github.com/earthdmitriy/langflower/blob/master/docs/RELEASE.md),
150
119
  [packages/cli/README.md](https://github.com/earthdmitriy/langflower/blob/master/packages/cli/README.md).
151
120
 
@@ -3,7 +3,7 @@ const require = __lfCreateRequire(import.meta.url);
3
3
 
4
4
  import {
5
5
  createProjectHarness
6
- } from "./chunk-WQZHA5FM.js";
6
+ } from "./chunk-NIRPHJLO.js";
7
7
  import "./chunk-EAZMCX37.js";
8
8
 
9
9
  // packages/cli/dist/eval-command.js
@@ -2,6 +2,75 @@ import { createRequire as __lfCreateRequire } from 'node:module';
2
2
  const require = __lfCreateRequire(import.meta.url);
3
3
 
4
4
 
5
+ // packages/tools/dist/builtins/args.js
6
+ var asString = (args, key) => {
7
+ const value = args[key];
8
+ return typeof value === "string" ? value : void 0;
9
+ };
10
+ var asNumber = (args, key) => {
11
+ const value = args[key];
12
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
13
+ };
14
+ var asBoolean = (args, key, defaultValue) => {
15
+ const value = args[key];
16
+ return typeof value === "boolean" ? value : defaultValue;
17
+ };
18
+
19
+ // packages/tools/dist/builtins/ask_user/tool.js
20
+ var ASK_USER_ABORTED = "ask_user aborted.";
21
+ var invokeWithAbort = (work, signal) => {
22
+ if (signal === void 0) {
23
+ return work;
24
+ }
25
+ if (signal.aborted) {
26
+ return Promise.reject(new Error(ASK_USER_ABORTED));
27
+ }
28
+ return new Promise((resolve, reject) => {
29
+ const onAbort = () => {
30
+ signal.removeEventListener("abort", onAbort);
31
+ reject(new Error(ASK_USER_ABORTED));
32
+ };
33
+ signal.addEventListener("abort", onAbort, { once: true });
34
+ void work.then((text) => {
35
+ signal.removeEventListener("abort", onAbort);
36
+ resolve(text);
37
+ }, (error) => {
38
+ signal.removeEventListener("abort", onAbort);
39
+ reject(error instanceof Error ? error : new Error(String(error)));
40
+ });
41
+ });
42
+ };
43
+ var invoke = async (ctx, args) => {
44
+ const question = asString(args, "question");
45
+ if (question === void 0 || question.trim().length === 0) {
46
+ throw new Error("ask_user requires string argument \xABquestion\xBB.");
47
+ }
48
+ if (ctx.askUser === void 0) {
49
+ throw new Error("ask_user requires a live HITL host. No askUser hook is bound on this harness.");
50
+ }
51
+ return invokeWithAbort(ctx.askUser({ question: question.trim() }), ctx.signal);
52
+ };
53
+ var askUserTool = {
54
+ id: "ask_user",
55
+ registration: {
56
+ toolId: "ask_user",
57
+ name: "ask_user",
58
+ description: "Do not guess. If you are not sure, ask the user. Pauses the run so the operator can provide additional information.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ question: {
63
+ type: "string",
64
+ description: "The question to show the operator. Be specific about what you need."
65
+ }
66
+ },
67
+ required: ["question"],
68
+ additionalProperties: false
69
+ }
70
+ },
71
+ invoke
72
+ };
73
+
5
74
  // packages/tools/dist/builtins/bash/tool.js
6
75
  import { spawn } from "node:child_process";
7
76
 
@@ -80,20 +149,6 @@ var formatNotFound = async (absolutePath, userPath) => {
80
149
  return `File not found: \xAB${userPath}\xBB.${hint}`;
81
150
  };
82
151
 
83
- // packages/tools/dist/builtins/args.js
84
- var asString = (args, key) => {
85
- const value = args[key];
86
- return typeof value === "string" ? value : void 0;
87
- };
88
- var asNumber = (args, key) => {
89
- const value = args[key];
90
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
91
- };
92
- var asBoolean = (args, key, defaultValue) => {
93
- const value = args[key];
94
- return typeof value === "boolean" ? value : defaultValue;
95
- };
96
-
97
152
  // packages/tools/dist/builtins/fence.js
98
153
  var fenceOptions = (ctx) => ({
99
154
  denyPaths: ctx.denyPaths,
@@ -103,7 +158,7 @@ var displayPath = (ctx, absolute) => toHarnessDisplayPath(ctx.projectRoot, absol
103
158
 
104
159
  // packages/tools/dist/builtins/bash/tool.js
105
160
  var MAX_BASH_OUTPUT = 1e5;
106
- var invoke = async (ctx, args) => {
161
+ var invoke2 = async (ctx, args) => {
107
162
  if (!ctx.bashEnabled) {
108
163
  throw new Error("bash is disabled (default-deny). Enable bash on the project harness to run shell commands.");
109
164
  }
@@ -170,13 +225,13 @@ var bashTool = {
170
225
  additionalProperties: false
171
226
  }
172
227
  },
173
- invoke
228
+ invoke: invoke2
174
229
  };
175
230
 
176
231
  // packages/tools/dist/builtins/create/tool.js
177
232
  import fs2 from "node:fs/promises";
178
233
  import path2 from "node:path";
179
- var invoke2 = async (ctx, args) => {
234
+ var invoke3 = async (ctx, args) => {
180
235
  const userPath = asString(args, "path");
181
236
  const content = asString(args, "content");
182
237
  if (userPath === void 0 || content === void 0) {
@@ -221,12 +276,12 @@ var createTool = {
221
276
  additionalProperties: false
222
277
  }
223
278
  },
224
- invoke: invoke2
279
+ invoke: invoke3
225
280
  };
226
281
 
227
282
  // packages/tools/dist/builtins/delete/tool.js
228
283
  import fs3 from "node:fs/promises";
229
- var invoke3 = async (ctx, args) => {
284
+ var invoke4 = async (ctx, args) => {
230
285
  const userPath = asString(args, "path");
231
286
  if (userPath === void 0) {
232
287
  throw new Error("delete requires string argument \xABpath\xBB.");
@@ -261,12 +316,12 @@ var deleteTool = {
261
316
  additionalProperties: false
262
317
  }
263
318
  },
264
- invoke: invoke3
319
+ invoke: invoke4
265
320
  };
266
321
 
267
322
  // packages/tools/dist/builtins/edit/tool.js
268
323
  import fs4 from "node:fs/promises";
269
- var invoke4 = async (ctx, args) => {
324
+ var invoke5 = async (ctx, args) => {
270
325
  const userPath = asString(args, "path");
271
326
  const oldString = asString(args, "oldString");
272
327
  const newString = asString(args, "newString");
@@ -323,7 +378,7 @@ var editTool = {
323
378
  additionalProperties: false
324
379
  }
325
380
  },
326
- invoke: invoke4
381
+ invoke: invoke5
327
382
  };
328
383
 
329
384
  // packages/tools/dist/builtins/glob/tool.js
@@ -546,7 +601,7 @@ var walkFiles = async (root, dir, options = true) => {
546
601
 
547
602
  // packages/tools/dist/builtins/glob/tool.js
548
603
  var MAX_GLOB_MATCHES = 200;
549
- var invoke5 = async (ctx, args) => {
604
+ var invoke6 = async (ctx, args) => {
550
605
  const pattern = asString(args, "pattern");
551
606
  if (pattern === void 0) {
552
607
  throw new Error("glob requires string argument \xABpattern\xBB.");
@@ -599,7 +654,7 @@ var globTool = {
599
654
  additionalProperties: false
600
655
  }
601
656
  },
602
- invoke: invoke5
657
+ invoke: invoke6
603
658
  };
604
659
 
605
660
  // packages/tools/dist/builtins/grep/tool.js
@@ -958,7 +1013,7 @@ var runGrepCascade = async (input, deps = {}) => {
958
1013
  };
959
1014
 
960
1015
  // packages/tools/dist/builtins/grep/tool.js
961
- var invoke6 = async (ctx, args) => {
1016
+ var invoke7 = async (ctx, args) => {
962
1017
  const pattern = asString(args, "pattern");
963
1018
  if (pattern === void 0) {
964
1019
  throw new Error("grep requires string argument \xABpattern\xBB.");
@@ -1014,7 +1069,7 @@ var grepTool = {
1014
1069
  additionalProperties: false
1015
1070
  }
1016
1071
  },
1017
- invoke: invoke6
1072
+ invoke: invoke7
1018
1073
  };
1019
1074
 
1020
1075
  // packages/tools/dist/builtins/read/tool.js
@@ -1048,7 +1103,7 @@ var sliceLines = (content, args) => {
1048
1103
  }
1049
1104
  return truncated;
1050
1105
  };
1051
- var invoke7 = async (ctx, args) => {
1106
+ var invoke8 = async (ctx, args) => {
1052
1107
  const userPath = asString(args, "path");
1053
1108
  if (userPath === void 0) {
1054
1109
  throw new Error("read requires string argument \xABpath\xBB.");
@@ -1104,13 +1159,13 @@ var readTool = {
1104
1159
  additionalProperties: false
1105
1160
  }
1106
1161
  },
1107
- invoke: invoke7
1162
+ invoke: invoke8
1108
1163
  };
1109
1164
 
1110
1165
  // packages/tools/dist/builtins/write/tool.js
1111
1166
  import fs10 from "node:fs/promises";
1112
1167
  import path8 from "node:path";
1113
- var invoke8 = async (ctx, args) => {
1168
+ var invoke9 = async (ctx, args) => {
1114
1169
  const userPath = asString(args, "path");
1115
1170
  const content = asString(args, "content");
1116
1171
  if (userPath === void 0 || content === void 0) {
@@ -1143,7 +1198,7 @@ var writeTool = {
1143
1198
  additionalProperties: false
1144
1199
  }
1145
1200
  },
1146
- invoke: invoke8
1201
+ invoke: invoke9
1147
1202
  };
1148
1203
 
1149
1204
  // packages/tools/dist/builtins/catalog.js
@@ -1155,7 +1210,8 @@ var BUILTIN_TOOLS = [
1155
1210
  writeTool,
1156
1211
  createTool,
1157
1212
  deleteTool,
1158
- bashTool
1213
+ bashTool,
1214
+ askUserTool
1159
1215
  ];
1160
1216
  var BUILTIN_TOOL_IDS = BUILTIN_TOOLS.map((tool) => tool.id);
1161
1217
  var BUILTIN_REGISTRATIONS = BUILTIN_TOOLS.map((tool) => tool.registration);
@@ -1172,7 +1228,8 @@ var DEFAULT_PERMISSION_CONFIG = {
1172
1228
  write: { "*": "allow" },
1173
1229
  create: { "*": "allow" },
1174
1230
  delete: { "*": "allow" },
1175
- bash: { "*": "allow" }
1231
+ bash: { "*": "allow" },
1232
+ ask_user: { "*": "allow" }
1176
1233
  };
1177
1234
  var DECISION_RANK = {
1178
1235
  deny: 3,
@@ -1261,6 +1318,9 @@ var permissionDetailForCall = (toolId, args) => {
1261
1318
  if (toolId === "bash") {
1262
1319
  return typeof args.command === "string" ? args.command : "";
1263
1320
  }
1321
+ if (toolId === "ask_user") {
1322
+ return typeof args.question === "string" ? args.question : "";
1323
+ }
1264
1324
  if (typeof args.path === "string" && args.path.length > 0) {
1265
1325
  return args.path.replace(/\\/g, "/");
1266
1326
  }
@@ -1385,7 +1445,8 @@ var createProjectHarness = (options) => {
1385
1445
  projectRoot: options.projectRoot,
1386
1446
  denyPaths: options.denyPaths ?? [],
1387
1447
  allowedRoots: options.allowedRoots ?? [],
1388
- bashEnabled: options.bashEnabled === true
1448
+ bashEnabled: options.bashEnabled === true,
1449
+ ...options.askUser !== void 0 ? { askUser: options.askUser } : {}
1389
1450
  };
1390
1451
  const permission = {
1391
1452
  ...DEFAULT_PERMISSION_CONFIG,