@ariaflowagents/core 0.6.1 → 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 +10 -3
- 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 +46 -20
- 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 -633
- 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,32 +690,12 @@ export class Runtime {
|
|
|
584
690
|
}
|
|
585
691
|
}
|
|
586
692
|
if (chunk.type === 'tool-call') {
|
|
587
|
-
const args =
|
|
588
|
-
// Deduplicate: Skip if same tool with same args was already called in this step
|
|
589
|
-
// Normalize args by sorting keys to handle different object key orders
|
|
590
|
-
const normalizeArgs = (obj) => {
|
|
591
|
-
if (obj === null || obj === undefined)
|
|
592
|
-
return String(obj);
|
|
593
|
-
if (typeof obj !== 'object')
|
|
594
|
-
return String(obj);
|
|
595
|
-
if (Array.isArray(obj))
|
|
596
|
-
return JSON.stringify(obj.map(normalizeArgs));
|
|
597
|
-
const sorted = {};
|
|
598
|
-
Object.keys(obj).sort().forEach(key => {
|
|
599
|
-
sorted[key] = normalizeArgs(obj[key]);
|
|
600
|
-
});
|
|
601
|
-
return JSON.stringify(sorted);
|
|
602
|
-
};
|
|
603
|
-
const isDuplicate = toolCalls.some(existing => existing.toolName === chunk.toolName &&
|
|
604
|
-
normalizeArgs(existing.args) === normalizeArgs(args));
|
|
605
|
-
if (isDuplicate) {
|
|
606
|
-
console.log(`[Runtime] Skipping duplicate tool call: ${chunk.toolName}`);
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
693
|
+
const args = getChunkArgs(chunk);
|
|
609
694
|
const callRecord = {
|
|
610
695
|
toolCallId: chunk.toolCallId,
|
|
611
696
|
toolName: chunk.toolName,
|
|
612
697
|
args,
|
|
698
|
+
idempotencyKey: this.buildToolIdempotencyKey(context, chunk.toolName, chunk.toolCallId),
|
|
613
699
|
success: true,
|
|
614
700
|
timestamp: Date.now(),
|
|
615
701
|
};
|
|
@@ -633,11 +719,10 @@ export class Runtime {
|
|
|
633
719
|
});
|
|
634
720
|
}
|
|
635
721
|
if (chunk.type === 'tool-error') {
|
|
636
|
-
const errText =
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
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);
|
|
641
726
|
if (callRecord) {
|
|
642
727
|
callRecord.success = false;
|
|
643
728
|
callRecord.error = new Error(errText);
|
|
@@ -645,17 +730,24 @@ export class Runtime {
|
|
|
645
730
|
callRecord.args = args;
|
|
646
731
|
context.toolCallHistory.push(callRecord);
|
|
647
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
|
+
}
|
|
648
740
|
}
|
|
649
741
|
yield* this.emit(context, {
|
|
650
742
|
type: 'tool-error',
|
|
651
|
-
toolCallId: chunk.toolCallId,
|
|
743
|
+
toolCallId: toolCallId ?? chunk.toolCallId,
|
|
652
744
|
toolName: chunk.toolName,
|
|
653
745
|
error: errText,
|
|
654
746
|
});
|
|
655
747
|
}
|
|
656
748
|
if (chunk.type === 'tool-result') {
|
|
657
749
|
const startTime = toolCalls.find(call => call.toolCallId === chunk.toolCallId)?.timestamp ?? Date.now();
|
|
658
|
-
const toolResult =
|
|
750
|
+
const toolResult = getChunkResult(chunk);
|
|
659
751
|
const callRecord = toolCalls.find(call => call.toolCallId === chunk.toolCallId);
|
|
660
752
|
if (callRecord) {
|
|
661
753
|
callRecord.result = toolResult;
|
|
@@ -682,6 +774,8 @@ export class Runtime {
|
|
|
682
774
|
callRecord.success = false;
|
|
683
775
|
callRecord.error = new Error(reason);
|
|
684
776
|
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
777
|
+
// Fail-closed: Track enforcement failures as critical errors.
|
|
778
|
+
turnToolErrors.push(callRecord);
|
|
685
779
|
yield* this.emit(context, {
|
|
686
780
|
type: 'tool-error',
|
|
687
781
|
toolCallId: chunk.toolCallId,
|
|
@@ -743,8 +837,17 @@ export class Runtime {
|
|
|
743
837
|
// If the model ended on tool-calls, we must persist the tool messages and
|
|
744
838
|
// continue the loop (or let AI SDK continue in maxSteps) instead of emitting
|
|
745
839
|
// synthetic assistant text, which can duplicate responses.
|
|
746
|
-
if (finalResult || (bufferOutput && finishReason !== 'tool-calls')) {
|
|
747
|
-
|
|
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
|
+
}
|
|
748
851
|
const processed = await this.runOutputProcessing(agent, context, rawText);
|
|
749
852
|
if (processed.tripwire) {
|
|
750
853
|
yield* this.emit(context, {
|
|
@@ -755,14 +858,14 @@ export class Runtime {
|
|
|
755
858
|
message: processed.tripwire.message,
|
|
756
859
|
});
|
|
757
860
|
}
|
|
758
|
-
if (bufferOutput) {
|
|
861
|
+
if (bufferOutput || turnToolErrors.length > 0) {
|
|
759
862
|
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
760
863
|
}
|
|
761
|
-
context.session
|
|
864
|
+
this.appendSessionMessage(context.session, { role: 'assistant', content: processed.text });
|
|
762
865
|
}
|
|
763
866
|
else {
|
|
764
867
|
const beforeLen = context.session.messages.length;
|
|
765
|
-
context.session
|
|
868
|
+
this.appendSessionMessages(context.session, response.messages);
|
|
766
869
|
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, beforeLen);
|
|
767
870
|
for (const t of tripwires) {
|
|
768
871
|
yield* this.emit(context, {
|
|
@@ -794,6 +897,21 @@ export class Runtime {
|
|
|
794
897
|
}
|
|
795
898
|
}
|
|
796
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
|
+
}
|
|
797
915
|
context.consecutiveErrors += 1;
|
|
798
916
|
await this.hookRunner.onError(context, error);
|
|
799
917
|
yield* this.emit(context, { type: 'error', error: error.message });
|
|
@@ -805,6 +923,17 @@ export class Runtime {
|
|
|
805
923
|
}
|
|
806
924
|
yield* this.emit(context, { type: 'agent-end', agentId: agent.id });
|
|
807
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
|
+
}
|
|
808
937
|
if (!handoffTo) {
|
|
809
938
|
break;
|
|
810
939
|
}
|
|
@@ -831,12 +960,34 @@ export class Runtime {
|
|
|
831
960
|
}
|
|
832
961
|
context.session.activeAgentId = handoffTo;
|
|
833
962
|
context.session.currentAgent = handoffTo;
|
|
963
|
+
await this.saveSessionCheckpoint(context.session);
|
|
834
964
|
context.agentId = handoffTo;
|
|
835
965
|
}
|
|
836
966
|
if (context.handoffStack.length >= this.maxHandoffs) {
|
|
837
967
|
yield* this.emit(context, { type: 'error', error: `Maximum handoffs (${this.maxHandoffs}) exceeded` });
|
|
838
968
|
}
|
|
839
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
|
+
}
|
|
840
991
|
createSession(id, userId) {
|
|
841
992
|
const now = new Date();
|
|
842
993
|
return {
|
|
@@ -866,26 +1017,47 @@ export class Runtime {
|
|
|
866
1017
|
isTriageAgent(agent) {
|
|
867
1018
|
return agent.type === 'triage';
|
|
868
1019
|
}
|
|
869
|
-
|
|
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) {
|
|
870
1041
|
const basePrompt = this.isTriageAgent(agent)
|
|
871
|
-
? this.buildTriagePrompt(agent)
|
|
1042
|
+
? this.buildTriagePrompt(agent, activeRoutes)
|
|
872
1043
|
: agent.systemPrompt;
|
|
873
1044
|
const autoBlock = autoContext?.text?.trim()
|
|
874
1045
|
? `\n\n## ${autoContext.label}\n${autoContext.text}`
|
|
875
1046
|
: '';
|
|
876
1047
|
const systemInjections = injectionQueue.getFor('system');
|
|
877
|
-
const memory = context?.session
|
|
1048
|
+
const memory = context?.session ? this.buildPromptMemoryView(context.session, agent) : {};
|
|
878
1049
|
const memoryBlock = Object.keys(memory).length > 0
|
|
879
1050
|
? `\n\n## Known Information\n${JSON.stringify(memory, null, 2)}`
|
|
880
1051
|
: '';
|
|
881
1052
|
const merged = `${basePrompt}${autoBlock}${memoryBlock}`;
|
|
882
1053
|
return systemInjections ? `${merged}\n\n${systemInjections}` : merged;
|
|
883
1054
|
}
|
|
884
|
-
buildStructuredTriagePrompt(agent) {
|
|
885
|
-
const
|
|
1055
|
+
buildStructuredTriagePrompt(agent, activeRoutes) {
|
|
1056
|
+
const routes = activeRoutes ?? agent.routes;
|
|
1057
|
+
const routeDescriptions = routes
|
|
886
1058
|
.map(route => `- ${route.agentId}: ${route.description}`)
|
|
887
1059
|
.join('\n');
|
|
888
|
-
const allowed =
|
|
1060
|
+
const allowed = routes.map(route => route.agentId);
|
|
889
1061
|
const defaultNote = agent.defaultAgent ? `Default: ${agent.defaultAgent}` : 'Default: none';
|
|
890
1062
|
return `${agent.systemPrompt}
|
|
891
1063
|
|
|
@@ -900,8 +1072,9 @@ Return a JSON object with:
|
|
|
900
1072
|
- stayWithCurrent: boolean (true only if current agent is best fit)
|
|
901
1073
|
${defaultNote}`;
|
|
902
1074
|
}
|
|
903
|
-
buildTriagePrompt(agent) {
|
|
904
|
-
const
|
|
1075
|
+
buildTriagePrompt(agent, activeRoutes) {
|
|
1076
|
+
const routes = activeRoutes ?? agent.routes;
|
|
1077
|
+
const routeDescriptions = routes
|
|
905
1078
|
.map(route => `- **${route.agentId}**: ${route.description}`)
|
|
906
1079
|
.join('\n');
|
|
907
1080
|
const defaultNote = agent.defaultAgent
|
|
@@ -917,9 +1090,10 @@ ${routeDescriptions}
|
|
|
917
1090
|
- When the customer needs specialized help, use the handoff tool
|
|
918
1091
|
- Always provide a brief reason for the handoff${defaultNote}`;
|
|
919
1092
|
}
|
|
920
|
-
getHandoffCandidates(agent) {
|
|
1093
|
+
getHandoffCandidates(agent, activeRoutes) {
|
|
921
1094
|
if (this.isTriageAgent(agent)) {
|
|
922
|
-
|
|
1095
|
+
const routes = activeRoutes ?? agent.routes;
|
|
1096
|
+
return routes
|
|
923
1097
|
.map(route => this.agents.get(route.agentId))
|
|
924
1098
|
.filter((candidate) => Boolean(candidate));
|
|
925
1099
|
}
|
|
@@ -939,15 +1113,45 @@ ${routeDescriptions}
|
|
|
939
1113
|
}
|
|
940
1114
|
return stored;
|
|
941
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
|
+
}
|
|
942
1127
|
setFlowState(session, agentId, state) {
|
|
943
1128
|
session.agentStates[agentId] = {
|
|
944
1129
|
agentId,
|
|
945
1130
|
state: state,
|
|
946
1131
|
lastActive: new Date(),
|
|
947
1132
|
};
|
|
1133
|
+
this.updateFlowStateSnapshot(session, agentId, state);
|
|
1134
|
+
this.touchSession(session);
|
|
948
1135
|
}
|
|
949
1136
|
clearFlowState(session, agentId) {
|
|
950
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);
|
|
951
1155
|
}
|
|
952
1156
|
buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond) {
|
|
953
1157
|
// If we don't need to inject a handoff tool and we don't need to suppress the initial autoRespond,
|
|
@@ -997,64 +1201,74 @@ ${routeDescriptions}
|
|
|
997
1201
|
}
|
|
998
1202
|
return agent.flow.nodes.find(node => node.id === nodeId);
|
|
999
1203
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const node = this.getFlowNode(agent, nodeId);
|
|
1009
|
-
if (!node) {
|
|
1010
|
-
return true;
|
|
1011
|
-
}
|
|
1012
|
-
const rules = agent.detourRules;
|
|
1013
|
-
if (rules) {
|
|
1014
|
-
const normalized = input.trim().toLowerCase();
|
|
1015
|
-
if (rules.allowShortAffirmations !== false &&
|
|
1016
|
-
/^(yes|yep|yeah|ok|okay|sure|great|thanks|thank you|please|proceed|go ahead)\b/.test(normalized)) {
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
if (rules.allowDateTime !== false &&
|
|
1020
|
-
(/\b20\d{2}-\d{2}-\d{2}\b/.test(normalized) || /\b\d{1,2}:\d{2}\b/.test(normalized))) {
|
|
1021
|
-
return true;
|
|
1022
|
-
}
|
|
1023
|
-
if (this.matchesDetourRule(normalized, rules.deny)) {
|
|
1024
|
-
return false;
|
|
1025
|
-
}
|
|
1026
|
-
if (this.matchesDetourRule(normalized, rules.allow)) {
|
|
1027
|
-
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;
|
|
1028
1212
|
}
|
|
1213
|
+
return { config, nodeId };
|
|
1029
1214
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const routerPrompt = `You are routing a message for a structured conversation flow.
|
|
1033
|
-
Decide if the user input is answering the current flow step or is a side question.
|
|
1034
|
-
|
|
1035
|
-
Current flow step:
|
|
1036
|
-
${renderedNodePrompt}
|
|
1037
|
-
|
|
1038
|
-
Collected data:
|
|
1039
|
-
${JSON.stringify(collectedData, null, 2)}
|
|
1040
|
-
|
|
1041
|
-
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.`;
|
|
1042
|
-
const result = await generateText({
|
|
1043
|
-
model: (agent.model ?? this.defaultModel),
|
|
1044
|
-
system: routerPrompt,
|
|
1045
|
-
prompt: input,
|
|
1046
|
-
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
1047
|
-
});
|
|
1048
|
-
const decision = result.text.trim().toLowerCase();
|
|
1049
|
-
if (decision.startsWith('detour')) {
|
|
1050
|
-
return false;
|
|
1051
|
-
}
|
|
1052
|
-
if (decision.startsWith('flow')) {
|
|
1053
|
-
return true;
|
|
1215
|
+
if (!agent.extraction) {
|
|
1216
|
+
return null;
|
|
1054
1217
|
}
|
|
1055
|
-
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);
|
|
1056
1231
|
}
|
|
1057
|
-
|
|
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);
|
|
1237
|
+
}
|
|
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) {
|
|
1058
1272
|
if (!patterns || patterns.length === 0) {
|
|
1059
1273
|
return false;
|
|
1060
1274
|
}
|
|
@@ -1073,426 +1287,6 @@ Return only one word: "flow" if the input should be handled by the flow step, or
|
|
|
1073
1287
|
}
|
|
1074
1288
|
return false;
|
|
1075
1289
|
}
|
|
1076
|
-
async *runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff) {
|
|
1077
|
-
const nodeId = flowState?.context.currentNode ?? agent.initialNode;
|
|
1078
|
-
const node = this.getFlowNode(agent, nodeId);
|
|
1079
|
-
const collectedData = flowState?.context.collectedData ?? {};
|
|
1080
|
-
const nodePrompt = node
|
|
1081
|
-
? renderFlowTemplate(node.prompt, collectedData, { missing: 'keep' })
|
|
1082
|
-
: 'Continue the flow.';
|
|
1083
|
-
const handoffLine = handoffTool
|
|
1084
|
-
? 'If the request should be handled by a specialist agent, use the handoff tool instead of answering.'
|
|
1085
|
-
: 'Do not attempt to route to other agents. Handle the detour here and then resume the flow.';
|
|
1086
|
-
const detourPrompt = `You are handling a short detour during a structured flow.
|
|
1087
|
-
Answer the user's question clearly and briefly. ${handoffLine} Then ask the user to continue with the current flow step.
|
|
1088
|
-
|
|
1089
|
-
Current flow step:
|
|
1090
|
-
${nodePrompt}
|
|
1091
|
-
|
|
1092
|
-
Collected data:
|
|
1093
|
-
${JSON.stringify(collectedData, null, 2)}
|
|
1094
|
-
|
|
1095
|
-
Do not change the flow requirements. Keep the reply concise.`;
|
|
1096
|
-
const result = streamText({
|
|
1097
|
-
model: (agent.model ?? this.defaultModel),
|
|
1098
|
-
system: detourPrompt,
|
|
1099
|
-
messages: context.session.messages,
|
|
1100
|
-
tools: handoffTool ? { handoff: handoffTool } : undefined,
|
|
1101
|
-
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
1102
|
-
});
|
|
1103
|
-
let handoffTriggered = false;
|
|
1104
|
-
const outputProcessors = this.getAgentOutputProcessors(agent);
|
|
1105
|
-
const bufferOutput = this.outputProcessorMode === 'buffer' && outputProcessors.length > 0;
|
|
1106
|
-
let bufferedText = '';
|
|
1107
|
-
for await (const chunk of result.fullStream) {
|
|
1108
|
-
if (chunk.type === 'text-delta') {
|
|
1109
|
-
if (handoffTriggered) {
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
if (bufferOutput) {
|
|
1113
|
-
bufferedText += chunk.text;
|
|
1114
|
-
}
|
|
1115
|
-
else {
|
|
1116
|
-
yield* this.emit(context, { type: 'text-delta', text: chunk.text });
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
if (chunk.type === 'tool-call') {
|
|
1120
|
-
const args = 'args' in chunk ? chunk.args : chunk.input;
|
|
1121
|
-
yield* this.emit(context, {
|
|
1122
|
-
type: 'tool-call',
|
|
1123
|
-
toolCallId: chunk.toolCallId,
|
|
1124
|
-
toolName: chunk.toolName,
|
|
1125
|
-
args,
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
if (chunk.type === 'tool-result') {
|
|
1129
|
-
const toolResult = 'result' in chunk ? chunk.result : chunk.output;
|
|
1130
|
-
yield* this.emit(context, {
|
|
1131
|
-
type: 'tool-result',
|
|
1132
|
-
toolCallId: chunk.toolCallId,
|
|
1133
|
-
toolName: chunk.toolName,
|
|
1134
|
-
result: toolResult,
|
|
1135
|
-
});
|
|
1136
|
-
if (isHandoffResult(toolResult)) {
|
|
1137
|
-
const targetAgent = toolResult.targetAgent ?? toolResult.targetAgentId;
|
|
1138
|
-
onHandoff(targetAgent, toolResult.reason);
|
|
1139
|
-
handoffTriggered = true;
|
|
1140
|
-
break;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
const response = await result.response;
|
|
1145
|
-
if (bufferOutput) {
|
|
1146
|
-
const processed = await this.runOutputProcessing(agent, context, bufferedText);
|
|
1147
|
-
if (processed.tripwire) {
|
|
1148
|
-
yield* this.emit(context, {
|
|
1149
|
-
type: 'tripwire',
|
|
1150
|
-
phase: 'output',
|
|
1151
|
-
processorId: processed.tripwire.processorId,
|
|
1152
|
-
reason: processed.tripwire.reason,
|
|
1153
|
-
message: processed.tripwire.message,
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
1157
|
-
context.session.messages.push({ role: 'assistant', content: processed.text });
|
|
1158
|
-
}
|
|
1159
|
-
else {
|
|
1160
|
-
const beforeLen = context.session.messages.length;
|
|
1161
|
-
context.session.messages.push(...response.messages);
|
|
1162
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, beforeLen);
|
|
1163
|
-
for (const t of tripwires) {
|
|
1164
|
-
yield* this.emit(context, {
|
|
1165
|
-
type: 'tripwire',
|
|
1166
|
-
phase: 'output',
|
|
1167
|
-
processorId: t.processorId,
|
|
1168
|
-
reason: t.reason,
|
|
1169
|
-
message: t.message,
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
const usage = await result.usage;
|
|
1174
|
-
const totalTokens = usage.totalTokens ?? 0;
|
|
1175
|
-
context.totalTokens += totalTokens;
|
|
1176
|
-
if (context.session.metadata) {
|
|
1177
|
-
context.session.metadata.totalTokens += totalTokens;
|
|
1178
|
-
context.session.metadata.totalSteps += 1;
|
|
1179
|
-
}
|
|
1180
|
-
if (!handoffTriggered) {
|
|
1181
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
async *runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls) {
|
|
1185
|
-
const model = agent.model ?? this.defaultModel;
|
|
1186
|
-
if (!model) {
|
|
1187
|
-
throw new Error(`Agent "${agent.id}" is missing a model`);
|
|
1188
|
-
}
|
|
1189
|
-
const detourRules = agent.detourRules;
|
|
1190
|
-
if (detourRules?.emergency && this.matchesDetourRule(input, detourRules.emergency)) {
|
|
1191
|
-
const emergencyText = detourRules.emergencyMessage
|
|
1192
|
-
?? 'This sounds urgent. Please call local emergency services immediately or go to the nearest emergency room.';
|
|
1193
|
-
const processed = await this.runOutputProcessing(agent, context, emergencyText);
|
|
1194
|
-
if (processed.tripwire) {
|
|
1195
|
-
yield* this.emit(context, {
|
|
1196
|
-
type: 'tripwire',
|
|
1197
|
-
phase: 'output',
|
|
1198
|
-
processorId: processed.tripwire.processorId,
|
|
1199
|
-
reason: processed.tripwire.reason,
|
|
1200
|
-
message: processed.tripwire.message,
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
context.session.messages.push({ role: 'assistant', content: processed.text });
|
|
1204
|
-
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
1205
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1206
|
-
if (detourRules.emergencyHandoffAgent) {
|
|
1207
|
-
onHandoff(detourRules.emergencyHandoffAgent, 'Emergency pattern matched');
|
|
1208
|
-
}
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
const flowState = this.getFlowState(context.session, agent.id);
|
|
1212
|
-
if (agent.mode === 'hybrid') {
|
|
1213
|
-
const shouldRunFlow = await this.shouldHandleFlowInput(agent, input, flowState);
|
|
1214
|
-
if (!shouldRunFlow) {
|
|
1215
|
-
yield* this.runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff);
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
const suppressAutoRespond = !flowState?.initialized;
|
|
1220
|
-
const flow = this.buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond);
|
|
1221
|
-
const flowManager = new FlowManager({
|
|
1222
|
-
flow,
|
|
1223
|
-
initialNode: agent.initialNode,
|
|
1224
|
-
model: model,
|
|
1225
|
-
defaultRolePrompt: systemPrompt,
|
|
1226
|
-
contextMessages: flowState?.context.messages ?? [],
|
|
1227
|
-
sessionMessages: context.session.messages,
|
|
1228
|
-
state: flowState,
|
|
1229
|
-
telemetry: agent.telemetry ?? this.config.telemetry,
|
|
1230
|
-
toolCallGuard: async ({ toolName, args }) => {
|
|
1231
|
-
const callRecord = {
|
|
1232
|
-
toolCallId: crypto.randomUUID(),
|
|
1233
|
-
toolName,
|
|
1234
|
-
args,
|
|
1235
|
-
success: true,
|
|
1236
|
-
timestamp: Date.now(),
|
|
1237
|
-
};
|
|
1238
|
-
const enforcement = await this.enforcer.check(callRecord, {
|
|
1239
|
-
previousCalls: context.toolCallHistory,
|
|
1240
|
-
currentStep: context.stepCount,
|
|
1241
|
-
sessionState: context.session.state ?? {},
|
|
1242
|
-
});
|
|
1243
|
-
if (!enforcement.allowed) {
|
|
1244
|
-
return { allowed: false, reason: enforcement.reason ?? 'Tool call blocked by enforcement' };
|
|
1245
|
-
}
|
|
1246
|
-
return { allowed: true };
|
|
1247
|
-
},
|
|
1248
|
-
});
|
|
1249
|
-
let persistedStartIndex = context.session.messages.length;
|
|
1250
|
-
if (!flowState?.initialized) {
|
|
1251
|
-
for await (const part of flowManager.initialize()) {
|
|
1252
|
-
switch (part.type) {
|
|
1253
|
-
case 'text-delta':
|
|
1254
|
-
yield* this.emit(context, { type: 'text-delta', text: part.text });
|
|
1255
|
-
break;
|
|
1256
|
-
case 'tool-call': {
|
|
1257
|
-
const toolCallId = part.toolCallId ?? crypto.randomUUID();
|
|
1258
|
-
const callRecord = {
|
|
1259
|
-
toolCallId,
|
|
1260
|
-
toolName: part.toolName,
|
|
1261
|
-
args: part.args,
|
|
1262
|
-
success: true,
|
|
1263
|
-
timestamp: Date.now(),
|
|
1264
|
-
};
|
|
1265
|
-
toolCalls.push(callRecord);
|
|
1266
|
-
await this.hookRunner.onToolCall(context, callRecord);
|
|
1267
|
-
yield* this.emit(context, {
|
|
1268
|
-
type: 'tool-call',
|
|
1269
|
-
toolCallId,
|
|
1270
|
-
toolName: part.toolName,
|
|
1271
|
-
args: part.args,
|
|
1272
|
-
});
|
|
1273
|
-
break;
|
|
1274
|
-
}
|
|
1275
|
-
case 'tool-result': {
|
|
1276
|
-
const toolCallId = part.toolCallId
|
|
1277
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.result === undefined)?.toolCallId
|
|
1278
|
-
?? crypto.randomUUID();
|
|
1279
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1280
|
-
if (callRecord) {
|
|
1281
|
-
callRecord.result = part.result;
|
|
1282
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1283
|
-
context.toolCallHistory.push(callRecord);
|
|
1284
|
-
await this.hookRunner.onToolResult(context, callRecord);
|
|
1285
|
-
const enforcement = await this.enforcer.checkResult(callRecord, {
|
|
1286
|
-
previousCalls: context.toolCallHistory,
|
|
1287
|
-
currentStep: context.stepCount,
|
|
1288
|
-
sessionState: context.session.state ?? {},
|
|
1289
|
-
});
|
|
1290
|
-
if (!enforcement.allowed) {
|
|
1291
|
-
const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
|
|
1292
|
-
callRecord.success = false;
|
|
1293
|
-
callRecord.error = new Error(reason);
|
|
1294
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1295
|
-
yield* this.emit(context, {
|
|
1296
|
-
type: 'tool-error',
|
|
1297
|
-
toolCallId,
|
|
1298
|
-
toolName: part.toolName,
|
|
1299
|
-
error: reason,
|
|
1300
|
-
});
|
|
1301
|
-
yield* this.emit(context, { type: 'error', error: reason });
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
yield* this.emit(context, {
|
|
1306
|
-
type: 'tool-result',
|
|
1307
|
-
toolCallId,
|
|
1308
|
-
toolName: part.toolName,
|
|
1309
|
-
result: part.result,
|
|
1310
|
-
});
|
|
1311
|
-
break;
|
|
1312
|
-
}
|
|
1313
|
-
case 'tool-error': {
|
|
1314
|
-
const toolCallId = part.toolCallId
|
|
1315
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.error === undefined)?.toolCallId
|
|
1316
|
-
?? crypto.randomUUID();
|
|
1317
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1318
|
-
if (callRecord) {
|
|
1319
|
-
callRecord.success = false;
|
|
1320
|
-
callRecord.error = new Error(part.error);
|
|
1321
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1322
|
-
context.toolCallHistory.push(callRecord);
|
|
1323
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1324
|
-
}
|
|
1325
|
-
yield* this.emit(context, {
|
|
1326
|
-
type: 'tool-error',
|
|
1327
|
-
toolCallId,
|
|
1328
|
-
toolName: part.toolName,
|
|
1329
|
-
error: part.error,
|
|
1330
|
-
});
|
|
1331
|
-
break;
|
|
1332
|
-
}
|
|
1333
|
-
case 'handoff':
|
|
1334
|
-
onHandoff(part.targetAgent, part.reason);
|
|
1335
|
-
break;
|
|
1336
|
-
case 'node-enter':
|
|
1337
|
-
yield* this.emit(context, { type: 'node-enter', nodeName: part.nodeName });
|
|
1338
|
-
break;
|
|
1339
|
-
case 'node-exit':
|
|
1340
|
-
yield* this.emit(context, { type: 'node-exit', nodeName: part.nodeName });
|
|
1341
|
-
break;
|
|
1342
|
-
case 'flow-transition':
|
|
1343
|
-
yield* this.emit(context, { type: 'flow-transition', from: part.from, to: part.to });
|
|
1344
|
-
break;
|
|
1345
|
-
case 'flow-end':
|
|
1346
|
-
yield* this.emit(context, { type: 'flow-end', reason: part.reason });
|
|
1347
|
-
break;
|
|
1348
|
-
case 'turn-end':
|
|
1349
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1350
|
-
break;
|
|
1351
|
-
case 'error':
|
|
1352
|
-
yield* this.emit(context, { type: 'error', error: part.error });
|
|
1353
|
-
break;
|
|
1354
|
-
default:
|
|
1355
|
-
break;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
// Sanitize/redact anything FlowManager persisted during initialization.
|
|
1359
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, persistedStartIndex);
|
|
1360
|
-
for (const t of tripwires) {
|
|
1361
|
-
yield* this.emit(context, {
|
|
1362
|
-
type: 'tripwire',
|
|
1363
|
-
phase: 'output',
|
|
1364
|
-
processorId: t.processorId,
|
|
1365
|
-
reason: t.reason,
|
|
1366
|
-
message: t.message,
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
persistedStartIndex = context.session.messages.length;
|
|
1370
|
-
}
|
|
1371
|
-
for await (const part of flowManager.process(input, { appendUserToSession: false })) {
|
|
1372
|
-
switch (part.type) {
|
|
1373
|
-
case 'text-delta':
|
|
1374
|
-
yield* this.emit(context, { type: 'text-delta', text: part.text });
|
|
1375
|
-
break;
|
|
1376
|
-
case 'tool-call': {
|
|
1377
|
-
const toolCallId = part.toolCallId ?? crypto.randomUUID();
|
|
1378
|
-
const callRecord = {
|
|
1379
|
-
toolCallId,
|
|
1380
|
-
toolName: part.toolName,
|
|
1381
|
-
args: part.args,
|
|
1382
|
-
success: true,
|
|
1383
|
-
timestamp: Date.now(),
|
|
1384
|
-
};
|
|
1385
|
-
toolCalls.push(callRecord);
|
|
1386
|
-
await this.hookRunner.onToolCall(context, callRecord);
|
|
1387
|
-
yield* this.emit(context, {
|
|
1388
|
-
type: 'tool-call',
|
|
1389
|
-
toolCallId,
|
|
1390
|
-
toolName: part.toolName,
|
|
1391
|
-
args: part.args,
|
|
1392
|
-
});
|
|
1393
|
-
break;
|
|
1394
|
-
}
|
|
1395
|
-
case 'tool-result': {
|
|
1396
|
-
const toolCallId = part.toolCallId
|
|
1397
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.result === undefined)?.toolCallId
|
|
1398
|
-
?? crypto.randomUUID();
|
|
1399
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1400
|
-
if (callRecord) {
|
|
1401
|
-
callRecord.result = part.result;
|
|
1402
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1403
|
-
context.toolCallHistory.push(callRecord);
|
|
1404
|
-
await this.hookRunner.onToolResult(context, callRecord);
|
|
1405
|
-
const enforcement = await this.enforcer.checkResult(callRecord, {
|
|
1406
|
-
previousCalls: context.toolCallHistory,
|
|
1407
|
-
currentStep: context.stepCount,
|
|
1408
|
-
sessionState: context.session.state ?? {},
|
|
1409
|
-
});
|
|
1410
|
-
if (!enforcement.allowed) {
|
|
1411
|
-
const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
|
|
1412
|
-
callRecord.success = false;
|
|
1413
|
-
callRecord.error = new Error(reason);
|
|
1414
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1415
|
-
yield* this.emit(context, {
|
|
1416
|
-
type: 'tool-error',
|
|
1417
|
-
toolCallId,
|
|
1418
|
-
toolName: part.toolName,
|
|
1419
|
-
error: reason,
|
|
1420
|
-
});
|
|
1421
|
-
yield* this.emit(context, { type: 'error', error: reason });
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
yield* this.emit(context, {
|
|
1426
|
-
type: 'tool-result',
|
|
1427
|
-
toolCallId,
|
|
1428
|
-
toolName: part.toolName,
|
|
1429
|
-
result: part.result,
|
|
1430
|
-
});
|
|
1431
|
-
break;
|
|
1432
|
-
}
|
|
1433
|
-
case 'tool-error': {
|
|
1434
|
-
const toolCallId = part.toolCallId
|
|
1435
|
-
?? toolCalls.find(call => call.toolName === part.toolName && call.error === undefined)?.toolCallId
|
|
1436
|
-
?? crypto.randomUUID();
|
|
1437
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
1438
|
-
if (callRecord) {
|
|
1439
|
-
callRecord.success = false;
|
|
1440
|
-
callRecord.error = new Error(part.error);
|
|
1441
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
1442
|
-
context.toolCallHistory.push(callRecord);
|
|
1443
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
1444
|
-
}
|
|
1445
|
-
yield* this.emit(context, {
|
|
1446
|
-
type: 'tool-error',
|
|
1447
|
-
toolCallId,
|
|
1448
|
-
toolName: part.toolName,
|
|
1449
|
-
error: part.error,
|
|
1450
|
-
});
|
|
1451
|
-
break;
|
|
1452
|
-
}
|
|
1453
|
-
case 'handoff':
|
|
1454
|
-
onHandoff(part.targetAgent, part.reason);
|
|
1455
|
-
break;
|
|
1456
|
-
case 'node-enter':
|
|
1457
|
-
yield* this.emit(context, { type: 'node-enter', nodeName: part.nodeName });
|
|
1458
|
-
break;
|
|
1459
|
-
case 'node-exit':
|
|
1460
|
-
yield* this.emit(context, { type: 'node-exit', nodeName: part.nodeName });
|
|
1461
|
-
break;
|
|
1462
|
-
case 'flow-transition':
|
|
1463
|
-
yield* this.emit(context, { type: 'flow-transition', from: part.from, to: part.to });
|
|
1464
|
-
break;
|
|
1465
|
-
case 'flow-end':
|
|
1466
|
-
yield* this.emit(context, { type: 'flow-end', reason: part.reason });
|
|
1467
|
-
break;
|
|
1468
|
-
case 'turn-end':
|
|
1469
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
1470
|
-
break;
|
|
1471
|
-
case 'error':
|
|
1472
|
-
yield* this.emit(context, { type: 'error', error: part.error });
|
|
1473
|
-
break;
|
|
1474
|
-
default:
|
|
1475
|
-
break;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
// Sanitize/redact anything FlowManager persisted during this user turn.
|
|
1479
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, persistedStartIndex);
|
|
1480
|
-
for (const t of tripwires) {
|
|
1481
|
-
yield* this.emit(context, {
|
|
1482
|
-
type: 'tripwire',
|
|
1483
|
-
phase: 'output',
|
|
1484
|
-
processorId: t.processorId,
|
|
1485
|
-
reason: t.reason,
|
|
1486
|
-
message: t.message,
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
if (flowManager.hasEnded) {
|
|
1490
|
-
this.clearFlowState(context.session, agent.id);
|
|
1491
|
-
}
|
|
1492
|
-
else {
|
|
1493
|
-
this.setFlowState(context.session, agent.id, flowManager.getState());
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
1290
|
async getSession(id) {
|
|
1497
1291
|
return this.sessionStore.get(id);
|
|
1498
1292
|
}
|
|
@@ -1505,11 +1299,89 @@ Do not change the flow requirements. Keep the reply concise.`;
|
|
|
1505
1299
|
getAllAgents() {
|
|
1506
1300
|
return Array.from(this.agents.values());
|
|
1507
1301
|
}
|
|
1508
|
-
|
|
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) {
|
|
1509
1380
|
return {
|
|
1510
1381
|
session: context.session,
|
|
1511
1382
|
agentId: context.agentId,
|
|
1512
1383
|
toolCallHistory: context.toolCallHistory,
|
|
1384
|
+
abortSignal,
|
|
1513
1385
|
};
|
|
1514
1386
|
}
|
|
1515
1387
|
getAgentOutputProcessors(agent) {
|
|
@@ -1531,11 +1403,12 @@ Do not change the flow requirements. Keep the reply concise.`;
|
|
|
1531
1403
|
const processors = this.getAgentOutputProcessors(agent);
|
|
1532
1404
|
let cur = text;
|
|
1533
1405
|
if (processors.length > 0) {
|
|
1406
|
+
const abortSignal = this.getActiveAbortController(context.session.id)?.signal;
|
|
1534
1407
|
const outcome = await runOutputProcessors({
|
|
1535
1408
|
processors,
|
|
1536
1409
|
text: cur,
|
|
1537
1410
|
messages: context.session.messages,
|
|
1538
|
-
context: this.buildProcessorContext(context),
|
|
1411
|
+
context: this.buildProcessorContext(context, abortSignal),
|
|
1539
1412
|
});
|
|
1540
1413
|
if (outcome.blocked) {
|
|
1541
1414
|
const msg = this.applyRedactionsToText(outcome.message);
|
|
@@ -1569,46 +1442,53 @@ Do not change the flow requirements. Keep the reply concise.`;
|
|
|
1569
1442
|
if (this.outputRedactions && this.outputRedactions.length > 0) {
|
|
1570
1443
|
const sessionId = context.session.id;
|
|
1571
1444
|
if (part.type === 'text-delta') {
|
|
1572
|
-
const next = this.applyOutputRedactions(
|
|
1445
|
+
const next = this.applyOutputRedactions(context.session, part.text, false);
|
|
1573
1446
|
if (next) {
|
|
1574
1447
|
const redacted = { ...part, text: next };
|
|
1575
|
-
|
|
1576
|
-
yield redacted;
|
|
1448
|
+
yield* this.emitWithHooks(context, redacted);
|
|
1577
1449
|
}
|
|
1578
1450
|
return;
|
|
1579
1451
|
}
|
|
1580
1452
|
if (part.type === 'turn-end' || part.type === 'done') {
|
|
1581
|
-
const flushed = this.applyOutputRedactions(
|
|
1453
|
+
const flushed = this.applyOutputRedactions(context.session, '', true);
|
|
1582
1454
|
if (flushed) {
|
|
1583
1455
|
const carryPart = { type: 'text-delta', text: flushed };
|
|
1584
|
-
|
|
1585
|
-
yield carryPart;
|
|
1456
|
+
yield* this.emitWithHooks(context, carryPart);
|
|
1586
1457
|
}
|
|
1587
|
-
this.
|
|
1458
|
+
delete context.session.workingMemory[this.redactCarryKey];
|
|
1588
1459
|
}
|
|
1589
1460
|
}
|
|
1461
|
+
yield* this.emitWithHooks(context, part);
|
|
1462
|
+
}
|
|
1463
|
+
async *emitWithHooks(context, part) {
|
|
1464
|
+
this.recordRuntimeEvent(context, part);
|
|
1590
1465
|
await this.hookRunner.onStreamPart(context, part);
|
|
1466
|
+
if (this.shouldCheckpointAfterPart(part)) {
|
|
1467
|
+
await this.saveSessionCheckpoint(context.session);
|
|
1468
|
+
}
|
|
1591
1469
|
yield part;
|
|
1592
1470
|
}
|
|
1593
|
-
applyOutputRedactions(
|
|
1471
|
+
applyOutputRedactions(session, text, flush) {
|
|
1594
1472
|
if (!this.outputRedactions || this.outputRedactions.length === 0)
|
|
1595
1473
|
return text;
|
|
1596
|
-
const carry = this.
|
|
1474
|
+
const carry = typeof session.workingMemory[this.redactCarryKey] === 'string'
|
|
1475
|
+
? session.workingMemory[this.redactCarryKey]
|
|
1476
|
+
: '';
|
|
1597
1477
|
let combined = `${carry}${text}`;
|
|
1598
1478
|
for (const r of this.outputRedactions) {
|
|
1599
1479
|
combined = combined.replace(r.re, r.replacement);
|
|
1600
1480
|
}
|
|
1601
1481
|
const keep = flush ? 0 : this.redactLookbehind;
|
|
1602
1482
|
if (keep === 0) {
|
|
1603
|
-
this.
|
|
1483
|
+
session.workingMemory[this.redactCarryKey] = '';
|
|
1604
1484
|
return combined;
|
|
1605
1485
|
}
|
|
1606
1486
|
if (combined.length <= keep) {
|
|
1607
|
-
this.
|
|
1487
|
+
session.workingMemory[this.redactCarryKey] = combined;
|
|
1608
1488
|
return '';
|
|
1609
1489
|
}
|
|
1610
1490
|
const out = combined.slice(0, combined.length - keep);
|
|
1611
|
-
this.
|
|
1491
|
+
session.workingMemory[this.redactCarryKey] = combined.slice(-keep);
|
|
1612
1492
|
return out;
|
|
1613
1493
|
}
|
|
1614
1494
|
}
|