langflower 0.0.9 → 0.0.10

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 (91) hide show
  1. package/package.json +2 -2
  2. package/ui-dist/index.html +2 -2
  3. package/ui-dist/main-Z7CPFXFA.js +313 -0
  4. package/ui-dist/styles-2TMDAUD7.css +1 -0
  5. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.js +0 -12
  6. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.d.ts +2 -7
  7. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.js +1 -1
  8. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.d.ts +0 -3
  9. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.js +22 -36
  10. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.d.ts +1 -7
  11. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.js +12 -35
  12. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.d.ts +3 -10
  13. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.js +9 -14
  14. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.d.ts +0 -3
  15. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.js +2 -15
  16. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.d.ts +1 -0
  17. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.js +1 -0
  18. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.d.ts +1 -1
  19. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.js +1 -1
  20. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.d.ts +1 -0
  21. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.js +1 -0
  22. package/vendor/common-nodes/dist/ai/nodes/critique/node.d.ts +3 -3
  23. package/vendor/common-nodes/dist/ai/nodes/critique/node.js +5 -18
  24. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.d.ts +3 -3
  25. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.js +3 -10
  26. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.d.ts +3 -3
  27. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.js +2 -9
  28. package/vendor/common-nodes/dist/ai/nodes/review/node.d.ts +3 -3
  29. package/vendor/common-nodes/dist/ai/nodes/review/node.js +5 -18
  30. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.d.ts +5 -5
  31. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.js +211 -96
  32. package/vendor/common-nodes/dist/catalog.js +2 -0
  33. package/vendor/common-nodes/dist/mcp/mcp-http/node.d.ts +4 -4
  34. package/vendor/common-nodes/dist/mcp/mcp-http/node.js +9 -10
  35. package/vendor/common-nodes/dist/mcp/mcp-stdio/node.d.ts +4 -4
  36. package/vendor/common-nodes/dist/mcp/mcp-stdio/node.js +9 -10
  37. package/vendor/common-nodes/dist/tools/collect-agent-tool-handles.d.ts +4 -4
  38. package/vendor/common-nodes/dist/tools/collect-agent-tool-handles.js +3 -11
  39. package/vendor/common-nodes/dist/tools/inventory-tool-round.d.ts +0 -10
  40. package/vendor/common-nodes/dist/tools/inventory-tool-round.js +0 -84
  41. package/vendor/common-nodes/dist/tools/tool-collection/node.d.ts +20 -0
  42. package/vendor/common-nodes/dist/tools/tool-collection/node.js +42 -0
  43. package/vendor/common-nodes/package.json +1 -5
  44. package/vendor/compiler/package.json +1 -1
  45. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.d.ts +3 -7
  46. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.js +4 -44
  47. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.d.ts +3 -5
  48. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.js +3 -5
  49. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.d.ts +1 -0
  50. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.js +1 -0
  51. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.d.ts +3 -4
  52. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.js +1 -1
  53. package/vendor/node-sdk/dist/node-factory/define-reactive-node/types.d.ts +2 -4
  54. package/vendor/node-sdk/dist/node-factory/define-reactive-node/ui-schema-inference.d.ts +1 -1
  55. package/vendor/node-sdk/package.json +1 -1
  56. package/vendor/runtime/dist/port-feed-override.d.ts +13 -0
  57. package/vendor/runtime/dist/port-feed-override.js +21 -0
  58. package/vendor/runtime/dist/port-signal-from-response.d.ts +15 -0
  59. package/vendor/runtime/dist/port-signal-from-response.js +48 -0
  60. package/vendor/runtime/dist/runtime-runner.d.ts +8 -3
  61. package/vendor/runtime/dist/runtime-runner.js +102 -81
  62. package/vendor/runtime/dist/runtime.d.ts +3 -2
  63. package/vendor/runtime/dist/runtime.js +1 -1
  64. package/vendor/runtime/dist/testing/workflows/workflow-events.d.ts +2 -0
  65. package/vendor/runtime/dist/testing/workflows/workflow-events.js +7 -5
  66. package/vendor/runtime/dist/types.d.ts +25 -8
  67. package/vendor/runtime/dist/types.js +3 -0
  68. package/vendor/runtime/package.json +1 -1
  69. package/vendor/server/dist/bridge/build-execution-context.js +3 -3
  70. package/vendor/server/dist/bridge/get-live-wired-tools.d.ts +3 -2
  71. package/vendor/server/dist/bridge/get-live-wired-tools.js +8 -5
  72. package/vendor/server/dist/bridge/wire-runner-handlers.js +2 -2
  73. package/vendor/server/dist/bridge/wire-workflow-handlers.js +23 -9
  74. package/vendor/server/dist/checkpoint/run-checkpoint-session.js +3 -3
  75. package/vendor/server/dist/session/reset-session-execution-feed.d.ts +6 -0
  76. package/vendor/server/dist/session/reset-session-execution-feed.js +8 -0
  77. package/vendor/server/skeleton/nodes/my-nodes/package.json +1 -1
  78. package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +36 -22
  79. package/vendor/server/skeleton/skills/langflower-helper/architecture.md +7 -1
  80. package/vendor/server/skeleton/skills/langflower-helper/layout.md +3 -2
  81. package/vendor/server/skeleton/skills/langflower-workflow-writer/SKILL.md +12 -8
  82. package/vendor/server/skeleton/workflows/kb-create.json +15 -43
  83. package/vendor/server/skeleton/workflows/kb-navigate.json +8 -22
  84. package/vendor/server/skeleton/workflows/simple-coder.json +18 -46
  85. package/vendor/server/skeleton/workflows/starter.json +8 -22
  86. package/vendor/shared/dist/execution/derive-run-settle-outcome.js +2 -2
  87. package/vendor/shared/dist/execution/derive-run-settle-outcome.test.js +11 -8
  88. package/vendor/shared/dist/langflower-ws-waits.d.ts +3 -1
  89. package/vendor/shared/dist/langflower-ws-waits.js +2 -2
  90. package/ui-dist/main-2JJGCNTD.js +0 -313
  91. package/ui-dist/styles-EOXODOI7.css +0 -1
@@ -1,13 +1,13 @@
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, filter, firstValueFrom, map, mergeMap, of, Subject, switchMap, take, tap, timeout, TimeoutError, } from 'rxjs';
3
5
  import { llmPanelUiSchema } from '../../features/ui-schema/llm-panel-ui-schema.js';
4
6
  import { llmCompactionUiSchema } from '../../features/ui-schema/llm-compaction-ui-schema.js';
5
7
  import { llmRecoveryUiSchema } from '../../features/ui-schema/llm-recovery-ui-schema.js';
6
8
  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';
9
+ import { appendToolInventory, demuxByKind, } from '../../features/llm-session/llm-session-shell.js';
8
10
  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
11
  import { collectAgentToolHandles } from '../../../tools/collect-agent-tool-handles.js';
12
12
  import { parseLlmRolePreset, resolveEffectiveSkillId, } from '../../features/llm-role-preset.js';
13
13
  import { resolveChatProviderModel } from '../../features/prompt/resolve-chat-provider-model.js';
@@ -24,23 +24,59 @@ const normalizeSkillIds = (raw) => {
24
24
  .map((entry) => String(entry ?? '').trim())
25
25
  .filter((entry) => entry.length > 0);
26
26
  };
27
- const acceptSpawn = (raw, nodeId, skillIds) => {
28
- if (!isSubAgentSpawnPayload(raw)) {
29
- return null;
27
+ const formatSpecialistHandleName = (displayName) => `${displayName}(subagent)`;
28
+ const slugToolId = (name, nodeId) => {
29
+ const slug = (raw) => raw
30
+ .replace(/[^a-zA-Z0-9_-]+/g, '_')
31
+ .replace(/^_+|_+$/g, '')
32
+ .slice(0, 64);
33
+ const fromName = slug(name.trim());
34
+ if (fromName.length > 0) {
35
+ return fromName;
30
36
  }
31
- if (raw.nodeId !== nodeId) {
32
- return null;
37
+ const fromNode = slug(nodeId);
38
+ return fromNode.length > 0 ? fromNode : 'subagent';
39
+ };
40
+ const formatSpawnUserContent = (task, skillId) => {
41
+ if (skillId.length === 0) {
42
+ return task;
43
+ }
44
+ return `[skill:${skillId}]\n${task}`;
45
+ };
46
+ const buildSpecialistDescription = (name, description, skillIds) => {
47
+ const parts = [
48
+ `${name} is a canvas Sub-Agent — a separate specialist agent, not a regular tool (not files, shell, or MCP).`,
49
+ 'Call this tool to delegate a task; the specialist runs its own model and tools and returns a text result.',
50
+ 'Do not try to do this specialist work yourself when this tool is the right owner.',
51
+ ];
52
+ if (description.trim().length > 0) {
53
+ parts.push(description.trim());
33
54
  }
34
- if (raw.skillId.length > 0 && !skillIds.includes(raw.skillId)) {
35
- return null;
55
+ if (skillIds.length > 0) {
56
+ parts.push(`Skills: ${skillIds.join(', ')}.`);
36
57
  }
37
- return raw;
58
+ parts.push('Required argument: task — the assignment for this Sub-Agent.');
59
+ return parts.join(' ');
38
60
  };
39
- const formatSpawnUserContent = (spawn) => {
40
- if (spawn.skillId.length === 0) {
41
- return spawn.task;
61
+ const buildSpecialistInputSchema = (skillIds) => {
62
+ const properties = {
63
+ task: {
64
+ type: 'string',
65
+ description: 'Task to delegate to this Sub-Agent specialist',
66
+ },
67
+ };
68
+ if (skillIds.length > 0) {
69
+ properties.skillId = {
70
+ type: 'string',
71
+ enum: [...skillIds],
72
+ description: 'Optional skill announced by this Sub-Agent Inspector selector',
73
+ };
42
74
  }
43
- return `[skill:${spawn.skillId}]\n${spawn.task}`;
75
+ return {
76
+ type: 'object',
77
+ properties,
78
+ required: ['task'],
79
+ };
44
80
  };
45
81
  const resolveTurnFactory = (context) => {
46
82
  if (context.scriptedTurns !== undefined) {
@@ -48,33 +84,78 @@ const resolveTurnFactory = (context) => {
48
84
  }
49
85
  return context.factory;
50
86
  };
51
- const requireChatConfig = (providerId, model, scripted) => {
87
+ const chatConfigError = (providerId, model, scripted) => {
52
88
  if (scripted) {
53
- return;
89
+ return undefined;
54
90
  }
55
91
  if (providerId.trim().length === 0) {
56
- throw new Error('Provider is required for Sub-Agent chat');
92
+ return 'Error: Provider is required for Sub-Agent chat';
57
93
  }
58
94
  if (model.trim().length === 0) {
59
- throw new Error('Model is required for Sub-Agent chat');
95
+ return 'Error: Model is required for Sub-Agent chat';
60
96
  }
97
+ return undefined;
61
98
  };
62
- const runSubAgentTurn = (context, spawn, history, subagentResult$) => {
99
+ const EMPTY_INVOKE_RESULT = 'Error: Sub-Agent returned no content';
100
+ const finalizeInvokeText = (responseText, lastDraft, lastReasoning) => {
101
+ if (responseText.trim().length > 0) {
102
+ return responseText;
103
+ }
104
+ if (lastDraft.trim().length > 0) {
105
+ return lastDraft;
106
+ }
107
+ if (lastReasoning.trim().length > 0) {
108
+ return lastReasoning;
109
+ }
110
+ return EMPTY_INVOKE_RESULT;
111
+ };
112
+ const seedInvokeHistory = (history, context) => {
113
+ const system = {
114
+ role: 'system',
115
+ content: appendToolInventory(context.effectiveSystemPrompt, context.tools),
116
+ };
117
+ if (history.length === 0) {
118
+ return [system];
119
+ }
120
+ if (history[0]?.role === 'system') {
121
+ return [system, ...history.slice(1)];
122
+ }
123
+ return [system, ...history];
124
+ };
125
+ const reduceInvokeHistory = (history, chunk) => {
126
+ if (chunk.kind === 'historySync') {
127
+ return [...chunk.messages];
128
+ }
129
+ if (chunk.kind === 'invokeDone' && chunk.text.trim().length > 0) {
130
+ return [...history, { role: 'assistant', content: chunk.text }];
131
+ }
132
+ return history;
133
+ };
134
+ const runSubAgentTurn = (context, turn, history) => {
135
+ const fail = (text) => of({ kind: 'response', text }, {
136
+ kind: 'invokeDone',
137
+ requestId: turn.requestId,
138
+ text,
139
+ });
63
140
  const scripted = context.scriptedTurns !== undefined;
64
141
  const factory = resolveTurnFactory(context);
65
142
  if (factory === undefined) {
66
- return new Observable((subscriber) => {
67
- subscriber.error(new Error('Sub-Agent chat is only available during server workflow runs'));
68
- });
143
+ return fail('Error: Sub-Agent chat is only available during server workflow runs');
144
+ }
145
+ const configError = chatConfigError(context.providerId, context.model, scripted);
146
+ if (configError !== undefined) {
147
+ return fail(configError);
69
148
  }
70
- requireChatConfig(context.providerId, context.model, scripted);
149
+ const userContent = formatSpawnUserContent(turn.task, turn.skillId);
150
+ let lastDraft = '';
151
+ let lastReasoning = '';
71
152
  return concat(of({
72
153
  kind: 'historySync',
73
154
  messages: [
74
155
  ...history,
75
156
  {
76
157
  role: 'user',
77
- content: formatSpawnUserContent(spawn),
158
+ content: userContent,
78
159
  },
79
160
  ],
80
161
  }), runAgentLoop({
@@ -85,50 +166,78 @@ const runSubAgentTurn = (context, spawn, history, subagentResult$) => {
85
166
  model: scripted
86
167
  ? context.model.trim() || 'scripted'
87
168
  : context.model,
88
- messages: [
89
- ...history,
90
- { role: 'user', content: formatSpawnUserContent(spawn) },
91
- ],
169
+ messages: [...history, { role: 'user', content: userContent }],
92
170
  tools: context.tools,
93
171
  toolCtx: context.toolCtx,
94
172
  maxIterations: context.maxIterations,
95
173
  compaction: context.compaction,
96
174
  recovery: context.recovery,
97
- subagentRegistrations: context.subagentRegistrations,
98
175
  ...(context.requestPermission !== undefined
99
176
  ? { requestPermission: context.requestPermission }
100
177
  : {}),
101
178
  ...(context.steerControl$ !== undefined
102
179
  ? { steerControl$: context.steerControl$ }
103
180
  : {}),
104
- ...(context.subagentRegistrations.length > 0
105
- ? {
106
- waitForSubagentResult: (callId, signal) => waitForSubagentResult(subagentResult$, callId, signal, context.recovery.subagentTimeoutMs),
107
- }
108
- : {}),
109
181
  }).pipe(mergeMap((chunk) => {
182
+ if (chunk.kind === 'reasoning' && chunk.text.length > 0) {
183
+ lastReasoning += chunk.text;
184
+ }
185
+ if (chunk.kind === 'draftResponse' && chunk.text.length > 0) {
186
+ lastDraft += chunk.text;
187
+ }
110
188
  if (chunk.kind !== 'response') {
111
189
  return of(chunk);
112
190
  }
113
- return of(chunk, {
114
- kind: 'subagentResult',
115
- payload: {
116
- callId: spawn.callId,
117
- result: chunk.text,
118
- },
191
+ const text = finalizeInvokeText(chunk.text, lastDraft, lastReasoning);
192
+ return of({ kind: 'response', text }, {
193
+ kind: 'invokeDone',
194
+ requestId: turn.requestId,
195
+ text,
119
196
  });
197
+ }), catchError((error) => {
198
+ const text = error instanceof Error
199
+ ? `Error: ${error.message}`
200
+ : 'Error: Sub-Agent turn failed.';
201
+ return fail(text);
120
202
  })));
121
203
  };
204
+ const parseInvokeTurn = (raw, skillIds) => {
205
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
206
+ return {
207
+ ok: false,
208
+ text: 'Error: Sub-Agent invoke payload is invalid.',
209
+ };
210
+ }
211
+ const record = raw;
212
+ const requestId = typeof record.requestId === 'string' && record.requestId.length > 0
213
+ ? record.requestId
214
+ : randomUUID();
215
+ const task = typeof record.task === 'string' ? record.task.trim() : '';
216
+ const skillId = typeof record.skillId === 'string' ? record.skillId.trim() : '';
217
+ if (task.length === 0) {
218
+ return {
219
+ ok: false,
220
+ text: 'Error: Sub-Agent requires a non-empty task.',
221
+ };
222
+ }
223
+ if (skillId.length > 0 && !skillIds.includes(skillId)) {
224
+ return {
225
+ ok: false,
226
+ text: `Error: Skill «${skillId}» is not announced by this Sub-Agent.`,
227
+ };
228
+ }
229
+ return { ok: true, turn: { requestId, task, skillId } };
230
+ };
122
231
  /**
123
- * Sub-Agent: ordinary OpenAI-compatible agent plus `registration` announce.
124
- * Parent spawn arrives on `task`; final text returns on `result` with callId.
232
+ * Sub-Agent: ordinary OpenAI-compatible agent that announces one ToolHandle
233
+ * on `subagent-registration`. Parent invoke runs this node's in-node loop.
125
234
  * @see docs/ADR.md ADR-021
126
235
  */
127
236
  export const subAgentNode = defineLlmNode({
128
237
  type: 'common-sub-agent',
129
238
  displayName: 'Sub-Agent',
130
239
  category: 'AI',
131
- description: 'OpenAI-compatible specialist agent that announces via registration and answers parent spawns on task → result.',
240
+ description: 'OpenAI-compatible specialist agent. Wire `tools` into a parent agent; invoke runs this node in-graph.',
132
241
  uiSchema: [
133
242
  {
134
243
  field: 'name',
@@ -153,47 +262,31 @@ export const subAgentNode = defineLlmNode({
153
262
  ...llmRecoveryUiSchema,
154
263
  ],
155
264
  bind(ctx, { makeInput, configureOutput, combineInputs }, inventory) {
156
- const { tools, mcp, subagentRegistration, subagentResult, steerControl, } = inventory;
265
+ const { tools, steerControl } = inventory;
157
266
  const systemPrompt = makeInput('systemPrompt', {
158
267
  name: 'systemPrompt',
159
268
  wireType: 'string',
160
269
  inline: 'text-multiline',
161
270
  defaultValue: '',
162
271
  });
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]) => {
272
+ const chunks$ = new Subject();
273
+ let latestContext;
274
+ let history = [];
275
+ let invokeTail = Promise.resolve();
276
+ const context$ = combineInputs([systemPrompt, tools, ctx], ([systemPromptValue, toolList, ec]) => {
183
277
  const rolePreset = parseLlmRolePreset(ec.params.rolePreset);
184
278
  const skillId = resolveEffectiveSkillId(rolePreset, ec.params.skillId);
185
279
  const hostServices = getRunHostServices(ec);
186
280
  const skillMarkdown = hostServices?.skillMarkdown ?? '';
187
281
  const agentsMarkdown = hostServices?.agentsMarkdown ?? '';
282
+ const name = String(ec.params.name ?? 'Sub-Agent').trim();
283
+ const inspectorDescription = String(ec.params.description ?? '');
188
284
  return {
189
285
  prompt: '',
190
286
  tools: collectAgentToolHandles({
191
287
  toolHandles: ec.toolHandles,
192
288
  toolsPort: toolList,
193
- mcpHandles: ec.mcpHandles,
194
- mcpPort: mcpList,
195
289
  }),
196
- subagentRegistrations: flattenSubAgentRegistrations(subagentList),
197
290
  ...resolveChatProviderModel(ec.params, hostServices),
198
291
  skillId,
199
292
  rolePreset,
@@ -234,26 +327,56 @@ export const subAgentNode = defineLlmNode({
234
327
  factory: hostServices?.createChatCompletionStream,
235
328
  scriptedTurns: parseScriptedTurns(ec.params['scriptedToolTurns']),
236
329
  nodeId: ec.nodeId,
330
+ displayName: name.length > 0 ? name : 'Sub-Agent',
331
+ inspectorDescription,
237
332
  skillIds: normalizeSkillIds(ec.params.skillIds),
238
333
  };
239
334
  });
240
- const cycle$ = createLlmSessionCycle$(context$, task.value$, (context) => ({
241
- history: [
335
+ // Inner chunks stay values. `statefulObservable({ input: chunks$ })`
336
+ // re-emits loading on every token; pipeValue fans it to all demux ports.
337
+ const cycle$ = context$.pipeValue(switchMap(() => chunks$));
338
+ const enqueueInvoke = (announced, args, _toolCtx) => {
339
+ const context = latestContext ?? announced;
340
+ const parsed = parseInvokeTurn({ ...args, requestId: randomUUID() }, announced.skillIds);
341
+ if (!parsed.ok) {
342
+ return Promise.resolve(parsed.text);
343
+ }
344
+ const run = () => {
345
+ history = seedInvokeHistory(history, context);
346
+ const timeoutMs = context.recovery.subagentTimeoutMs;
347
+ let done$ = runSubAgentTurn(context, parsed.turn, history).pipe(tap((chunk) => {
348
+ history = reduceInvokeHistory(history, chunk);
349
+ chunks$.next(chunk);
350
+ }), filter((chunk) => chunk.kind === 'invokeDone'), map((chunk) => chunk.text), take(1));
351
+ if (timeoutMs > 0) {
352
+ done$ = done$.pipe(timeout({ first: timeoutMs }), catchError((error) => of(error instanceof TimeoutError
353
+ ? `Error: Sub-Agent timed out after ${timeoutMs}ms.`
354
+ : error instanceof Error
355
+ ? `Error: ${error.message}`
356
+ : 'Error: Sub-Agent turn failed.')));
357
+ }
358
+ return firstValueFrom(done$, {
359
+ defaultValue: EMPTY_INVOKE_RESULT,
360
+ });
361
+ };
362
+ const next = invokeTail.then(run, run);
363
+ invokeTail = next.then(() => undefined, () => undefined);
364
+ return next;
365
+ };
366
+ const specialistTools$ = context$.pipeValue(tap((context) => {
367
+ latestContext = context;
368
+ }), map((context) => {
369
+ const handleName = formatSpecialistHandleName(context.displayName);
370
+ return [
242
371
  {
243
- role: 'system',
244
- content: appendToolInventory(context.effectiveSystemPrompt, context.tools),
372
+ toolId: slugToolId(handleName, context.nodeId),
373
+ name: handleName,
374
+ description: buildSpecialistDescription(handleName, context.inspectorDescription, context.skillIds),
375
+ inputSchema: buildSpecialistInputSchema(context.skillIds),
376
+ invoke: (args, toolCtx) => enqueueInvoke(context, args, toolCtx),
245
377
  },
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 });
378
+ ];
379
+ }));
257
380
  const reasoning$ = cycle$.pipeValue(demuxByKind('reasoning', (chunk) => chunk
258
381
  .text));
259
382
  const draftResponse$ = cycle$.pipeValue(demuxByKind('draftResponse', (chunk) => chunk
@@ -265,14 +388,12 @@ export const subAgentNode = defineLlmNode({
265
388
  }));
266
389
  const response$ = cycle$.pipeValue(demuxByKind('response', (chunk) => chunk
267
390
  .text));
268
- const result$ = cycle$.pipeValue(demuxByKind('subagentResult', (chunk) => chunk.payload));
269
- const nestedSubagent$ = cycle$.pipeValue(demuxByKind('subagentSpawn', (chunk) => chunk
270
- .payload));
271
391
  return {
272
- inputs: [systemPrompt, task],
392
+ inputs: [systemPrompt],
273
393
  outputs: [
274
- configureOutput('registration', registration$, {
275
- wireType: SUBAGENT_REGISTRATION_WIRE_TYPE,
394
+ configureOutput('subagent-registration', specialistTools$, {
395
+ name: 'subagent-registration',
396
+ wireType: TOOL_HANDLE_WIRE_TYPE,
276
397
  }),
277
398
  configureOutput('reasoning', reasoning$, {
278
399
  wireType: 'string',
@@ -286,16 +407,10 @@ export const subAgentNode = defineLlmNode({
286
407
  wireType: 'string',
287
408
  feed: { role: 'result' },
288
409
  }),
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
410
  ],
295
411
  inventoryOutputs: {
296
412
  toolLog$,
297
413
  recovery$,
298
- subagent$: nestedSubagent$,
299
414
  },
300
415
  };
301
416
  },
@@ -34,6 +34,7 @@ import { fetchUrlNode } from './crawl/fetch-url/node.js';
34
34
  import { savePageNode } from './crawl/save-page/node.js';
35
35
  import { memoryToolsNode } from './memory/memory-tools/node.js';
36
36
  import { langflowerToolsNode } from './langflower-tools/node.js';
37
+ import { toolCollectionNode } from './tools/tool-collection/node.js';
37
38
  const COMMON_REACTIVE_NODES = [
38
39
  booleanNode,
39
40
  numberNode,
@@ -66,6 +67,7 @@ const COMMON_REACTIVE_NODES = [
66
67
  switchNode,
67
68
  memoryToolsNode,
68
69
  langflowerToolsNode,
70
+ toolCollectionNode,
69
71
  fetchUrlNode,
70
72
  extractLinksNode,
71
73
  savePageNode,
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Owns HTTP MCP connect/close (optional local launch); emits live {@link McpHandle}.
3
- * Handle id = graph nodeId; server name and tools come from MCP initialize /
4
- * tools/list. Connect/initialize/build failure → output port **error** (not
5
- * silent EMPTY).
2
+ * Owns HTTP MCP connect/close (optional local launch); emits live
3
+ * {@link ToolHandle}[] for LLM `tools`. Server name and tools come from MCP
4
+ * initialize / tools/list. Session stays in invoke closures.
5
+ * Connect/initialize/build failure → output port **error** (not silent EMPTY).
6
6
  */
7
7
  export declare const mcpHttpNode: {
8
8
  readonly emitOncePerActivation: boolean;
@@ -1,5 +1,4 @@
1
- import { defineReactiveNode } from '@langflower/node-sdk';
2
- import { MCP_HANDLE_WIRE_TYPE } from '@langflower/node-sdk/mcp';
1
+ import { defineReactiveNode, TOOL_HANDLE_WIRE_TYPE, } from '@langflower/node-sdk';
3
2
  import { buildMcpHandle } from '@langflower/tools/build-mcp-handle';
4
3
  import { formatMcpConnectError } from '@langflower/tools/format-mcp-connect-error';
5
4
  import { connectMcpHttpWithOptionalLaunch } from '@langflower/tools/mcp-http-client';
@@ -11,16 +10,16 @@ const paramsKey = (params) => JSON.stringify({
11
10
  command: params.command,
12
11
  });
13
12
  /**
14
- * Owns HTTP MCP connect/close (optional local launch); emits live {@link McpHandle}.
15
- * Handle id = graph nodeId; server name and tools come from MCP initialize /
16
- * tools/list. Connect/initialize/build failure → output port **error** (not
17
- * silent EMPTY).
13
+ * Owns HTTP MCP connect/close (optional local launch); emits live
14
+ * {@link ToolHandle}[] for LLM `tools`. Server name and tools come from MCP
15
+ * initialize / tools/list. Session stays in invoke closures.
16
+ * Connect/initialize/build failure → output port **error** (not silent EMPTY).
18
17
  */
19
18
  export const mcpHttpNode = defineReactiveNode({
20
19
  type: 'common-mcp-http',
21
20
  displayName: 'MCP http',
22
21
  category: 'Tools',
23
- description: 'Connects to an MCP server over Streamable HTTP. Optional shell command launches a local process first; URL alone targets an already-running server.',
22
+ description: 'Connects to an MCP server over Streamable HTTP and emits its tools for LLM `tools` ports. Optional shell command launches a local process first; URL alone targets an already-running server.',
24
23
  uiSchema: [],
25
24
  bind(ctx, { makeInput, configureOutput, combineInputs }) {
26
25
  const url = makeInput('url', {
@@ -82,7 +81,7 @@ export const mcpHttpNode = defineReactiveNode({
82
81
  await session.close();
83
82
  return;
84
83
  }
85
- subscriber.next(handle);
84
+ subscriber.next(handle.tools);
86
85
  };
87
86
  void run().catch(fail);
88
87
  return () => {
@@ -94,8 +93,8 @@ export const mcpHttpNode = defineReactiveNode({
94
93
  return {
95
94
  inputs: [url, command],
96
95
  outputs: [
97
- configureOutput('mcpTransport', handle$, {
98
- wireType: MCP_HANDLE_WIRE_TYPE,
96
+ configureOutput('tools', handle$, {
97
+ wireType: TOOL_HANDLE_WIRE_TYPE,
99
98
  }),
100
99
  ],
101
100
  };
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Owns stdio MCP connect/close; emits live {@link McpHandle} for LLM `mcp`.
3
- * Handle id = graph nodeId; server name and tools come from MCP initialize /
4
- * tools/list. Connect/initialize/build failure → output port **error** (not
5
- * silent EMPTY).
2
+ * Owns stdio MCP connect/close; emits live {@link ToolHandle}[] for LLM `tools`.
3
+ * Server name and tools come from MCP initialize / tools/list. Session stays
4
+ * in invoke closures. Connect/initialize/build failure → output port **error**
5
+ * (not silent EMPTY).
6
6
  */
7
7
  export declare const mcpStdioNode: {
8
8
  readonly emitOncePerActivation: boolean;
@@ -1,5 +1,4 @@
1
- import { defineReactiveNode } from '@langflower/node-sdk';
2
- import { MCP_HANDLE_WIRE_TYPE } from '@langflower/node-sdk/mcp';
1
+ import { defineReactiveNode, TOOL_HANDLE_WIRE_TYPE, } from '@langflower/node-sdk';
3
2
  import { buildMcpHandle } from '@langflower/tools/build-mcp-handle';
4
3
  import { formatMcpConnectError } from '@langflower/tools/format-mcp-connect-error';
5
4
  import { connectMcpStdioFromCli } from '@langflower/tools/mcp-stdio-client';
@@ -10,16 +9,16 @@ const paramsKey = (params) => JSON.stringify({
10
9
  command: params.command,
11
10
  });
12
11
  /**
13
- * Owns stdio MCP connect/close; emits live {@link McpHandle} for LLM `mcp`.
14
- * Handle id = graph nodeId; server name and tools come from MCP initialize /
15
- * tools/list. Connect/initialize/build failure → output port **error** (not
16
- * silent EMPTY).
12
+ * Owns stdio MCP connect/close; emits live {@link ToolHandle}[] for LLM `tools`.
13
+ * Server name and tools come from MCP initialize / tools/list. Session stays
14
+ * in invoke closures. Connect/initialize/build failure → output port **error**
15
+ * (not silent EMPTY).
17
16
  */
18
17
  export const mcpStdioNode = defineReactiveNode({
19
18
  type: 'common-mcp-stdio',
20
19
  displayName: 'MCP stdio',
21
20
  category: 'Tools',
22
- description: 'Launches an MCP server over stdio from a shell command line and emits a live handle for LLM `mcp` ports.',
21
+ description: 'Launches an MCP server over stdio from a shell command line and emits its tools for LLM `tools` ports.',
23
22
  uiSchema: [],
24
23
  bind(ctx, { makeInput, configureOutput, combineInputs }) {
25
24
  const command = makeInput('command', {
@@ -71,7 +70,7 @@ export const mcpStdioNode = defineReactiveNode({
71
70
  await client.close();
72
71
  return;
73
72
  }
74
- subscriber.next(handle);
73
+ subscriber.next(handle.tools);
75
74
  };
76
75
  void run().catch(fail);
77
76
  return () => {
@@ -83,8 +82,8 @@ export const mcpStdioNode = defineReactiveNode({
83
82
  return {
84
83
  inputs: [command],
85
84
  outputs: [
86
- configureOutput('mcpTransport', handle$, {
87
- wireType: MCP_HANDLE_WIRE_TYPE,
85
+ configureOutput('tools', handle$, {
86
+ wireType: TOOL_HANDLE_WIRE_TYPE,
88
87
  }),
89
88
  ],
90
89
  };
@@ -1,12 +1,12 @@
1
1
  import type { ToolHandle } from '@langflower/node-sdk';
2
- import type { McpHandle } from '@langflower/node-sdk/mcp';
2
+ /** Flatten multi-wire values that may be single handles or arrays (packs). */
3
+ export declare const flattenToolHandles: (wired: readonly unknown[]) => readonly ToolHandle[];
3
4
  /**
4
- * Agent inventory: EC ∪ port for tools and MCP (flatten MCP → ToolHandle).
5
+ * Agent inventory: EC ∪ port for tools.
5
6
  * No catalog list, no enabledToolIds filter (server already filtered EC).
7
+ * Later `toolHandles` last-wins on `toolId` (jsonc MCP after builtins).
6
8
  */
7
9
  export declare const collectAgentToolHandles: (options: {
8
10
  readonly toolHandles: readonly ToolHandle[] | undefined;
9
11
  readonly toolsPort: unknown;
10
- readonly mcpHandles: readonly McpHandle[] | undefined;
11
- readonly mcpPort: unknown;
12
12
  }) => readonly ToolHandle[];
@@ -1,4 +1,3 @@
1
- import { flattenMcpHandles } from '../mcp/flatten-mcp-handles.js';
2
1
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
3
2
  const isToolHandle = (value) => {
4
3
  if (!isRecord(value)) {
@@ -12,7 +11,7 @@ const isToolHandle = (value) => {
12
11
  typeof value.invoke === 'function');
13
12
  };
14
13
  /** Flatten multi-wire values that may be single handles or arrays (packs). */
15
- const flattenToolHandles = (wired) => wired.flatMap((item) => {
14
+ export const flattenToolHandles = (wired) => wired.flatMap((item) => {
16
15
  if (Array.isArray(item)) {
17
16
  return item.filter(isToolHandle);
18
17
  }
@@ -22,8 +21,9 @@ const flattenToolHandles = (wired) => wired.flatMap((item) => {
22
21
  return [];
23
22
  });
24
23
  /**
25
- * Agent inventory: EC ∪ port for tools and MCP (flatten MCP → ToolHandle).
24
+ * Agent inventory: EC ∪ port for tools.
26
25
  * No catalog list, no enabledToolIds filter (server already filtered EC).
26
+ * Later `toolHandles` last-wins on `toolId` (jsonc MCP after builtins).
27
27
  */
28
28
  export const collectAgentToolHandles = (options) => {
29
29
  const portTools = Array.isArray(options.toolsPort)
@@ -38,13 +38,5 @@ export const collectAgentToolHandles = (options) => {
38
38
  for (const handle of options.toolHandles ?? []) {
39
39
  byId.set(handle.toolId, handle);
40
40
  }
41
- for (const mcp of [
42
- ...(options.mcpHandles ?? []),
43
- ...flattenMcpHandles(options.mcpPort),
44
- ]) {
45
- for (const handle of mcp.tools) {
46
- byId.set(handle.toolId, handle);
47
- }
48
- }
49
41
  return [...byId.values()];
50
42
  };
@@ -2,8 +2,6 @@ import type { ToolHandle } from '@langflower/node-sdk';
2
2
  import { type Harness } from '@langflower/tools/create-project-harness';
3
3
  import type { ToolHandlerContext } from '@langflower/tools/domain-tool-configs';
4
4
  import type { ChatCompletionToolCall, ChatCompletionToolDefinition } from '../ai/features/chat-completion-stream.js';
5
- import type { SubAgentRegistration, SubAgentSpawnPayload } from '../ai/features/sub-agent-protocol.js';
6
- export declare const SPAWN_SUBAGENT_TOOL = "spawn_subagent";
7
5
  export declare const previewToolLogText: (text: string) => string;
8
6
  export declare const toChatToolDefinitions: (tools: readonly ToolHandle[]) => readonly ChatCompletionToolDefinition[];
9
7
  export declare const parseToolArgs: (raw: string) => Readonly<Record<string, unknown>>;
@@ -20,11 +18,3 @@ export declare const invokeInventoryTool: (harness: Harness | undefined, tools:
20
18
  readonly ok: boolean;
21
19
  readonly text: string;
22
20
  }>;
23
- export declare const buildSpawnSubagentChatTool: (registrations: readonly SubAgentRegistration[]) => ChatCompletionToolDefinition;
24
- export declare const resolveSpawnPayload: (call: ChatCompletionToolCall, registrations: readonly SubAgentRegistration[], openCallId: string | undefined) => {
25
- readonly ok: true;
26
- readonly payload: SubAgentSpawnPayload;
27
- } | {
28
- readonly ok: false;
29
- readonly text: string;
30
- };