@falai/agent 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/MemoryAdapter.js +29 -33
- package/dist/adapters/MemoryAdapter.js.map +1 -1
- package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
- package/dist/adapters/PostgreSQLAdapter.js +16 -11
- package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
- package/dist/adapters/RedisAdapter.d.ts +1 -0
- package/dist/adapters/RedisAdapter.d.ts.map +1 -1
- package/dist/adapters/RedisAdapter.js +75 -25
- package/dist/adapters/RedisAdapter.js.map +1 -1
- package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
- package/dist/adapters/SQLiteAdapter.js +7 -29
- package/dist/adapters/SQLiteAdapter.js.map +1 -1
- package/dist/adapters/sessionRow.d.ts +22 -0
- package/dist/adapters/sessionRow.d.ts.map +1 -0
- package/dist/adapters/sessionRow.js +48 -0
- package/dist/adapters/sessionRow.js.map +1 -0
- package/dist/cjs/adapters/MemoryAdapter.js +29 -33
- package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
- package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/PostgreSQLAdapter.js +16 -11
- package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
- package/dist/cjs/adapters/RedisAdapter.d.ts +1 -0
- package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/RedisAdapter.js +75 -25
- package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
- package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/SQLiteAdapter.js +7 -29
- package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
- package/dist/cjs/adapters/sessionRow.d.ts +22 -0
- package/dist/cjs/adapters/sessionRow.d.ts.map +1 -0
- package/dist/cjs/adapters/sessionRow.js +52 -0
- package/dist/cjs/adapters/sessionRow.js.map +1 -0
- package/dist/cjs/core/Agent.d.ts +8 -0
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +43 -3
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/AutoChainExecutor.d.ts +8 -18
- package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
- package/dist/cjs/core/AutoChainExecutor.js +23 -26
- package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts +14 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +30 -6
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
- package/dist/cjs/core/FlowRouter.js +32 -10
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -1
- package/dist/cjs/core/ResponseGenerationError.js +3 -5
- package/dist/cjs/core/ResponseGenerationError.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts +29 -0
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +141 -18
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ResponsePipeline.d.ts +36 -6
- package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/cjs/core/ResponsePipeline.js +208 -73
- package/dist/cjs/core/ResponsePipeline.js.map +1 -1
- package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -1
- package/dist/cjs/core/SessionFinalizer.js +30 -3
- package/dist/cjs/core/SessionFinalizer.js.map +1 -1
- package/dist/cjs/core/SessionManager.d.ts +10 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +43 -16
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
- package/dist/cjs/core/SignalProcessor.js +5 -77
- package/dist/cjs/core/SignalProcessor.js.map +1 -1
- package/dist/cjs/core/Step.d.ts.map +1 -1
- package/dist/cjs/core/Step.js +50 -2
- package/dist/cjs/core/Step.js.map +1 -1
- package/dist/cjs/core/StepLifecycle.d.ts +16 -6
- package/dist/cjs/core/StepLifecycle.d.ts.map +1 -1
- package/dist/cjs/core/StepLifecycle.js +97 -14
- package/dist/cjs/core/StepLifecycle.js.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.js +28 -4
- package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +5 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +156 -64
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/core/ToolManager.d.ts +1 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js +40 -17
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/core/flow-namespace.d.ts +15 -0
- package/dist/cjs/core/flow-namespace.d.ts.map +1 -1
- package/dist/cjs/core/flow-namespace.js +22 -0
- package/dist/cjs/core/flow-namespace.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +10 -3
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +54 -73
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +9 -3
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +24 -68
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +9 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.js +101 -74
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +1 -1
- package/dist/cjs/providers/errorClassification.d.ts +7 -0
- package/dist/cjs/providers/errorClassification.d.ts.map +1 -1
- package/dist/cjs/providers/errorClassification.js +27 -0
- package/dist/cjs/providers/errorClassification.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +35 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/flow.d.ts +49 -32
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -1
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/tool.d.ts +2 -0
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/cjs/types/tool.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +4 -4
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +10 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +99 -1
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +140 -3
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/cjs/utils/serialize.d.ts +17 -0
- package/dist/cjs/utils/serialize.d.ts.map +1 -1
- package/dist/cjs/utils/serialize.js +33 -0
- package/dist/cjs/utils/serialize.js.map +1 -1
- package/dist/cjs/utils/session.d.ts +21 -1
- package/dist/cjs/utils/session.d.ts.map +1 -1
- package/dist/cjs/utils/session.js +36 -4
- package/dist/cjs/utils/session.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -0
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +43 -3
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/AutoChainExecutor.d.ts +8 -18
- package/dist/core/AutoChainExecutor.d.ts.map +1 -1
- package/dist/core/AutoChainExecutor.js +23 -26
- package/dist/core/AutoChainExecutor.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts +14 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +30 -6
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowRouter.d.ts.map +1 -1
- package/dist/core/FlowRouter.js +32 -10
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/ResponseGenerationError.d.ts.map +1 -1
- package/dist/core/ResponseGenerationError.js +3 -5
- package/dist/core/ResponseGenerationError.js.map +1 -1
- package/dist/core/ResponseModal.d.ts +29 -0
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +142 -19
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ResponsePipeline.d.ts +36 -6
- package/dist/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/core/ResponsePipeline.js +208 -73
- package/dist/core/ResponsePipeline.js.map +1 -1
- package/dist/core/SessionFinalizer.d.ts.map +1 -1
- package/dist/core/SessionFinalizer.js +31 -4
- package/dist/core/SessionFinalizer.js.map +1 -1
- package/dist/core/SessionManager.d.ts +10 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +44 -17
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SignalProcessor.d.ts.map +1 -1
- package/dist/core/SignalProcessor.js +5 -77
- package/dist/core/SignalProcessor.js.map +1 -1
- package/dist/core/Step.d.ts.map +1 -1
- package/dist/core/Step.js +50 -2
- package/dist/core/Step.js.map +1 -1
- package/dist/core/StepLifecycle.d.ts +16 -6
- package/dist/core/StepLifecycle.d.ts.map +1 -1
- package/dist/core/StepLifecycle.js +97 -14
- package/dist/core/StepLifecycle.js.map +1 -1
- package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/core/StreamingToolExecutor.js +28 -4
- package/dist/core/StreamingToolExecutor.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +5 -1
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +156 -64
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/core/ToolManager.d.ts +1 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js +41 -18
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/core/flow-namespace.d.ts +15 -0
- package/dist/core/flow-namespace.d.ts.map +1 -1
- package/dist/core/flow-namespace.js +22 -0
- package/dist/core/flow-namespace.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +10 -3
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +56 -75
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +9 -3
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +26 -70
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.d.ts +9 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.js +103 -76
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +1 -1
- package/dist/providers/errorClassification.d.ts +7 -0
- package/dist/providers/errorClassification.d.ts.map +1 -1
- package/dist/providers/errorClassification.js +26 -0
- package/dist/providers/errorClassification.js.map +1 -1
- package/dist/types/agent.d.ts +35 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/flow.d.ts +49 -32
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/tool.d.ts +2 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/types/tool.js.map +1 -1
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +99 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +137 -3
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/serialize.d.ts +17 -0
- package/dist/utils/serialize.d.ts.map +1 -1
- package/dist/utils/serialize.js +31 -0
- package/dist/utils/serialize.js.map +1 -1
- package/dist/utils/session.d.ts +21 -1
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +33 -4
- package/dist/utils/session.js.map +1 -1
- package/docs/concepts/architecture.md +3 -3
- package/docs/concepts/directives.md +1 -1
- package/docs/guides/error-handling.md +46 -45
- package/docs/guides/flow-control.md +8 -1
- package/docs/guides/instructions.md +15 -6
- package/docs/guides/persistence.md +12 -5
- package/docs/guides/streaming.md +10 -0
- package/docs/migration/README.md +4 -0
- package/docs/migration/v2-3-to-v2-4.md +4 -0
- package/docs/migration/v2-6-to-v2-7.md +246 -0
- package/docs/reference/adapters.md +15 -1
- package/docs/reference/create-agent.md +30 -0
- package/docs/reference/directive.md +1 -1
- package/docs/reference/errors.md +29 -31
- package/docs/reference/providers.md +23 -19
- package/docs/reference/step.md +28 -21
- package/docs/reference/tool.md +14 -5
- package/docs/start/02-first-agent.md +8 -4
- package/docs/start/03-collect-data.md +19 -10
- package/examples/01-quickstart.ts +1 -1
- package/examples/02-data-extraction.ts +1 -1
- package/examples/03-tools.ts +1 -1
- package/examples/04-instructions.ts +1 -1
- package/examples/05-branching.ts +1 -1
- package/examples/06-flow-control.ts +3 -3
- package/examples/07-streaming.ts +1 -1
- package/examples/08-persistence.ts +1 -1
- package/examples/09-signals.ts +1 -1
- package/package.json +2 -2
- package/src/adapters/MemoryAdapter.ts +29 -33
- package/src/adapters/PostgreSQLAdapter.ts +23 -18
- package/src/adapters/RedisAdapter.ts +81 -34
- package/src/adapters/SQLiteAdapter.ts +10 -31
- package/src/adapters/sessionRow.ts +57 -0
- package/src/core/Agent.ts +51 -3
- package/src/core/AutoChainExecutor.ts +37 -50
- package/src/core/CompactionEngine.ts +40 -6
- package/src/core/FlowRouter.ts +38 -12
- package/src/core/ResponseGenerationError.ts +3 -6
- package/src/core/ResponseModal.ts +185 -19
- package/src/core/ResponsePipeline.ts +259 -85
- package/src/core/SessionFinalizer.ts +35 -5
- package/src/core/SessionManager.ts +55 -21
- package/src/core/SignalProcessor.ts +5 -85
- package/src/core/Step.ts +79 -2
- package/src/core/StepLifecycle.ts +125 -22
- package/src/core/StreamingToolExecutor.ts +31 -6
- package/src/core/ToolLoopExecutor.ts +201 -75
- package/src/core/ToolManager.ts +45 -18
- package/src/core/flow-namespace.ts +29 -0
- package/src/index.ts +4 -1
- package/src/providers/AnthropicProvider.ts +83 -131
- package/src/providers/GeminiProvider.ts +42 -116
- package/src/providers/OpenAICompatibleProvider.ts +132 -133
- package/src/providers/OpenAIProvider.ts +2 -2
- package/src/providers/errorClassification.ts +34 -0
- package/src/types/agent.ts +34 -0
- package/src/types/flow.ts +37 -39
- package/src/types/index.ts +1 -0
- package/src/types/tool.ts +2 -0
- package/src/utils/index.ts +22 -3
- package/src/utils/retry.ts +251 -2
- package/src/utils/serialize.ts +38 -0
- package/src/utils/session.ts +41 -4
- package/dist/cjs/core/DirectiveBus.d.ts +0 -88
- package/dist/cjs/core/DirectiveBus.d.ts.map +0 -1
- package/dist/cjs/core/DirectiveBus.js +0 -196
- package/dist/cjs/core/DirectiveBus.js.map +0 -1
- package/dist/core/DirectiveBus.d.ts +0 -88
- package/dist/core/DirectiveBus.d.ts.map +0 -1
- package/dist/core/DirectiveBus.js +0 -192
- package/dist/core/DirectiveBus.js.map +0 -1
- package/src/core/DirectiveBus.ts +0 -248
|
@@ -15,8 +15,9 @@ import type { AgentOptions, SessionState } from "../types";
|
|
|
15
15
|
import type { CompactionOptions } from "../types/compaction";
|
|
16
16
|
import type { PersistenceManager } from "./PersistenceManager";
|
|
17
17
|
import type { StepLifecycle } from "./StepLifecycle";
|
|
18
|
+
import { flow } from "./flow-namespace";
|
|
18
19
|
import { CompactionEngine } from "./CompactionEngine";
|
|
19
|
-
import { logger } from "../utils";
|
|
20
|
+
import { boundConversationHistory, DEFAULT_MAX_HISTORY_MESSAGES, logger } from "../utils";
|
|
20
21
|
|
|
21
22
|
export class SessionFinalizer<TContext = unknown, TData = unknown> {
|
|
22
23
|
constructor(
|
|
@@ -36,6 +37,24 @@ export class SessionFinalizer<TContext = unknown, TData = unknown> {
|
|
|
36
37
|
* Handle session persistence and finalization.
|
|
37
38
|
*/
|
|
38
39
|
async finalize(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
40
|
+
const agentOptions = this.deps.getAgentOptions();
|
|
41
|
+
|
|
42
|
+
// Hard history bound — backstop when no compaction is configured (or
|
|
43
|
+
// after it ran). Unbounded growth eventually bricks the thread at the
|
|
44
|
+
// provider context limit.
|
|
45
|
+
const historyCap = agentOptions.maxHistoryMessages ?? DEFAULT_MAX_HISTORY_MESSAGES;
|
|
46
|
+
if (historyCap > 0 && session.history && session.history.length > historyCap) {
|
|
47
|
+
const bounded = boundConversationHistory(session.history, historyCap);
|
|
48
|
+
if (bounded !== session.history) {
|
|
49
|
+
logger.warn(
|
|
50
|
+
`[SessionFinalizer] Session "${session.id}" history exceeded ${historyCap} entries ` +
|
|
51
|
+
`(${session.history.length}); trimmed the oldest. Configure compaction for ` +
|
|
52
|
+
`summarization instead of truncation, or raise maxHistoryMessages.`
|
|
53
|
+
);
|
|
54
|
+
session.history = bounded;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
// Deterministic compaction: runs on every finalize (not just addMessage)
|
|
40
59
|
// so respond()-only callers get bounded history too
|
|
41
60
|
const compactionOptions = this.deps.getCompactionOptions();
|
|
@@ -54,9 +73,23 @@ export class SessionFinalizer<TContext = unknown, TData = unknown> {
|
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
75
|
|
|
76
|
+
// Execute finalize BEFORE persisting so its state writes (which now
|
|
77
|
+
// land on this turn's session) are captured by the save below. A
|
|
78
|
+
// control-flow directive returned by finalize queues on the session —
|
|
79
|
+
// the pendingDirective applier applies it at the START of the next turn.
|
|
80
|
+
let finalizeDirective;
|
|
81
|
+
try {
|
|
82
|
+
finalizeDirective = await this.deps.stepLifecycle.runFinalize(session, context);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
logger.error("[SessionFinalizer] Step finalize hook failed:", error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
if (finalizeDirective) {
|
|
88
|
+
flow.queuePending(session, finalizeDirective);
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
// Auto-save session step to persistence if configured
|
|
58
92
|
const persistenceManager = this.deps.getPersistenceManager();
|
|
59
|
-
const agentOptions = this.deps.getAgentOptions();
|
|
60
93
|
if (
|
|
61
94
|
persistenceManager &&
|
|
62
95
|
session.id &&
|
|
@@ -66,9 +99,6 @@ export class SessionFinalizer<TContext = unknown, TData = unknown> {
|
|
|
66
99
|
logger.debug(`[SessionFinalizer] Auto-saved session step to persistence: ${session.id}`);
|
|
67
100
|
}
|
|
68
101
|
|
|
69
|
-
// Execute finalize function
|
|
70
|
-
await this.deps.stepLifecycle.runFinalize(session, context);
|
|
71
|
-
|
|
72
102
|
// Update current session if we have one
|
|
73
103
|
const currentSession = this.deps.getCurrentSession();
|
|
74
104
|
if (currentSession) {
|
|
@@ -12,7 +12,7 @@ import { PersistenceManager } from "./PersistenceManager";
|
|
|
12
12
|
import { CompactionEngine } from "./CompactionEngine";
|
|
13
13
|
import type { PersistenceAdapter } from "../types/persistence";
|
|
14
14
|
import type { Agent } from "./Agent";
|
|
15
|
-
import { createSession } from "../utils";
|
|
15
|
+
import { boundConversationHistory, createSession, DEFAULT_MAX_HISTORY_MESSAGES } from "../utils";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* SessionManager handles session lifecycle and conversation history
|
|
@@ -24,6 +24,9 @@ export class SessionManager<TData = unknown> {
|
|
|
24
24
|
private agent?: Agent<any, TData>;
|
|
25
25
|
private defaultSessionId?: string;
|
|
26
26
|
|
|
27
|
+
/** Coalesces concurrent getOrCreate calls for the same session id. */
|
|
28
|
+
private inflightGets = new Map<string, Promise<SessionState<TData>>>();
|
|
29
|
+
|
|
27
30
|
constructor(
|
|
28
31
|
persistenceManagerOrAdapter?:
|
|
29
32
|
| PersistenceManager<TData>
|
|
@@ -56,7 +59,13 @@ export class SessionManager<TData = unknown> {
|
|
|
56
59
|
|
|
57
60
|
/**
|
|
58
61
|
* Core method: getOrCreate handles both existing and new sessions
|
|
59
|
-
* Works for sessionIds that exist, don't exist, or auto-generated IDs
|
|
62
|
+
* Works for sessionIds that exist, don't exist, or auto-generated IDs.
|
|
63
|
+
*
|
|
64
|
+
* Concurrent calls for the same id are COALESCED into one in-flight load:
|
|
65
|
+
* the Agent constructor fires an un-awaited getOrCreate while a first turn
|
|
66
|
+
* may already be starting — without coalescing, both calls can observe a
|
|
67
|
+
* missing row and create two competing sessions (one blanking the other's
|
|
68
|
+
* persisted state).
|
|
60
69
|
*/
|
|
61
70
|
async getOrCreate(sessionId?: string): Promise<SessionState<TData>> {
|
|
62
71
|
// SECURITY: a non-string sessionId (e.g. `{ $ne: null }` from an HTTP body
|
|
@@ -69,6 +78,23 @@ export class SessionManager<TData = unknown> {
|
|
|
69
78
|
// Use provided sessionId or fall back to default
|
|
70
79
|
const effectiveSessionId = sessionId || this.defaultSessionId;
|
|
71
80
|
|
|
81
|
+
const inflightKey = effectiveSessionId || "__auto__";
|
|
82
|
+
const inflight = this.inflightGets.get(inflightKey);
|
|
83
|
+
if (inflight) {
|
|
84
|
+
return inflight;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const promise = this.getOrCreateInner(effectiveSessionId).finally(() => {
|
|
88
|
+
this.inflightGets.delete(inflightKey);
|
|
89
|
+
});
|
|
90
|
+
this.inflightGets.set(inflightKey, promise);
|
|
91
|
+
return promise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async getOrCreateInner(
|
|
95
|
+
effectiveSessionId: string | undefined
|
|
96
|
+
): Promise<SessionState<TData>> {
|
|
97
|
+
|
|
72
98
|
// If we already have a session and no sessionId specified, return it
|
|
73
99
|
if (this.currentSession && !effectiveSessionId) {
|
|
74
100
|
return this.currentSession;
|
|
@@ -79,27 +105,25 @@ export class SessionManager<TData = unknown> {
|
|
|
79
105
|
return this.currentSession;
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
// If sessionId provided, try to load it first
|
|
108
|
+
// If sessionId provided, try to load it first. A missing row returns
|
|
109
|
+
// `null` (→ create below); a FAILED load throws — swallowing it here used
|
|
110
|
+
// to fall through to create(), which saved a blank session over the
|
|
111
|
+
// existing row and erased the user's conversation on a transient DB error.
|
|
83
112
|
if (effectiveSessionId && this.persistenceManager) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
this.currentSession = session;
|
|
98
|
-
return session;
|
|
113
|
+
const session = await this.persistenceManager.loadSessionState(
|
|
114
|
+
effectiveSessionId
|
|
115
|
+
);
|
|
116
|
+
if (session) {
|
|
117
|
+
// Loaded session data wins over anything staged before it existed
|
|
118
|
+
const pending = this.agent?.consumePendingData();
|
|
119
|
+
if (pending && Object.keys(pending).length > 0) {
|
|
120
|
+
log.debug(
|
|
121
|
+
"SessionManager: discarding pre-session staged data in favor of loaded session state",
|
|
122
|
+
pending
|
|
123
|
+
);
|
|
99
124
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Session doesn't exist, will create new one with this ID
|
|
125
|
+
this.currentSession = session;
|
|
126
|
+
return session;
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
129
|
|
|
@@ -150,6 +174,16 @@ export class SessionManager<TData = unknown> {
|
|
|
150
174
|
};
|
|
151
175
|
|
|
152
176
|
session.history.push(historyItem);
|
|
177
|
+
// Same hard bound the finalizer applies — keeps interim auto-saves bounded
|
|
178
|
+
// on the chat()/stream() path too.
|
|
179
|
+
const cap = this.agent?.getAgentOptions().maxHistoryMessages ?? DEFAULT_MAX_HISTORY_MESSAGES;
|
|
180
|
+
if (cap > 0 && session.history) {
|
|
181
|
+
const bounded = boundConversationHistory(session.history, cap);
|
|
182
|
+
if (bounded !== session.history) {
|
|
183
|
+
log.warn(`SessionManager: history exceeded ${cap} entries; trimmed the oldest.`);
|
|
184
|
+
session.history = bounded;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
153
187
|
session.metadata!.lastUpdatedAt = new Date();
|
|
154
188
|
|
|
155
189
|
// Ensure currentSession is updated
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
SignalTriggerState,
|
|
26
26
|
} from "../types/signals";
|
|
27
27
|
import type { SignalEvaluator } from "./SignalEvaluator";
|
|
28
|
+
import { flow } from "./flow-namespace";
|
|
28
29
|
import { logger } from "../utils";
|
|
29
30
|
|
|
30
31
|
/** Recorded on `SignalFiring.extractionError` when a matched extraction-mode
|
|
@@ -654,90 +655,9 @@ export class SignalProcessor<TContext = unknown, TData = unknown> {
|
|
|
654
655
|
private mergeDirectives(
|
|
655
656
|
directives: SignalDirective<TContext, TData>[],
|
|
656
657
|
): Directive<TContext, TData> {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (d.goTo !== undefined) {
|
|
662
|
-
merged.goTo = d.goTo;
|
|
663
|
-
// Clear other position fields
|
|
664
|
-
delete merged.goToStep;
|
|
665
|
-
delete merged.complete;
|
|
666
|
-
delete merged.abort;
|
|
667
|
-
delete merged.reset;
|
|
668
|
-
}
|
|
669
|
-
if (d.goToStep !== undefined) {
|
|
670
|
-
merged.goToStep = d.goToStep;
|
|
671
|
-
delete merged.goTo;
|
|
672
|
-
delete merged.complete;
|
|
673
|
-
delete merged.abort;
|
|
674
|
-
delete merged.reset;
|
|
675
|
-
}
|
|
676
|
-
if (d.complete !== undefined) {
|
|
677
|
-
merged.complete = d.complete;
|
|
678
|
-
delete merged.goTo;
|
|
679
|
-
delete merged.goToStep;
|
|
680
|
-
delete merged.abort;
|
|
681
|
-
delete merged.reset;
|
|
682
|
-
}
|
|
683
|
-
if (d.abort !== undefined) {
|
|
684
|
-
merged.abort = d.abort;
|
|
685
|
-
delete merged.goTo;
|
|
686
|
-
delete merged.goToStep;
|
|
687
|
-
delete merged.complete;
|
|
688
|
-
delete merged.reset;
|
|
689
|
-
}
|
|
690
|
-
if (d.reset !== undefined) {
|
|
691
|
-
merged.reset = d.reset;
|
|
692
|
-
delete merged.goTo;
|
|
693
|
-
delete merged.goToStep;
|
|
694
|
-
delete merged.complete;
|
|
695
|
-
delete merged.abort;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// halt — last-write-wins
|
|
699
|
-
if (d.halt !== undefined) {
|
|
700
|
-
merged.halt = d.halt;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// reply — last-write-wins
|
|
704
|
-
if (d.reply !== undefined) {
|
|
705
|
-
merged.reply = d.reply;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// appendPrompt — array-concat
|
|
709
|
-
if (d.appendPrompt != null) {
|
|
710
|
-
merged.appendPrompt = [
|
|
711
|
-
...(merged.appendPrompt ?? []),
|
|
712
|
-
...d.appendPrompt,
|
|
713
|
-
];
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// injectTools — array-concat
|
|
717
|
-
if (d.injectTools != null) {
|
|
718
|
-
merged.injectTools = [
|
|
719
|
-
...(merged.injectTools ?? []),
|
|
720
|
-
...d.injectTools,
|
|
721
|
-
];
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// dataUpdate — shallow-merge
|
|
725
|
-
if (d.dataUpdate != null) {
|
|
726
|
-
merged.dataUpdate = {
|
|
727
|
-
...(merged.dataUpdate ?? {}),
|
|
728
|
-
...d.dataUpdate,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// contextUpdate — shallow-merge
|
|
733
|
-
if (d.contextUpdate != null) {
|
|
734
|
-
merged.contextUpdate = {
|
|
735
|
-
...(merged.contextUpdate ?? {}),
|
|
736
|
-
...d.contextUpdate,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return merged;
|
|
658
|
+
// Canonical Algorithm 4 merge — one algorithm shared with tools and
|
|
659
|
+
// step hooks (position precedence, reply last-wins, state shallow-merge,
|
|
660
|
+
// halt OR). Empty in, empty ({}) out.
|
|
661
|
+
return flow.mergeAll(directives) ?? {};
|
|
742
662
|
}
|
|
743
663
|
}
|
package/src/core/Step.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { ConditionWhen, ConditionIf } from "../types/flow";
|
|
|
17
17
|
import { generateStepId, logger } from "../utils";
|
|
18
18
|
import { splitWhenConditions } from "../utils/condition";
|
|
19
19
|
import { Agent } from './Agent'
|
|
20
|
+
import { flow } from "./flow-namespace";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Error thrown when a step's configuration violates auto-step constraints.
|
|
@@ -28,6 +29,27 @@ export class FlowConfigurationError extends Error {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** The function-form of step prepare/finalize. */
|
|
33
|
+
type StepPrepareFn<TContext, TData> = (
|
|
34
|
+
context: TContext,
|
|
35
|
+
data?: Partial<TData>
|
|
36
|
+
) => void | PrepareResult<TContext, TData> | Promise<void | PrepareResult<TContext, TData>>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merge two handler results (shorthand + hooks.* desugar). Scalar/object
|
|
40
|
+
* results merge via the canonical Algorithm 4; void/undefined passes through.
|
|
41
|
+
*/
|
|
42
|
+
function mergeHookResults<TContext, TData>(
|
|
43
|
+
a: void | PrepareResult<TContext, TData>,
|
|
44
|
+
b: void | PrepareResult<TContext, TData>
|
|
45
|
+
): PrepareResult<TContext, TData> | undefined {
|
|
46
|
+
if (a == null) return b ?? undefined;
|
|
47
|
+
if (b == null) return a;
|
|
48
|
+
// PrepareResult is a structural subset of Directive — the canonical merge
|
|
49
|
+
// accepts both sides and its output remains a valid PrepareResult.
|
|
50
|
+
return flow.merge<TContext, TData>(a, b);
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
/**
|
|
32
54
|
* Represents a step within a route
|
|
33
55
|
*/
|
|
@@ -89,8 +111,63 @@ export class Step<TContext = unknown, TData = unknown> {
|
|
|
89
111
|
this.reply = options.reply;
|
|
90
112
|
this.when = options.when;
|
|
91
113
|
this.if = options.if;
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
|
|
115
|
+
// Desugar `hooks.prepare` / `hooks.finalize` onto the shorthand fields so
|
|
116
|
+
// both spellings run through the same lifecycle machinery. When both are
|
|
117
|
+
// declared, both run — the shorthand first, then the hook — with their
|
|
118
|
+
// directive returns merged via Algorithm 4 (same desugar pattern as
|
|
119
|
+
// flow-level onComplete).
|
|
120
|
+
const hooks = options.hooks;
|
|
121
|
+
|
|
122
|
+
/** Compose a function-form shorthand with a function-form hook. */
|
|
123
|
+
const composeWithHook = (
|
|
124
|
+
base: StepPrepareFn<TContext, TData> | undefined,
|
|
125
|
+
hook: StepPrepareFn<TContext, TData>
|
|
126
|
+
): StepPrepareFn<TContext, TData> => {
|
|
127
|
+
if (!base) return hook;
|
|
128
|
+
return async (context, data) =>
|
|
129
|
+
mergeHookResults<TContext, TData>(
|
|
130
|
+
await base(context, data),
|
|
131
|
+
await hook(context, data)
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (hooks?.prepare) {
|
|
136
|
+
const hookPrepare: StepPrepareFn<TContext, TData> = (context, data) =>
|
|
137
|
+
hooks.prepare!(context, data);
|
|
138
|
+
const base = options.prepare;
|
|
139
|
+
// A tool-reference shorthand cannot compose with a function hook in a
|
|
140
|
+
// single field — refuse loudly instead of silently dropping one.
|
|
141
|
+
if (base != null && typeof base !== "function") {
|
|
142
|
+
throw new FlowConfigurationError(
|
|
143
|
+
`[FlowConfigurationError] Step "${this.id}" declares both a tool-based prepare and hooks.prepare. A step can only have one prepare handler — remove one or inline the tool call inside the function.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
this.prepare = composeWithHook(
|
|
147
|
+
typeof base === "function" ? base : undefined,
|
|
148
|
+
hookPrepare
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
this.prepare = options.prepare;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (hooks?.finalize) {
|
|
155
|
+
const hookFinalize: StepPrepareFn<TContext, TData> = (context, data) =>
|
|
156
|
+
hooks.finalize!(context, data);
|
|
157
|
+
const base = options.finalize;
|
|
158
|
+
if (base != null && typeof base !== "function") {
|
|
159
|
+
throw new FlowConfigurationError(
|
|
160
|
+
`[FlowConfigurationError] Step "${this.id}" declares both a tool-based finalize and hooks.finalize. A step can only have one finalize handler — remove one or inline the tool call inside the function.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
this.finalize = composeWithHook(
|
|
164
|
+
typeof base === "function" ? base : undefined,
|
|
165
|
+
hookFinalize
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
this.finalize = options.finalize;
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
this.tools = options.tools;
|
|
95
172
|
|
|
96
173
|
// Validate when/if split: functions belong on `if`, not `when`
|
|
@@ -4,14 +4,44 @@
|
|
|
4
4
|
* A handler may be a function, a registered tool id, or an inline tool.
|
|
5
5
|
* Used by the response prepare phase (prepare) and by SessionFinalizer
|
|
6
6
|
* (finalize).
|
|
7
|
+
*
|
|
8
|
+
* Handlers may emit directives three ways: a function's return value,
|
|
9
|
+
* `ctx.dispatch()` inside a tool, or a `{ directive }` tool return. All are
|
|
10
|
+
* merged via the canonical Algorithm 4 (`flow.merge`) and validated with
|
|
11
|
+
* `flow.validate`. State writes (`dataUpdate`/`contextUpdate`) are applied
|
|
12
|
+
* immediately to the TURN session — never to agent-level live state — so
|
|
13
|
+
* persistence captures them. Remaining control-flow fields (position fields,
|
|
14
|
+
* verbatim replies, halts) are returned to the caller to act on.
|
|
7
15
|
*/
|
|
8
16
|
|
|
9
|
-
import type { PrepareResult, SessionState, Tool } from "../types";
|
|
17
|
+
import type { Directive, PrepareResult, SessionState, Tool } from "../types";
|
|
10
18
|
import type { Flow } from "./Flow";
|
|
11
19
|
import type { Step } from "./Step";
|
|
12
20
|
import type { ToolManager } from "./ToolManager";
|
|
21
|
+
import { flow } from "./flow-namespace";
|
|
13
22
|
import { logger } from "../utils";
|
|
14
23
|
|
|
24
|
+
/** Fields a caller can still act on after state writes have been applied. */
|
|
25
|
+
const CONTROL_FIELDS = [
|
|
26
|
+
"goTo",
|
|
27
|
+
"goToStep",
|
|
28
|
+
"reset",
|
|
29
|
+
"complete",
|
|
30
|
+
"abort",
|
|
31
|
+
"reply",
|
|
32
|
+
"halt",
|
|
33
|
+
"appendPrompt",
|
|
34
|
+
"injectTools",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
function hasControlFields(directive: Directive): boolean {
|
|
38
|
+
return CONTROL_FIELDS.some(
|
|
39
|
+
(field) =>
|
|
40
|
+
(directive as Record<string, unknown>)[field] !== undefined &&
|
|
41
|
+
(directive as Record<string, unknown>)[field] !== null
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
export class StepLifecycle<TContext = unknown, TData = unknown> {
|
|
16
46
|
constructor(
|
|
17
47
|
private readonly deps: {
|
|
@@ -23,10 +53,14 @@ export class StepLifecycle<TContext = unknown, TData = unknown> {
|
|
|
23
53
|
) { }
|
|
24
54
|
|
|
25
55
|
/**
|
|
26
|
-
* Execute prepare
|
|
56
|
+
* Execute prepare for the session's current step if available.
|
|
57
|
+
* Returns the merged control-flow directive, if the handler emitted one.
|
|
27
58
|
* Auto-steps are skipped — their prepare is handled by AutoChainExecutor.
|
|
28
59
|
*/
|
|
29
|
-
async runPrepare(
|
|
60
|
+
async runPrepare(
|
|
61
|
+
session: SessionState<TData>,
|
|
62
|
+
context: TContext
|
|
63
|
+
): Promise<Directive<TContext, TData> | undefined> {
|
|
30
64
|
if (session.currentFlow && session.currentStep) {
|
|
31
65
|
const currentFlow = this.deps.getFlows().find(
|
|
32
66
|
(r) => r.id === session.currentFlow?.id
|
|
@@ -36,26 +70,31 @@ export class StepLifecycle<TContext = unknown, TData = unknown> {
|
|
|
36
70
|
// Skip auto-steps — their prepare is handled by AutoChainExecutor
|
|
37
71
|
if (currentStep?.auto) {
|
|
38
72
|
logger.debug(`[StepLifecycle] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
|
|
39
|
-
return;
|
|
73
|
+
return undefined;
|
|
40
74
|
}
|
|
41
75
|
if (currentStep?.prepare) {
|
|
42
76
|
logger.debug(`[StepLifecycle] Executing prepare for step: ${currentStep.id}`);
|
|
43
|
-
|
|
77
|
+
return this.executePrepareFinalize(
|
|
44
78
|
currentStep.prepare,
|
|
45
79
|
context,
|
|
46
|
-
session
|
|
80
|
+
session,
|
|
47
81
|
currentFlow,
|
|
48
82
|
currentStep
|
|
49
83
|
);
|
|
50
84
|
}
|
|
51
85
|
}
|
|
52
86
|
}
|
|
87
|
+
return undefined;
|
|
53
88
|
}
|
|
54
89
|
|
|
55
90
|
/**
|
|
56
|
-
* Execute finalize
|
|
91
|
+
* Execute finalize for the session's current step if available.
|
|
92
|
+
* Returns the merged control-flow directive, if the handler emitted one.
|
|
57
93
|
*/
|
|
58
|
-
async runFinalize(
|
|
94
|
+
async runFinalize(
|
|
95
|
+
session: SessionState<TData>,
|
|
96
|
+
context: TContext
|
|
97
|
+
): Promise<Directive<TContext, TData> | undefined> {
|
|
59
98
|
if (session.currentFlow && session.currentStep) {
|
|
60
99
|
const currentFlow = this.deps.getFlows().find(
|
|
61
100
|
(r) => r.id === session.currentFlow?.id
|
|
@@ -66,58 +105,75 @@ export class StepLifecycle<TContext = unknown, TData = unknown> {
|
|
|
66
105
|
logger.debug(
|
|
67
106
|
`[StepLifecycle] Executing finalize for step: ${currentStep.id}`
|
|
68
107
|
);
|
|
69
|
-
|
|
108
|
+
return this.executePrepareFinalize(
|
|
70
109
|
currentStep.finalize,
|
|
71
110
|
context,
|
|
72
|
-
session
|
|
111
|
+
session,
|
|
73
112
|
currentFlow,
|
|
74
113
|
currentStep
|
|
75
114
|
);
|
|
76
115
|
}
|
|
77
116
|
}
|
|
78
117
|
}
|
|
118
|
+
return undefined;
|
|
79
119
|
}
|
|
80
120
|
|
|
81
121
|
/**
|
|
82
|
-
* Execute a prepare or finalize
|
|
122
|
+
* Execute a prepare or finalize handler and resolve its emitted directives.
|
|
83
123
|
*/
|
|
84
124
|
private async executePrepareFinalize(
|
|
85
125
|
prepareOrFinalize:
|
|
86
126
|
| string
|
|
87
127
|
| Tool<TContext, TData>
|
|
88
|
-
| ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult
|
|
128
|
+
| ((context: TContext, data?: Partial<TData>) => void | PrepareResult<TContext, TData> | Promise<void | PrepareResult<TContext, TData>>)
|
|
89
129
|
| undefined,
|
|
90
130
|
context: TContext,
|
|
91
|
-
|
|
92
|
-
|
|
131
|
+
session: SessionState<TData>,
|
|
132
|
+
flowRef?: Flow<TContext, TData>,
|
|
93
133
|
step?: Step<TContext, TData>
|
|
94
|
-
): Promise<
|
|
95
|
-
if (!prepareOrFinalize) return;
|
|
134
|
+
): Promise<Directive<TContext, TData> | undefined> {
|
|
135
|
+
if (!prepareOrFinalize) return undefined;
|
|
136
|
+
|
|
137
|
+
const collected: Directive<TContext, TData>[] = [];
|
|
138
|
+
|
|
139
|
+
/** State writes land on the TURN session so persistence captures them. */
|
|
140
|
+
const writeToSession = (updates: Partial<TData> | Record<string, unknown> | undefined): void => {
|
|
141
|
+
if (!updates) return;
|
|
142
|
+
session.data = { ...(session.data ?? {}), ...updates };
|
|
143
|
+
};
|
|
96
144
|
|
|
97
145
|
if (typeof prepareOrFinalize === "function") {
|
|
98
|
-
// It's a function - call it
|
|
99
|
-
await prepareOrFinalize(context, data);
|
|
146
|
+
// It's a function - call it and honor its return value
|
|
147
|
+
const result = await prepareOrFinalize(context, session.data);
|
|
148
|
+
if (result && typeof result === "object") {
|
|
149
|
+
collected.push(result);
|
|
150
|
+
}
|
|
100
151
|
} else {
|
|
101
152
|
// It's a tool reference - find and execute the tool
|
|
102
153
|
let tool: Tool<TContext, TData> | undefined;
|
|
103
154
|
|
|
104
155
|
if (typeof prepareOrFinalize === "string") {
|
|
105
156
|
// Tool ID - use ToolManager for unified resolution
|
|
106
|
-
tool = this.deps.toolManager.find(prepareOrFinalize, undefined, step,
|
|
157
|
+
tool = this.deps.toolManager.find(prepareOrFinalize, undefined, step, flowRef);
|
|
107
158
|
} else {
|
|
108
159
|
// Tool object - use directly
|
|
109
160
|
tool = prepareOrFinalize;
|
|
110
161
|
}
|
|
111
162
|
|
|
112
163
|
if (tool) {
|
|
113
|
-
// Use ToolManager for unified tool execution
|
|
164
|
+
// Use ToolManager for unified tool execution. updateData writes
|
|
165
|
+
// into the turn session (previously it mutated the agent's live
|
|
166
|
+
// session, losing writes made after the persistence snapshot).
|
|
114
167
|
const result = await this.deps.toolManager.executeTool({
|
|
115
168
|
tool,
|
|
116
169
|
context,
|
|
117
170
|
updateContext: this.deps.updateContext,
|
|
118
|
-
updateData:
|
|
171
|
+
updateData: async (updates) => {
|
|
172
|
+
writeToSession(updates);
|
|
173
|
+
await this.deps.updateData(updates);
|
|
174
|
+
},
|
|
119
175
|
history: [], // Empty history for prepare/finalize
|
|
120
|
-
data,
|
|
176
|
+
data: session.data,
|
|
121
177
|
});
|
|
122
178
|
|
|
123
179
|
if (!result.success) {
|
|
@@ -126,6 +182,16 @@ export class StepLifecycle<TContext = unknown, TData = unknown> {
|
|
|
126
182
|
);
|
|
127
183
|
throw new Error(`Tool execution failed: ${result.error}`);
|
|
128
184
|
}
|
|
185
|
+
|
|
186
|
+
// Collect directives (ctx.dispatch / {directive} returns)
|
|
187
|
+
if (result.directives?.length) {
|
|
188
|
+
collected.push(...(result.directives as Directive<TContext, TData>[]));
|
|
189
|
+
}
|
|
190
|
+
// Honor ToolResult-level state updates
|
|
191
|
+
writeToSession(result.dataUpdate as Partial<TData> | undefined);
|
|
192
|
+
if (result.contextUpdate) {
|
|
193
|
+
await this.deps.updateContext(result.contextUpdate as Partial<TContext>);
|
|
194
|
+
}
|
|
129
195
|
} else {
|
|
130
196
|
logger.warn(
|
|
131
197
|
`[StepLifecycle] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
|
|
@@ -135,5 +201,42 @@ export class StepLifecycle<TContext = unknown, TData = unknown> {
|
|
|
135
201
|
);
|
|
136
202
|
}
|
|
137
203
|
}
|
|
204
|
+
|
|
205
|
+
if (collected.length === 0) return undefined;
|
|
206
|
+
|
|
207
|
+
// Canonical merge + validation — one algorithm shared with tools,
|
|
208
|
+
// signals, and auto-steps.
|
|
209
|
+
for (const directive of collected) {
|
|
210
|
+
flow.validate(directive);
|
|
211
|
+
}
|
|
212
|
+
const merged = flow.mergeAll(collected);
|
|
213
|
+
if (!merged) return undefined;
|
|
214
|
+
|
|
215
|
+
// Apply state writes immediately; they are not control flow.
|
|
216
|
+
writeToSession(merged.dataUpdate);
|
|
217
|
+
if (merged.contextUpdate) {
|
|
218
|
+
await this.deps.updateContext(merged.contextUpdate);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!hasControlFields(merged)) return undefined;
|
|
222
|
+
|
|
223
|
+
// Pre-LLM augmentation fields (appendPrompt / injectTools / halt / reply)
|
|
224
|
+
// have no sink on non-auto steps yet — surface loudly instead of silently
|
|
225
|
+
// dropping them.
|
|
226
|
+
if (
|
|
227
|
+
(merged.appendPrompt?.length || merged.injectTools?.length || merged.halt || merged.reply) &&
|
|
228
|
+
!step?.auto
|
|
229
|
+
) {
|
|
230
|
+
logger.warn(
|
|
231
|
+
`[StepLifecycle] Directive from "${step?.id ?? "step"}" ${flowRef ? `in flow "${flowRef.title}" ` : ""}` +
|
|
232
|
+
`carries pre-LLM fields (appendPrompt/injectTools/halt/reply) that only auto-steps honor today. ` +
|
|
233
|
+
`Use an auto step for prompt augmentation or a signal handler for halt+reply.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const control = { ...merged };
|
|
238
|
+
delete control.dataUpdate;
|
|
239
|
+
delete control.contextUpdate;
|
|
240
|
+
return control;
|
|
138
241
|
}
|
|
139
242
|
}
|