@ariaflowagents/core 0.6.3 → 0.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 +58 -5
- package/dist/agents/Agent.d.ts +1 -0
- package/dist/agents/Agent.d.ts.map +1 -1
- package/dist/agents/Agent.js +10 -1
- package/dist/agents/Agent.js.map +1 -1
- package/dist/agents/CompositeAgent.d.ts +8 -12
- package/dist/agents/CompositeAgent.d.ts.map +1 -1
- package/dist/agents/CompositeAgent.js +30 -16
- package/dist/agents/CompositeAgent.js.map +1 -1
- package/dist/agents/FlowAgent.js +2 -2
- package/dist/agents/FlowAgent.js.map +1 -1
- package/dist/agents/LLMAgent.d.ts.map +1 -1
- package/dist/agents/LLMAgent.js +4 -25
- package/dist/agents/LLMAgent.js.map +1 -1
- package/dist/agents/TriageAgent.d.ts.map +1 -1
- package/dist/agents/TriageAgent.js +8 -29
- package/dist/agents/TriageAgent.js.map +1 -1
- package/dist/callbacks/httpCallback.d.ts +1 -0
- package/dist/callbacks/httpCallback.d.ts.map +1 -1
- package/dist/callbacks/httpCallback.js +20 -6
- package/dist/callbacks/httpCallback.js.map +1 -1
- package/dist/callbacks/streamCallback.d.ts +26 -0
- package/dist/callbacks/streamCallback.d.ts.map +1 -0
- package/dist/callbacks/streamCallback.js +281 -0
- package/dist/callbacks/streamCallback.js.map +1 -0
- package/dist/flows/FlowManager.d.ts +19 -4
- package/dist/flows/FlowManager.d.ts.map +1 -1
- package/dist/flows/FlowManager.js +355 -131
- package/dist/flows/FlowManager.js.map +1 -1
- package/dist/flows/extraction.d.ts +17 -0
- package/dist/flows/extraction.d.ts.map +1 -0
- package/dist/flows/extraction.js +56 -0
- package/dist/flows/extraction.js.map +1 -0
- package/dist/flows/index.d.ts +1 -2
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/validation.d.ts +7 -0
- package/dist/flows/validation.d.ts.map +1 -0
- package/dist/flows/validation.js +42 -0
- package/dist/flows/validation.js.map +1 -0
- package/dist/hooks/builtin/metrics.d.ts +4 -34
- package/dist/hooks/builtin/metrics.d.ts.map +1 -1
- package/dist/hooks/builtin/metrics.js +3 -65
- package/dist/hooks/builtin/metrics.js.map +1 -1
- package/dist/hooks/helpers.d.ts +8 -47
- package/dist/hooks/helpers.d.ts.map +1 -1
- package/dist/hooks/helpers.js +38 -104
- package/dist/hooks/helpers.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/processors/ProcessorRunner.d.ts.map +1 -1
- package/dist/processors/ProcessorRunner.js +13 -0
- package/dist/processors/ProcessorRunner.js.map +1 -1
- package/dist/prompts/PromptBuilder.d.ts.map +1 -1
- package/dist/prompts/PromptBuilder.js +1 -0
- package/dist/prompts/PromptBuilder.js.map +1 -1
- package/dist/prompts/types.d.ts +2 -1
- package/dist/prompts/types.d.ts.map +1 -1
- package/dist/prompts/types.js +29 -7
- package/dist/prompts/types.js.map +1 -1
- package/dist/runtime/ContextManager.d.ts +1 -1
- package/dist/runtime/ContextManager.d.ts.map +1 -1
- package/dist/runtime/ContextManager.js +141 -20
- package/dist/runtime/ContextManager.js.map +1 -1
- package/dist/runtime/ExtractionEngine.d.ts +34 -0
- package/dist/runtime/ExtractionEngine.d.ts.map +1 -0
- package/dist/runtime/ExtractionEngine.js +155 -0
- package/dist/runtime/ExtractionEngine.js.map +1 -0
- package/dist/runtime/FlowExecutor.d.ts +51 -0
- package/dist/runtime/FlowExecutor.d.ts.map +1 -0
- package/dist/runtime/FlowExecutor.js +523 -0
- package/dist/runtime/FlowExecutor.js.map +1 -0
- package/dist/runtime/InjectionQueue.d.ts +8 -1
- package/dist/runtime/InjectionQueue.d.ts.map +1 -1
- package/dist/runtime/InjectionQueue.js +33 -0
- package/dist/runtime/InjectionQueue.js.map +1 -1
- package/dist/runtime/Runtime.d.ts +32 -2
- package/dist/runtime/Runtime.d.ts.map +1 -1
- package/dist/runtime/Runtime.js +513 -612
- package/dist/runtime/Runtime.js.map +1 -1
- package/dist/runtime/SessionEventManager.d.ts +17 -0
- package/dist/runtime/SessionEventManager.d.ts.map +1 -0
- package/dist/runtime/SessionEventManager.js +149 -0
- package/dist/runtime/SessionEventManager.js.map +1 -0
- package/dist/runtime/SuggestionManager.d.ts +7 -0
- package/dist/runtime/SuggestionManager.d.ts.map +1 -0
- package/dist/runtime/SuggestionManager.js +50 -0
- package/dist/runtime/SuggestionManager.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/services/MetricsService.d.ts +55 -0
- package/dist/services/MetricsService.d.ts.map +1 -0
- package/dist/services/MetricsService.js +86 -0
- package/dist/services/MetricsService.js.map +1 -0
- package/dist/services/TracingService.d.ts +13 -0
- package/dist/services/TracingService.d.ts.map +1 -0
- package/dist/services/TracingService.js +62 -0
- package/dist/services/TracingService.js.map +1 -0
- package/dist/session/stores/MemoryStore.js +1 -1
- package/dist/session/stores/MemoryStore.js.map +1 -1
- package/dist/tools/Tool.d.ts +25 -3
- package/dist/tools/Tool.d.ts.map +1 -1
- package/dist/tools/Tool.js.map +1 -1
- package/dist/tools/errorHandling.d.ts +1 -1
- package/dist/tools/errorHandling.d.ts.map +1 -1
- package/dist/tools/errorHandling.js +27 -20
- package/dist/tools/errorHandling.js.map +1 -1
- package/dist/tools/http.d.ts.map +1 -1
- package/dist/tools/http.js +53 -17
- package/dist/tools/http.js.map +1 -1
- package/dist/types/index.d.ts +179 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/telemetry.d.ts +52 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/dist/types/telemetry.js +2 -0
- package/dist/types/telemetry.js.map +1 -0
- package/dist/utils/aiStream.d.ts +7 -0
- package/dist/utils/aiStream.d.ts.map +1 -0
- package/dist/utils/aiStream.js +41 -0
- package/dist/utils/aiStream.js.map +1 -0
- package/dist/utils/chrono.d.ts +3 -46
- package/dist/utils/chrono.d.ts.map +1 -1
- package/dist/utils/chrono.js.map +1 -1
- package/dist/utils/isRecord.d.ts +2 -0
- package/dist/utils/isRecord.d.ts.map +1 -0
- package/dist/utils/isRecord.js +4 -0
- package/dist/utils/isRecord.js.map +1 -0
- package/dist/utils/messageNormalization.d.ts +3 -0
- package/dist/utils/messageNormalization.d.ts.map +1 -0
- package/dist/utils/messageNormalization.js +121 -0
- package/dist/utils/messageNormalization.js.map +1 -0
- package/dist/utils/streamChunk.d.ts +5 -0
- package/dist/utils/streamChunk.d.ts.map +1 -0
- package/dist/utils/streamChunk.js +50 -0
- package/dist/utils/streamChunk.js.map +1 -0
- package/guides/EXAMPLE_VERIFICATION.md +53 -0
- package/guides/FLOWS.md +29 -0
- package/guides/GETTING_STARTED.md +14 -1
- package/guides/README.md +3 -1
- package/guides/RUNTIME.md +75 -0
- package/guides/TOOLS.md +6 -0
- package/package.json +2 -2
- package/dist/flows/AgentFlowManager.d.ts +0 -161
- package/dist/flows/AgentFlowManager.d.ts.map +0 -1
- package/dist/flows/AgentFlowManager.js +0 -448
- package/dist/flows/AgentFlowManager.js.map +0 -1
package/dist/runtime/Runtime.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { streamText, generateText, Output, stepCountIs } from 'ai';
|
|
2
3
|
import { z } from 'zod';
|
|
3
|
-
import { FlowManager } from '../flows/FlowManager.js';
|
|
4
4
|
import { createHandoffTool, isHandoffResult } from '../tools/handoff.js';
|
|
5
5
|
import { isFinalResult } from '../tools/final.js';
|
|
6
|
-
import { InjectionQueue,
|
|
6
|
+
import { InjectionQueue, getPolicyProfileInjections } from './InjectionQueue.js';
|
|
7
7
|
import { checkStopConditions, defaultStopConditions } from '../guards/StopConditions.js';
|
|
8
8
|
import { HookRunner } from '../hooks/HookRunner.js';
|
|
9
9
|
import { ToolEnforcer } from '../guards/ToolEnforcer.js';
|
|
10
10
|
import { defaultEnforcementRules } from '../guards/rules.js';
|
|
11
11
|
import { MemoryStore } from '../session/stores/MemoryStore.js';
|
|
12
12
|
import { createHttpCallback } from '../callbacks/httpCallback.js';
|
|
13
|
-
import {
|
|
13
|
+
import { createStreamCallbackAdapter } from '../callbacks/streamCallback.js';
|
|
14
|
+
import { compileSanitizePattern } from '../flows/template.js';
|
|
14
15
|
import { runInputProcessors, runOutputProcessors } from '../processors/ProcessorRunner.js';
|
|
16
|
+
import { getChunkArgs, getChunkErrorMessage, getChunkResult, getChunkToolCallId } from '../utils/streamChunk.js';
|
|
17
|
+
import { normalizeModelMessage } from '../utils/messageNormalization.js';
|
|
18
|
+
import { isRecord } from '../utils/isRecord.js';
|
|
19
|
+
import { ExtractionEngine } from './ExtractionEngine.js';
|
|
20
|
+
import { SessionEventManager } from './SessionEventManager.js';
|
|
21
|
+
import { SuggestionManager } from './SuggestionManager.js';
|
|
22
|
+
import { FlowExecutor } from './FlowExecutor.js';
|
|
23
|
+
import { SessionWorkingMemory } from './WorkingMemory.js';
|
|
15
24
|
export class Runtime {
|
|
16
25
|
config;
|
|
17
26
|
agents = new Map();
|
|
@@ -32,9 +41,22 @@ export class Runtime {
|
|
|
32
41
|
outputProcessors;
|
|
33
42
|
outputProcessorMode;
|
|
34
43
|
outputRedactions;
|
|
35
|
-
|
|
44
|
+
redactCarryKey = '__ariaRedactCarry';
|
|
36
45
|
redactLookbehind = 64;
|
|
46
|
+
runtimeEventLogKey = 'runtimeEventLog';
|
|
47
|
+
runtimeSessionTurnKey = '__ariaSessionTurn';
|
|
48
|
+
runtimeEventLogMaxEntries = 2000;
|
|
49
|
+
checkpointEventTypes = new Set([
|
|
50
|
+
'tool-result',
|
|
51
|
+
'tool-error',
|
|
52
|
+
'flow-transition',
|
|
53
|
+
]);
|
|
54
|
+
extractionEngine;
|
|
55
|
+
sessionEventManager;
|
|
56
|
+
suggestionManager;
|
|
57
|
+
flowExecutor;
|
|
37
58
|
wrapToolsWithEnforcement(context, tools) {
|
|
59
|
+
console.log('[Runtime] Wrapping tools:', Object.keys(tools));
|
|
38
60
|
const wrapped = {};
|
|
39
61
|
for (const [toolName, toolDef] of Object.entries(tools ?? {})) {
|
|
40
62
|
const exec = toolDef?.execute;
|
|
@@ -45,10 +67,15 @@ export class Runtime {
|
|
|
45
67
|
wrapped[toolName] = {
|
|
46
68
|
...toolDef,
|
|
47
69
|
execute: async (args, options) => {
|
|
70
|
+
const toolCallId = typeof options?.toolCallId === 'string'
|
|
71
|
+
? options.toolCallId
|
|
72
|
+
: crypto.randomUUID();
|
|
73
|
+
const idempotencyKey = this.buildToolIdempotencyKey(context, toolName, toolCallId);
|
|
48
74
|
const callRecord = {
|
|
49
|
-
toolCallId
|
|
75
|
+
toolCallId,
|
|
50
76
|
toolName,
|
|
51
77
|
args,
|
|
78
|
+
idempotencyKey,
|
|
52
79
|
success: true,
|
|
53
80
|
timestamp: Date.now(),
|
|
54
81
|
};
|
|
@@ -66,7 +93,15 @@ export class Runtime {
|
|
|
66
93
|
throw callRecord.error;
|
|
67
94
|
}
|
|
68
95
|
// Preserve AI SDK tool execution context (toolCallId, messages, experimental_context, etc.).
|
|
69
|
-
|
|
96
|
+
const enrichedOptions = this.withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey);
|
|
97
|
+
try {
|
|
98
|
+
console.log(`[Runtime] Executing tool ${toolName}...`);
|
|
99
|
+
return await exec(args, enrichedOptions);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(`[Runtime] Tool execution failed for ${toolName}:`, error);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
70
105
|
},
|
|
71
106
|
};
|
|
72
107
|
}
|
|
@@ -84,20 +119,31 @@ export class Runtime {
|
|
|
84
119
|
this.stopConditions = config.stopConditions ?? defaultStopConditions;
|
|
85
120
|
this._sessionStore = config.sessionStore ?? new MemoryStore();
|
|
86
121
|
const hooks = { ...config.hooks };
|
|
87
|
-
|
|
88
|
-
console.log('[Runtime] HTTP callback configured for:', config.callback.url);
|
|
89
|
-
console.log('[Runtime] Callback config:', JSON.stringify(config.callback, null, 2));
|
|
90
|
-
console.log('[Runtime] Hooks before wiring:', Object.keys(hooks));
|
|
91
|
-
const httpCallback = createHttpCallback(config.callback);
|
|
122
|
+
const appendOnStreamPart = (fn) => {
|
|
92
123
|
const originalHook = hooks.onStreamPart;
|
|
93
124
|
hooks.onStreamPart = async (context, part) => {
|
|
94
|
-
console.log('[Runtime] onStreamPart triggered for:', part.type);
|
|
95
125
|
if (originalHook) {
|
|
96
126
|
await originalHook(context, part);
|
|
97
127
|
}
|
|
98
|
-
await
|
|
128
|
+
await fn(context, part);
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
if (config.callback) {
|
|
132
|
+
const httpCallback = createHttpCallback(config.callback);
|
|
133
|
+
appendOnStreamPart(httpCallback);
|
|
134
|
+
}
|
|
135
|
+
if (config.streamCallback) {
|
|
136
|
+
const adapter = createStreamCallbackAdapter(config.streamCallback);
|
|
137
|
+
appendOnStreamPart(adapter.onStreamPart);
|
|
138
|
+
const originalOnEnd = hooks.onEnd;
|
|
139
|
+
hooks.onEnd = async (context, result) => {
|
|
140
|
+
if (originalOnEnd) {
|
|
141
|
+
await originalOnEnd(context, result);
|
|
142
|
+
}
|
|
143
|
+
if (config.streamCallback?.flushOnEnd) {
|
|
144
|
+
await adapter.flush(config.streamCallback.flushTimeoutMs ?? 2000);
|
|
145
|
+
}
|
|
99
146
|
};
|
|
100
|
-
console.log('[Runtime] Hooks after wiring:', Object.keys(hooks));
|
|
101
147
|
}
|
|
102
148
|
this.hookRunner = new HookRunner(hooks);
|
|
103
149
|
this.enforcer = new ToolEnforcer(config.enforcementRules ?? defaultEnforcementRules);
|
|
@@ -115,6 +161,16 @@ export class Runtime {
|
|
|
115
161
|
return { re, replacement: r.replacement };
|
|
116
162
|
});
|
|
117
163
|
}
|
|
164
|
+
this.extractionEngine = new ExtractionEngine({
|
|
165
|
+
defaultModel: this.defaultModel,
|
|
166
|
+
telemetry: config.telemetry,
|
|
167
|
+
});
|
|
168
|
+
this.sessionEventManager = new SessionEventManager({
|
|
169
|
+
touchSession: session => this.touchSession(session),
|
|
170
|
+
getSessionTurn: session => this.getSessionTurn(session),
|
|
171
|
+
});
|
|
172
|
+
this.suggestionManager = new SuggestionManager(config);
|
|
173
|
+
this.flowExecutor = new FlowExecutor(config);
|
|
118
174
|
}
|
|
119
175
|
get sessionStore() {
|
|
120
176
|
return this._sessionStore;
|
|
@@ -176,12 +232,8 @@ export class Runtime {
|
|
|
176
232
|
consecutiveErrors: 0,
|
|
177
233
|
toolCallHistory: [],
|
|
178
234
|
};
|
|
179
|
-
|
|
180
|
-
injectionQueue.
|
|
181
|
-
injectionQueue.add(commonInjections.noGuessing);
|
|
182
|
-
injectionQueue.add(commonInjections.noSecrets);
|
|
183
|
-
injectionQueue.add(commonInjections.invisibleHandoffs);
|
|
184
|
-
injectionQueue.add(commonInjections.confirmDestructive);
|
|
235
|
+
this.bumpSessionTurn(session);
|
|
236
|
+
const injectionQueue = this.buildPolicyInjectionQueue();
|
|
185
237
|
// Yield input through emit so hooks see it (once).
|
|
186
238
|
yield* this.emit(context, { type: 'input', text: input, userId: session.userId || undefined });
|
|
187
239
|
const controller = new AbortController();
|
|
@@ -190,11 +242,17 @@ export class Runtime {
|
|
|
190
242
|
this.abortControllers.delete(session.id);
|
|
191
243
|
};
|
|
192
244
|
controller.signal.addEventListener('abort', abortHandler);
|
|
245
|
+
let externalAbortHandler;
|
|
193
246
|
if (abortSignal) {
|
|
194
|
-
|
|
247
|
+
externalAbortHandler = () => {
|
|
195
248
|
controller.abort(abortSignal.reason ?? 'External abort');
|
|
196
249
|
};
|
|
197
|
-
abortSignal.
|
|
250
|
+
if (abortSignal.aborted) {
|
|
251
|
+
controller.abort(abortSignal.reason ?? 'External abort');
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
abortSignal.addEventListener('abort', externalAbortHandler);
|
|
255
|
+
}
|
|
198
256
|
}
|
|
199
257
|
await this.hookRunner.onStart(context);
|
|
200
258
|
// Run input processors BEFORE persisting the user message.
|
|
@@ -210,6 +268,7 @@ export class Runtime {
|
|
|
210
268
|
session,
|
|
211
269
|
agentId: activeAgentId,
|
|
212
270
|
toolCallHistory: context.toolCallHistory,
|
|
271
|
+
abortSignal: controller.signal,
|
|
213
272
|
};
|
|
214
273
|
const outcome = await runInputProcessors({
|
|
215
274
|
processors: turnInputProcessors,
|
|
@@ -233,18 +292,19 @@ export class Runtime {
|
|
|
233
292
|
processedInput = outcome.input;
|
|
234
293
|
}
|
|
235
294
|
// Persist the (possibly modified) user message after input processors.
|
|
236
|
-
|
|
295
|
+
this.appendSessionMessage(session, { role: 'user', content: processedInput });
|
|
237
296
|
if (this.contextManager) {
|
|
238
297
|
const messagesBefore = session.messages.length;
|
|
239
298
|
const totalTokens = session.messages.reduce((sum, m) => {
|
|
240
299
|
const text = typeof m.content === 'string' ? m.content : '';
|
|
241
300
|
return sum + Math.ceil(text.length / 4);
|
|
242
301
|
}, 0);
|
|
243
|
-
|
|
302
|
+
const compacted = await this.contextManager.beforeTurn(session.messages, {
|
|
244
303
|
turnCount: this.turnCount,
|
|
245
304
|
totalTokens,
|
|
246
305
|
sessionId: session.id,
|
|
247
306
|
});
|
|
307
|
+
session.messages = this.normalizeSessionHistory(compacted);
|
|
248
308
|
const messagesAfter = session.messages.length;
|
|
249
309
|
if (messagesBefore !== messagesAfter) {
|
|
250
310
|
yield* this.emit(context, {
|
|
@@ -261,86 +321,75 @@ export class Runtime {
|
|
|
261
321
|
await this.hookRunner.onEnd(context, { success: true });
|
|
262
322
|
}
|
|
263
323
|
catch (error) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
324
|
+
if (controller.signal.aborted) {
|
|
325
|
+
yield* this.emit(context, {
|
|
326
|
+
type: 'interrupted',
|
|
327
|
+
sessionId: context.session.id,
|
|
328
|
+
reason: controller.signal.reason ?? 'Operation cancelled',
|
|
329
|
+
timestamp: new Date(),
|
|
330
|
+
lastAgentId: context.agentId,
|
|
331
|
+
lastStep: context.stepCount,
|
|
332
|
+
});
|
|
333
|
+
await this.hookRunner.onEnd(context, { success: false });
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
await this.hookRunner.onError(context, error);
|
|
337
|
+
await this.hookRunner.onEnd(context, { success: false, error: error });
|
|
338
|
+
yield* this.emit(context, { type: 'error', error: error.message });
|
|
339
|
+
}
|
|
267
340
|
}
|
|
268
341
|
finally {
|
|
269
342
|
controller.signal.removeEventListener('abort', abortHandler);
|
|
343
|
+
if (abortSignal && externalAbortHandler) {
|
|
344
|
+
abortSignal.removeEventListener('abort', externalAbortHandler);
|
|
345
|
+
}
|
|
270
346
|
this.abortControllers.delete(session.id);
|
|
271
|
-
await this.
|
|
347
|
+
await this.saveSessionCheckpoint(session);
|
|
272
348
|
yield* this.emit(context, { type: 'done', sessionId: session.id });
|
|
349
|
+
await this.saveSessionCheckpoint(session);
|
|
273
350
|
}
|
|
274
351
|
}
|
|
275
352
|
async *generateSuggestions(context) {
|
|
276
|
-
|
|
277
|
-
if (!suggestionModel)
|
|
278
|
-
return;
|
|
279
|
-
const count = this.config.suggestionCount ?? 3;
|
|
280
|
-
const prompt = this.config.suggestionPrompt ??
|
|
281
|
-
`Based on the conversation above, suggest ${count} short, relevant follow-up questions or actions the user might want to take next.
|
|
282
|
-
Keep each suggestion under 5 words.
|
|
283
|
-
Return them as an array of strings.`;
|
|
284
|
-
try {
|
|
285
|
-
const result = streamObject({
|
|
286
|
-
model: suggestionModel,
|
|
287
|
-
schema: z.object({
|
|
288
|
-
suggestions: z.array(z.string()).min(1).max(count),
|
|
289
|
-
}),
|
|
290
|
-
messages: [
|
|
291
|
-
...context.session.messages,
|
|
292
|
-
{ role: 'user', content: prompt }
|
|
293
|
-
],
|
|
294
|
-
});
|
|
295
|
-
for await (const partialObject of result.partialObjectStream) {
|
|
296
|
-
if (partialObject.suggestions) {
|
|
297
|
-
yield {
|
|
298
|
-
type: 'suggested-questions',
|
|
299
|
-
suggestions: partialObject.suggestions.filter((s) => !!s),
|
|
300
|
-
isPartial: true
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const finalObject = await result.object;
|
|
305
|
-
yield {
|
|
306
|
-
type: 'suggested-questions',
|
|
307
|
-
suggestions: finalObject.suggestions,
|
|
308
|
-
isPartial: false
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
console.error('[Runtime] Failed to generate suggestions:', error);
|
|
313
|
-
// Don't yield error to user, just skip suggestions
|
|
314
|
-
}
|
|
353
|
+
yield* this.suggestionManager.generateSuggestions(context);
|
|
315
354
|
}
|
|
316
355
|
async *chat(sessionId, input, userId) {
|
|
317
356
|
yield* this.stream({ input, sessionId, userId });
|
|
318
357
|
}
|
|
319
358
|
async *runLoop(context, injectionQueue, input, abortController) {
|
|
320
359
|
let currentInput = input;
|
|
360
|
+
const abortSignal = abortController?.signal;
|
|
361
|
+
let interruptionEmitted = false;
|
|
362
|
+
let circularRecoveryActive = false;
|
|
363
|
+
let circularRecoveryAttempts = 0;
|
|
321
364
|
while (context.handoffStack.length < this.maxHandoffs) {
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
365
|
+
if (abortSignal?.aborted) {
|
|
366
|
+
if (!interruptionEmitted) {
|
|
367
|
+
interruptionEmitted = true;
|
|
368
|
+
yield* this.emit(context, {
|
|
369
|
+
type: 'interrupted',
|
|
370
|
+
sessionId: context.session.id,
|
|
371
|
+
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
372
|
+
timestamp: new Date(),
|
|
373
|
+
lastAgentId: context.agentId,
|
|
374
|
+
lastStep: context.stepCount,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
331
377
|
return;
|
|
332
378
|
}
|
|
333
379
|
// Allow a single "bounce back" (A -> B -> A) for detours and multi-intent turns.
|
|
334
380
|
// Stop only when an agent would be visited a third time in the same user turn.
|
|
335
381
|
const priorVisits = context.handoffStack.filter(id => id === context.agentId).length;
|
|
336
|
-
if (priorVisits >= 2) {
|
|
382
|
+
if (priorVisits >= 2 && !circularRecoveryActive) {
|
|
337
383
|
const err = `Circular handoff detected: ${context.handoffStack.join(' -> ')} -> ${context.agentId}`;
|
|
338
384
|
yield* this.emit(context, { type: 'error', error: err });
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
385
|
+
circularRecoveryAttempts += 1;
|
|
386
|
+
if (circularRecoveryAttempts > 1) {
|
|
387
|
+
yield* this.emitCircularHandoffFallback(context);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
circularRecoveryActive = true;
|
|
391
|
+
// Reset path tracking for this turn and force a single in-agent recovery pass.
|
|
392
|
+
context.handoffStack = [];
|
|
344
393
|
}
|
|
345
394
|
context.handoffStack.push(context.agentId);
|
|
346
395
|
const agent = this.agents.get(context.agentId);
|
|
@@ -355,6 +404,7 @@ export class Runtime {
|
|
|
355
404
|
session: context.session,
|
|
356
405
|
agentId: agent.id,
|
|
357
406
|
toolCallHistory: context.toolCallHistory,
|
|
407
|
+
abortSignal,
|
|
358
408
|
};
|
|
359
409
|
const outcome = await runInputProcessors({
|
|
360
410
|
processors: agent.inputProcessors,
|
|
@@ -379,6 +429,7 @@ export class Runtime {
|
|
|
379
429
|
const last = context.session.messages[context.session.messages.length - 1];
|
|
380
430
|
if (last && last.role === 'user' && typeof last.content === 'string') {
|
|
381
431
|
last.content = currentInput;
|
|
432
|
+
this.touchSession(context.session);
|
|
382
433
|
}
|
|
383
434
|
}
|
|
384
435
|
}
|
|
@@ -388,10 +439,12 @@ export class Runtime {
|
|
|
388
439
|
if (agent.autoRetrieve) {
|
|
389
440
|
const toolName = agent.autoRetrieve.toolName ?? 'auto_retrieve';
|
|
390
441
|
const toolCallId = crypto.randomUUID();
|
|
442
|
+
const idempotencyKey = this.buildToolIdempotencyKey(context, toolName, toolCallId);
|
|
391
443
|
const callRecord = {
|
|
392
444
|
toolCallId,
|
|
393
445
|
toolName,
|
|
394
446
|
args: { input: currentInput },
|
|
447
|
+
idempotencyKey,
|
|
395
448
|
success: true,
|
|
396
449
|
timestamp: Date.now(),
|
|
397
450
|
};
|
|
@@ -410,7 +463,7 @@ export class Runtime {
|
|
|
410
463
|
});
|
|
411
464
|
let result = null;
|
|
412
465
|
try {
|
|
413
|
-
result = await agent.autoRetrieve.run({ input: currentInput, context });
|
|
466
|
+
result = await agent.autoRetrieve.run({ input: currentInput, context, abortSignal });
|
|
414
467
|
callRecord.result = result;
|
|
415
468
|
}
|
|
416
469
|
catch (error) {
|
|
@@ -449,8 +502,36 @@ export class Runtime {
|
|
|
449
502
|
};
|
|
450
503
|
}
|
|
451
504
|
}
|
|
452
|
-
const
|
|
453
|
-
|
|
505
|
+
const extractionSnapshot = await this.runTurnExtraction(agent, context, currentInput, abortSignal);
|
|
506
|
+
let activeRoutes;
|
|
507
|
+
if (this.isTriageAgent(agent)) {
|
|
508
|
+
const agentContext = this.buildAgentContext(currentInput, context, abortSignal);
|
|
509
|
+
const results = await Promise.all(agent.routes.map(async (route) => {
|
|
510
|
+
if (!route.condition)
|
|
511
|
+
return { route, active: true };
|
|
512
|
+
try {
|
|
513
|
+
const active = await route.condition(currentInput, agentContext);
|
|
514
|
+
return { route, active };
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.error(`Error evaluating condition for route ${route.agentId}:`, error);
|
|
518
|
+
return { route, active: false }; // Fail-closed on error
|
|
519
|
+
}
|
|
520
|
+
}));
|
|
521
|
+
activeRoutes = results.filter(r => r.active).map(r => r.route);
|
|
522
|
+
}
|
|
523
|
+
let system = this.buildSystemPrompt(agent, injectionQueue, autoContext, context, activeRoutes);
|
|
524
|
+
const turnToolErrors = [];
|
|
525
|
+
if (extractionSnapshot?.includeInSystemPrompt) {
|
|
526
|
+
system += this.extractionEngine.buildExtractionSystemBlock(extractionSnapshot);
|
|
527
|
+
}
|
|
528
|
+
if (circularRecoveryActive) {
|
|
529
|
+
system = `${system}\n\n## Routing Recovery
|
|
530
|
+
A circular handoff was detected in this turn.
|
|
531
|
+
Stay with the current agent and provide the best direct answer.
|
|
532
|
+
Do not call or suggest handoff/transfer tools in this response.`;
|
|
533
|
+
}
|
|
534
|
+
const handoffCandidates = circularRecoveryActive ? [] : this.getHandoffCandidates(agent, activeRoutes);
|
|
454
535
|
const handoffTool = handoffCandidates.length > 0
|
|
455
536
|
? createHandoffTool(handoffCandidates, context.agentId)
|
|
456
537
|
: null;
|
|
@@ -475,7 +556,7 @@ export class Runtime {
|
|
|
475
556
|
for await (const part of this.runFlowAgent(agent, context, currentInput, system, handoffTool ?? undefined, (target, reason) => {
|
|
476
557
|
handoffTo = target;
|
|
477
558
|
handoffReason = reason ?? 'No reason provided';
|
|
478
|
-
}, toolCalls)) {
|
|
559
|
+
}, toolCalls, abortSignal)) {
|
|
479
560
|
yield part;
|
|
480
561
|
}
|
|
481
562
|
context.consecutiveErrors = 0;
|
|
@@ -491,6 +572,20 @@ export class Runtime {
|
|
|
491
572
|
});
|
|
492
573
|
}
|
|
493
574
|
catch (error) {
|
|
575
|
+
if (abortSignal?.aborted) {
|
|
576
|
+
if (!interruptionEmitted) {
|
|
577
|
+
interruptionEmitted = true;
|
|
578
|
+
yield* this.emit(context, {
|
|
579
|
+
type: 'interrupted',
|
|
580
|
+
sessionId: context.session.id,
|
|
581
|
+
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
582
|
+
timestamp: new Date(),
|
|
583
|
+
lastAgentId: context.agentId,
|
|
584
|
+
lastStep: context.stepCount,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
494
589
|
context.consecutiveErrors += 1;
|
|
495
590
|
await this.hookRunner.onError(context, error);
|
|
496
591
|
yield* this.emit(context, { type: 'error', error: error.message });
|
|
@@ -525,21 +620,27 @@ export class Runtime {
|
|
|
525
620
|
confidence: z.number().min(0).max(1).describe('Routing confidence from 0 to 1.'),
|
|
526
621
|
stayWithCurrent: z.boolean().describe('True only if the current agent is best fit.'),
|
|
527
622
|
});
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
target =
|
|
623
|
+
try {
|
|
624
|
+
// AI SDK v6+ recommended structured output approach.
|
|
625
|
+
const { output: decision } = await generateText({
|
|
626
|
+
model: model,
|
|
627
|
+
output: Output.object({ schema }),
|
|
628
|
+
system: this.buildStructuredTriagePrompt(agent, activeRoutes),
|
|
629
|
+
messages: context.session.messages,
|
|
630
|
+
abortSignal,
|
|
631
|
+
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
632
|
+
});
|
|
633
|
+
const allowed = new Set(agent.routes.map(route => route.agentId));
|
|
634
|
+
let target = decision.agentId;
|
|
635
|
+
if (!allowed.has(target)) {
|
|
636
|
+
target = agent.defaultAgent ?? agent.routes[0]?.agentId ?? agent.id;
|
|
637
|
+
}
|
|
638
|
+
handoffTo = target;
|
|
639
|
+
handoffReason = decision.reason ?? 'Routed by triage';
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
throw err;
|
|
540
643
|
}
|
|
541
|
-
handoffTo = target;
|
|
542
|
-
handoffReason = decision.reason ?? 'Routed by triage';
|
|
543
644
|
yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
544
645
|
await this.hookRunner.onStepEnd(context, context.stepCount, {
|
|
545
646
|
toolCalls: [],
|
|
@@ -554,6 +655,7 @@ export class Runtime {
|
|
|
554
655
|
system,
|
|
555
656
|
messages: context.session.messages,
|
|
556
657
|
tools,
|
|
658
|
+
abortSignal,
|
|
557
659
|
// Let the AI SDK handle tool-calling steps internally.
|
|
558
660
|
// Default is stepCountIs(1), which ends right after tool-calls.
|
|
559
661
|
stopWhen: stepCountIs(agent.toolMaxSteps ?? 5),
|
|
@@ -576,6 +678,10 @@ export class Runtime {
|
|
|
576
678
|
if (finalResult) {
|
|
577
679
|
continue;
|
|
578
680
|
}
|
|
681
|
+
// Fail-closed: Suppress text delta if critical tool errors have occurred in this turn.
|
|
682
|
+
if (turnToolErrors.length > 0) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
579
685
|
if (bufferOutput) {
|
|
580
686
|
bufferedText += chunk.text;
|
|
581
687
|
}
|
|
@@ -584,11 +690,12 @@ export class Runtime {
|
|
|
584
690
|
}
|
|
585
691
|
}
|
|
586
692
|
if (chunk.type === 'tool-call') {
|
|
587
|
-
const args =
|
|
693
|
+
const args = getChunkArgs(chunk);
|
|
588
694
|
const callRecord = {
|
|
589
695
|
toolCallId: chunk.toolCallId,
|
|
590
696
|
toolName: chunk.toolName,
|
|
591
697
|
args,
|
|
698
|
+
idempotencyKey: this.buildToolIdempotencyKey(context, chunk.toolName, chunk.toolCallId),
|
|
592
699
|
success: true,
|
|
593
700
|
timestamp: Date.now(),
|
|
594
701
|
};
|
|
@@ -612,11 +719,10 @@ export class Runtime {
|
|
|
612
719
|
});
|
|
613
720
|
}
|
|
614
721
|
if (chunk.type === 'tool-error') {
|
|
615
|
-
const errText =
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
const callRecord = toolCalls.find(call => call.toolCallId === chunk.toolCallId);
|
|
722
|
+
const errText = getChunkErrorMessage(chunk);
|
|
723
|
+
const args = getChunkArgs(chunk);
|
|
724
|
+
const toolCallId = getChunkToolCallId(chunk);
|
|
725
|
+
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
620
726
|
if (callRecord) {
|
|
621
727
|
callRecord.success = false;
|
|
622
728
|
callRecord.error = new Error(errText);
|
|
@@ -624,17 +730,24 @@ export class Runtime {
|
|
|
624
730
|
callRecord.args = args;
|
|
625
731
|
context.toolCallHistory.push(callRecord);
|
|
626
732
|
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
733
|
+
// Fail-closed: Track critical tool errors to block turn finalization.
|
|
734
|
+
const toolPolicy = agent.toolPolicies?.[callRecord.toolName];
|
|
735
|
+
const toolDef = agent.tools?.[callRecord.toolName];
|
|
736
|
+
const isCritical = toolPolicy?.critical ?? toolDef?.critical ?? true;
|
|
737
|
+
if (isCritical) {
|
|
738
|
+
turnToolErrors.push(callRecord);
|
|
739
|
+
}
|
|
627
740
|
}
|
|
628
741
|
yield* this.emit(context, {
|
|
629
742
|
type: 'tool-error',
|
|
630
|
-
toolCallId: chunk.toolCallId,
|
|
743
|
+
toolCallId: toolCallId ?? chunk.toolCallId,
|
|
631
744
|
toolName: chunk.toolName,
|
|
632
745
|
error: errText,
|
|
633
746
|
});
|
|
634
747
|
}
|
|
635
748
|
if (chunk.type === 'tool-result') {
|
|
636
749
|
const startTime = toolCalls.find(call => call.toolCallId === chunk.toolCallId)?.timestamp ?? Date.now();
|
|
637
|
-
const toolResult =
|
|
750
|
+
const toolResult = getChunkResult(chunk);
|
|
638
751
|
const callRecord = toolCalls.find(call => call.toolCallId === chunk.toolCallId);
|
|
639
752
|
if (callRecord) {
|
|
640
753
|
callRecord.result = toolResult;
|
|
@@ -661,6 +774,8 @@ export class Runtime {
|
|
|
661
774
|
callRecord.success = false;
|
|
662
775
|
callRecord.error = new Error(reason);
|
|
663
776
|
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
777
|
+
// Fail-closed: Track enforcement failures as critical errors.
|
|
778
|
+
turnToolErrors.push(callRecord);
|
|
664
779
|
yield* this.emit(context, {
|
|
665
780
|
type: 'tool-error',
|
|
666
781
|
toolCallId: chunk.toolCallId,
|
|
@@ -722,8 +837,17 @@ export class Runtime {
|
|
|
722
837
|
// If the model ended on tool-calls, we must persist the tool messages and
|
|
723
838
|
// continue the loop (or let AI SDK continue in maxSteps) instead of emitting
|
|
724
839
|
// synthetic assistant text, which can duplicate responses.
|
|
725
|
-
if (finalResult || (bufferOutput && finishReason !== 'tool-calls')) {
|
|
726
|
-
|
|
840
|
+
if (finalResult || (bufferOutput && finishReason !== 'tool-calls') || turnToolErrors.length > 0) {
|
|
841
|
+
let rawText = finalResult ? finalResult.text : bufferedText;
|
|
842
|
+
// Fail-closed: Suppress success message if critical tool failed.
|
|
843
|
+
if (turnToolErrors.length > 0) {
|
|
844
|
+
const failedTools = turnToolErrors.map(e => `\`${e.toolName}\``).join(', ');
|
|
845
|
+
rawText = `I encountered an error while using the following tools: ${failedTools}. I cannot proceed with the requested action at this time.`;
|
|
846
|
+
yield* this.emit(context, {
|
|
847
|
+
type: 'error',
|
|
848
|
+
error: `Turn blocked by critical tool failures: ${failedTools}`,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
727
851
|
const processed = await this.runOutputProcessing(agent, context, rawText);
|
|
728
852
|
if (processed.tripwire) {
|
|
729
853
|
yield* this.emit(context, {
|
|
@@ -734,14 +858,14 @@ export class Runtime {
|
|
|
734
858
|
message: processed.tripwire.message,
|
|
735
859
|
});
|
|
736
860
|
}
|
|
737
|
-
if (bufferOutput) {
|
|
861
|
+
if (bufferOutput || turnToolErrors.length > 0) {
|
|
738
862
|
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
739
863
|
}
|
|
740
|
-
context.session
|
|
864
|
+
this.appendSessionMessage(context.session, { role: 'assistant', content: processed.text });
|
|
741
865
|
}
|
|
742
866
|
else {
|
|
743
867
|
const beforeLen = context.session.messages.length;
|
|
744
|
-
context.session
|
|
868
|
+
this.appendSessionMessages(context.session, response.messages);
|
|
745
869
|
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, beforeLen);
|
|
746
870
|
for (const t of tripwires) {
|
|
747
871
|
yield* this.emit(context, {
|
|
@@ -773,6 +897,21 @@ export class Runtime {
|
|
|
773
897
|
}
|
|
774
898
|
}
|
|
775
899
|
catch (error) {
|
|
900
|
+
console.error('RUNTIME_LOOP_ERROR:', error);
|
|
901
|
+
if (abortSignal?.aborted) {
|
|
902
|
+
if (!interruptionEmitted) {
|
|
903
|
+
interruptionEmitted = true;
|
|
904
|
+
yield* this.emit(context, {
|
|
905
|
+
type: 'interrupted',
|
|
906
|
+
sessionId: context.session.id,
|
|
907
|
+
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
908
|
+
timestamp: new Date(),
|
|
909
|
+
lastAgentId: context.agentId,
|
|
910
|
+
lastStep: context.stepCount,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
776
915
|
context.consecutiveErrors += 1;
|
|
777
916
|
await this.hookRunner.onError(context, error);
|
|
778
917
|
yield* this.emit(context, { type: 'error', error: error.message });
|
|
@@ -784,6 +923,17 @@ export class Runtime {
|
|
|
784
923
|
}
|
|
785
924
|
yield* this.emit(context, { type: 'agent-end', agentId: agent.id });
|
|
786
925
|
await this.hookRunner.onAgentEnd(context, agent.id);
|
|
926
|
+
if (circularRecoveryActive && handoffTo) {
|
|
927
|
+
yield* this.emit(context, {
|
|
928
|
+
type: 'error',
|
|
929
|
+
error: `Circular handoff recovery failed: attempted handoff ${context.agentId} -> ${handoffTo}`,
|
|
930
|
+
});
|
|
931
|
+
yield* this.emitCircularHandoffFallback(context);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (circularRecoveryActive) {
|
|
935
|
+
circularRecoveryActive = false;
|
|
936
|
+
}
|
|
787
937
|
if (!handoffTo) {
|
|
788
938
|
break;
|
|
789
939
|
}
|
|
@@ -810,12 +960,34 @@ export class Runtime {
|
|
|
810
960
|
}
|
|
811
961
|
context.session.activeAgentId = handoffTo;
|
|
812
962
|
context.session.currentAgent = handoffTo;
|
|
963
|
+
await this.saveSessionCheckpoint(context.session);
|
|
813
964
|
context.agentId = handoffTo;
|
|
814
965
|
}
|
|
815
966
|
if (context.handoffStack.length >= this.maxHandoffs) {
|
|
816
967
|
yield* this.emit(context, { type: 'error', error: `Maximum handoffs (${this.maxHandoffs}) exceeded` });
|
|
817
968
|
}
|
|
818
969
|
}
|
|
970
|
+
getCircularHandoffFallbackMessage() {
|
|
971
|
+
const configured = this.config.circularHandoffFallbackMessage?.trim();
|
|
972
|
+
if (configured) {
|
|
973
|
+
return configured;
|
|
974
|
+
}
|
|
975
|
+
return 'I hit a routing issue between agents. I will continue here. Tell me the exact outcome you need in one sentence.';
|
|
976
|
+
}
|
|
977
|
+
async *emitCircularHandoffFallback(context) {
|
|
978
|
+
const fallback = this.getCircularHandoffFallbackMessage();
|
|
979
|
+
this.appendSessionMessage(context.session, { role: 'assistant', content: fallback });
|
|
980
|
+
yield* this.emit(context, { type: 'text-delta', text: fallback });
|
|
981
|
+
}
|
|
982
|
+
buildPolicyInjectionQueue() {
|
|
983
|
+
const queue = new InjectionQueue();
|
|
984
|
+
const profile = this.config.policyProfile ?? 'minimal';
|
|
985
|
+
queue.addBatch(getPolicyProfileInjections(profile));
|
|
986
|
+
if (this.config.policyInjections && this.config.policyInjections.length > 0) {
|
|
987
|
+
queue.addBatch(this.config.policyInjections);
|
|
988
|
+
}
|
|
989
|
+
return queue;
|
|
990
|
+
}
|
|
819
991
|
createSession(id, userId) {
|
|
820
992
|
const now = new Date();
|
|
821
993
|
return {
|
|
@@ -845,26 +1017,47 @@ export class Runtime {
|
|
|
845
1017
|
isTriageAgent(agent) {
|
|
846
1018
|
return agent.type === 'triage';
|
|
847
1019
|
}
|
|
848
|
-
|
|
1020
|
+
buildPromptMemoryView(session, agent) {
|
|
1021
|
+
const memory = session.workingMemory ?? {};
|
|
1022
|
+
const allowlist = agent.promptMemoryAllowlist ?? this.config.promptMemoryAllowlist;
|
|
1023
|
+
const internalKeys = [
|
|
1024
|
+
this.runtimeEventLogKey,
|
|
1025
|
+
this.runtimeSessionTurnKey,
|
|
1026
|
+
this.redactCarryKey,
|
|
1027
|
+
];
|
|
1028
|
+
const filtered = {};
|
|
1029
|
+
for (const [key, value] of Object.entries(memory)) {
|
|
1030
|
+
if (internalKeys.includes(key)) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (allowlist && !allowlist.includes(key)) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
filtered[key] = value;
|
|
1037
|
+
}
|
|
1038
|
+
return filtered;
|
|
1039
|
+
}
|
|
1040
|
+
buildSystemPrompt(agent, injectionQueue, autoContext, context, activeRoutes) {
|
|
849
1041
|
const basePrompt = this.isTriageAgent(agent)
|
|
850
|
-
? this.buildTriagePrompt(agent)
|
|
1042
|
+
? this.buildTriagePrompt(agent, activeRoutes)
|
|
851
1043
|
: agent.systemPrompt;
|
|
852
1044
|
const autoBlock = autoContext?.text?.trim()
|
|
853
1045
|
? `\n\n## ${autoContext.label}\n${autoContext.text}`
|
|
854
1046
|
: '';
|
|
855
1047
|
const systemInjections = injectionQueue.getFor('system');
|
|
856
|
-
const memory = context?.session
|
|
1048
|
+
const memory = context?.session ? this.buildPromptMemoryView(context.session, agent) : {};
|
|
857
1049
|
const memoryBlock = Object.keys(memory).length > 0
|
|
858
1050
|
? `\n\n## Known Information\n${JSON.stringify(memory, null, 2)}`
|
|
859
1051
|
: '';
|
|
860
1052
|
const merged = `${basePrompt}${autoBlock}${memoryBlock}`;
|
|
861
1053
|
return systemInjections ? `${merged}\n\n${systemInjections}` : merged;
|
|
862
1054
|
}
|
|
863
|
-
buildStructuredTriagePrompt(agent) {
|
|
864
|
-
const
|
|
1055
|
+
buildStructuredTriagePrompt(agent, activeRoutes) {
|
|
1056
|
+
const routes = activeRoutes ?? agent.routes;
|
|
1057
|
+
const routeDescriptions = routes
|
|
865
1058
|
.map(route => `- ${route.agentId}: ${route.description}`)
|
|
866
1059
|
.join('\n');
|
|
867
|
-
const allowed =
|
|
1060
|
+
const allowed = routes.map(route => route.agentId);
|
|
868
1061
|
const defaultNote = agent.defaultAgent ? `Default: ${agent.defaultAgent}` : 'Default: none';
|
|
869
1062
|
return `${agent.systemPrompt}
|
|
870
1063
|
|
|
@@ -879,8 +1072,9 @@ Return a JSON object with:
|
|
|
879
1072
|
- stayWithCurrent: boolean (true only if current agent is best fit)
|
|
880
1073
|
${defaultNote}`;
|
|
881
1074
|
}
|
|
882
|
-
buildTriagePrompt(agent) {
|
|
883
|
-
const
|
|
1075
|
+
buildTriagePrompt(agent, activeRoutes) {
|
|
1076
|
+
const routes = activeRoutes ?? agent.routes;
|
|
1077
|
+
const routeDescriptions = routes
|
|
884
1078
|
.map(route => `- **${route.agentId}**: ${route.description}`)
|
|
885
1079
|
.join('\n');
|
|
886
1080
|
const defaultNote = agent.defaultAgent
|
|
@@ -896,9 +1090,10 @@ ${routeDescriptions}
|
|
|
896
1090
|
- When the customer needs specialized help, use the handoff tool
|
|
897
1091
|
- Always provide a brief reason for the handoff${defaultNote}`;
|
|
898
1092
|
}
|
|
899
|
-
getHandoffCandidates(agent) {
|
|
1093
|
+
getHandoffCandidates(agent, activeRoutes) {
|
|
900
1094
|
if (this.isTriageAgent(agent)) {
|
|
901
|
-
|
|
1095
|
+
const routes = activeRoutes ?? agent.routes;
|
|
1096
|
+
return routes
|
|
902
1097
|
.map(route => this.agents.get(route.agentId))
|
|
903
1098
|
.filter((candidate) => Boolean(candidate));
|
|
904
1099
|
}
|
|
@@ -918,15 +1113,45 @@ ${routeDescriptions}
|
|
|
918
1113
|
}
|
|
919
1114
|
return stored;
|
|
920
1115
|
}
|
|
1116
|
+
buildAgentContext(input, context, abortSignal) {
|
|
1117
|
+
return {
|
|
1118
|
+
session: context.session,
|
|
1119
|
+
messages: context.session.messages,
|
|
1120
|
+
workingMemory: new SessionWorkingMemory(context.session),
|
|
1121
|
+
currentAgent: context.agentId,
|
|
1122
|
+
turnCount: this.turnCount,
|
|
1123
|
+
metadata: context.session.metadata,
|
|
1124
|
+
abortSignal,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
921
1127
|
setFlowState(session, agentId, state) {
|
|
922
1128
|
session.agentStates[agentId] = {
|
|
923
1129
|
agentId,
|
|
924
1130
|
state: state,
|
|
925
1131
|
lastActive: new Date(),
|
|
926
1132
|
};
|
|
1133
|
+
this.updateFlowStateSnapshot(session, agentId, state);
|
|
1134
|
+
this.touchSession(session);
|
|
927
1135
|
}
|
|
928
1136
|
clearFlowState(session, agentId) {
|
|
929
1137
|
delete session.agentStates[agentId];
|
|
1138
|
+
this.touchSession(session);
|
|
1139
|
+
}
|
|
1140
|
+
updateFlowStateSnapshot(session, agentId, state) {
|
|
1141
|
+
const existing = session.workingMemory.flowStateByAgent;
|
|
1142
|
+
const byAgent = typeof existing === 'object' && existing !== null
|
|
1143
|
+
? { ...existing }
|
|
1144
|
+
: {};
|
|
1145
|
+
byAgent[agentId] = {
|
|
1146
|
+
currentNode: state.context.currentNode,
|
|
1147
|
+
collectedData: state.context.collectedData,
|
|
1148
|
+
nodeHistory: state.context.nodeHistory,
|
|
1149
|
+
initialized: state.initialized,
|
|
1150
|
+
flowEnded: state.flowEnded,
|
|
1151
|
+
updatedAt: new Date().toISOString(),
|
|
1152
|
+
};
|
|
1153
|
+
session.workingMemory.flowStateByAgent = byAgent;
|
|
1154
|
+
this.touchSession(session);
|
|
930
1155
|
}
|
|
931
1156
|
buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond) {
|
|
932
1157
|
// If we don't need to inject a handoff tool and we don't need to suppress the initial autoRespond,
|
|
@@ -976,64 +1201,74 @@ ${routeDescriptions}
|
|
|
976
1201
|
}
|
|
977
1202
|
return agent.flow.nodes.find(node => node.id === nodeId);
|
|
978
1203
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const node = this.getFlowNode(agent, nodeId);
|
|
988
|
-
if (!node) {
|
|
989
|
-
return true;
|
|
990
|
-
}
|
|
991
|
-
const rules = agent.detourRules;
|
|
992
|
-
if (rules) {
|
|
993
|
-
const normalized = input.trim().toLowerCase();
|
|
994
|
-
if (rules.allowShortAffirmations !== false &&
|
|
995
|
-
/^(yes|yep|yeah|ok|okay|sure|great|thanks|thank you|please|proceed|go ahead)\b/.test(normalized)) {
|
|
996
|
-
return true;
|
|
997
|
-
}
|
|
998
|
-
if (rules.allowDateTime !== false &&
|
|
999
|
-
(/\b20\d{2}-\d{2}-\d{2}\b/.test(normalized) || /\b\d{1,2}:\d{2}\b/.test(normalized))) {
|
|
1000
|
-
return true;
|
|
1204
|
+
getExtractionConfig(agent, session) {
|
|
1205
|
+
if (this.isFlowAgent(agent)) {
|
|
1206
|
+
const flowState = this.getFlowState(session, agent.id);
|
|
1207
|
+
const nodeId = flowState?.context.currentNode ?? agent.initialNode;
|
|
1208
|
+
const node = this.getFlowNode(agent, nodeId);
|
|
1209
|
+
const config = node?.extraction ?? agent.extraction;
|
|
1210
|
+
if (!config) {
|
|
1211
|
+
return null;
|
|
1001
1212
|
}
|
|
1002
|
-
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
if (this.matchesDetourRule(normalized, rules.allow)) {
|
|
1006
|
-
return true;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
const collectedData = flowState?.context.collectedData ?? {};
|
|
1010
|
-
const renderedNodePrompt = renderFlowTemplate(node.prompt, collectedData, { missing: 'keep' });
|
|
1011
|
-
const routerPrompt = `You are routing a message for a structured conversation flow.
|
|
1012
|
-
Decide if the user input is answering the current flow step or is a side question.
|
|
1013
|
-
|
|
1014
|
-
Current flow step:
|
|
1015
|
-
${renderedNodePrompt}
|
|
1016
|
-
|
|
1017
|
-
Collected data:
|
|
1018
|
-
${JSON.stringify(collectedData, null, 2)}
|
|
1019
|
-
|
|
1020
|
-
Return only one word: "flow" if the input should be handled by the flow step, or "detour" if it should be answered outside the flow then resume.`;
|
|
1021
|
-
const result = await generateText({
|
|
1022
|
-
model: (agent.model ?? this.defaultModel),
|
|
1023
|
-
system: routerPrompt,
|
|
1024
|
-
prompt: input,
|
|
1025
|
-
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
1026
|
-
});
|
|
1027
|
-
const decision = result.text.trim().toLowerCase();
|
|
1028
|
-
if (decision.startsWith('detour')) {
|
|
1029
|
-
return false;
|
|
1213
|
+
return { config, nodeId };
|
|
1030
1214
|
}
|
|
1031
|
-
if (
|
|
1032
|
-
return
|
|
1215
|
+
if (!agent.extraction) {
|
|
1216
|
+
return null;
|
|
1033
1217
|
}
|
|
1034
|
-
return
|
|
1218
|
+
return { config: agent.extraction };
|
|
1219
|
+
}
|
|
1220
|
+
async runTurnExtraction(agent, context, input, abortSignal) {
|
|
1221
|
+
return this.extractionEngine.runTurnExtraction(agent, context, input, {
|
|
1222
|
+
getFlowState: (s, id) => this.getFlowState(s, id),
|
|
1223
|
+
setFlowState: (s, id, st) => this.setFlowState(s, id, st),
|
|
1224
|
+
getFlowNode: (a, nid) => this.getFlowNode(a, nid),
|
|
1225
|
+
isFlowAgent: (a) => this.isFlowAgent(a),
|
|
1226
|
+
touchSession: (s) => this.touchSession(s),
|
|
1227
|
+
}, abortSignal);
|
|
1228
|
+
}
|
|
1229
|
+
async shouldHandleFlowInput(agent, input, flowState, abortSignal) {
|
|
1230
|
+
return this.flowExecutor.shouldHandleFlowInput(agent, input, flowState, this.getFlowExecutorHelpers(), abortSignal);
|
|
1231
|
+
}
|
|
1232
|
+
async *runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff, abortSignal) {
|
|
1233
|
+
yield* this.flowExecutor.runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff, this.getFlowExecutorHelpers(), abortSignal);
|
|
1234
|
+
}
|
|
1235
|
+
async *runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls, abortSignal) {
|
|
1236
|
+
yield* this.flowExecutor.runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls, this.getFlowExecutorHelpers(), abortSignal);
|
|
1035
1237
|
}
|
|
1036
|
-
|
|
1238
|
+
getFlowExecutorHelpers() {
|
|
1239
|
+
return {
|
|
1240
|
+
emit: (ctx, part) => this.emit(ctx, part),
|
|
1241
|
+
runOutputProcessing: (agent, ctx, text) => this.runOutputProcessing(agent, ctx, text),
|
|
1242
|
+
appendSessionMessage: (s, m) => this.appendSessionMessage(s, m),
|
|
1243
|
+
appendSessionMessages: (s, ms) => this.appendSessionMessages(s, ms),
|
|
1244
|
+
postProcessPersistedAssistantMessages: (agent, ctx, start) => this.postProcessPersistedAssistantMessages(agent, ctx, start),
|
|
1245
|
+
getAgentOutputProcessors: (agent) => this.getAgentOutputProcessors(agent),
|
|
1246
|
+
getSessionTurn: (s) => this.getSessionTurn(s),
|
|
1247
|
+
buildToolIdempotencyKey: (ctx, tn, tcid) => this.buildToolIdempotencyKey(ctx, tn, tcid),
|
|
1248
|
+
setFlowState: (s, id, st) => this.setFlowState(s, id, st),
|
|
1249
|
+
getFlowState: (s, id) => this.getFlowState(s, id),
|
|
1250
|
+
updateFlowStateSnapshot: (s, id, st) => this.updateFlowStateSnapshot(s, id, st),
|
|
1251
|
+
clearFlowState: (s, id) => this.clearFlowState(s, id),
|
|
1252
|
+
buildFlowWithHandoff: (a, ht, s) => this.buildFlowWithHandoff(a, ht, s),
|
|
1253
|
+
getFlowNode: (a, ni) => this.getFlowNode(a, ni),
|
|
1254
|
+
touchSession: (s) => this.touchSession(s),
|
|
1255
|
+
matchesDetourRule: (i, p) => this.matchesDetourRuleHelper(i, p),
|
|
1256
|
+
enforcecheck: (call, ctx) => this.enforcer.check(call, {
|
|
1257
|
+
previousCalls: ctx.toolCallHistory,
|
|
1258
|
+
currentStep: ctx.stepCount,
|
|
1259
|
+
sessionState: ctx.session.state ?? {},
|
|
1260
|
+
}),
|
|
1261
|
+
enforcecheckResult: (call, ctx) => this.enforcer.checkResult(call, {
|
|
1262
|
+
previousCalls: ctx.toolCallHistory,
|
|
1263
|
+
currentStep: ctx.stepCount,
|
|
1264
|
+
sessionState: ctx.session.state ?? {},
|
|
1265
|
+
}),
|
|
1266
|
+
onToolCallHook: (ctx, call) => this.hookRunner.onToolCall(ctx, call),
|
|
1267
|
+
onToolResultHook: (ctx, call) => this.hookRunner.onToolResult(ctx, call),
|
|
1268
|
+
onToolErrorHook: (ctx, call, error) => this.hookRunner.onToolError(ctx, call, error),
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
matchesDetourRuleHelper(input, patterns) {
|
|
1037
1272
|
if (!patterns || patterns.length === 0) {
|
|
1038
1273
|
return false;
|
|
1039
1274
|
}
|
|
@@ -1052,426 +1287,6 @@ Return only one word: "flow" if the input should be handled by the flow step, or
|
|
|
1052
1287
|
}
|
|
1053
1288
|
return false;
|
|
1054
1289
|
}
|
|
1055
|
-
async *runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff) {
|
|
1056
|
-
const nodeId = flowState?.context.currentNode ?? agent.initialNode;
|
|
1057
|
-
const node = this.getFlowNode(agent, nodeId);
|
|
1058
|
-
const collectedData = flowState?.context.collectedData ?? {};
|
|
1059
|
-
const nodePrompt = node
|
|
1060
|
-
? renderFlowTemplate(node.prompt, collectedData, { missing: 'keep' })
|
|
1061
|
-
: 'Continue the flow.';
|
|
1062
|
-
const handoffLine = handoffTool
|
|
1063
|
-
? 'If the request should be handled by a specialist agent, use the handoff tool instead of answering.'
|
|
1064
|
-
: 'Do not attempt to route to other agents. Handle the detour here and then resume the flow.';
|
|
1065
|
-
const detourPrompt = `You are handling a short detour during a structured flow.
|
|
1066
|
-
Answer the user's question clearly and briefly. ${handoffLine} Then ask the user to continue with the current flow step.
|
|
1067
|
-
|
|
1068
|
-
Current flow step:
|
|
1069
|
-
${nodePrompt}
|
|
1070
|
-
|
|
1071
|
-
Collected data:
|
|
1072
|
-
${JSON.stringify(collectedData, null, 2)}
|
|
1073
|
-
|
|
1074
|
-
Do not change the flow requirements. Keep the reply concise.`;
|
|
1075
|
-
const result = streamText({
|
|
1076
|
-
model: (agent.model ?? this.defaultModel),
|
|
1077
|
-
system: detourPrompt,
|
|
1078
|
-
messages: context.session.messages,
|
|
1079
|
-
tools: handoffTool ? { handoff: handoffTool } : undefined,
|
|
1080
|
-
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
1081
|
-
});
|
|
1082
|
-
let handoffTriggered = false;
|
|
1083
|
-
const outputProcessors = this.getAgentOutputProcessors(agent);
|
|
1084
|
-
const bufferOutput = this.outputProcessorMode === 'buffer' && outputProcessors.length > 0;
|
|
1085
|
-
let bufferedText = '';
|
|
1086
|
-
for await (const chunk of result.fullStream) {
|
|
1087
|
-
if (chunk.type === 'text-delta') {
|
|
1088
|
-
if (handoffTriggered) {
|
|
1089
|
-
continue;
|
|
1090
|
-
}
|
|
1091
|
-
if (bufferOutput) {
|
|
1092
|
-
bufferedText += chunk.text;
|
|
1093
|
-
}
|
|
1094
|
-
else {
|
|
1095
|
-
yield* this.emit(context, { type: 'text-delta', text: chunk.text });
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (chunk.type === 'tool-call') {
|
|
1099
|
-
const args = 'args' in chunk ? chunk.args : chunk.input;
|
|
1100
|
-
yield* this.emit(context, {
|
|
1101
|
-
type: 'tool-call',
|
|
1102
|
-
toolCallId: chunk.toolCallId,
|
|
1103
|
-
toolName: chunk.toolName,
|
|
1104
|
-
args,
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
if (chunk.type === 'tool-result') {
|
|
1108
|
-
const toolResult = 'result' in chunk ? chunk.result : chunk.output;
|
|
1109
|
-
yield* this.emit(context, {
|
|
1110
|
-
type: 'tool-result',
|
|
1111
|
-
toolCallId: chunk.toolCallId,
|
|
1112
|
-
toolName: chunk.toolName,
|
|
1113
|
-
result: toolResult,
|
|
1114
|
-
});
|
|
1115
|
-
if (isHandoffResult(toolResult)) {
|
|
1116
|
-
const targetAgent = toolResult.targetAgent ?? toolResult.targetAgentId;
|
|
1117
|
-
onHandoff(targetAgent, toolResult.reason);
|
|
1118
|
-
handoffTriggered = true;
|
|
1119
|
-
break;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
const response = await result.response;
|
|
1124
|
-
if (bufferOutput) {
|
|
1125
|
-
const processed = await this.runOutputProcessing(agent, context, bufferedText);
|
|
1126
|
-
if (processed.tripwire) {
|
|
1127
|
-
yield* this.emit(context, {
|
|
1128
|
-
type: 'tripwire',
|
|
1129
|
-
phase: 'output',
|
|
1130
|
-
processorId: processed.tripwire.processorId,
|
|
1131
|
-
reason: processed.tripwire.reason,
|
|
1132
|
-
message: processed.tripwire.message,
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
1136
|
-
context.session.messages.push({ role: 'assistant', content: processed.text });
|
|
1137
|
-
}
|
|
1138
|
-
else {
|
|
1139
|
-
const beforeLen = context.session.messages.length;
|
|
1140
|
-
context.session.messages.push(...response.messages);
|
|
1141
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, beforeLen);
|
|
1142
|
-
for (const t of tripwires) {
|
|
1143
|
-
yield* this.emit(context, {
|
|
1144
|
-
type: 'tripwire',
|
|
1145
|
-
phase: 'output',
|
|
1146
|
-
processorId: t.processorId,
|
|
1147
|
-
reason: t.reason,
|
|
1148
|
-
message: t.message,
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
const usage = await result.usage;
|
|
1153
|
-
const totalTokens = usage.totalTokens ?? 0;
|
|
1154
|
-
context.totalTokens += totalTokens;
|
|
1155
|
-
if (context.session.metadata) {
|
|
1156
|
-
context.session.metadata.totalTokens += totalTokens;
|
|
1157
|
-
context.session.metadata.totalSteps += 1;
|
|
1158
|
-
}
|
|
1159
|
-
if (!handoffTriggered) {
|
|
1160
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
async *runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls) {
|
|
1164
|
-
const model = agent.model ?? this.defaultModel;
|
|
1165
|
-
if (!model) {
|
|
1166
|
-
throw new Error(`Agent "${agent.id}" is missing a model`);
|
|
1167
|
-
}
|
|
1168
|
-
const detourRules = agent.detourRules;
|
|
1169
|
-
if (detourRules?.emergency && this.matchesDetourRule(input, detourRules.emergency)) {
|
|
1170
|
-
const emergencyText = detourRules.emergencyMessage
|
|
1171
|
-
?? 'This sounds urgent. Please call local emergency services immediately or go to the nearest emergency room.';
|
|
1172
|
-
const processed = await this.runOutputProcessing(agent, context, emergencyText);
|
|
1173
|
-
if (processed.tripwire) {
|
|
1174
|
-
yield* this.emit(context, {
|
|
1175
|
-
type: 'tripwire',
|
|
1176
|
-
phase: 'output',
|
|
1177
|
-
processorId: processed.tripwire.processorId,
|
|
1178
|
-
reason: processed.tripwire.reason,
|
|
1179
|
-
message: processed.tripwire.message,
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
context.session.messages.push({ role: 'assistant', content: processed.text });
|
|
1183
|
-
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
1184
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1185
|
-
if (detourRules.emergencyHandoffAgent) {
|
|
1186
|
-
onHandoff(detourRules.emergencyHandoffAgent, 'Emergency pattern matched');
|
|
1187
|
-
}
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
const flowState = this.getFlowState(context.session, agent.id);
|
|
1191
|
-
if (agent.mode === 'hybrid') {
|
|
1192
|
-
const shouldRunFlow = await this.shouldHandleFlowInput(agent, input, flowState);
|
|
1193
|
-
if (!shouldRunFlow) {
|
|
1194
|
-
yield* this.runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff);
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
const suppressAutoRespond = !flowState?.initialized;
|
|
1199
|
-
const flow = this.buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond);
|
|
1200
|
-
const flowManager = new FlowManager({
|
|
1201
|
-
flow,
|
|
1202
|
-
initialNode: agent.initialNode,
|
|
1203
|
-
model: model,
|
|
1204
|
-
defaultRolePrompt: systemPrompt,
|
|
1205
|
-
contextMessages: flowState?.context.messages ?? [],
|
|
1206
|
-
sessionMessages: context.session.messages,
|
|
1207
|
-
state: flowState,
|
|
1208
|
-
telemetry: agent.telemetry ?? this.config.telemetry,
|
|
1209
|
-
toolCallGuard: async ({ toolName, args }) => {
|
|
1210
|
-
const callRecord = {
|
|
1211
|
-
toolCallId: crypto.randomUUID(),
|
|
1212
|
-
toolName,
|
|
1213
|
-
args,
|
|
1214
|
-
success: true,
|
|
1215
|
-
timestamp: Date.now(),
|
|
1216
|
-
};
|
|
1217
|
-
const enforcement = await this.enforcer.check(callRecord, {
|
|
1218
|
-
previousCalls: context.toolCallHistory,
|
|
1219
|
-
currentStep: context.stepCount,
|
|
1220
|
-
sessionState: context.session.state ?? {},
|
|
1221
|
-
});
|
|
1222
|
-
if (!enforcement.allowed) {
|
|
1223
|
-
return { allowed: false, reason: enforcement.reason ?? 'Tool call blocked by enforcement' };
|
|
1224
|
-
}
|
|
1225
|
-
return { allowed: true };
|
|
1226
|
-
},
|
|
1227
|
-
});
|
|
1228
|
-
let persistedStartIndex = context.session.messages.length;
|
|
1229
|
-
if (!flowState?.initialized) {
|
|
1230
|
-
for await (const part of flowManager.initialize()) {
|
|
1231
|
-
switch (part.type) {
|
|
1232
|
-
case 'text-delta':
|
|
1233
|
-
yield* this.emit(context, { type: 'text-delta', text: part.text });
|
|
1234
|
-
break;
|
|
1235
|
-
case 'tool-call': {
|
|
1236
|
-
const toolCallId = part.toolCallId ?? crypto.randomUUID();
|
|
1237
|
-
const callRecord = {
|
|
1238
|
-
toolCallId,
|
|
1239
|
-
toolName: part.toolName,
|
|
1240
|
-
args: part.args,
|
|
1241
|
-
success: true,
|
|
1242
|
-
timestamp: Date.now(),
|
|
1243
|
-
};
|
|
1244
|
-
toolCalls.push(callRecord);
|
|
1245
|
-
await this.hookRunner.onToolCall(context, callRecord);
|
|
1246
|
-
yield* this.emit(context, {
|
|
1247
|
-
type: 'tool-call',
|
|
1248
|
-
toolCallId,
|
|
1249
|
-
toolName: part.toolName,
|
|
1250
|
-
args: part.args,
|
|
1251
|
-
});
|
|
1252
|
-
break;
|
|
1253
|
-
}
|
|
1254
|
-
case 'tool-result': {
|
|
1255
|
-
const toolCallId = part.toolCallId
|
|
1256
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.result === undefined)?.toolCallId
|
|
1257
|
-
?? crypto.randomUUID();
|
|
1258
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1259
|
-
if (callRecord) {
|
|
1260
|
-
callRecord.result = part.result;
|
|
1261
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1262
|
-
context.toolCallHistory.push(callRecord);
|
|
1263
|
-
await this.hookRunner.onToolResult(context, callRecord);
|
|
1264
|
-
const enforcement = await this.enforcer.checkResult(callRecord, {
|
|
1265
|
-
previousCalls: context.toolCallHistory,
|
|
1266
|
-
currentStep: context.stepCount,
|
|
1267
|
-
sessionState: context.session.state ?? {},
|
|
1268
|
-
});
|
|
1269
|
-
if (!enforcement.allowed) {
|
|
1270
|
-
const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
|
|
1271
|
-
callRecord.success = false;
|
|
1272
|
-
callRecord.error = new Error(reason);
|
|
1273
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1274
|
-
yield* this.emit(context, {
|
|
1275
|
-
type: 'tool-error',
|
|
1276
|
-
toolCallId,
|
|
1277
|
-
toolName: part.toolName,
|
|
1278
|
-
error: reason,
|
|
1279
|
-
});
|
|
1280
|
-
yield* this.emit(context, { type: 'error', error: reason });
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
yield* this.emit(context, {
|
|
1285
|
-
type: 'tool-result',
|
|
1286
|
-
toolCallId,
|
|
1287
|
-
toolName: part.toolName,
|
|
1288
|
-
result: part.result,
|
|
1289
|
-
});
|
|
1290
|
-
break;
|
|
1291
|
-
}
|
|
1292
|
-
case 'tool-error': {
|
|
1293
|
-
const toolCallId = part.toolCallId
|
|
1294
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.error === undefined)?.toolCallId
|
|
1295
|
-
?? crypto.randomUUID();
|
|
1296
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1297
|
-
if (callRecord) {
|
|
1298
|
-
callRecord.success = false;
|
|
1299
|
-
callRecord.error = new Error(part.error);
|
|
1300
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1301
|
-
context.toolCallHistory.push(callRecord);
|
|
1302
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1303
|
-
}
|
|
1304
|
-
yield* this.emit(context, {
|
|
1305
|
-
type: 'tool-error',
|
|
1306
|
-
toolCallId,
|
|
1307
|
-
toolName: part.toolName,
|
|
1308
|
-
error: part.error,
|
|
1309
|
-
});
|
|
1310
|
-
break;
|
|
1311
|
-
}
|
|
1312
|
-
case 'handoff':
|
|
1313
|
-
onHandoff(part.targetAgent, part.reason);
|
|
1314
|
-
break;
|
|
1315
|
-
case 'node-enter':
|
|
1316
|
-
yield* this.emit(context, { type: 'node-enter', nodeName: part.nodeName });
|
|
1317
|
-
break;
|
|
1318
|
-
case 'node-exit':
|
|
1319
|
-
yield* this.emit(context, { type: 'node-exit', nodeName: part.nodeName });
|
|
1320
|
-
break;
|
|
1321
|
-
case 'flow-transition':
|
|
1322
|
-
yield* this.emit(context, { type: 'flow-transition', from: part.from, to: part.to });
|
|
1323
|
-
break;
|
|
1324
|
-
case 'flow-end':
|
|
1325
|
-
yield* this.emit(context, { type: 'flow-end', reason: part.reason });
|
|
1326
|
-
break;
|
|
1327
|
-
case 'turn-end':
|
|
1328
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1329
|
-
break;
|
|
1330
|
-
case 'error':
|
|
1331
|
-
yield* this.emit(context, { type: 'error', error: part.error });
|
|
1332
|
-
break;
|
|
1333
|
-
default:
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
// Sanitize/redact anything FlowManager persisted during initialization.
|
|
1338
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, persistedStartIndex);
|
|
1339
|
-
for (const t of tripwires) {
|
|
1340
|
-
yield* this.emit(context, {
|
|
1341
|
-
type: 'tripwire',
|
|
1342
|
-
phase: 'output',
|
|
1343
|
-
processorId: t.processorId,
|
|
1344
|
-
reason: t.reason,
|
|
1345
|
-
message: t.message,
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
persistedStartIndex = context.session.messages.length;
|
|
1349
|
-
}
|
|
1350
|
-
for await (const part of flowManager.process(input, { appendUserToSession: false })) {
|
|
1351
|
-
switch (part.type) {
|
|
1352
|
-
case 'text-delta':
|
|
1353
|
-
yield* this.emit(context, { type: 'text-delta', text: part.text });
|
|
1354
|
-
break;
|
|
1355
|
-
case 'tool-call': {
|
|
1356
|
-
const toolCallId = part.toolCallId ?? crypto.randomUUID();
|
|
1357
|
-
const callRecord = {
|
|
1358
|
-
toolCallId,
|
|
1359
|
-
toolName: part.toolName,
|
|
1360
|
-
args: part.args,
|
|
1361
|
-
success: true,
|
|
1362
|
-
timestamp: Date.now(),
|
|
1363
|
-
};
|
|
1364
|
-
toolCalls.push(callRecord);
|
|
1365
|
-
await this.hookRunner.onToolCall(context, callRecord);
|
|
1366
|
-
yield* this.emit(context, {
|
|
1367
|
-
type: 'tool-call',
|
|
1368
|
-
toolCallId,
|
|
1369
|
-
toolName: part.toolName,
|
|
1370
|
-
args: part.args,
|
|
1371
|
-
});
|
|
1372
|
-
break;
|
|
1373
|
-
}
|
|
1374
|
-
case 'tool-result': {
|
|
1375
|
-
const toolCallId = part.toolCallId
|
|
1376
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.result === undefined)?.toolCallId
|
|
1377
|
-
?? crypto.randomUUID();
|
|
1378
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1379
|
-
if (callRecord) {
|
|
1380
|
-
callRecord.result = part.result;
|
|
1381
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1382
|
-
context.toolCallHistory.push(callRecord);
|
|
1383
|
-
await this.hookRunner.onToolResult(context, callRecord);
|
|
1384
|
-
const enforcement = await this.enforcer.checkResult(callRecord, {
|
|
1385
|
-
previousCalls: context.toolCallHistory,
|
|
1386
|
-
currentStep: context.stepCount,
|
|
1387
|
-
sessionState: context.session.state ?? {},
|
|
1388
|
-
});
|
|
1389
|
-
if (!enforcement.allowed) {
|
|
1390
|
-
const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
|
|
1391
|
-
callRecord.success = false;
|
|
1392
|
-
callRecord.error = new Error(reason);
|
|
1393
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1394
|
-
yield* this.emit(context, {
|
|
1395
|
-
type: 'tool-error',
|
|
1396
|
-
toolCallId,
|
|
1397
|
-
toolName: part.toolName,
|
|
1398
|
-
error: reason,
|
|
1399
|
-
});
|
|
1400
|
-
yield* this.emit(context, { type: 'error', error: reason });
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
yield* this.emit(context, {
|
|
1405
|
-
type: 'tool-result',
|
|
1406
|
-
toolCallId,
|
|
1407
|
-
toolName: part.toolName,
|
|
1408
|
-
result: part.result,
|
|
1409
|
-
});
|
|
1410
|
-
break;
|
|
1411
|
-
}
|
|
1412
|
-
case 'tool-error': {
|
|
1413
|
-
const toolCallId = part.toolCallId
|
|
1414
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.error === undefined)?.toolCallId
|
|
1415
|
-
?? crypto.randomUUID();
|
|
1416
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1417
|
-
if (callRecord) {
|
|
1418
|
-
callRecord.success = false;
|
|
1419
|
-
callRecord.error = new Error(part.error);
|
|
1420
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1421
|
-
context.toolCallHistory.push(callRecord);
|
|
1422
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1423
|
-
}
|
|
1424
|
-
yield* this.emit(context, {
|
|
1425
|
-
type: 'tool-error',
|
|
1426
|
-
toolCallId,
|
|
1427
|
-
toolName: part.toolName,
|
|
1428
|
-
error: part.error,
|
|
1429
|
-
});
|
|
1430
|
-
break;
|
|
1431
|
-
}
|
|
1432
|
-
case 'handoff':
|
|
1433
|
-
onHandoff(part.targetAgent, part.reason);
|
|
1434
|
-
break;
|
|
1435
|
-
case 'node-enter':
|
|
1436
|
-
yield* this.emit(context, { type: 'node-enter', nodeName: part.nodeName });
|
|
1437
|
-
break;
|
|
1438
|
-
case 'node-exit':
|
|
1439
|
-
yield* this.emit(context, { type: 'node-exit', nodeName: part.nodeName });
|
|
1440
|
-
break;
|
|
1441
|
-
case 'flow-transition':
|
|
1442
|
-
yield* this.emit(context, { type: 'flow-transition', from: part.from, to: part.to });
|
|
1443
|
-
break;
|
|
1444
|
-
case 'flow-end':
|
|
1445
|
-
yield* this.emit(context, { type: 'flow-end', reason: part.reason });
|
|
1446
|
-
break;
|
|
1447
|
-
case 'turn-end':
|
|
1448
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1449
|
-
break;
|
|
1450
|
-
case 'error':
|
|
1451
|
-
yield* this.emit(context, { type: 'error', error: part.error });
|
|
1452
|
-
break;
|
|
1453
|
-
default:
|
|
1454
|
-
break;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
// Sanitize/redact anything FlowManager persisted during this user turn.
|
|
1458
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, persistedStartIndex);
|
|
1459
|
-
for (const t of tripwires) {
|
|
1460
|
-
yield* this.emit(context, {
|
|
1461
|
-
type: 'tripwire',
|
|
1462
|
-
phase: 'output',
|
|
1463
|
-
processorId: t.processorId,
|
|
1464
|
-
reason: t.reason,
|
|
1465
|
-
message: t.message,
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
if (flowManager.hasEnded) {
|
|
1469
|
-
this.clearFlowState(context.session, agent.id);
|
|
1470
|
-
}
|
|
1471
|
-
else {
|
|
1472
|
-
this.setFlowState(context.session, agent.id, flowManager.getState());
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
1290
|
async getSession(id) {
|
|
1476
1291
|
return this.sessionStore.get(id);
|
|
1477
1292
|
}
|
|
@@ -1484,11 +1299,89 @@ Do not change the flow requirements. Keep the reply concise.`;
|
|
|
1484
1299
|
getAllAgents() {
|
|
1485
1300
|
return Array.from(this.agents.values());
|
|
1486
1301
|
}
|
|
1487
|
-
|
|
1302
|
+
normalizeSessionMessage(message) {
|
|
1303
|
+
return normalizeModelMessage(message);
|
|
1304
|
+
}
|
|
1305
|
+
normalizeSessionHistory(messages) {
|
|
1306
|
+
const normalized = [];
|
|
1307
|
+
for (const message of messages) {
|
|
1308
|
+
const next = this.normalizeSessionMessage(message);
|
|
1309
|
+
if (next) {
|
|
1310
|
+
normalized.push(next);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return normalized;
|
|
1314
|
+
}
|
|
1315
|
+
appendSessionMessage(session, message) {
|
|
1316
|
+
const normalized = this.normalizeSessionMessage(message);
|
|
1317
|
+
if (!normalized) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
session.messages.push(normalized);
|
|
1321
|
+
this.touchSession(session);
|
|
1322
|
+
}
|
|
1323
|
+
appendSessionMessages(session, messages) {
|
|
1324
|
+
for (const message of messages) {
|
|
1325
|
+
this.appendSessionMessage(session, message);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
touchSession(session) {
|
|
1329
|
+
const now = new Date();
|
|
1330
|
+
session.updatedAt = now;
|
|
1331
|
+
if (session.metadata) {
|
|
1332
|
+
session.metadata.lastActiveAt = now;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
getSessionTurn(session) {
|
|
1336
|
+
const value = session.workingMemory[this.runtimeSessionTurnKey];
|
|
1337
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
1338
|
+
}
|
|
1339
|
+
bumpSessionTurn(session) {
|
|
1340
|
+
const next = this.getSessionTurn(session) + 1;
|
|
1341
|
+
session.workingMemory[this.runtimeSessionTurnKey] = next;
|
|
1342
|
+
this.touchSession(session);
|
|
1343
|
+
return next;
|
|
1344
|
+
}
|
|
1345
|
+
async saveSessionCheckpoint(session) {
|
|
1346
|
+
this.touchSession(session);
|
|
1347
|
+
await this._sessionStore.save(session);
|
|
1348
|
+
}
|
|
1349
|
+
shouldCheckpointAfterPart(part) {
|
|
1350
|
+
return this.checkpointEventTypes.has(part.type);
|
|
1351
|
+
}
|
|
1352
|
+
recordRuntimeEvent(context, part) {
|
|
1353
|
+
this.sessionEventManager.recordRuntimeEvent(context, part);
|
|
1354
|
+
}
|
|
1355
|
+
buildToolIdempotencyKey(context, toolName, toolCallId) {
|
|
1356
|
+
return `${context.session.id}:${context.agentId}:${context.stepCount}:${toolName}:${toolCallId}`;
|
|
1357
|
+
}
|
|
1358
|
+
withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey) {
|
|
1359
|
+
const baseOptions = isRecord(options) ? options : {};
|
|
1360
|
+
const existingContext = isRecord(baseOptions.experimental_context)
|
|
1361
|
+
? baseOptions.experimental_context
|
|
1362
|
+
: {};
|
|
1363
|
+
return {
|
|
1364
|
+
...baseOptions,
|
|
1365
|
+
toolCallId,
|
|
1366
|
+
experimental_context: {
|
|
1367
|
+
...existingContext,
|
|
1368
|
+
session: context.session,
|
|
1369
|
+
sessionId: context.session.id,
|
|
1370
|
+
agentId: context.agentId,
|
|
1371
|
+
step: context.stepCount,
|
|
1372
|
+
turn: this.getSessionTurn(context.session),
|
|
1373
|
+
toolName,
|
|
1374
|
+
toolCallId,
|
|
1375
|
+
idempotencyKey,
|
|
1376
|
+
},
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
buildProcessorContext(context, abortSignal) {
|
|
1488
1380
|
return {
|
|
1489
1381
|
session: context.session,
|
|
1490
1382
|
agentId: context.agentId,
|
|
1491
1383
|
toolCallHistory: context.toolCallHistory,
|
|
1384
|
+
abortSignal,
|
|
1492
1385
|
};
|
|
1493
1386
|
}
|
|
1494
1387
|
getAgentOutputProcessors(agent) {
|
|
@@ -1510,11 +1403,12 @@ Do not change the flow requirements. Keep the reply concise.`;
|
|
|
1510
1403
|
const processors = this.getAgentOutputProcessors(agent);
|
|
1511
1404
|
let cur = text;
|
|
1512
1405
|
if (processors.length > 0) {
|
|
1406
|
+
const abortSignal = this.getActiveAbortController(context.session.id)?.signal;
|
|
1513
1407
|
const outcome = await runOutputProcessors({
|
|
1514
1408
|
processors,
|
|
1515
1409
|
text: cur,
|
|
1516
1410
|
messages: context.session.messages,
|
|
1517
|
-
context: this.buildProcessorContext(context),
|
|
1411
|
+
context: this.buildProcessorContext(context, abortSignal),
|
|
1518
1412
|
});
|
|
1519
1413
|
if (outcome.blocked) {
|
|
1520
1414
|
const msg = this.applyRedactionsToText(outcome.message);
|
|
@@ -1548,46 +1442,53 @@ Do not change the flow requirements. Keep the reply concise.`;
|
|
|
1548
1442
|
if (this.outputRedactions && this.outputRedactions.length > 0) {
|
|
1549
1443
|
const sessionId = context.session.id;
|
|
1550
1444
|
if (part.type === 'text-delta') {
|
|
1551
|
-
const next = this.applyOutputRedactions(
|
|
1445
|
+
const next = this.applyOutputRedactions(context.session, part.text, false);
|
|
1552
1446
|
if (next) {
|
|
1553
1447
|
const redacted = { ...part, text: next };
|
|
1554
|
-
|
|
1555
|
-
yield redacted;
|
|
1448
|
+
yield* this.emitWithHooks(context, redacted);
|
|
1556
1449
|
}
|
|
1557
1450
|
return;
|
|
1558
1451
|
}
|
|
1559
1452
|
if (part.type === 'turn-end' || part.type === 'done') {
|
|
1560
|
-
const flushed = this.applyOutputRedactions(
|
|
1453
|
+
const flushed = this.applyOutputRedactions(context.session, '', true);
|
|
1561
1454
|
if (flushed) {
|
|
1562
1455
|
const carryPart = { type: 'text-delta', text: flushed };
|
|
1563
|
-
|
|
1564
|
-
yield carryPart;
|
|
1456
|
+
yield* this.emitWithHooks(context, carryPart);
|
|
1565
1457
|
}
|
|
1566
|
-
this.
|
|
1458
|
+
delete context.session.workingMemory[this.redactCarryKey];
|
|
1567
1459
|
}
|
|
1568
1460
|
}
|
|
1461
|
+
yield* this.emitWithHooks(context, part);
|
|
1462
|
+
}
|
|
1463
|
+
async *emitWithHooks(context, part) {
|
|
1464
|
+
this.recordRuntimeEvent(context, part);
|
|
1569
1465
|
await this.hookRunner.onStreamPart(context, part);
|
|
1466
|
+
if (this.shouldCheckpointAfterPart(part)) {
|
|
1467
|
+
await this.saveSessionCheckpoint(context.session);
|
|
1468
|
+
}
|
|
1570
1469
|
yield part;
|
|
1571
1470
|
}
|
|
1572
|
-
applyOutputRedactions(
|
|
1471
|
+
applyOutputRedactions(session, text, flush) {
|
|
1573
1472
|
if (!this.outputRedactions || this.outputRedactions.length === 0)
|
|
1574
1473
|
return text;
|
|
1575
|
-
const carry = this.
|
|
1474
|
+
const carry = typeof session.workingMemory[this.redactCarryKey] === 'string'
|
|
1475
|
+
? session.workingMemory[this.redactCarryKey]
|
|
1476
|
+
: '';
|
|
1576
1477
|
let combined = `${carry}${text}`;
|
|
1577
1478
|
for (const r of this.outputRedactions) {
|
|
1578
1479
|
combined = combined.replace(r.re, r.replacement);
|
|
1579
1480
|
}
|
|
1580
1481
|
const keep = flush ? 0 : this.redactLookbehind;
|
|
1581
1482
|
if (keep === 0) {
|
|
1582
|
-
this.
|
|
1483
|
+
session.workingMemory[this.redactCarryKey] = '';
|
|
1583
1484
|
return combined;
|
|
1584
1485
|
}
|
|
1585
1486
|
if (combined.length <= keep) {
|
|
1586
|
-
this.
|
|
1487
|
+
session.workingMemory[this.redactCarryKey] = combined;
|
|
1587
1488
|
return '';
|
|
1588
1489
|
}
|
|
1589
1490
|
const out = combined.slice(0, combined.length - keep);
|
|
1590
|
-
this.
|
|
1491
|
+
session.workingMemory[this.redactCarryKey] = combined.slice(-keep);
|
|
1591
1492
|
return out;
|
|
1592
1493
|
}
|
|
1593
1494
|
}
|