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.
- package/package.json +1 -1
- package/ui-dist/{chunk-Y6VMBLSJ.js → chunk-7UMVYN3A.js} +1 -1
- package/ui-dist/{chunk-6DQ2XSRW.js → chunk-MDGXX73Z.js} +3 -3
- package/ui-dist/index.html +1 -1
- package/ui-dist/main-2JJGCNTD.js +313 -0
- package/vendor/common-nodes/dist/ai/chat-completion-stream.d.ts +2 -0
- package/vendor/common-nodes/dist/ai/critique/node.d.ts +246 -0
- package/vendor/common-nodes/dist/ai/critique/node.js +2 -2
- package/vendor/common-nodes/dist/ai/fake-llm/node.d.ts +246 -0
- package/vendor/common-nodes/dist/ai/features/chat-completion-stream.d.ts +53 -0
- package/vendor/common-nodes/dist/ai/features/chat-completion-stream.js +1 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/autokick-recovery.d.ts +13 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/autokick-recovery.js +17 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/classify-llm-failure.d.ts +2 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/classify-llm-failure.js +113 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/dead-loop-detector.d.ts +28 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/dead-loop-detector.js +194 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/live-get-tools.d.ts +9 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/live-get-tools.js +21 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.d.ts +2 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.js +216 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.d.ts +125 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.js +39 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/normalize-llm-recovery-policy.d.ts +2 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/normalize-llm-recovery-policy.js +59 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/operators/normalize-tool-result.d.ts +1 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/operators/normalize-tool-result.js +7 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/operators/observe-provider-stream.d.ts +43 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/operators/observe-provider-stream.js +96 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.d.ts +32 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.js +58 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.d.ts +80 -0
- package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.js +681 -0
- package/vendor/common-nodes/dist/ai/features/llm-role-preset.d.ts +69 -0
- package/vendor/common-nodes/dist/ai/features/llm-role-preset.js +265 -0
- package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.d.ts +148 -0
- package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.js +198 -0
- package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.d.ts +21 -0
- package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.js +118 -0
- package/vendor/common-nodes/dist/ai/features/openai/context-length-error.d.ts +14 -0
- package/vendor/common-nodes/dist/ai/features/openai/context-length-error.js +63 -0
- package/vendor/common-nodes/dist/ai/features/openai/create-chat-completion-stream.d.ts +19 -0
- package/vendor/common-nodes/dist/ai/features/openai/create-chat-completion-stream.js +182 -0
- package/vendor/common-nodes/dist/ai/features/openai/list-provider-models.d.ts +28 -0
- package/vendor/common-nodes/dist/ai/features/openai/list-provider-models.js +52 -0
- package/vendor/common-nodes/dist/ai/features/openai/llm-context-compaction.d.ts +31 -0
- package/vendor/common-nodes/dist/ai/features/openai/llm-context-compaction.js +326 -0
- package/vendor/common-nodes/dist/ai/features/openai/normalize-compaction-params.d.ts +14 -0
- package/vendor/common-nodes/dist/ai/features/openai/normalize-compaction-params.js +21 -0
- package/vendor/common-nodes/dist/ai/features/openai/prepare-chat-completion.d.ts +30 -0
- package/vendor/common-nodes/dist/ai/features/openai/prepare-chat-completion.js +132 -0
- package/vendor/common-nodes/dist/ai/features/path-choice/control-tools.d.ts +20 -0
- package/vendor/common-nodes/dist/ai/features/path-choice/control-tools.js +70 -0
- package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.d.ts +35 -0
- package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.js +106 -0
- package/vendor/common-nodes/dist/ai/features/prompt/build-effective-system-prompt.d.ts +9 -0
- package/vendor/common-nodes/dist/ai/features/prompt/build-effective-system-prompt.js +21 -0
- package/vendor/common-nodes/dist/ai/features/prompt/normalize-max-iterations.d.ts +13 -0
- package/vendor/common-nodes/dist/ai/features/prompt/normalize-max-iterations.js +23 -0
- package/vendor/common-nodes/dist/ai/features/prompt/resolve-chat-provider-model.d.ts +10 -0
- package/vendor/common-nodes/dist/ai/features/prompt/resolve-chat-provider-model.js +8 -0
- package/vendor/common-nodes/dist/ai/features/run-host-services.d.ts +54 -0
- package/vendor/common-nodes/dist/ai/features/run-host-services.js +29 -0
- package/vendor/common-nodes/dist/ai/features/scripted-chat-completion-stream.d.ts +17 -0
- package/vendor/common-nodes/dist/ai/features/scripted-chat-completion-stream.js +61 -0
- package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.d.ts +39 -0
- package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.js +43 -0
- package/vendor/common-nodes/dist/ai/features/ui-schema/llm-compaction-ui-schema.d.ts +14 -0
- package/vendor/common-nodes/dist/ai/features/ui-schema/llm-compaction-ui-schema.js +18 -0
- package/vendor/common-nodes/dist/ai/features/ui-schema/llm-panel-ui-schema.d.ts +80 -0
- package/vendor/common-nodes/dist/ai/features/ui-schema/llm-panel-ui-schema.js +76 -0
- package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.d.ts +113 -0
- package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.js +131 -0
- package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.d.ts +6 -0
- package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.js +30 -0
- package/vendor/common-nodes/dist/ai/llm-loop/autokick-recovery.d.ts +13 -0
- package/vendor/common-nodes/dist/ai/llm-loop/autokick-recovery.js +17 -0
- package/vendor/common-nodes/dist/ai/llm-loop/dead-loop-detector.d.ts +28 -0
- package/vendor/common-nodes/dist/ai/llm-loop/dead-loop-detector.js +194 -0
- package/vendor/common-nodes/dist/ai/llm-loop/llm-loop-reducer.js +36 -0
- package/vendor/common-nodes/dist/ai/llm-loop/llm-loop-types.d.ts +35 -0
- package/vendor/common-nodes/dist/ai/llm-loop/llm-loop-types.js +19 -0
- package/vendor/common-nodes/dist/ai/llm-loop/normalize-llm-recovery-policy.js +52 -9
- package/vendor/common-nodes/dist/ai/llm-loop/operators/observe-provider-stream.d.ts +8 -2
- package/vendor/common-nodes/dist/ai/llm-loop/operators/observe-provider-stream.js +43 -13
- package/vendor/common-nodes/dist/ai/llm-loop/run-llm-loop.d.ts +3 -5
- package/vendor/common-nodes/dist/ai/llm-loop/run-llm-loop.js +116 -1
- package/vendor/common-nodes/dist/ai/llm-recovery-ui-schema.d.ts +82 -0
- package/vendor/common-nodes/dist/ai/llm-recovery-ui-schema.js +95 -0
- package/vendor/common-nodes/dist/ai/llm-session-shell.d.ts +4 -9
- package/vendor/common-nodes/dist/ai/llm-session-shell.js +2 -4
- package/vendor/common-nodes/dist/ai/nodes/critique/node.d.ts +588 -0
- package/vendor/common-nodes/dist/ai/nodes/critique/node.js +240 -0
- package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.d.ts +571 -0
- package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.js +208 -0
- package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.d.ts +587 -0
- package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.js +91 -0
- package/vendor/common-nodes/dist/ai/nodes/review/node.d.ts +593 -0
- package/vendor/common-nodes/dist/ai/nodes/review/node.js +240 -0
- package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.d.ts +630 -0
- package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.js +302 -0
- package/vendor/common-nodes/dist/ai/openai/create-chat-completion-stream.js +6 -0
- package/vendor/common-nodes/dist/ai/openai/prepare-chat-completion.d.ts +2 -0
- package/vendor/common-nodes/dist/ai/openai/prepare-chat-completion.js +18 -0
- package/vendor/common-nodes/dist/ai/openai-llm/node.d.ts +246 -0
- package/vendor/common-nodes/dist/ai/review/node.d.ts +246 -0
- package/vendor/common-nodes/dist/ai/review/node.js +2 -2
- package/vendor/common-nodes/dist/ai/sub-agent/node.d.ts +246 -0
- package/vendor/common-nodes/dist/ai/sub-agent/node.js +2 -2
- package/vendor/common-nodes/dist/catalog.js +7 -5
- package/vendor/common-nodes/dist/compile-custom-nodes/node.d.ts +21 -0
- package/vendor/common-nodes/dist/compile-custom-nodes/node.js +45 -0
- package/vendor/common-nodes/dist/crawl/crawl/node.js +1 -1
- package/vendor/common-nodes/dist/crawl/fetch-url/node.js +1 -1
- package/vendor/common-nodes/dist/flow/repeat/node.js +6 -1
- package/vendor/common-nodes/dist/langflower-tools/emit-registration-tools.d.ts +18 -0
- package/vendor/common-nodes/dist/langflower-tools/emit-registration-tools.js +29 -0
- package/vendor/common-nodes/dist/langflower-tools/node.d.ts +20 -0
- package/vendor/common-nodes/dist/langflower-tools/node.js +77 -0
- package/vendor/common-nodes/dist/text/append-file/node.js +1 -1
- package/vendor/common-nodes/dist/text/read-file/node.js +1 -1
- package/vendor/common-nodes/dist/text/write-file/node.js +1 -1
- package/vendor/common-nodes/dist/tools/inventory-tool-round.d.ts +2 -2
- package/vendor/common-nodes/package.json +10 -10
- package/vendor/compiler/dist/compile-project-nodes.d.ts +2 -1
- package/vendor/compiler/dist/compile-project-nodes.js +35 -6
- package/vendor/compiler/dist/resolve-host-types.d.ts +0 -5
- package/vendor/compiler/dist/resolve-host-types.js +0 -8
- package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.d.ts +1 -1
- package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.js +1 -1
- package/vendor/node-sdk/dist/node-factory/define-llm-node/recovery-notice.d.ts +8 -0
- package/vendor/node-sdk/dist/node-factory/define-llm-node/recovery-notice.js +14 -0
- package/vendor/runtime/dist/runtime-editor.d.ts +5 -1
- package/vendor/runtime/dist/runtime-editor.js +19 -0
- package/vendor/runtime/dist/runtime-runner.js +17 -17
- package/vendor/runtime/dist/runtime.d.ts +2 -1
- package/vendor/runtime/dist/runtime.js +1 -0
- package/vendor/runtime/dist/testing/workflows/workflow-events.d.ts +8 -6
- package/vendor/runtime/dist/testing/workflows/workflow-events.js +19 -31
- package/vendor/runtime/dist/types.d.ts +33 -38
- package/vendor/runtime/dist/types.js +2 -1
- package/vendor/server/dist/bridge/attach-langflower-bridge.js +3 -2
- package/vendor/server/dist/bridge/bridge-event-log.js +17 -102
- package/vendor/server/dist/bridge/build-execution-context.d.ts +5 -2
- package/vendor/server/dist/bridge/build-execution-context.js +45 -5
- package/vendor/server/dist/bridge/forward-runner-event.d.ts +1 -1
- package/vendor/server/dist/bridge/forward-runner-event.js +7 -12
- package/vendor/server/dist/bridge/get-live-wired-tools.d.ts +12 -0
- package/vendor/server/dist/bridge/get-live-wired-tools.js +80 -0
- package/vendor/server/dist/bridge/langflower-tools-rpc.d.ts +7 -0
- package/vendor/server/dist/bridge/langflower-tools-rpc.js +29 -0
- package/vendor/server/dist/bridge/settings-draft-controller.js +26 -3
- package/vendor/server/dist/bridge/wire-custom-palette-handlers.d.ts +2 -1
- package/vendor/server/dist/bridge/wire-custom-palette-handlers.js +3 -5
- package/vendor/server/dist/bridge/wire-runner-handlers.js +15 -8
- package/vendor/server/dist/checkpoint/run-checkpoint-session.d.ts +1 -1
- package/vendor/server/dist/checkpoint/run-checkpoint-session.js +10 -9
- package/vendor/server/dist/palette/compile-and-hot-swap-custom-nodes.d.ts +10 -0
- package/vendor/server/dist/palette/compile-and-hot-swap-custom-nodes.js +23 -0
- package/vendor/server/dist/workflow/apply-editor-mutation.d.ts +5 -0
- package/vendor/server/dist/workflow/apply-editor-mutation.js +32 -0
- package/vendor/server/skeleton/instructions.md +9 -5
- package/vendor/server/skeleton/nodes/my-nodes/README.md +4 -3
- package/vendor/server/skeleton/schemas/langflower-config.schema.json +11 -1
- package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +49 -19
- package/vendor/server/skeleton/skills/langflower-helper/architecture.md +22 -9
- package/vendor/server/skeleton/skills/langflower-helper/layout.md +15 -15
- package/vendor/server/skeleton/skills/langflower-node-writer/SKILL.md +19 -9
- package/vendor/server/skeleton/skills/langflower-workflow-writer/SKILL.md +14 -10
- package/vendor/server/skeleton/workflows/starter.json +30 -3
- package/vendor/shared/dist/execution/derive-run-settle-outcome.d.ts +1 -1
- package/vendor/shared/dist/execution/derive-run-settle-outcome.js +4 -3
- package/vendor/shared/dist/execution/derive-run-settle-outcome.test.js +9 -9
- package/vendor/shared/dist/langflower-bus-config.d.ts +6 -14
- package/vendor/shared/dist/langflower-bus-config.js +4 -12
- package/vendor/shared/dist/langflower-config/resolve-wired-tool-options.js +6 -0
- package/vendor/shared/dist/langflower-config/resolve-wired-tool-options.test.js +30 -0
- package/vendor/shared/dist/langflower-ws-waits.d.ts +7 -14
- package/vendor/shared/dist/langflower-ws-waits.js +14 -14
- package/vendor/websocket-bridge/dist/bridge-codec.d.ts +1 -0
- package/vendor/websocket-bridge/dist/bridge-codec.js +11 -11
- package/vendor/websocket-bridge/dist/bridge-frame.js +1 -1
- package/vendor/websocket-bridge/dist/bridge-subjects.d.ts +1 -1
- package/vendor/websocket-bridge/dist/bridge-subjects.js +2 -2
- package/vendor/websocket-bridge/dist/bridge-types.d.ts +13 -1
- package/vendor/websocket-bridge/dist/create-client.js +3 -3
- package/vendor/websocket-bridge/dist/create-server.js +12 -6
- package/vendor/websocket-bridge/package.json +8 -0
- 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
|
+
});
|