langflower 0.1.2 → 0.2.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 (124) hide show
  1. package/README.md +573 -114
  2. package/dist/{chunk-WV2YWHOC.js → chunk-26UB55E5.js} +44 -4
  3. package/dist/{chunk-AO33IM2J.js → chunk-2LE3E2QP.js} +24 -23
  4. package/dist/{chunk-NIRPHJLO.js → chunk-3Z7Q6SQ5.js} +501 -242
  5. package/dist/{chunk-CVTPGIZE.js → chunk-RZUZX6PW.js} +2 -4
  6. package/dist/index.js +41152 -41395
  7. package/docs/public/README.md +1 -0
  8. package/docs/public/extending.md +21 -3
  9. package/docs/public/getting-started.md +9 -1
  10. package/docs/public/how-it-works.md +2 -2
  11. package/docs/public/launcher.md +111 -0
  12. package/package.json +1 -1
  13. package/ui-dist/apple-touch-icon.png +0 -0
  14. package/ui-dist/{chunk-EZTB7Q33.js → chunk-HEF6F5HN.js} +3 -3
  15. package/ui-dist/{chunk-BKIP5WRK.js → chunk-IASSR2ON.js} +1 -1
  16. package/ui-dist/favicon-32x32.png +0 -0
  17. package/ui-dist/favicon.ico +0 -0
  18. package/ui-dist/index.html +5 -2
  19. package/ui-dist/main-NBS6BFBP.js +323 -0
  20. package/ui-dist/styles-M2URERWI.css +1 -0
  21. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.js +1 -1
  22. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.d.ts +9 -21
  23. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.js +9 -3
  24. package/vendor/node-sdk/dist/node-factory/define-llm-node/recovery-notice.d.ts +0 -1
  25. package/vendor/node-sdk/dist/node-factory/define-llm-node/recovery-notice.js +0 -9
  26. package/vendor/node-sdk/dist/node-factory/define-node/define-node.d.ts +5 -1
  27. package/vendor/node-sdk/dist/node-factory/define-node/define-node.js +6 -3
  28. package/vendor/node-sdk/dist/node-factory/define-reactive-node/capabilities.d.ts +128 -0
  29. package/vendor/node-sdk/dist/node-factory/define-reactive-node/capabilities.js +25 -0
  30. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.d.ts +29 -17
  31. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.js +14 -6
  32. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.d.ts +19 -5
  33. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.js +4 -3
  34. package/vendor/node-sdk/dist/node-factory/define-reactive-node/port-meta.d.ts +6 -1
  35. package/vendor/node-sdk/dist/node-factory/define-reactive-node/types.d.ts +40 -22
  36. package/vendor/node-sdk/dist/node-factory/define-reactive-node/types.js +2 -0
  37. package/vendor/node-sdk/dist/node-factory/define-reactive-node/ui-schema-inference.d.ts +1 -1
  38. package/vendor/node-sdk/dist/node-factory/define-tool-registrations/define-tool-registrations.d.ts +13 -25
  39. package/vendor/node-sdk/dist/node-factory/define-tool-registrations/define-tool-registrations.js +9 -8
  40. package/vendor/node-sdk/dist/testing/create-node-harness.d.ts +9 -2
  41. package/vendor/node-sdk/dist/testing/create-node-harness.js +6 -2
  42. package/vendor/node-sdk/package.json +5 -0
  43. package/vendor/runtime/dist/bypass-ports.d.ts +0 -6
  44. package/vendor/runtime/dist/bypass-ports.js +0 -7
  45. package/vendor/runtime/dist/runtime-editor.d.ts +1 -1
  46. package/vendor/runtime/dist/runtime-editor.js +1 -1
  47. package/vendor/runtime/dist/runtime-helpers.d.ts +0 -1
  48. package/vendor/runtime/dist/runtime-helpers.js +0 -33
  49. package/vendor/runtime/dist/runtime-runner.d.ts +4 -0
  50. package/vendor/runtime/dist/runtime-runner.js +60 -36
  51. package/vendor/runtime/dist/runtime.d.ts +0 -1
  52. package/vendor/runtime/dist/runtime.js +0 -1
  53. package/vendor/runtime/dist/types.d.ts +27 -17
  54. package/vendor/server/skeleton/instructions.md +6 -0
  55. package/vendor/server/skeleton/nodes/my-nodes/README.md +19 -1
  56. package/vendor/server/skeleton/schemas/langflower-config.schema.json +1 -1
  57. package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +44 -10
  58. package/vendor/server/skeleton/skills/langflower-helper/architecture.md +4 -1
  59. package/vendor/server/skeleton/skills/langflower-helper/layout.md +6 -4
  60. package/vendor/server/skeleton/skills/langflower-node-writer/SKILL.md +47 -3
  61. package/vendor/server/skeleton/skills/langflower-workflow-writer/SKILL.md +29 -17
  62. package/vendor/server/skeleton/workflows/advanced-coder.json +24 -2
  63. package/vendor/server/skeleton/workflows/kb-create.json +24 -16
  64. package/vendor/server/skeleton/workflows/kb-manual-search.json +1 -1
  65. package/vendor/server/skeleton/workflows/kb-navigate.json +24 -2
  66. package/vendor/server/skeleton/workflows/kb-rag.json +1 -0
  67. package/vendor/server/skeleton/workflows/simple-coder.json +3 -0
  68. package/vendor/server/skeleton/workflows/starter.json +2 -0
  69. package/ui-dist/main-JJURZFKA.js +0 -318
  70. package/ui-dist/styles-VA3HMZK7.css +0 -1
  71. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.d.ts +0 -5
  72. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.js +0 -5
  73. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm.d.ts +0 -3
  74. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm.js +0 -2
  75. package/vendor/node-sdk/dist/node-factory/define-mcp/define-mcp.d.ts +0 -50
  76. package/vendor/node-sdk/dist/node-factory/define-mcp/define-mcp.js +0 -60
  77. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.d.ts +0 -15
  78. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.js +0 -2
  79. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-server-config.d.ts +0 -17
  80. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-server-config.js +0 -2
  81. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-transport.d.ts +0 -20
  82. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-transport.js +0 -2
  83. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp.d.ts +0 -3
  84. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp.js +0 -2
  85. package/vendor/node-sdk/dist/node-factory/define-node/test/samples/gate-node.d.ts +0 -20
  86. package/vendor/node-sdk/dist/node-factory/define-node/test/samples/gate-node.js +0 -22
  87. package/vendor/node-sdk/dist/node-factory/define-reactive-node/chat-completion-stream.d.ts +0 -50
  88. package/vendor/node-sdk/dist/node-factory/define-reactive-node/chat-completion-stream.js +0 -2
  89. package/vendor/node-sdk/dist/node-factory/define-reactive-node/crawl-context.d.ts +0 -22
  90. package/vendor/node-sdk/dist/node-factory/define-reactive-node/crawl-context.js +0 -2
  91. package/vendor/node-sdk/dist/node-factory/define-reactive-node/create-embedding.d.ts +0 -6
  92. package/vendor/node-sdk/dist/node-factory/define-reactive-node/create-embedding.js +0 -2
  93. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-tool-registrations.d.ts +0 -52
  94. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-tool-registrations.js +0 -52
  95. package/vendor/node-sdk/dist/node-factory/define-reactive-node/kb-context.d.ts +0 -111
  96. package/vendor/node-sdk/dist/node-factory/define-reactive-node/kb-context.js +0 -2
  97. package/vendor/node-sdk/dist/node-factory/define-reactive-node/memory-context.d.ts +0 -12
  98. package/vendor/node-sdk/dist/node-factory/define-reactive-node/memory-context.js +0 -2
  99. package/vendor/node-sdk/dist/node-factory/define-reactive-node/project-files-context.d.ts +0 -10
  100. package/vendor/node-sdk/dist/node-factory/define-reactive-node/project-files-context.js +0 -2
  101. package/vendor/node-sdk/dist/node-factory/define-reactive-node/project-harness.d.ts +0 -56
  102. package/vendor/node-sdk/dist/node-factory/define-reactive-node/project-harness.js +0 -2
  103. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/agent-node.d.ts +0 -29
  104. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/agent-node.js +0 -60
  105. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/combine-node.d.ts +0 -29
  106. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/combine-node.js +0 -26
  107. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/constant-node.d.ts +0 -29
  108. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/constant-node.js +0 -21
  109. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/delay-node.d.ts +0 -29
  110. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/delay-node.js +0 -27
  111. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/finish-node.d.ts +0 -17
  112. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/finish-node.js +0 -21
  113. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/hitl-node.d.ts +0 -21
  114. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/hitl-node.js +0 -43
  115. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/join-node.d.ts +0 -29
  116. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/join-node.js +0 -31
  117. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/preview-node.d.ts +0 -17
  118. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/preview-node.js +0 -19
  119. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/router-node.d.ts +0 -18
  120. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/router-node.js +0 -12
  121. package/vendor/node-sdk/dist/node-factory/define-reactive-node/tool-registration.d.ts +0 -36
  122. package/vendor/node-sdk/dist/node-factory/define-reactive-node/tool-registration.js +0 -2
  123. package/vendor/node-sdk/dist/node-factory/define-tool-registrations/tool-registration.d.ts +0 -46
  124. package/vendor/node-sdk/dist/node-factory/define-tool-registrations/tool-registration.js +0 -2
@@ -4,7 +4,7 @@ import type { StatefulObservable } from '@rx-evo/stateful-observable';
4
4
  *
5
5
  * Intentionally owned here (not imported from `@langflower/runtime`) so custom
6
6
  * node packs peer only on `@langflower/node-sdk` (+ rxjs /
7
- * stateful-observable). See [ADR-027](../../../../docs/ADR.md#adr-027--author-sdk-owns-port-types-no-production-runtime-dep).
7
+ * stateful-observable). See [ADR-027](../../../../docs/architecture/ADR.md#adr-027--author-sdk-owns-port-types-no-production-runtime-dep).
8
8
  * Structural parity with runtime is locked by `runtime-parity.types.test.ts`.
9
9
  */
10
10
  /** Same brand as runtime `RuntimeWireType` so bypass maps stay assignable. */
@@ -36,6 +36,11 @@ export type FeedRole = 'none' | 'reasoning' | 'progress' | 'draft' | 'tool' | 's
36
36
  export type FeedPortMeta = {
37
37
  readonly role?: FeedRole;
38
38
  readonly streaming?: boolean;
39
+ /**
40
+ * Runtime stamps this from the node-level `feedVisitBoundary` flag.
41
+ * Authors set `feedVisitBoundary` on the definition, not this field.
42
+ */
43
+ readonly closesPreviousVisit?: true;
39
44
  };
40
45
  /**
41
46
  * Typed metadata on every `statefulConnection` / `statefulObservable` `meta`.
@@ -1,30 +1,30 @@
1
1
  import type { combineStatefulObservables, StatefulConnection, StatefulObservable } from '@rx-evo/stateful-observable';
2
+ import type { CapsFor, LlmRequiredCapabilityId, NodeCapabilityId } from './capabilities.js';
2
3
  import type { ResolveSecret } from './resolve-secret.js';
3
- import type { ToolHandle } from '../define-tool-registrations/tool-handle.js';
4
4
  import type { CtxError } from './ctx-error.js';
5
5
  import { configureOutput, InputConfig, makeInput, OutputConfig } from './io-helpers.js';
6
6
  import type { PortMeta, WireType } from './port-meta.js';
7
- import type { AssertConstUISchema, ParamsFromUISchema, TypedUISchema, UISchemaConstItem } from './ui-schema-inference.js';
8
- /**
9
- * Identity + panel + keyed secret lookup. Host I/O (files/kb/crawl/…) is
10
- * owned by specialized common-nodes internally — not part of the author
11
- * ExecutionContext API. LLM inventory arrives via {@link LlmExecutionCaps}
12
- * (`toolHandles`). {@link resolveSecret} looks up one id (`lf_secret:ID` /
13
- * `env:ID`); authors cannot list the bag.
14
- */
15
- export type ExecutionContext<UI extends readonly UISchemaConstItem[] = readonly UISchemaConstItem[], Caps extends object = Record<string, never>> = {
7
+ import type { AssertConstUISchema, ParamsFromUISchema, UISchemaConstItem } from './ui-schema-inference.js';
8
+ type ExecutionContextHostFields<UI extends readonly UISchemaConstItem[] = readonly UISchemaConstItem[]> = {
16
9
  readonly projectDir: string;
17
10
  readonly runId: string;
18
11
  readonly nodeId: string;
19
12
  readonly params: ParamsFromUISchema<UI>;
20
- readonly uiSchema: TypedUISchema<UI>;
13
+ readonly uiSchema: UI;
21
14
  readonly resolveSecret: ResolveSecret;
22
- readonly amendInput?: (patch: Readonly<Record<string, unknown>>) => void;
23
- } & Caps;
24
- /** Caps for {@link defineLlmNode} — outside world via ToolHandle only. */
25
- export type LlmExecutionCaps = {
26
- readonly toolHandles?: readonly ToolHandle[];
27
15
  };
16
+ /**
17
+ * Identity + panel + keyed secret lookup. Host I/O (files/kb/crawl/…) is
18
+ * owned by specialized common-nodes internally — not part of the author
19
+ * ExecutionContext API. Declared capabilities arrive via {@link CapsFor}
20
+ * (`requires` on the definition). {@link resolveSecret} looks up one id
21
+ * (`lf_secret:ID` / `env:ID`); authors cannot list the bag.
22
+ * Do not restore `amendInput` or other host-mutation / I/O fields here —
23
+ * keyed by `assertExecutionContextHostKeys` below.
24
+ */
25
+ export type ExecutionContext<UI extends readonly UISchemaConstItem[] = readonly UISchemaConstItem[], Caps extends object = Record<string, never>> = ExecutionContextHostFields<UI> & Caps;
26
+ /** Caps for {@link defineLlmNode} — LLM set from {@link CapsFor}. */
27
+ export type LlmExecutionCaps = CapsFor<readonly LlmRequiredCapabilityId[]>;
28
28
  type ReactiveNodeBindResult = {
29
29
  readonly inputs: InputConfig[];
30
30
  readonly outputs: OutputConfig[];
@@ -33,8 +33,9 @@ type ReactiveNodeBindResult = {
33
33
  export type DefinedReactiveNodeConfig<UI extends readonly UISchemaConstItem[] = readonly UISchemaConstItem[], Caps extends object = Record<string, never>> = {
34
34
  /**
35
35
  * Stable catalog / persisted workflow id (`WorkflowNodePersisted.type`).
36
- * Server resolves the definition with `resolveDefinition({ type })` and
37
- * keys the common-nodes catalog by this string.
36
+ * Server resolves the definition with
37
+ * `resolveDefinition({ type, params })` and keys the common-nodes
38
+ * catalog by `type`.
38
39
  */
39
40
  readonly type: string;
40
41
  /**
@@ -63,10 +64,21 @@ export type DefinedReactiveNodeConfig<UI extends readonly UISchemaConstItem[] =
63
64
  */
64
65
  readonly description?: string;
65
66
  /**
66
- * Optional string copied onto the returned definition (and thus
67
- * `PaletteNodeDefinition`). No current UI or runtime consumer reads it.
67
+ * Canvas box used when the node has no persisted width: the editor locks
68
+ * both axes instead of auto-sizing, so payload cannot grow the node.
69
+ * Omitted → canvas auto-sizes. Persisted operator resize always wins.
70
+ */
71
+ readonly defaultCanvasSize?: {
72
+ readonly width: number;
73
+ readonly height: number;
74
+ };
75
+ /**
76
+ * When `true`, the first work-log frame from this node **closes the
77
+ * previous visit**, so a delegating node gets its own feed card instead of
78
+ * continuing the caller's. Omitted/`false` → frames follow the normal
79
+ * `feed.streaming` visit rules. Used by `common-sub-agent`.
68
80
  */
69
- readonly icon?: string;
81
+ readonly feedVisitBoundary?: boolean;
70
82
  /**
71
83
  * When `true` on the live instance, the first **value** emission on any
72
84
  * watched output schedules `RuntimeRunner.finishRun` (`done` → `idle`).
@@ -87,6 +99,12 @@ export type DefinedReactiveNodeConfig<UI extends readonly UISchemaConstItem[] =
87
99
  * `false`. Used by `common-chat-input`.
88
100
  */
89
101
  readonly chatEntry?: boolean;
102
+ /**
103
+ * Host capabilities this node needs. Copied onto
104
+ * {@link ReactiveNodeDefinition.requires} (default `[]`). `bind` sees
105
+ * {@link CapsFor} fields as non-optional.
106
+ */
107
+ readonly requires?: readonly NodeCapabilityId[];
90
108
  /**
91
109
  * Router bypass base port id → wire type. Runtime materializes each key
92
110
  * as a multi-input base; channel outputs (`ch`, `ch@1`, …) are slot views
@@ -122,8 +140,8 @@ export type ReactiveNodeInstance<UI extends readonly UISchemaConstItem[] = reado
122
140
  readonly bypassPorts: Record<string, WireType>;
123
141
  readonly stopsRun?: boolean;
124
142
  readonly chatEntry?: boolean;
143
+ readonly feedVisitBoundary?: boolean;
125
144
  readonly emitOncePerActivation?: boolean;
126
- readonly skipExecutionTelemetry?: boolean;
127
145
  readonly ctxConnection: StatefulConnection<ExecutionContext<UI, Caps>, CtxError, PortMeta>;
128
146
  };
129
147
  export {};
@@ -1 +1,3 @@
1
+ const assertExecutionContextHostKeys = true;
2
+ void assertExecutionContextHostKeys;
1
3
  export {};
@@ -5,7 +5,7 @@ export type UISchemaConstItem = {
5
5
  readonly type: string;
6
6
  readonly label?: string;
7
7
  readonly default?: unknown;
8
- readonly placement?: 'panel' | 'inline';
8
+ readonly placement?: 'panel';
9
9
  /** Number field constraints — same semantics as canvas inline `{ type: 'number' }`. */
10
10
  readonly min?: number;
11
11
  readonly max?: number;
@@ -1,4 +1,13 @@
1
- import { type ToolHandler } from './tool-handle.js';
1
+ import { type ToolHandle, type ToolHandler } from './tool-handle.js';
2
+ type RegistrationTool = {
3
+ readonly toolId: string;
4
+ readonly name?: string;
5
+ readonly description: string;
6
+ readonly inputSchema: object;
7
+ readonly handler: ToolHandler;
8
+ };
9
+ /** Map domain registrations onto wire {@link ToolHandle}s. */
10
+ export declare const toToolHandles: (tools: readonly RegistrationTool[]) => readonly ToolHandle[];
2
11
  /**
3
12
  * Purpose utility atop {@link defineReactiveNode}: emit a `tools` pack as
4
13
  * {@link ToolHandle}[]. Config still takes domain `handler`s; factory maps
@@ -21,27 +30,6 @@ export declare const defineToolRegistrations: (config: {
21
30
  readonly displayName: string;
22
31
  readonly category?: string;
23
32
  readonly description?: string;
24
- readonly icon?: string;
25
- readonly tools: readonly {
26
- readonly toolId: string;
27
- readonly name?: string;
28
- readonly description: string;
29
- readonly inputSchema: object;
30
- readonly handler: ToolHandler;
31
- }[];
32
- }) => {
33
- readonly emitOncePerActivation: boolean;
34
- readonly stopsRun: boolean;
35
- readonly chatEntry: boolean;
36
- readonly uiSchema: readonly [];
37
- readonly bypassPorts: Record<string, import("../define-reactive-node/port-meta.js").WireType>;
38
- readonly inputsConfigs: readonly [import("../define-reactive-node/io-helpers.js").InputPortMeta<import("../define-reactive-node/types.js").ExecutionContext<readonly [], Record<string, never>>>, ...import("../define-reactive-node/io-helpers.js").InputPortMeta<unknown>[]];
39
- readonly outputsConfigs: (import("../define-reactive-node/io-helpers.js").OutputParams & import("../define-reactive-node/port-meta.js").PortMeta)[];
40
- readonly getInstance: () => import("../define-reactive-node/types.js").ReactiveNodeInstance<readonly [], Record<string, never>>;
41
- readonly paletteSecondary?: true;
42
- readonly description?: string;
43
- readonly type: string;
44
- readonly displayName: string;
45
- readonly category: string | undefined;
46
- readonly icon: string | undefined;
47
- };
33
+ readonly tools: readonly RegistrationTool[];
34
+ }) => import("../define-reactive-node/define-reactive-node.js").ReactiveNodeDefinition;
35
+ export {};
@@ -2,6 +2,14 @@ import { statefulObservable } from '@rx-evo/stateful-observable';
2
2
  import { of } from 'rxjs';
3
3
  import { defineReactiveNode } from '../define-reactive-node/define-reactive-node.js';
4
4
  import { TOOL_HANDLE_WIRE_TYPE, } from './tool-handle.js';
5
+ /** Map domain registrations onto wire {@link ToolHandle}s. */
6
+ export const toToolHandles = (tools) => tools.map((tool) => ({
7
+ toolId: tool.toolId,
8
+ name: tool.name ?? tool.toolId,
9
+ description: tool.description,
10
+ inputSchema: tool.inputSchema,
11
+ invoke: tool.handler,
12
+ }));
5
13
  /**
6
14
  * Purpose utility atop {@link defineReactiveNode}: emit a `tools` pack as
7
15
  * {@link ToolHandle}[]. Config still takes domain `handler`s; factory maps
@@ -26,17 +34,10 @@ export const defineToolRegistrations = (config) => defineReactiveNode({
26
34
  ...(config.description !== undefined
27
35
  ? { description: config.description }
28
36
  : {}),
29
- ...(config.icon !== undefined ? { icon: config.icon } : {}),
30
37
  uiSchema: [],
31
38
  bind(_ctx, { configureOutput }) {
32
39
  const tools$ = statefulObservable({
33
- loader: () => of(config.tools.map((tool) => ({
34
- toolId: tool.toolId,
35
- name: tool.name ?? tool.toolId,
36
- description: tool.description,
37
- inputSchema: tool.inputSchema,
38
- invoke: tool.handler,
39
- }))),
40
+ loader: () => of(toToolHandles(config.tools)),
40
41
  });
41
42
  return {
42
43
  inputs: [],
@@ -2,7 +2,7 @@ import type { ReactiveNodeDefinition, ReactiveNodeInstance } from '../node-facto
2
2
  import { type UISchemaConstItem } from '../node-factory/define-reactive-node/ui-schema-inference.js';
3
3
  type HarnessUi = readonly UISchemaConstItem[];
4
4
  type HarnessInstance = ReactiveNodeInstance<HarnessUi, object>;
5
- export type NodeHarnessOptions = {
5
+ type NodeHarnessBaseOptions = {
6
6
  readonly projectDir?: string;
7
7
  readonly runId?: string;
8
8
  readonly nodeId?: string;
@@ -12,6 +12,13 @@ export type NodeHarnessOptions = {
12
12
  /** Env map for `env:ID`. Omitted → `process.env`. */
13
13
  readonly env?: Readonly<Record<string, string | undefined>>;
14
14
  };
15
+ export type NodeHarnessOptions<Caps extends object = Record<string, never>> = [
16
+ keyof Caps
17
+ ] extends [never] ? NodeHarnessBaseOptions & {
18
+ readonly caps?: Caps;
19
+ } : NodeHarnessBaseOptions & {
20
+ readonly caps: Caps;
21
+ };
15
22
  export type CollectedPort<T> = {
16
23
  readonly values: readonly T[];
17
24
  readonly stop: () => void;
@@ -30,5 +37,5 @@ export type NodeHarness = {
30
37
  * Subscribe with `next` or `collect` **before** `send` when the graph can
31
38
  * emit synchronously.
32
39
  */
33
- export declare const createNodeHarness: (definition: Pick<ReactiveNodeDefinition, "getInstance" | "uiSchema">, options?: NodeHarnessOptions) => NodeHarness;
40
+ export declare const createNodeHarness: <Caps extends object = Record<string, never>>(definition: Pick<ReactiveNodeDefinition, "getInstance" | "uiSchema">, options?: NodeHarnessOptions<Caps>) => NodeHarness;
34
41
  export {};
@@ -17,7 +17,7 @@ export const createNodeHarness = (definition, options) => {
17
17
  ...defaultParamsFromUiSchema(uiSchema),
18
18
  ...options?.params,
19
19
  };
20
- instance.ctxConnection.connect(of({
20
+ const base = {
21
21
  projectDir: options?.projectDir ?? '/tmp',
22
22
  runId: options?.runId ?? 'test',
23
23
  nodeId: options?.nodeId ?? 'node-1',
@@ -27,7 +27,11 @@ export const createNodeHarness = (definition, options) => {
27
27
  secrets: options?.secrets ?? {},
28
28
  ...(options?.env !== undefined ? { env: options.env } : {}),
29
29
  }),
30
- }));
30
+ };
31
+ const ctx = options !== undefined && options.caps !== undefined
32
+ ? { ...base, ...options.caps }
33
+ : base;
34
+ instance.ctxConnection.connect(of(ctx));
31
35
  const send = (portId, value) => {
32
36
  const input = instance.inputs[portId];
33
37
  if (input === undefined) {
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@langflower/node-sdk",
3
3
  "version": "0.1.0",
4
+ "description": "Author SDK for Langflower nodes (defineNode / defineReactiveNode)",
5
+ "license": "MIT",
4
6
  "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
5
10
  "main": "./dist/node-factory/define-reactive-node/define-reactive-node.js",
6
11
  "types": "./dist/node-factory/define-reactive-node/define-reactive-node.d.ts",
7
12
  "exports": {
@@ -16,17 +16,12 @@ export type BypassSlot = {
16
16
  readonly basePortId: string;
17
17
  readonly slotIndex: number;
18
18
  };
19
- /** Edge `fromPort` / `toPort` shape for a bypass slot. */
20
- export type BypassEdgePort = readonly [basePortId: string, slotIndex: number];
21
19
  export declare const bypassSlot: (basePortId: string, slotIndex: number) => BypassSlot;
22
- export declare const bypassSlotFromEdgePort: (port: readonly [string, number]) => BypassSlot;
23
- export declare const bypassEdgePort: (slot: BypassSlot) => BypassEdgePort;
24
20
  /**
25
21
  * Checkpoint / outputs-map / tap key for a bypass slot (`ch`, `ch@1`, …).
26
22
  * Resume and telemetry must look up snapshots with this id.
27
23
  */
28
24
  export declare const bypassOutputPortId: (basePortId: string, slotIndex: number) => string;
29
- export declare const bypassOutputPortIdFromSlot: (slot: BypassSlot) => string;
30
25
  /** Inverse of {@link bypassOutputPortId}. */
31
26
  export declare const parseBypassOutputPortId: (outputPortId: string) => BypassSlot;
32
27
  /**
@@ -34,7 +29,6 @@ export declare const parseBypassOutputPortId: (outputPortId: string) => BypassSl
34
29
  * materialized `ch@n` output id as the portId segment.
35
30
  */
36
31
  export declare const bypassSlotKey: (nodeId: NodeId, basePortId: string, slotIndex: number) => SlotKey;
37
- export declare const bypassSlotKeyFromEdgePort: (nodeId: NodeId, port: readonly [string, number]) => SlotKey;
38
32
  /**
39
33
  * Checkpoint / telemetry portId for a slot. Bypass bases use
40
34
  * {@link bypassOutputPortId}; regular ports keep `portId` (slot 0).
@@ -4,17 +4,11 @@ export const bypassSlot = (basePortId, slotIndex) => ({
4
4
  basePortId,
5
5
  slotIndex,
6
6
  });
7
- export const bypassSlotFromEdgePort = (port) => bypassSlot(port[0], port[1]);
8
- export const bypassEdgePort = (slot) => [
9
- slot.basePortId,
10
- slot.slotIndex,
11
- ];
12
7
  /**
13
8
  * Checkpoint / outputs-map / tap key for a bypass slot (`ch`, `ch@1`, …).
14
9
  * Resume and telemetry must look up snapshots with this id.
15
10
  */
16
11
  export const bypassOutputPortId = (basePortId, slotIndex) => (slotIndex === 0 ? basePortId : `${basePortId}@${slotIndex}`);
17
- export const bypassOutputPortIdFromSlot = (slot) => bypassOutputPortId(slot.basePortId, slot.slotIndex);
18
12
  /** Inverse of {@link bypassOutputPortId}. */
19
13
  export const parseBypassOutputPortId = (outputPortId) => {
20
14
  const at = outputPortId.lastIndexOf('@');
@@ -31,7 +25,6 @@ export const parseBypassOutputPortId = (outputPortId) => {
31
25
  * materialized `ch@n` output id as the portId segment.
32
26
  */
33
27
  export const bypassSlotKey = (nodeId, basePortId, slotIndex) => slotKey(nodeId, basePortId, slotIndex);
34
- export const bypassSlotKeyFromEdgePort = (nodeId, port) => bypassSlotKey(nodeId, port[0], port[1]);
35
28
  /**
36
29
  * Checkpoint / telemetry portId for a slot. Bypass bases use
37
30
  * {@link bypassOutputPortId}; regular ports keep `portId` (slot 0).
@@ -34,7 +34,7 @@ export declare class RuntimeEditor implements RuntimeEditorApi {
34
34
  nodes: RuntimeNode[];
35
35
  edges: RuntimeEdge[];
36
36
  };
37
- getClusterByNodeId(nodeId: NodeId): GraphCluster;
37
+ getClusterByNodeId(nodeId: NodeId): GraphCluster | false;
38
38
  setLocked(locked: boolean): void;
39
39
  dispose(): void;
40
40
  private refreshClusters;
@@ -193,7 +193,7 @@ export class RuntimeEditor {
193
193
  }
194
194
  getClusterByNodeId(nodeId) {
195
195
  if (!this.nodes.has(nodeId)) {
196
- throw new Error(`Node ${nodeId} not found`);
196
+ return false;
197
197
  }
198
198
  return resolveClusterForNode(this.allClusters, nodeId);
199
199
  }
@@ -16,4 +16,3 @@ export declare function detectGraphClusters(nodeIds: Iterable<NodeId>, edges: re
16
16
  export declare const clusterHasChatEntry: (cluster: GraphCluster, getNode: (nodeId: NodeId) => RuntimeNode | false) => boolean;
17
17
  export declare function resolveClusterForNode(clusters: readonly GraphCluster[], nodeId: NodeId): GraphCluster;
18
18
  export declare function collectClusterSlotKeys(nodes: ReadonlyMap<NodeId, RuntimeNode>, clusterNodeIds: ReadonlySet<NodeId>): SlotKey[];
19
- export declare function graphHasCycle(edges: readonly RuntimeEdge[]): boolean;
@@ -126,36 +126,3 @@ export function collectClusterSlotKeys(nodes, clusterNodeIds) {
126
126
  }
127
127
  return keys;
128
128
  }
129
- export function graphHasCycle(edges) {
130
- const adjacency = new Map();
131
- for (const edge of edges) {
132
- const next = adjacency.get(edge.fromNodeId) ?? [];
133
- next.push(edge.toNodeId);
134
- adjacency.set(edge.fromNodeId, next);
135
- }
136
- const visited = new Set();
137
- const stack = new Set();
138
- const visit = (nodeId) => {
139
- if (stack.has(nodeId)) {
140
- return true;
141
- }
142
- if (visited.has(nodeId)) {
143
- return false;
144
- }
145
- visited.add(nodeId);
146
- stack.add(nodeId);
147
- for (const nextId of adjacency.get(nodeId) ?? []) {
148
- if (visit(nextId)) {
149
- return true;
150
- }
151
- }
152
- stack.delete(nodeId);
153
- return false;
154
- };
155
- for (const nodeId of adjacency.keys()) {
156
- if (visit(nodeId)) {
157
- return true;
158
- }
159
- }
160
- return false;
161
- }
@@ -34,6 +34,7 @@ export declare class RuntimeRunner implements RuntimeRunnerApi {
34
34
  private assertNotDisposed;
35
35
  private setStatus;
36
36
  private emitRunnerEvent;
37
+ private targetInputFeed;
37
38
  private emitPortEvent;
38
39
  private canPreparePushedInput;
39
40
  private isInputOccupiedByEdge;
@@ -48,8 +49,11 @@ export declare class RuntimeRunner implements RuntimeRunnerApi {
48
49
  * subscriber, which is what previously reordered events against downstream
49
50
  * `input-received`s. Must still be subscribed/connected somewhere
50
51
  * (downstream `connect`, or the end-node driver) to run.
52
+ * `tap` is telemetry only: `stopsRun` completion is a named subscribe
53
+ * after wrap in `wireScope`.
51
54
  */
52
55
  private tapOutputPort;
56
+ private scheduleFinishOnSuccessValue;
53
57
  /**
54
58
  * Returns `source` wrapped with a `tap` that emits `input-received`
55
59
  * telemetry for the target `(nodeId, portId, slotIndex)`. Like
@@ -1,5 +1,5 @@
1
1
  import { combineStatefulObservables, serializeResponse, statefulObservable, } from '@rx-evo/stateful-observable';
2
- import { BehaviorSubject, filter, merge, of, Subject, Subscription, tap, zip, } from 'rxjs';
2
+ import { BehaviorSubject, filter, isObservable, merge, of, Subject, Subscription, tap, zip, } from 'rxjs';
3
3
  import { checkpointPortIdForSlot, getBypassConnection, isBypassPort, } from './bypass-ports.js';
4
4
  import { normalizePortErrorValue } from './normalize-port-error-value.js';
5
5
  import { edgePortSlotKey, parseSlotKey, slotKey, } from './port-meta.js';
@@ -73,8 +73,10 @@ export class RuntimeRunner {
73
73
  return this.runScope(scopeNodeIds, scopeEdgeIds, initialPayload, resolvedRunId);
74
74
  }
75
75
  startNode(nodeId, initialPayload, runId) {
76
- // Composer: resolve cluster → start scoped run
77
76
  const cluster = this.editor.getClusterByNodeId(nodeId);
77
+ if (cluster === false) {
78
+ return false;
79
+ }
78
80
  return this.runScope(cluster.nodeIds, cluster.edgeIds, initialPayload, runId);
79
81
  }
80
82
  resume(options) {
@@ -128,11 +130,8 @@ export class RuntimeRunner {
128
130
  this.isInputOccupiedByEdge(cfg.nodeId, cfg.portId)) {
129
131
  return false;
130
132
  }
131
- let cluster;
132
- try {
133
- cluster = this.editor.getClusterByNodeId(cfg.nodeId);
134
- }
135
- catch {
133
+ const cluster = this.editor.getClusterByNodeId(cfg.nodeId);
134
+ if (cluster === false) {
136
135
  return false;
137
136
  }
138
137
  const runId = this.runScope(cluster.nodeIds, cluster.edgeIds);
@@ -209,6 +208,10 @@ export class RuntimeRunner {
209
208
  }
210
209
  this.eventsSubject.next(event);
211
210
  }
211
+ targetInputFeed = (nodeId, portId) => {
212
+ const node = this.editor.getNode(nodeId);
213
+ return node === false ? undefined : node.inputs[portId]?.meta.feed;
214
+ };
212
215
  emitPortEvent(_runId, portDir, nodeId, portId, portIdx, raw, edgeIds, defaultFeed) {
213
216
  const dto = toResponseDto(raw);
214
217
  if (dto === null) {
@@ -217,7 +220,12 @@ export class RuntimeRunner {
217
220
  const response = 'error' in dto
218
221
  ? { error: normalizePortErrorValue(dto.error) }
219
222
  : dto;
220
- const feed = defaultFeed ?? null;
223
+ const node = this.editor.getNode(nodeId);
224
+ const closesPreviousVisit = node !== false && node.feedVisitBoundary === true;
225
+ const portFeed = defaultFeed;
226
+ const feed = closesPreviousVisit === true
227
+ ? { ...(portFeed ?? {}), closesPreviousVisit: true }
228
+ : (portFeed ?? null);
221
229
  const portIdStr = typeof portId === 'string' ? portId : String(portId);
222
230
  this.emitRunnerEvent([
223
231
  portDir,
@@ -260,7 +268,7 @@ export class RuntimeRunner {
260
268
  if (existing !== undefined) {
261
269
  // Telemetry before deliver so HITL input-received precedes cascade
262
270
  // (output-emitted / downstream input-received) in the event log.
263
- this.emitPortEvent(run.runId, 'in', cfg.nodeId, cfg.portId, 0, cfg.payload, []);
271
+ this.emitPortEvent(run.runId, 'in', cfg.nodeId, cfg.portId, 0, cfg.payload, [], node.inputs[cfg.portId]?.meta.feed);
264
272
  existing.source.next(cfg.payload);
265
273
  return run.runId;
266
274
  }
@@ -279,7 +287,7 @@ export class RuntimeRunner {
279
287
  connection,
280
288
  });
281
289
  run.pushedInputSources.set(key, { source });
282
- this.emitPortEvent(run.runId, 'in', cfg.nodeId, cfg.portId, 0, cfg.payload, []);
290
+ this.emitPortEvent(run.runId, 'in', cfg.nodeId, cfg.portId, 0, cfg.payload, [], connection.meta.feed);
283
291
  source.next(cfg.payload);
284
292
  return run.runId;
285
293
  }
@@ -293,25 +301,16 @@ export class RuntimeRunner {
293
301
  * subscriber, which is what previously reordered events against downstream
294
302
  * `input-received`s. Must still be subscribed/connected somewhere
295
303
  * (downstream `connect`, or the end-node driver) to run.
304
+ * `tap` is telemetry only: `stopsRun` completion is a named subscribe
305
+ * after wrap in `wireScope`.
296
306
  */
297
307
  tapOutputPort(run, nodeId, portId, output, edgeIds) {
298
308
  if (typeof output.meta.portId !== 'string')
299
309
  return output;
300
- const node = this.editor.getNode(nodeId);
301
310
  return output
302
311
  .pipe(tap({
303
312
  next: (response) => {
304
- const emittedValue = this.emitPortEvent(run.runId, 'out', nodeId, portId, 0, response, edgeIds, output.meta.feed);
305
- if (emittedValue &&
306
- node !== false &&
307
- node.stopsRun === true) {
308
- const finishRunId = run.runId;
309
- queueMicrotask(() => {
310
- if (this.activeRun?.runId === finishRunId) {
311
- this.finishRun(finishRunId);
312
- }
313
- });
314
- }
313
+ this.emitPortEvent(run.runId, 'out', nodeId, portId, 0, response, edgeIds, output.meta.feed);
315
314
  },
316
315
  }), filter((response) => {
317
316
  const dto = toResponseDto(response);
@@ -319,6 +318,17 @@ export class RuntimeRunner {
319
318
  }))
320
319
  .with({ meta: output.meta });
321
320
  }
321
+ scheduleFinishOnSuccessValue(runId, response) {
322
+ const dto = toResponseDto(response);
323
+ if (dto === null || !('value' in dto)) {
324
+ return;
325
+ }
326
+ queueMicrotask(() => {
327
+ if (this.activeRun?.runId === runId) {
328
+ this.finishRun(runId);
329
+ }
330
+ });
331
+ }
322
332
  /**
323
333
  * Returns `source` wrapped with a `tap` that emits `input-received`
324
334
  * telemetry for the target `(nodeId, portId, slotIndex)`. Like
@@ -331,7 +341,7 @@ export class RuntimeRunner {
331
341
  return source
332
342
  .pipe(tap({
333
343
  next: (response) => {
334
- this.emitPortEvent(run.runId, 'in', nodeId, portId, slotIndex, response, edgeIds);
344
+ this.emitPortEvent(run.runId, 'in', nodeId, portId, slotIndex, response, edgeIds, this.targetInputFeed(nodeId, portId));
335
345
  },
336
346
  }))
337
347
  .with({ meta: source.meta });
@@ -408,7 +418,7 @@ export class RuntimeRunner {
408
418
  this.flushPendingWire();
409
419
  }
410
420
  catch {
411
- // Status/lock already restored in pendingWire.
421
+ this.eventsSubject.next(['done', resolvedRunId]);
412
422
  }
413
423
  });
414
424
  return resolvedRunId;
@@ -451,10 +461,13 @@ export class RuntimeRunner {
451
461
  (meta.mode === 'single' && seed.slotIndex !== 0)) {
452
462
  throw new Error(`Unknown seed slot ${nodeId}.${String(seed.portId)}[${seed.slotIndex}]`);
453
463
  }
454
- connection.connect(of(seed.value).pipe(tap({
464
+ const source$ = isObservable(seed.value)
465
+ ? seed.value
466
+ : of(seed.value);
467
+ connection.connect(source$.pipe(tap({
455
468
  next: (value) => {
456
469
  if (typeof seed.portId !== 'symbol') {
457
- this.emitPortEvent(runId, 'in', nodeId, seed.portId, seed.slotIndex, value, []);
470
+ this.emitPortEvent(runId, 'in', nodeId, seed.portId, seed.slotIndex, value, [], connection.meta.feed);
458
471
  }
459
472
  },
460
473
  })));
@@ -492,7 +505,7 @@ export class RuntimeRunner {
492
505
  }
493
506
  connection.connect(of(defaultValue).pipe(tap({
494
507
  next: (value) => {
495
- this.emitPortEvent(runId, 'in', nodeId, portId, 0, value, []);
508
+ this.emitPortEvent(runId, 'in', nodeId, portId, 0, value, [], connection.meta.feed);
496
509
  },
497
510
  })));
498
511
  wiredSlotKeys.add(key);
@@ -726,7 +739,7 @@ export class RuntimeRunner {
726
739
  }).with({ meta });
727
740
  group.connection.connect(combined.pipe(tap({
728
741
  next: (response) => {
729
- this.emitPortEvent(run.runId, 'in', group.nodeId, group.portId, 0, response, group.edges.map((entry) => entry.edge.edgeId));
742
+ this.emitPortEvent(run.runId, 'in', group.nodeId, group.portId, 0, response, group.edges.map((entry) => entry.edge.edgeId), group.connection.meta.feed);
730
743
  },
731
744
  })));
732
745
  wiredSlots.push({
@@ -734,18 +747,29 @@ export class RuntimeRunner {
734
747
  connection: group.connection,
735
748
  });
736
749
  }
737
- // Subscribe end-node outputs *before* defaults/seeds so multi-value
738
- // shared streams (shareReplay bufferSize 1) still deliver every chunk
739
- // to unwired ports such as LLM `reasoning` / `draftResponse`. Seeds
740
- // must not emit while only edge-driven outputs are subscribed.
750
+ // Demand-pull unwired outputs *before* defaults/seeds so multi-value
751
+ // shared streams still deliver every chunk to ports such as LLM
752
+ // `reasoning` / `draftResponse`. Seeds must not emit while only
753
+ // edge-driven outputs are subscribed.
754
+ // `stopsRun` completion is this named edge (not `tap`): wired finish
755
+ // outputs also subscribe so teardown is not hidden in telemetry.
741
756
  for (const key of scopeOutputKeys) {
742
- if ((outputEdgeIds.get(key) ?? []).length > 0) {
757
+ const watched = watchedByKey.get(key);
758
+ if (watched === undefined) {
743
759
  continue;
744
760
  }
745
- const watched = watchedByKey.get(key);
746
- if (watched !== undefined) {
747
- run.subscriptions.add(watched.subscribe(() => { }));
761
+ const { nodeId } = parseSlotKey(key);
762
+ const node = nodesById.get(nodeId);
763
+ const isUnwired = (outputEdgeIds.get(key) ?? []).length === 0;
764
+ const stopsRun = node?.stopsRun === true;
765
+ if (!isUnwired && !stopsRun) {
766
+ continue;
748
767
  }
768
+ run.subscriptions.add(watched.subscribe((response) => {
769
+ if (stopsRun) {
770
+ this.scheduleFinishOnSuccessValue(run.runId, response);
771
+ }
772
+ }));
749
773
  }
750
774
  const defaultScopeNodeIds = completedNodeIds.size === 0
751
775
  ? scopeNodeIds
@@ -3,7 +3,6 @@ import { RuntimeRunner } from './runtime-runner.js';
3
3
  import type { RuntimeOptions } from './types.js';
4
4
  export { bypassOutputPortId, parseBypassOutputPortId } from './bypass-ports.js';
5
5
  export { RuntimeEditor } from './runtime-editor.js';
6
- export { graphHasCycle } from './runtime-helpers.js';
7
6
  export type { GraphCluster } from './runtime-helpers.js';
8
7
  export { RuntimeRunner } from './runtime-runner.js';
9
8
  export type { MetaFromStatefulObservable, NodeId, EdgeId, RunId, PortMeta, RuntimeEdge, RuntimeEditorApi, RuntimeFeedPortMeta, RuntimeFeedRole, PortTelemetry, RuntimeDoneTelemetry, RuntimeNode, RuntimeResumeOptions, RuntimeRunnerApi, RuntimeRunnerEvent, RuntimeRunnerStatus, RuntimeSeedPortValue, RuntimeWireType, SwapNodeResult, } from './types.js';
@@ -2,7 +2,6 @@ import { RuntimeEditor } from './runtime-editor.js';
2
2
  import { RuntimeRunner } from './runtime-runner.js';
3
3
  export { bypassOutputPortId, parseBypassOutputPortId } from './bypass-ports.js';
4
4
  export { RuntimeEditor } from './runtime-editor.js';
5
- export { graphHasCycle } from './runtime-helpers.js';
6
5
  export { RuntimeRunner } from './runtime-runner.js';
7
6
  export { isPortErrorTelemetry, isPortPendingTelemetry, isPortTelemetry, isPortValueTelemetry, isRuntimeDone, } from './types.js';
8
7
  export class RuntimeFacade {