langflower 0.0.7 → 0.0.9

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 (189) hide show
  1. package/package.json +1 -1
  2. package/ui-dist/{chunk-Y6VMBLSJ.js → chunk-7UMVYN3A.js} +1 -1
  3. package/ui-dist/{chunk-6DQ2XSRW.js → chunk-MDGXX73Z.js} +3 -3
  4. package/ui-dist/index.html +1 -1
  5. package/ui-dist/main-2JJGCNTD.js +313 -0
  6. package/vendor/common-nodes/dist/ai/chat-completion-stream.d.ts +2 -0
  7. package/vendor/common-nodes/dist/ai/critique/node.d.ts +246 -0
  8. package/vendor/common-nodes/dist/ai/critique/node.js +2 -2
  9. package/vendor/common-nodes/dist/ai/fake-llm/node.d.ts +246 -0
  10. package/vendor/common-nodes/dist/ai/features/chat-completion-stream.d.ts +53 -0
  11. package/vendor/common-nodes/dist/ai/features/chat-completion-stream.js +1 -0
  12. package/vendor/common-nodes/dist/ai/features/llm-loop/autokick-recovery.d.ts +13 -0
  13. package/vendor/common-nodes/dist/ai/features/llm-loop/autokick-recovery.js +17 -0
  14. package/vendor/common-nodes/dist/ai/features/llm-loop/classify-llm-failure.d.ts +2 -0
  15. package/vendor/common-nodes/dist/ai/features/llm-loop/classify-llm-failure.js +113 -0
  16. package/vendor/common-nodes/dist/ai/features/llm-loop/dead-loop-detector.d.ts +28 -0
  17. package/vendor/common-nodes/dist/ai/features/llm-loop/dead-loop-detector.js +194 -0
  18. package/vendor/common-nodes/dist/ai/features/llm-loop/live-get-tools.d.ts +9 -0
  19. package/vendor/common-nodes/dist/ai/features/llm-loop/live-get-tools.js +21 -0
  20. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.d.ts +2 -0
  21. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.js +216 -0
  22. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.d.ts +125 -0
  23. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.js +39 -0
  24. package/vendor/common-nodes/dist/ai/features/llm-loop/normalize-llm-recovery-policy.d.ts +2 -0
  25. package/vendor/common-nodes/dist/ai/features/llm-loop/normalize-llm-recovery-policy.js +59 -0
  26. package/vendor/common-nodes/dist/ai/features/llm-loop/operators/normalize-tool-result.d.ts +1 -0
  27. package/vendor/common-nodes/dist/ai/features/llm-loop/operators/normalize-tool-result.js +7 -0
  28. package/vendor/common-nodes/dist/ai/features/llm-loop/operators/observe-provider-stream.d.ts +43 -0
  29. package/vendor/common-nodes/dist/ai/features/llm-loop/operators/observe-provider-stream.js +96 -0
  30. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.d.ts +32 -0
  31. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.js +58 -0
  32. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.d.ts +80 -0
  33. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.js +681 -0
  34. package/vendor/common-nodes/dist/ai/features/llm-role-preset.d.ts +69 -0
  35. package/vendor/common-nodes/dist/ai/features/llm-role-preset.js +265 -0
  36. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.d.ts +148 -0
  37. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.js +198 -0
  38. package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.d.ts +21 -0
  39. package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.js +118 -0
  40. package/vendor/common-nodes/dist/ai/features/openai/context-length-error.d.ts +14 -0
  41. package/vendor/common-nodes/dist/ai/features/openai/context-length-error.js +63 -0
  42. package/vendor/common-nodes/dist/ai/features/openai/create-chat-completion-stream.d.ts +19 -0
  43. package/vendor/common-nodes/dist/ai/features/openai/create-chat-completion-stream.js +182 -0
  44. package/vendor/common-nodes/dist/ai/features/openai/list-provider-models.d.ts +28 -0
  45. package/vendor/common-nodes/dist/ai/features/openai/list-provider-models.js +52 -0
  46. package/vendor/common-nodes/dist/ai/features/openai/llm-context-compaction.d.ts +31 -0
  47. package/vendor/common-nodes/dist/ai/features/openai/llm-context-compaction.js +326 -0
  48. package/vendor/common-nodes/dist/ai/features/openai/normalize-compaction-params.d.ts +14 -0
  49. package/vendor/common-nodes/dist/ai/features/openai/normalize-compaction-params.js +21 -0
  50. package/vendor/common-nodes/dist/ai/features/openai/prepare-chat-completion.d.ts +30 -0
  51. package/vendor/common-nodes/dist/ai/features/openai/prepare-chat-completion.js +132 -0
  52. package/vendor/common-nodes/dist/ai/features/path-choice/control-tools.d.ts +20 -0
  53. package/vendor/common-nodes/dist/ai/features/path-choice/control-tools.js +70 -0
  54. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.d.ts +35 -0
  55. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.js +106 -0
  56. package/vendor/common-nodes/dist/ai/features/prompt/build-effective-system-prompt.d.ts +9 -0
  57. package/vendor/common-nodes/dist/ai/features/prompt/build-effective-system-prompt.js +21 -0
  58. package/vendor/common-nodes/dist/ai/features/prompt/normalize-max-iterations.d.ts +13 -0
  59. package/vendor/common-nodes/dist/ai/features/prompt/normalize-max-iterations.js +23 -0
  60. package/vendor/common-nodes/dist/ai/features/prompt/resolve-chat-provider-model.d.ts +10 -0
  61. package/vendor/common-nodes/dist/ai/features/prompt/resolve-chat-provider-model.js +8 -0
  62. package/vendor/common-nodes/dist/ai/features/run-host-services.d.ts +54 -0
  63. package/vendor/common-nodes/dist/ai/features/run-host-services.js +29 -0
  64. package/vendor/common-nodes/dist/ai/features/scripted-chat-completion-stream.d.ts +17 -0
  65. package/vendor/common-nodes/dist/ai/features/scripted-chat-completion-stream.js +61 -0
  66. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.d.ts +39 -0
  67. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.js +43 -0
  68. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-compaction-ui-schema.d.ts +14 -0
  69. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-compaction-ui-schema.js +18 -0
  70. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-panel-ui-schema.d.ts +80 -0
  71. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-panel-ui-schema.js +76 -0
  72. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.d.ts +113 -0
  73. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.js +131 -0
  74. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.d.ts +6 -0
  75. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.js +30 -0
  76. package/vendor/common-nodes/dist/ai/llm-loop/autokick-recovery.d.ts +13 -0
  77. package/vendor/common-nodes/dist/ai/llm-loop/autokick-recovery.js +17 -0
  78. package/vendor/common-nodes/dist/ai/llm-loop/dead-loop-detector.d.ts +28 -0
  79. package/vendor/common-nodes/dist/ai/llm-loop/dead-loop-detector.js +194 -0
  80. package/vendor/common-nodes/dist/ai/llm-loop/llm-loop-reducer.js +36 -0
  81. package/vendor/common-nodes/dist/ai/llm-loop/llm-loop-types.d.ts +35 -0
  82. package/vendor/common-nodes/dist/ai/llm-loop/llm-loop-types.js +19 -0
  83. package/vendor/common-nodes/dist/ai/llm-loop/normalize-llm-recovery-policy.js +52 -9
  84. package/vendor/common-nodes/dist/ai/llm-loop/operators/observe-provider-stream.d.ts +8 -2
  85. package/vendor/common-nodes/dist/ai/llm-loop/operators/observe-provider-stream.js +43 -13
  86. package/vendor/common-nodes/dist/ai/llm-loop/run-llm-loop.d.ts +3 -5
  87. package/vendor/common-nodes/dist/ai/llm-loop/run-llm-loop.js +116 -1
  88. package/vendor/common-nodes/dist/ai/llm-recovery-ui-schema.d.ts +82 -0
  89. package/vendor/common-nodes/dist/ai/llm-recovery-ui-schema.js +95 -0
  90. package/vendor/common-nodes/dist/ai/llm-session-shell.d.ts +4 -9
  91. package/vendor/common-nodes/dist/ai/llm-session-shell.js +2 -4
  92. package/vendor/common-nodes/dist/ai/nodes/critique/node.d.ts +588 -0
  93. package/vendor/common-nodes/dist/ai/nodes/critique/node.js +240 -0
  94. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.d.ts +571 -0
  95. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.js +208 -0
  96. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.d.ts +587 -0
  97. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.js +91 -0
  98. package/vendor/common-nodes/dist/ai/nodes/review/node.d.ts +593 -0
  99. package/vendor/common-nodes/dist/ai/nodes/review/node.js +240 -0
  100. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.d.ts +630 -0
  101. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.js +302 -0
  102. package/vendor/common-nodes/dist/ai/openai/create-chat-completion-stream.js +6 -0
  103. package/vendor/common-nodes/dist/ai/openai/prepare-chat-completion.d.ts +2 -0
  104. package/vendor/common-nodes/dist/ai/openai/prepare-chat-completion.js +18 -0
  105. package/vendor/common-nodes/dist/ai/openai-llm/node.d.ts +246 -0
  106. package/vendor/common-nodes/dist/ai/review/node.d.ts +246 -0
  107. package/vendor/common-nodes/dist/ai/review/node.js +2 -2
  108. package/vendor/common-nodes/dist/ai/sub-agent/node.d.ts +246 -0
  109. package/vendor/common-nodes/dist/ai/sub-agent/node.js +2 -2
  110. package/vendor/common-nodes/dist/catalog.js +7 -5
  111. package/vendor/common-nodes/dist/compile-custom-nodes/node.d.ts +21 -0
  112. package/vendor/common-nodes/dist/compile-custom-nodes/node.js +45 -0
  113. package/vendor/common-nodes/dist/crawl/crawl/node.js +1 -1
  114. package/vendor/common-nodes/dist/crawl/fetch-url/node.js +1 -1
  115. package/vendor/common-nodes/dist/flow/repeat/node.js +6 -1
  116. package/vendor/common-nodes/dist/langflower-tools/emit-registration-tools.d.ts +18 -0
  117. package/vendor/common-nodes/dist/langflower-tools/emit-registration-tools.js +29 -0
  118. package/vendor/common-nodes/dist/langflower-tools/node.d.ts +20 -0
  119. package/vendor/common-nodes/dist/langflower-tools/node.js +77 -0
  120. package/vendor/common-nodes/dist/text/append-file/node.js +1 -1
  121. package/vendor/common-nodes/dist/text/read-file/node.js +1 -1
  122. package/vendor/common-nodes/dist/text/write-file/node.js +1 -1
  123. package/vendor/common-nodes/dist/tools/inventory-tool-round.d.ts +2 -2
  124. package/vendor/common-nodes/package.json +10 -10
  125. package/vendor/compiler/dist/compile-project-nodes.d.ts +2 -1
  126. package/vendor/compiler/dist/compile-project-nodes.js +35 -6
  127. package/vendor/compiler/dist/resolve-host-types.d.ts +0 -5
  128. package/vendor/compiler/dist/resolve-host-types.js +0 -8
  129. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.d.ts +1 -1
  130. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.js +1 -1
  131. package/vendor/node-sdk/dist/node-factory/define-llm-node/recovery-notice.d.ts +8 -0
  132. package/vendor/node-sdk/dist/node-factory/define-llm-node/recovery-notice.js +14 -0
  133. package/vendor/runtime/dist/runtime-editor.d.ts +5 -1
  134. package/vendor/runtime/dist/runtime-editor.js +19 -0
  135. package/vendor/runtime/dist/runtime-runner.js +17 -17
  136. package/vendor/runtime/dist/runtime.d.ts +2 -1
  137. package/vendor/runtime/dist/runtime.js +1 -0
  138. package/vendor/runtime/dist/testing/workflows/workflow-events.d.ts +8 -6
  139. package/vendor/runtime/dist/testing/workflows/workflow-events.js +19 -31
  140. package/vendor/runtime/dist/types.d.ts +33 -38
  141. package/vendor/runtime/dist/types.js +2 -1
  142. package/vendor/server/dist/bridge/attach-langflower-bridge.js +3 -2
  143. package/vendor/server/dist/bridge/bridge-event-log.js +17 -102
  144. package/vendor/server/dist/bridge/build-execution-context.d.ts +5 -2
  145. package/vendor/server/dist/bridge/build-execution-context.js +45 -5
  146. package/vendor/server/dist/bridge/forward-runner-event.d.ts +1 -1
  147. package/vendor/server/dist/bridge/forward-runner-event.js +7 -12
  148. package/vendor/server/dist/bridge/get-live-wired-tools.d.ts +12 -0
  149. package/vendor/server/dist/bridge/get-live-wired-tools.js +80 -0
  150. package/vendor/server/dist/bridge/langflower-tools-rpc.d.ts +7 -0
  151. package/vendor/server/dist/bridge/langflower-tools-rpc.js +29 -0
  152. package/vendor/server/dist/bridge/settings-draft-controller.js +26 -3
  153. package/vendor/server/dist/bridge/wire-custom-palette-handlers.d.ts +2 -1
  154. package/vendor/server/dist/bridge/wire-custom-palette-handlers.js +3 -5
  155. package/vendor/server/dist/bridge/wire-runner-handlers.js +15 -8
  156. package/vendor/server/dist/checkpoint/run-checkpoint-session.d.ts +1 -1
  157. package/vendor/server/dist/checkpoint/run-checkpoint-session.js +10 -9
  158. package/vendor/server/dist/palette/compile-and-hot-swap-custom-nodes.d.ts +10 -0
  159. package/vendor/server/dist/palette/compile-and-hot-swap-custom-nodes.js +23 -0
  160. package/vendor/server/dist/workflow/apply-editor-mutation.d.ts +5 -0
  161. package/vendor/server/dist/workflow/apply-editor-mutation.js +32 -0
  162. package/vendor/server/skeleton/instructions.md +9 -5
  163. package/vendor/server/skeleton/nodes/my-nodes/README.md +4 -3
  164. package/vendor/server/skeleton/schemas/langflower-config.schema.json +11 -1
  165. package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +49 -19
  166. package/vendor/server/skeleton/skills/langflower-helper/architecture.md +22 -9
  167. package/vendor/server/skeleton/skills/langflower-helper/layout.md +15 -15
  168. package/vendor/server/skeleton/skills/langflower-node-writer/SKILL.md +19 -9
  169. package/vendor/server/skeleton/skills/langflower-workflow-writer/SKILL.md +14 -10
  170. package/vendor/server/skeleton/workflows/starter.json +30 -3
  171. package/vendor/shared/dist/execution/derive-run-settle-outcome.d.ts +1 -1
  172. package/vendor/shared/dist/execution/derive-run-settle-outcome.js +4 -3
  173. package/vendor/shared/dist/execution/derive-run-settle-outcome.test.js +9 -9
  174. package/vendor/shared/dist/langflower-bus-config.d.ts +6 -14
  175. package/vendor/shared/dist/langflower-bus-config.js +4 -12
  176. package/vendor/shared/dist/langflower-config/resolve-wired-tool-options.js +6 -0
  177. package/vendor/shared/dist/langflower-config/resolve-wired-tool-options.test.js +30 -0
  178. package/vendor/shared/dist/langflower-ws-waits.d.ts +7 -14
  179. package/vendor/shared/dist/langflower-ws-waits.js +14 -14
  180. package/vendor/websocket-bridge/dist/bridge-codec.d.ts +1 -0
  181. package/vendor/websocket-bridge/dist/bridge-codec.js +11 -11
  182. package/vendor/websocket-bridge/dist/bridge-frame.js +1 -1
  183. package/vendor/websocket-bridge/dist/bridge-subjects.d.ts +1 -1
  184. package/vendor/websocket-bridge/dist/bridge-subjects.js +2 -2
  185. package/vendor/websocket-bridge/dist/bridge-types.d.ts +13 -1
  186. package/vendor/websocket-bridge/dist/create-client.js +3 -3
  187. package/vendor/websocket-bridge/dist/create-server.js +12 -6
  188. package/vendor/websocket-bridge/package.json +8 -0
  189. package/ui-dist/main-SMO3L7U5.js +0 -311
@@ -0,0 +1,681 @@
1
+ import { isSteerControlContinue, isSteerControlPause, } from '@langflower/node-sdk/llm';
2
+ import { EMPTY, catchError, concat, concatMap, defer, delay, expand, filter, finalize, from, fromEvent, map, mergeMap, NEVER, of, race, scan, switchMap, take, takeUntil, tap, throwError, TimeoutError, timeout, } from 'rxjs';
3
+ import { invokeInventoryTool, previewToolLogText, resolveSpawnPayload, SPAWN_SUBAGENT_TOOL, toChatToolDefinitions, } from '../../../tools/inventory-tool-round.js';
4
+ import { prepareChatCompletion } from '../openai/prepare-chat-completion.js';
5
+ import { compactMessagesWithSummary, estimateRequestTokens, resolveForceTargetTokens, } from '../openai/llm-context-compaction.js';
6
+ import { classifyLlmFailure } from './classify-llm-failure.js';
7
+ import { autokickBackoffMs, autokickPenalties } from './autokick-recovery.js';
8
+ import { reduceLlmLoop } from './llm-loop-reducer.js';
9
+ import { initialLlmLoopState, } from './llm-loop-types.js';
10
+ import { observeProviderStream, } from './operators/observe-provider-stream.js';
11
+ import { normalizeToolResult } from './operators/normalize-tool-result.js';
12
+ const inventoryChatName = (handle) => handle.toolId.length > 0 ? handle.toolId : handle.name;
13
+ const resolveGetTools = (options) => options.getTools;
14
+ const resolveInventoryTools = (options) => {
15
+ const getTools = resolveGetTools(options);
16
+ return getTools === undefined ? options.inventoryTools : getTools();
17
+ };
18
+ /**
19
+ * Rebuild provider `tools` from live inventory. Keep extras (Review
20
+ * accept/feedback, spawn_subagent) in their original positions; drop
21
+ * removed inventory ids; append brand-new ids.
22
+ */
23
+ const resolveChatTools = (options) => {
24
+ if (resolveGetTools(options) === undefined) {
25
+ return options.chatTools;
26
+ }
27
+ const liveDefs = toChatToolDefinitions(resolveInventoryTools(options));
28
+ const originalInventoryNames = new Set(options.inventoryTools.map(inventoryChatName));
29
+ const extraNames = new Set(options.chatTools
30
+ .filter((tool) => !originalInventoryNames.has(tool.function.name))
31
+ .map((tool) => tool.function.name));
32
+ const liveByName = new Map(liveDefs.map((def) => [def.function.name, def]));
33
+ const merged = [];
34
+ const usedLive = new Set();
35
+ for (const tool of options.chatTools) {
36
+ const name = tool.function.name;
37
+ if (!originalInventoryNames.has(name)) {
38
+ merged.push(tool);
39
+ continue;
40
+ }
41
+ const liveDef = liveByName.get(name);
42
+ if (liveDef !== undefined) {
43
+ merged.push(liveDef);
44
+ usedLive.add(name);
45
+ }
46
+ }
47
+ for (const def of liveDefs) {
48
+ const name = def.function.name;
49
+ if (!usedLive.has(name) && !extraNames.has(name)) {
50
+ merged.push(def);
51
+ }
52
+ }
53
+ return merged;
54
+ };
55
+ const emit = (chunk) => ({ kind: 'emit', chunk });
56
+ const transition = (state) => ({
57
+ kind: 'transition',
58
+ state,
59
+ });
60
+ const failureText = (failure) => {
61
+ const status = failure.status === undefined ? '' : ` HTTP ${failure.status}`;
62
+ return `Provider failure${status}: ${failure.message}`;
63
+ };
64
+ const joinsAutokickWait = (kind) => kind === 'rate-limit' ||
65
+ kind === 'provider-unavailable' ||
66
+ kind === 'network';
67
+ const retryReasonFromFailure = (kind) => joinsAutokickWait(kind) ? kind : undefined;
68
+ const isIncompleteToolCallJson = (calls) => {
69
+ for (const call of calls) {
70
+ const args = call.arguments.trim();
71
+ if (args.length === 0) {
72
+ return true;
73
+ }
74
+ try {
75
+ JSON.parse(args);
76
+ }
77
+ catch {
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ };
83
+ const isOutputTruncation = (fact) => {
84
+ if (fact.finishReason === 'length') {
85
+ return true;
86
+ }
87
+ const calls = fact.toolCalls ?? [];
88
+ return calls.length > 0 && isIncompleteToolCallJson(calls);
89
+ };
90
+ const canScheduleAutokick = (state, recovery) => recovery.autokickOnIdle &&
91
+ (recovery.maxAutokickAttempts === 0 ||
92
+ state.autokickAttempts < recovery.maxAutokickAttempts);
93
+ const autokickPackets = (state, options, args) => {
94
+ if (!canScheduleAutokick(state, options.recovery)) {
95
+ return concat(of(emit({
96
+ kind: 'recoveryNotice',
97
+ code: 'suspended',
98
+ text: args.suspendedText,
99
+ })), of(transition(state)));
100
+ }
101
+ const attempt = state.autokickAttempts + 1;
102
+ const backoffMs = args.reason === 'dead-loop'
103
+ ? options.recovery.retryBaseDelayMs
104
+ : Math.max(autokickBackoffMs(attempt, options.recovery.autokickBackoffMs, options.recovery.autokickMaxBackoffMs), args.retryAfterMs ?? 0);
105
+ const nowMs = Date.now();
106
+ const interrupt$ = (options.steerControl$ ?? NEVER).pipe(filter((payload) => isSteerControlPause(payload) || payload.kind === 'steer'), take(1), map((payload) => ({ kind: 'interrupt', payload })));
107
+ return concat(of(emit({
108
+ kind: 'recoveryNotice',
109
+ code: 'retry',
110
+ text: `⚠ ${args.retryDetail}. Retrying ${attempt} in ${backoffMs}ms.`,
111
+ attempt,
112
+ reason: args.reason,
113
+ backoffMs,
114
+ nextAttemptAt: nowMs + backoffMs,
115
+ ...(state.lastAutokickAt !== undefined
116
+ ? { lastAttemptAt: state.lastAutokickAt }
117
+ : {}),
118
+ })), race(of({ kind: 'elapsed' }).pipe(delay(backoffMs)), interrupt$).pipe(switchMap((event) => {
119
+ if (event.kind === 'interrupt') {
120
+ if (isSteerControlPause(event.payload)) {
121
+ return concat(of(emit({
122
+ kind: 'toolLog',
123
+ text: 'Paused. Send Steer feedback or Resume to continue.',
124
+ })), of(transition(reduceLlmLoop(state, {
125
+ type: 'stream.paused',
126
+ }))));
127
+ }
128
+ const text = event.payload.kind === 'steer'
129
+ ? event.payload.text
130
+ : undefined;
131
+ return of(transition(reduceLlmLoop(state, {
132
+ type: 'steer.received',
133
+ ...(text !== undefined ? { text } : {}),
134
+ })));
135
+ }
136
+ return of(transition(reduceLlmLoop(state, {
137
+ type: 'autokick.scheduled',
138
+ atMs: Date.now(),
139
+ ...(args.kickUserMessage !== undefined
140
+ ? { kickUserMessage: args.kickUserMessage }
141
+ : {}),
142
+ })));
143
+ })));
144
+ };
145
+ const recoveryPackets = (state, failure, options) => {
146
+ if (!failure.recoverable) {
147
+ return concat(of(emit({
148
+ kind: 'toolLog',
149
+ text: `⚠ ${failureText(failure)}`,
150
+ })), of(transition(reduceLlmLoop(state, {
151
+ type: 'failure.fatal',
152
+ failure,
153
+ }))));
154
+ }
155
+ // Structural history/compaction protocol failures will not heal on retry.
156
+ if (failure.kind === 'protocol') {
157
+ return concat(of(emit({
158
+ kind: 'recoveryNotice',
159
+ code: 'suspended',
160
+ text: `⚠ ${failureText(failure)}. Paused for Steer or Resume.`,
161
+ })), of(transition(reduceLlmLoop(state, {
162
+ type: 'provider.failed',
163
+ failure,
164
+ }))));
165
+ }
166
+ if (state.transientAttempts < options.recovery.maxTransientRetries) {
167
+ const attempt = state.transientAttempts + 1;
168
+ const delayMs = failure.retryAfterMs ??
169
+ options.recovery.retryBaseDelayMs * 2 ** (attempt - 1);
170
+ const next = reduceLlmLoop(state, { type: 'retry.scheduled' });
171
+ const reason = retryReasonFromFailure(failure.kind);
172
+ return concat(of(emit({
173
+ kind: 'recoveryNotice',
174
+ code: 'retry',
175
+ text: `⚠ ${failureText(failure)}. Retrying ${attempt}/${options.recovery.maxTransientRetries} in ${delayMs}ms.`,
176
+ attempt,
177
+ backoffMs: delayMs,
178
+ nextAttemptAt: Date.now() + delayMs,
179
+ ...(reason !== undefined ? { reason } : {}),
180
+ ...(state.lastAutokickAt !== undefined
181
+ ? { lastAttemptAt: state.lastAutokickAt }
182
+ : {}),
183
+ })), of(transition(next)).pipe(delay(delayMs)));
184
+ }
185
+ if (joinsAutokickWait(failure.kind) &&
186
+ canScheduleAutokick(state, options.recovery)) {
187
+ return autokickPackets(state, options, {
188
+ retryDetail: failureText(failure),
189
+ suspendedText: `⚠ ${failureText(failure)}. Retry budget exhausted; paused for Steer or Resume.`,
190
+ reason: failure.kind,
191
+ ...(failure.retryAfterMs !== undefined
192
+ ? { retryAfterMs: failure.retryAfterMs }
193
+ : {}),
194
+ });
195
+ }
196
+ return concat(of(emit({
197
+ kind: 'recoveryNotice',
198
+ code: 'suspended',
199
+ text: `⚠ ${failureText(failure)}. Retry budget exhausted; paused for Steer or Resume.`,
200
+ })), of(transition(reduceLlmLoop(state, {
201
+ type: 'provider.failed',
202
+ failure,
203
+ }))));
204
+ };
205
+ const truncationRecoveryPackets = (state, options, detail) => {
206
+ const failure = {
207
+ kind: 'output-truncated',
208
+ message: detail,
209
+ recoverable: true,
210
+ };
211
+ const notice = emit({
212
+ kind: 'recoveryNotice',
213
+ code: 'retry',
214
+ text: `⚠ ${detail}`,
215
+ });
216
+ if (options.compaction.contextSize > 0 &&
217
+ state.transientAttempts < options.recovery.maxTransientRetries) {
218
+ const attempt = state.transientAttempts + 1;
219
+ const chatTools = resolveChatTools(options);
220
+ const targetTokens = resolveForceTargetTokens(options.compaction, estimateRequestTokens(state.roundCheckpoint, chatTools));
221
+ return concat(of(notice), defer(() => compactMessagesWithSummary({
222
+ factory: options.factory,
223
+ providerId: options.providerId,
224
+ model: options.model,
225
+ messages: state.roundCheckpoint,
226
+ tools: chatTools,
227
+ targetTokens,
228
+ })).pipe(catchError((error) => of({
229
+ ok: false,
230
+ message: classifyLlmFailure(error).message,
231
+ })), mergeMap((compacted) => {
232
+ if (!compacted.ok) {
233
+ return recoveryPackets(state, {
234
+ ...failure,
235
+ message: `${detail} Compaction failed: ${compacted.message}`,
236
+ }, options);
237
+ }
238
+ const next = {
239
+ ...state,
240
+ phase: 'prepare',
241
+ committedMessages: [...compacted.messages],
242
+ roundCheckpoint: [...compacted.messages],
243
+ iteration: state.iteration,
244
+ transientAttempts: attempt,
245
+ partial: { reasoning: '', draft: '' },
246
+ pendingToolCalls: [],
247
+ suspendedBy: undefined,
248
+ failure: undefined,
249
+ };
250
+ return from([
251
+ emit({
252
+ kind: 'toolLog',
253
+ text: `Compacted history after output truncation ` +
254
+ `(retry ${attempt}/${options.recovery.maxTransientRetries}): ` +
255
+ `~${compacted.beforeTokens} → ~${compacted.afterTokens} approx tokens`,
256
+ }),
257
+ emit({
258
+ kind: 'historySync',
259
+ messages: [...compacted.messages],
260
+ }),
261
+ transition(next),
262
+ ]);
263
+ })));
264
+ }
265
+ return recoveryPackets(state, failure, options);
266
+ };
267
+ const providerFactAction = (fact) => {
268
+ switch (fact.kind) {
269
+ case 'provider.reasoning':
270
+ return { type: 'stream.reasoning', text: fact.text };
271
+ case 'provider.draft':
272
+ return { type: 'stream.draft', text: fact.text };
273
+ case 'provider.done':
274
+ if (isOutputTruncation(fact)) {
275
+ return undefined;
276
+ }
277
+ // Policy (`run-tools`) owns the sole assistant+tool_calls commit.
278
+ // Scan must not append here — a double stream.done leaves an orphan
279
+ // incomplete block that breaks later compaction.
280
+ if ((fact.toolCalls?.length ?? 0) > 0) {
281
+ return undefined;
282
+ }
283
+ return {
284
+ type: 'stream.done',
285
+ text: fact.text,
286
+ };
287
+ case 'provider.paused':
288
+ return { type: 'stream.paused' };
289
+ case 'provider.idle':
290
+ return { type: 'stream.idle', idleMs: fact.idleMs };
291
+ case 'provider.dead-loop':
292
+ return {
293
+ type: 'stream.dead-loop',
294
+ channel: fact.channel,
295
+ reason: fact.reason,
296
+ };
297
+ case 'provider.failed':
298
+ return undefined;
299
+ }
300
+ };
301
+ const providerFactPackets = (frame, options) => {
302
+ const fact = frame.fact;
303
+ if (fact === undefined) {
304
+ return EMPTY;
305
+ }
306
+ switch (fact.kind) {
307
+ case 'provider.reasoning':
308
+ return of(emit({
309
+ kind: 'reasoning',
310
+ text: fact.text,
311
+ }));
312
+ case 'provider.draft':
313
+ return of(emit({
314
+ kind: 'draftResponse',
315
+ text: fact.text,
316
+ }));
317
+ case 'provider.paused':
318
+ return concat(of(emit({
319
+ kind: 'toolLog',
320
+ text: 'Paused. Send Steer feedback or Resume to continue.',
321
+ })), of(transition(frame.state)));
322
+ case 'provider.idle': {
323
+ const failure = {
324
+ kind: 'stream-idle',
325
+ message: `Provider stream emitted no chunks for ${fact.idleMs}ms.`,
326
+ recoverable: true,
327
+ };
328
+ const hasPartial = frame.state.partial.reasoning.length > 0 ||
329
+ frame.state.partial.draft.length > 0;
330
+ const suspendedText = hasPartial
331
+ ? `⚠ ${failure.message} Paused to avoid repeating partial work.`
332
+ : `⚠ ${failure.message}. Retry budget exhausted; paused for Steer or Resume.`;
333
+ if (options.recovery.autokickOnIdle) {
334
+ return autokickPackets(frame.state, options, {
335
+ retryDetail: failure.message,
336
+ suspendedText,
337
+ reason: 'idle',
338
+ kickUserMessage: options.recovery.autokickUserMessage,
339
+ });
340
+ }
341
+ return hasPartial
342
+ ? concat(of(emit({
343
+ kind: 'recoveryNotice',
344
+ code: 'suspended',
345
+ text: suspendedText,
346
+ })), of(transition(frame.state)))
347
+ : recoveryPackets(frame.state, failure, options);
348
+ }
349
+ case 'provider.dead-loop':
350
+ return autokickPackets(frame.state, options, {
351
+ retryDetail: 'Provider output entered a repetition loop',
352
+ suspendedText: '⚠ Provider output entered a repetition loop. Paused to avoid repeating partial work.',
353
+ reason: 'dead-loop',
354
+ kickUserMessage: options.recovery.autokickUserMessage,
355
+ });
356
+ case 'provider.failed':
357
+ return recoveryPackets(frame.state, fact.failure, options);
358
+ case 'provider.done': {
359
+ if (isOutputTruncation(fact)) {
360
+ const incomplete = (fact.toolCalls?.length ?? 0) > 0 &&
361
+ isIncompleteToolCallJson(fact.toolCalls ?? []);
362
+ const reason = fact.finishReason === 'length'
363
+ ? 'Provider finished with finish_reason=length (output truncated).'
364
+ : 'Provider finished with incomplete tool-call JSON (output truncated).';
365
+ const detail = incomplete
366
+ ? `${reason} Incomplete tool arguments will not be executed.`
367
+ : reason;
368
+ return truncationRecoveryPackets(frame.state, options, detail);
369
+ }
370
+ const calls = fact.toolCalls ?? [];
371
+ const decision = options.policy.decideCompletion({
372
+ state: frame.state,
373
+ text: fact.text.length > 0
374
+ ? fact.text
375
+ : frame.state.partial.draft,
376
+ toolCalls: calls,
377
+ });
378
+ return decisionPackets(frame.state, decision);
379
+ }
380
+ }
381
+ };
382
+ const decisionPackets = (state, decision) => {
383
+ switch (decision.kind) {
384
+ case 'complete':
385
+ return concat(from(decision.chunks).pipe(map((chunk) => emit(chunk))), of(transition(reduceLlmLoop(state, {
386
+ type: 'round.completed',
387
+ }))));
388
+ case 'run-tools':
389
+ return of(transition(reduceLlmLoop(state, {
390
+ type: 'stream.done',
391
+ text: state.partial.draft,
392
+ toolCalls: decision.calls,
393
+ })));
394
+ case 'continue': {
395
+ const next = {
396
+ ...state,
397
+ phase: 'prepare',
398
+ committedMessages: [...decision.messages],
399
+ roundCheckpoint: [...decision.messages],
400
+ iteration: state.iteration + 1,
401
+ transientAttempts: 0,
402
+ partial: { reasoning: '', draft: '' },
403
+ pendingToolCalls: [],
404
+ };
405
+ return concat(from(decision.chunks).pipe(map((chunk) => emit(chunk))), of(transition(next)));
406
+ }
407
+ case 'fail':
408
+ return concat(of(emit({
409
+ kind: 'toolLog',
410
+ text: `⚠ ${decision.failure.message}`,
411
+ })), of(transition(reduceLlmLoop(state, {
412
+ type: 'failure.fatal',
413
+ failure: decision.failure,
414
+ }))));
415
+ }
416
+ };
417
+ const prepareAndStream = (state, options, cancel$, cancelSignal) => {
418
+ if (options.maxIterations > 0 && state.iteration >= options.maxIterations) {
419
+ const ask = options.requestPermission;
420
+ if (ask === undefined) {
421
+ return decisionPackets(state, options.policy.maxIterationsFailure(options.maxIterations));
422
+ }
423
+ const waitingText = `Tool-loop reached ${options.maxIterations} iterations ` +
424
+ '(maxIterations). Allow to continue for another budget, ' +
425
+ 'or Deny to stop.';
426
+ return concat(of(emit({ kind: 'toolLog', text: waitingText })), from(ask({
427
+ toolId: 'agent.maxIterations',
428
+ detail: String(options.maxIterations),
429
+ summary: `Tool-loop reached ${options.maxIterations} ` +
430
+ 'iterations. Allow to continue?',
431
+ })).pipe(switchMap((decision) => {
432
+ if (decision === 'allow') {
433
+ return prepareAndStream({ ...state, iteration: 0 }, options, cancel$, cancelSignal);
434
+ }
435
+ return decisionPackets(state, options.policy.maxIterationsFailure(options.maxIterations));
436
+ })));
437
+ }
438
+ return defer(() => {
439
+ const attempt = new AbortController();
440
+ const handleCancel = () => attempt.abort();
441
+ cancelSignal.addEventListener('abort', handleCancel, { once: true });
442
+ const penalties = state.autokickKickAttempts > 0
443
+ ? autokickPenalties(state.autokickKickAttempts, options.recovery.autokickPenaltyDelta)
444
+ : undefined;
445
+ const preparationSource$ = from(prepareChatCompletion({
446
+ factory: options.factory,
447
+ providerId: options.providerId,
448
+ model: options.model,
449
+ messages: state.committedMessages,
450
+ tools: resolveChatTools(options),
451
+ signal: attempt.signal,
452
+ compaction: options.compaction,
453
+ ...(penalties === undefined
454
+ ? {}
455
+ : {
456
+ frequency_penalty: penalties.frequency,
457
+ presence_penalty: penalties.presence,
458
+ }),
459
+ })).pipe(map((prepared) => ({ kind: 'prepared', prepared })), catchError((error) => of({ kind: 'failed', error })));
460
+ const preparation$ = options.recovery.streamIdleTimeoutMs > 0
461
+ ? preparationSource$.pipe(timeout({
462
+ first: options.recovery.streamIdleTimeoutMs,
463
+ with: () => of({ kind: 'idle' }),
464
+ }))
465
+ : preparationSource$;
466
+ const pause$ = options.steerControl$ === undefined
467
+ ? NEVER
468
+ : options.steerControl$.pipe(filter(isSteerControlPause), take(1), tap(() => attempt.abort()), map(() => ({ kind: 'paused' })));
469
+ return race(preparation$, pause$).pipe(tap((outcome) => {
470
+ if (outcome.kind === 'idle') {
471
+ attempt.abort();
472
+ }
473
+ }), switchMap((prepared) => {
474
+ if (prepared.kind === 'paused') {
475
+ return concat(of(emit({
476
+ kind: 'toolLog',
477
+ text: 'Paused. Send Steer feedback or Resume to continue.',
478
+ })), of(transition(reduceLlmLoop(state, {
479
+ type: 'stream.paused',
480
+ }))));
481
+ }
482
+ if (prepared.kind === 'idle') {
483
+ return recoveryPackets(state, {
484
+ kind: 'stream-idle',
485
+ message: `Provider stream creation emitted no result for ${options.recovery.streamIdleTimeoutMs}ms.`,
486
+ recoverable: true,
487
+ }, options);
488
+ }
489
+ if (prepared.kind === 'failed') {
490
+ return recoveryPackets(state, classifyLlmFailure(prepared.error), options);
491
+ }
492
+ const preparedResult = prepared.prepared;
493
+ if (!preparedResult.ok) {
494
+ return recoveryPackets(state, classifyLlmFailure(preparedResult.error), options);
495
+ }
496
+ const preparedState = reduceLlmLoop(state, {
497
+ type: 'round.prepared',
498
+ messages: preparedResult.messages,
499
+ });
500
+ const compactionPackets = preparedResult.compaction === undefined
501
+ ? EMPTY
502
+ : from([
503
+ emit({
504
+ kind: 'toolLog',
505
+ text: `Compacted history (${preparedResult.compaction.reason}): ~${preparedResult.compaction.beforeTokens} → ~${preparedResult.compaction.afterTokens} approx tokens`,
506
+ }),
507
+ emit({
508
+ kind: 'historySync',
509
+ messages: [...preparedResult.messages],
510
+ }),
511
+ ]);
512
+ const pause$ = options.steerControl$ ?? NEVER;
513
+ const streamPackets = observeProviderStream({
514
+ createStream: async () => preparedResult.stream,
515
+ pause$,
516
+ cancel$,
517
+ idleTimeoutMs: options.recovery.streamIdleTimeoutMs,
518
+ onAbort: () => attempt.abort(),
519
+ deadLoop: options.recovery.deadLoopEnabled
520
+ ? options.recovery.deadLoop
521
+ : false,
522
+ }).pipe(scan((frame, fact) => {
523
+ const action = providerFactAction(fact);
524
+ return {
525
+ state: action === undefined
526
+ ? frame.state
527
+ : reduceLlmLoop(frame.state, action),
528
+ fact,
529
+ };
530
+ }, { state: preparedState }), concatMap((frame) => providerFactPackets(frame, options)));
531
+ return concat(compactionPackets, streamPackets);
532
+ }), finalize(() => {
533
+ cancelSignal.removeEventListener('abort', handleCancel);
534
+ attempt.abort();
535
+ }));
536
+ });
537
+ };
538
+ const invokeTool = (state, call, options, cancelSignal) => {
539
+ const callLog = emit({
540
+ kind: 'toolLog',
541
+ text: `→ ${call.name}(${previewToolLogText(call.arguments)})`,
542
+ });
543
+ if (call.name === SPAWN_SUBAGENT_TOOL) {
544
+ const resolved = resolveSpawnPayload(call, options.subagentRegistrations ?? [], state.openSpawnCallId);
545
+ if (!resolved.ok || options.waitForSubagentResult === undefined) {
546
+ const result = !resolved.ok
547
+ ? `Error: ${resolved.text}`
548
+ : 'Error: Sub-Agent spawn is not wired (no result wait).';
549
+ return toolResultPackets(state, call, result, options, [callLog]);
550
+ }
551
+ const { payload } = resolved;
552
+ const wait$ = defer(() => options.waitForSubagentResult?.(payload.callId, cancelSignal) ??
553
+ Promise.reject(new Error('Sub-Agent wait unavailable')));
554
+ const boundedWait$ = options.recovery.subagentTimeoutMs > 0
555
+ ? wait$.pipe(timeout({
556
+ first: options.recovery.subagentTimeoutMs,
557
+ }))
558
+ : wait$;
559
+ return concat(of(callLog, emit({
560
+ kind: 'subagentSpawn',
561
+ payload,
562
+ }), emit({
563
+ kind: 'toolLog',
564
+ text: `… waiting for subagentResult callId=${payload.callId}`,
565
+ })), boundedWait$.pipe(catchError((error) => of(error instanceof TimeoutError
566
+ ? `Error: Sub-Agent ${payload.callId} timed out after ${options.recovery.subagentTimeoutMs}ms.`
567
+ : `Error: ${classifyLlmFailure(error).message}`)), mergeMap((result) => toolResultPackets(state, call, result, options, []))));
568
+ }
569
+ const toolAbort = new AbortController();
570
+ const abortTool = () => {
571
+ toolAbort.abort();
572
+ };
573
+ cancelSignal.addEventListener('abort', abortTool);
574
+ const invokeOptions = {
575
+ ...(options.policy.toolNotAllowedText === undefined
576
+ ? {}
577
+ : {
578
+ notInAllowlistText: options.policy.toolNotAllowedText,
579
+ }),
580
+ signal: toolAbort.signal,
581
+ };
582
+ const invocation$ = defer(() => invokeInventoryTool(options.harness, resolveInventoryTools(options), call, options.toolCtx, invokeOptions).catch((error) => {
583
+ // Avoid unhandled rejection when timeout/cancel aborts in-flight work.
584
+ if (toolAbort.signal.aborted) {
585
+ return {
586
+ ok: false,
587
+ text: 'Tool aborted.',
588
+ };
589
+ }
590
+ throw error;
591
+ })).pipe(finalize(() => {
592
+ cancelSignal.removeEventListener('abort', abortTool);
593
+ }));
594
+ const boundedInvocation$ = options.recovery.toolTimeoutMs > 0
595
+ ? invocation$.pipe(timeout({
596
+ first: options.recovery.toolTimeoutMs,
597
+ }))
598
+ : invocation$;
599
+ return concat(of(callLog), boundedInvocation$.pipe(map((result) => result.ok ? result.text : `Error: ${result.text}`), catchError((error) => {
600
+ abortTool();
601
+ return of(error instanceof TimeoutError
602
+ ? `Error: Tool ${call.name} timed out after ${options.recovery.toolTimeoutMs}ms.`
603
+ : `Error: ${classifyLlmFailure(error).message}`);
604
+ }), mergeMap((result) => toolResultPackets(state, call, result, options, []))));
605
+ };
606
+ const toolResultPackets = (state, call, result, options, prefix) => {
607
+ const normalized = normalizeToolResult(result, options.recovery.maxToolResultChars);
608
+ const next = reduceLlmLoop(state, {
609
+ type: 'tool.completed',
610
+ call,
611
+ result: normalized,
612
+ });
613
+ return from([
614
+ ...prefix,
615
+ emit({
616
+ kind: 'toolLog',
617
+ text: `← ${call.name}: ${previewToolLogText(normalized)}`,
618
+ }),
619
+ transition(next),
620
+ ]);
621
+ };
622
+ const awaitSteer = (state, options, cancel$) => {
623
+ if (options.steerControl$ === undefined) {
624
+ const failure = state.failure ??
625
+ {
626
+ kind: 'unknown',
627
+ message: 'LLM loop suspended without a steerControl input.',
628
+ recoverable: false,
629
+ };
630
+ return of(transition(reduceLlmLoop(state, {
631
+ type: 'failure.fatal',
632
+ failure,
633
+ })));
634
+ }
635
+ return options.steerControl$.pipe(filter(isSteerControlContinue), take(1), map((payload) => transition(reduceLlmLoop(state, {
636
+ type: 'steer.received',
637
+ ...(payload.kind === 'steer' &&
638
+ payload.text.trim().length > 0
639
+ ? { text: payload.text.trim() }
640
+ : {}),
641
+ }))), takeUntil(cancel$));
642
+ };
643
+ const executePhase = (state, options, cancel$, cancelSignal) => {
644
+ switch (state.phase) {
645
+ case 'prepare':
646
+ case 'streaming':
647
+ return prepareAndStream(state, options, cancel$, cancelSignal);
648
+ case 'tools': {
649
+ const call = state.pendingToolCalls[0];
650
+ return call === undefined
651
+ ? of(transition({
652
+ ...state,
653
+ phase: 'prepare',
654
+ committedMessages: [...state.roundCheckpoint],
655
+ pendingToolCalls: [],
656
+ }))
657
+ : invokeTool(state, call, options, cancelSignal);
658
+ }
659
+ case 'suspended':
660
+ return awaitSteer(state, options, cancel$);
661
+ case 'waiting-subagent':
662
+ return EMPTY;
663
+ case 'failed':
664
+ return throwError(() => state.failure?.message ??
665
+ 'LLM loop failed without failure details.');
666
+ case 'complete':
667
+ case 'cancelled':
668
+ return EMPTY;
669
+ }
670
+ };
671
+ /**
672
+ * One reactive state machine for provider, tool, Sub-Agent, retry, and Steer
673
+ * phases. Policy owns only the semantic completion decision.
674
+ */
675
+ export const runLlmLoop = (options) => defer(() => {
676
+ const cancel = new AbortController();
677
+ const cancel$ = fromEvent(cancel.signal, 'abort').pipe(map(() => undefined), take(1));
678
+ return of(transition(initialLlmLoopState(options.messages))).pipe(expand((packet) => packet.kind === 'emit'
679
+ ? EMPTY
680
+ : executePhase(packet.state, options, cancel$, cancel.signal)), filter((packet) => packet.kind === 'emit'), map((packet) => packet.chunk), finalize(() => cancel.abort()));
681
+ });