@falai/agent 2.4.1 → 2.4.3
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/dist/cjs/core/PersistenceManager.d.ts +13 -0
- package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
- package/dist/cjs/core/PersistenceManager.js +29 -6
- package/dist/cjs/core/PersistenceManager.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +15 -3
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +7 -0
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
- package/dist/cjs/core/SignalProcessor.js +28 -11
- package/dist/cjs/core/SignalProcessor.js.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.js +11 -0
- package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +16 -2
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +115 -54
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js +8 -31
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/core/toolGates.d.ts +24 -0
- package/dist/cjs/core/toolGates.d.ts.map +1 -0
- package/dist/cjs/core/toolGates.js +52 -0
- package/dist/cjs/core/toolGates.js.map +1 -0
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +24 -3
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +24 -3
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.js +25 -3
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/types/signals.d.ts +6 -0
- package/dist/cjs/types/signals.d.ts.map +1 -1
- package/dist/cjs/utils/index.d.ts +2 -2
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +2 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +21 -0
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +37 -0
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/core/PersistenceManager.d.ts +13 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -1
- package/dist/core/PersistenceManager.js +29 -6
- package/dist/core/PersistenceManager.js.map +1 -1
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +15 -3
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +7 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SignalProcessor.d.ts.map +1 -1
- package/dist/core/SignalProcessor.js +28 -11
- package/dist/core/SignalProcessor.js.map +1 -1
- package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/core/StreamingToolExecutor.js +11 -0
- package/dist/core/StreamingToolExecutor.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +16 -2
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +115 -54
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js +8 -31
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/core/toolGates.d.ts +24 -0
- package/dist/core/toolGates.d.ts.map +1 -0
- package/dist/core/toolGates.js +49 -0
- package/dist/core/toolGates.js.map +1 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +25 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +25 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.js +26 -4
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/types/signals.d.ts +6 -0
- package/dist/types/signals.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +21 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +36 -0
- package/dist/utils/retry.js.map +1 -1
- package/docs/reference/signals.md +3 -0
- package/package.json +1 -1
- package/src/core/PersistenceManager.ts +30 -6
- package/src/core/ResponseModal.ts +16 -3
- package/src/core/SessionManager.ts +7 -0
- package/src/core/SignalProcessor.ts +34 -11
- package/src/core/StreamingToolExecutor.ts +12 -0
- package/src/core/ToolLoopExecutor.ts +133 -58
- package/src/core/ToolManager.ts +8 -32
- package/src/core/toolGates.ts +72 -0
- package/src/providers/AnthropicProvider.ts +35 -8
- package/src/providers/GeminiProvider.ts +37 -4
- package/src/providers/OpenAICompatibleProvider.ts +36 -8
- package/src/types/signals.ts +6 -0
- package/src/utils/index.ts +2 -2
- package/src/utils/retry.ts +55 -0
package/dist/utils/retry.js
CHANGED
|
@@ -68,4 +68,40 @@ export const withTimeoutAndRetry = async (operation, timeoutMs = 60000, maxRetri
|
|
|
68
68
|
},
|
|
69
69
|
});
|
|
70
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Streaming analog of {@link withTimeoutAndRetry}. Re-runs an async-generator
|
|
73
|
+
* factory as long as it fails *before yielding its first chunk* — e.g. an
|
|
74
|
+
* empty completion that throws "No response", or an error while establishing
|
|
75
|
+
* the stream. Once any chunk has been yielded the stream is committed and
|
|
76
|
+
* further errors propagate, so a retry can never double-emit deltas that the
|
|
77
|
+
* consumer has already received.
|
|
78
|
+
*
|
|
79
|
+
* This mirrors the non-streaming path, where the provider throws on an empty
|
|
80
|
+
* completion inside `withTimeoutAndRetry` and is retried on the same model
|
|
81
|
+
* before the caller falls through to backup models.
|
|
82
|
+
*/
|
|
83
|
+
export async function* withStreamRetry(factory, options = {}) {
|
|
84
|
+
const { maxRetries = 3, delay = (attempt) => Math.min(1000 * Math.pow(2, attempt), 5000), operationName = "AI stream", } = options;
|
|
85
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
86
|
+
let yielded = false;
|
|
87
|
+
try {
|
|
88
|
+
for await (const chunk of factory()) {
|
|
89
|
+
yielded = true;
|
|
90
|
+
yield chunk;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Can't retry once deltas are out, and don't retry past the budget.
|
|
96
|
+
if (yielded || attempt === maxRetries) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
logger.error(`[${operationName}] Failed attempt ${attempt + 1}:`, message);
|
|
101
|
+
const delayMs = delay(attempt);
|
|
102
|
+
logger.debug(`[${operationName}] Retrying in ${delayMs}ms... (attempt ${attempt + 2}/${maxRetries + 1})`);
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
71
107
|
//# sourceMappingURL=retry.js.map
|
package/dist/utils/retry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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"}
|
|
@@ -99,6 +99,7 @@ interface SignalFiring<TContext = unknown, TData = unknown> {
|
|
|
99
99
|
extracted?: unknown;
|
|
100
100
|
directive?: ResolvedSignalDirective<TContext, TData>;
|
|
101
101
|
handlerError?: string;
|
|
102
|
+
extractionError?: string;
|
|
102
103
|
durationMs?: number;
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -205,6 +206,7 @@ One entry per signal that fired this turn. Populated in fire order across both p
|
|
|
205
206
|
| `extracted` | `unknown` | Extracted payload when in extraction mode. |
|
|
206
207
|
| `directive` | `ResolvedSignalDirective \| undefined` | The directive returned (or dispatched) by the handler, with `replyWith` already resolved onto `reply`. |
|
|
207
208
|
| `handlerError` | `string \| undefined` | Error message if the handler threw. The turn continues — handler errors never break a turn. |
|
|
209
|
+
| `extractionError` | `string \| undefined` | Set when an extraction-mode signal matched but the classifier returned no `extracted` payload (model omitted the field or returned `null`). The handler still runs with `extracted: undefined`; this makes the miss observable instead of silent. Independent of `handlerError`. |
|
|
208
210
|
| `durationMs` | `number \| undefined` | Wall-clock duration of the handler invocation. |
|
|
209
211
|
|
|
210
212
|
### `SignalsState` and `SignalTriggerState`
|
|
@@ -353,6 +355,7 @@ Soft failures handled in-band (no thrown error, turn continues):
|
|
|
353
355
|
|
|
354
356
|
- **Handler throws** — recorded as `firings[i].handlerError`; iteration continues with the next signal.
|
|
355
357
|
- **Classifier call fails** — all LLM-conditioned signals in that batch are treated as non-match; code-only and unconditional signals continue normally.
|
|
358
|
+
- **Extraction-mode signal matched but no payload returned** — recorded as `firings[i].extractionError` with a WARN log; the handler still runs with `extracted: undefined`. Usually means the `extract` schema used a keyword the provider ignores (e.g. `nullable: true` instead of `type: ['string', 'null']`), so the model omitted the field.
|
|
356
359
|
- **Post-phase emits pre-LLM-only fields** (`halt` / `appendPrompt` / `injectTools`) — dropped with debug warning.
|
|
357
360
|
- **`behavior: 'cooldown'` with no `cooldownMs`** — debug warning at construction; runtime treats as `'always'`.
|
|
358
361
|
|
package/package.json
CHANGED
|
@@ -42,6 +42,24 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
42
42
|
this.messageRepository = config.adapter.messageRepository;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Reject a non-string id before it reaches a repository query filter.
|
|
47
|
+
*
|
|
48
|
+
* SECURITY: session/user ids flow into adapter query VALUE positions (e.g.
|
|
49
|
+
* Mongo `findOne({ id })`). A non-string value such as `{ $ne: null }` —
|
|
50
|
+
* trivially produced by an HTTP body or an Express `?id[$ne]=` query — would
|
|
51
|
+
* be interpreted by a NoSQL driver as query operators, turning an equality
|
|
52
|
+
* lookup into an operator query (cross-tenant read / mass delete). Callers
|
|
53
|
+
* type these as `string`, but that guarantee is erased at runtime, so we
|
|
54
|
+
* enforce it here: the single layer every public session/user lookup funnels
|
|
55
|
+
* through.
|
|
56
|
+
*/
|
|
57
|
+
private assertScalarId(value: unknown, label: string): asserts value is string {
|
|
58
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
59
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
/**
|
|
46
64
|
* Create a new session
|
|
47
65
|
*/
|
|
@@ -67,6 +85,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
67
85
|
* Get session by ID
|
|
68
86
|
*/
|
|
69
87
|
async getSession(sessionId: string): Promise<SessionData<TData> | null> {
|
|
88
|
+
this.assertScalarId(sessionId, "Session ID");
|
|
70
89
|
return await this.sessionRepository.findById(sessionId);
|
|
71
90
|
}
|
|
72
91
|
|
|
@@ -80,6 +99,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
80
99
|
"userId must be provided or configured in PersistenceConfig"
|
|
81
100
|
);
|
|
82
101
|
}
|
|
102
|
+
this.assertScalarId(effectiveUserId, "User ID");
|
|
83
103
|
return await this.sessionRepository.findActiveByUserId(effectiveUserId);
|
|
84
104
|
}
|
|
85
105
|
|
|
@@ -96,6 +116,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
96
116
|
"userId must be provided or configured in PersistenceConfig"
|
|
97
117
|
);
|
|
98
118
|
}
|
|
119
|
+
this.assertScalarId(effectiveUserId, "User ID");
|
|
99
120
|
return await this.sessionRepository.findByUserId(effectiveUserId, limit);
|
|
100
121
|
}
|
|
101
122
|
|
|
@@ -170,6 +191,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
170
191
|
sessionId: string,
|
|
171
192
|
limit?: number
|
|
172
193
|
): Promise<MessageData[]> {
|
|
194
|
+
this.assertScalarId(sessionId, "Session ID");
|
|
173
195
|
return await this.messageRepository.findBySessionId(sessionId, limit);
|
|
174
196
|
}
|
|
175
197
|
|
|
@@ -186,6 +208,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
186
208
|
"userId must be provided or configured in PersistenceConfig"
|
|
187
209
|
);
|
|
188
210
|
}
|
|
211
|
+
this.assertScalarId(effectiveUserId, "User ID");
|
|
189
212
|
return await this.messageRepository.findByUserId(effectiveUserId, limit);
|
|
190
213
|
}
|
|
191
214
|
|
|
@@ -193,6 +216,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
193
216
|
* Delete a session and all its messages
|
|
194
217
|
*/
|
|
195
218
|
async deleteSession(sessionId: string): Promise<boolean> {
|
|
219
|
+
this.assertScalarId(sessionId, "Session ID");
|
|
196
220
|
// Delete all messages first
|
|
197
221
|
await this.messageRepository.deleteBySessionId(sessionId);
|
|
198
222
|
|
|
@@ -260,9 +284,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
260
284
|
sessionStep: SessionState<TData>
|
|
261
285
|
): Promise<SessionData<TData> | null> {
|
|
262
286
|
// Validate input parameters
|
|
263
|
-
|
|
264
|
-
throw new Error('Session ID must be a non-empty string');
|
|
265
|
-
}
|
|
287
|
+
this.assertScalarId(sessionId, "Session ID");
|
|
266
288
|
|
|
267
289
|
if (!sessionStep || typeof sessionStep !== 'object') {
|
|
268
290
|
throw new Error('Session step must be a valid object');
|
|
@@ -307,9 +329,10 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
307
329
|
} else {
|
|
308
330
|
saved = await this.sessionRepository.create({
|
|
309
331
|
id: sessionId,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
332
|
+
// Owner is the authenticated principal from config — never derived
|
|
333
|
+
// from collectedData.metadata (model/user-influenced state), which
|
|
334
|
+
// would let collected data mis-attribute a session's tenant.
|
|
335
|
+
userId: this.config.userId,
|
|
313
336
|
status: "active",
|
|
314
337
|
currentFlow: persistenceData.currentFlow,
|
|
315
338
|
currentStep: persistenceData.currentStep,
|
|
@@ -342,6 +365,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
|
|
|
342
365
|
async loadSessionState(
|
|
343
366
|
sessionId: string
|
|
344
367
|
): Promise<SessionState<TData> | null> {
|
|
368
|
+
this.assertScalarId(sessionId, "Session ID");
|
|
345
369
|
const sessionData = await this.sessionRepository.findById(sessionId);
|
|
346
370
|
|
|
347
371
|
if (!sessionData) {
|
|
@@ -1243,6 +1243,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1243
1243
|
// Stream chunks with unified tool handling
|
|
1244
1244
|
for await (const chunk of stream) {
|
|
1245
1245
|
let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
|
|
1246
|
+
// Final message/structured may be replaced by a forced post-tool
|
|
1247
|
+
// response (see runStreamingBatch / gap: tools-ran-but-no-text).
|
|
1248
|
+
let finalAccumulated = chunk.accumulated;
|
|
1249
|
+
let finalStructured = chunk.structured;
|
|
1246
1250
|
|
|
1247
1251
|
// Extract tool calls from AI response on final chunk
|
|
1248
1252
|
if (chunk.done && chunk.structured?.toolCalls) {
|
|
@@ -1265,9 +1269,18 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1265
1269
|
});
|
|
1266
1270
|
session = batchResult.session;
|
|
1267
1271
|
toolCalls = batchResult.toolCalls;
|
|
1272
|
+
|
|
1273
|
+
// Tools ran but the model produced no result-aware text — use
|
|
1274
|
+
// the forced closing message so we never emit the bare
|
|
1275
|
+
// preamble (or an empty message) as the final response.
|
|
1276
|
+
if (batchResult.finalMessage) {
|
|
1277
|
+
finalAccumulated = batchResult.finalMessage;
|
|
1278
|
+
finalStructured = batchResult.structured ?? finalStructured;
|
|
1279
|
+
}
|
|
1268
1280
|
}
|
|
1269
1281
|
|
|
1270
|
-
// Extract collected data on final chunk
|
|
1282
|
+
// Extract collected data on final chunk (from the model's own
|
|
1283
|
+
// structured output for this step, not the forced follow-up)
|
|
1271
1284
|
if (chunk.done && chunk.structured && nextStep.collect) {
|
|
1272
1285
|
session = await this.collectDataFromResponse({
|
|
1273
1286
|
result: { structured: chunk.structured },
|
|
@@ -1283,7 +1296,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1283
1296
|
// - session.currentStep: reflects the executed step
|
|
1284
1297
|
yield {
|
|
1285
1298
|
delta: chunk.delta,
|
|
1286
|
-
accumulated:
|
|
1299
|
+
accumulated: finalAccumulated,
|
|
1287
1300
|
done: chunk.done,
|
|
1288
1301
|
session,
|
|
1289
1302
|
toolCalls,
|
|
@@ -1291,7 +1304,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1291
1304
|
executedSteps: chunk.done ? [{ id: nextStep.id, flowId: selectedFlow.id }] : undefined,
|
|
1292
1305
|
stoppedReason: chunk.done ? 'needs_input' : undefined,
|
|
1293
1306
|
metadata: chunk.metadata,
|
|
1294
|
-
structured:
|
|
1307
|
+
structured: finalStructured,
|
|
1295
1308
|
appliedInstructions: chunk.done ? appliedInstructions : undefined,
|
|
1296
1309
|
};
|
|
1297
1310
|
}
|
|
@@ -59,6 +59,13 @@ export class SessionManager<TData = unknown> {
|
|
|
59
59
|
* Works for sessionIds that exist, don't exist, or auto-generated IDs
|
|
60
60
|
*/
|
|
61
61
|
async getOrCreate(sessionId?: string): Promise<SessionState<TData>> {
|
|
62
|
+
// SECURITY: a non-string sessionId (e.g. `{ $ne: null }` from an HTTP body
|
|
63
|
+
// or `?sessionId[$ne]=` query) forwarded into a NoSQL query becomes an
|
|
64
|
+
// operator clause. Reject it at the entry point; `undefined` is allowed and
|
|
65
|
+
// auto-generates an id, and an empty string keeps its fall-back semantics.
|
|
66
|
+
if (sessionId !== undefined && typeof sessionId !== "string") {
|
|
67
|
+
throw new Error("sessionId must be a string");
|
|
68
|
+
}
|
|
62
69
|
// Use provided sessionId or fall back to default
|
|
63
70
|
const effectiveSessionId = sessionId || this.defaultSessionId;
|
|
64
71
|
|
|
@@ -27,6 +27,11 @@ import type {
|
|
|
27
27
|
import type { SignalEvaluator } from "./SignalEvaluator";
|
|
28
28
|
import { logger } from "../utils";
|
|
29
29
|
|
|
30
|
+
/** Recorded on `SignalFiring.extractionError` when a matched extraction-mode
|
|
31
|
+
* signal yields no payload (model omitted the field or returned null). */
|
|
32
|
+
const EXTRACTION_MISSING_MESSAGE =
|
|
33
|
+
"Signal matched in extraction mode but the classifier returned no extracted payload.";
|
|
34
|
+
|
|
30
35
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
31
36
|
// behaviorAllowsExecution — Algorithm 2
|
|
32
37
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -454,23 +459,29 @@ export class SignalProcessor<TContext = unknown, TData = unknown> {
|
|
|
454
459
|
}
|
|
455
460
|
}
|
|
456
461
|
|
|
457
|
-
// ── STEP 6b:
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
// ── STEP 6b: extraction for code-matched signals ─────────────────────
|
|
463
|
+
// Signals matched by code — unconditional (no `when`/`if`) or code-only
|
|
464
|
+
// (`if` without `when`) — that carry an `extract` schema still need a
|
|
465
|
+
// dedicated extraction call. LLM-conditioned signals already had their
|
|
466
|
+
// extraction merged into the STEP 6 classifier batch, so they are
|
|
467
|
+
// excluded here by identity. Without this, an `if` + `extract` signal
|
|
468
|
+
// (e.g. the documented `leadStage` example) would reach its handler with
|
|
469
|
+
// `ctx.extracted` permanently undefined.
|
|
470
|
+
const codeMatchedWithExtract = matched.filter(
|
|
471
|
+
(m) => m.signal.extract != null && !llmConditioned.includes(m.signal),
|
|
472
|
+
);
|
|
473
|
+
if (codeMatchedWithExtract.length > 0) {
|
|
460
474
|
const extractResults = await this.evaluator.evaluateSignals({
|
|
461
|
-
signals:
|
|
475
|
+
signals: codeMatchedWithExtract.map((m) => m.signal),
|
|
462
476
|
session,
|
|
463
477
|
history,
|
|
464
478
|
context,
|
|
465
479
|
});
|
|
466
480
|
|
|
467
|
-
// Attach extracted data
|
|
468
|
-
for (const
|
|
469
|
-
const id =
|
|
470
|
-
|
|
471
|
-
if (entry) {
|
|
472
|
-
entry.extracted = extractResults[id]?.extracted;
|
|
473
|
-
}
|
|
481
|
+
// Attach extracted data back onto the already-matched entries.
|
|
482
|
+
for (const m of codeMatchedWithExtract) {
|
|
483
|
+
const id = m.signal.id ?? 'unknown';
|
|
484
|
+
m.extracted = extractResults[id]?.extracted;
|
|
474
485
|
}
|
|
475
486
|
}
|
|
476
487
|
|
|
@@ -490,6 +501,16 @@ export class SignalProcessor<TContext = unknown, TData = unknown> {
|
|
|
490
501
|
// Turn-local directive bus for this handler (captures dispatch calls)
|
|
491
502
|
const handlerBus: SignalDirective<TContext, TData>[] = [];
|
|
492
503
|
|
|
504
|
+
// A signal in extraction mode that matched but produced no payload
|
|
505
|
+
// would otherwise pass `extracted: undefined` to the handler with no
|
|
506
|
+
// trace. Surface the miss on the firing and in logs instead.
|
|
507
|
+
const extractionMissing = m.signal.extract != null && m.extracted == null;
|
|
508
|
+
if (extractionMissing) {
|
|
509
|
+
logger.warn(
|
|
510
|
+
`[Signals] Signal "${m.signal.id ?? 'unknown'}" matched in extraction mode ` +
|
|
511
|
+
`but the classifier returned no extracted payload; handler receives extracted: undefined.`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
493
514
|
|
|
494
515
|
const sigCtx = buildSignalContext<TContext, TData, any>({
|
|
495
516
|
signal: m.signal,
|
|
@@ -555,6 +576,7 @@ export class SignalProcessor<TContext = unknown, TData = unknown> {
|
|
|
555
576
|
extracted: m.extracted,
|
|
556
577
|
directive: resolvedDirective,
|
|
557
578
|
durationMs: handlerDurationMs,
|
|
579
|
+
...(extractionMissing ? { extractionError: EXTRACTION_MISSING_MESSAGE } : {}),
|
|
558
580
|
});
|
|
559
581
|
|
|
560
582
|
// stopOtherSignals → break iteration
|
|
@@ -574,6 +596,7 @@ export class SignalProcessor<TContext = unknown, TData = unknown> {
|
|
|
574
596
|
reason: m.reason,
|
|
575
597
|
handlerError: errorMessage,
|
|
576
598
|
durationMs: handlerDurationMs,
|
|
599
|
+
...(extractionMissing ? { extractionError: EXTRACTION_MISSING_MESSAGE } : {}),
|
|
577
600
|
});
|
|
578
601
|
|
|
579
602
|
// Continue iteration — handler errors never break the turn
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
Tool,
|
|
20
20
|
TrackedTool,
|
|
21
21
|
} from "../types/tool";
|
|
22
|
+
import { evaluateToolGates } from "./toolGates";
|
|
22
23
|
|
|
23
24
|
/** Options for the StreamingToolExecutor */
|
|
24
25
|
interface StreamingToolExecutorOptions {
|
|
@@ -221,6 +222,17 @@ export class StreamingToolExecutor<TContext = unknown, TData = unknown> {
|
|
|
221
222
|
const batchAbortController = this.siblingAbortController;
|
|
222
223
|
|
|
223
224
|
try {
|
|
225
|
+
// Pre-execution gates (validateInput → checkPermissions), shared with
|
|
226
|
+
// ToolManager.executeTool. These MUST run on the streaming path too:
|
|
227
|
+
// when a gate denies, the handler is NOT invoked. Run before any abort
|
|
228
|
+
// wiring so a denial returns cleanly with nothing to unwind.
|
|
229
|
+
const gateDenial = await evaluateToolGates(tool, toolCall.arguments, this.toolContext);
|
|
230
|
+
if (gateDenial) {
|
|
231
|
+
tracked.results.push(gateDenial);
|
|
232
|
+
tracked.status = "completed";
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
224
236
|
// Create a combined abort signal from parent + sibling
|
|
225
237
|
const toolAbortController = new AbortController();
|
|
226
238
|
const abortTool = () => {
|
|
@@ -147,7 +147,9 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
|
|
150
|
-
|
|
150
|
+
// `??` so an explicit `maxToolLoops: 0` is honored instead of being
|
|
151
|
+
// clobbered to the default by a falsy-zero check.
|
|
152
|
+
const MAX_TOOL_LOOPS = this.deps.maxToolLoops ?? 5;
|
|
151
153
|
let toolLoopCount = 0;
|
|
152
154
|
let hasToolCalls = toolCalls && toolCalls.length > 0;
|
|
153
155
|
let finalMessage: string | undefined;
|
|
@@ -296,63 +298,26 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
// If tools were executed but no final text message was produced,
|
|
299
|
-
// make one more LLM call to generate a proper text response from tool
|
|
300
|
-
// This prevents the original tool-invocation message (e.g.
|
|
301
|
-
// from being returned as the final user-facing
|
|
301
|
+
// make one more LLM call to generate a proper text response from tool
|
|
302
|
+
// results. This prevents the original tool-invocation message (e.g.
|
|
303
|
+
// "Let me check...") from being returned as the final user-facing
|
|
304
|
+
// response. Shared with the streaming path via forceFinalTextFromTools.
|
|
302
305
|
if (!finalMessage && toolLoopCount > 0) {
|
|
303
306
|
logger.debug(`[ToolLoopExecutor] No final message after tool loop, making additional LLM call for text response`);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
id: toolName,
|
|
316
|
-
name: toolName,
|
|
317
|
-
arguments: toolArgsMap.get(toolName) || {},
|
|
318
|
-
}],
|
|
319
|
-
});
|
|
320
|
-
finalToolResultHistoryItems.push({
|
|
321
|
-
role: "tool" as const,
|
|
322
|
-
tool_call_id: toolName,
|
|
323
|
-
name: toolName,
|
|
324
|
-
content: toolResult,
|
|
325
|
-
});
|
|
307
|
+
const forced = await this.forceFinalTextFromTools({
|
|
308
|
+
history,
|
|
309
|
+
toolResultsMap,
|
|
310
|
+
toolArgsMap,
|
|
311
|
+
responsePrompt,
|
|
312
|
+
responseSchema,
|
|
313
|
+
context,
|
|
314
|
+
signal,
|
|
315
|
+
});
|
|
316
|
+
if (forced.finalMessage) {
|
|
317
|
+
finalMessage = forced.finalMessage;
|
|
326
318
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const agentOptions = this.deps.getAgentOptions();
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
const textResult = await agentOptions.provider.generateMessage({
|
|
333
|
-
prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
|
|
334
|
-
history: finalHistory,
|
|
335
|
-
context,
|
|
336
|
-
tools: [], // No tools - force text response
|
|
337
|
-
parameters: responseSchema ? {
|
|
338
|
-
jsonSchema: responseSchema,
|
|
339
|
-
schemaName: "tool_final_text",
|
|
340
|
-
} : undefined,
|
|
341
|
-
signal,
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
finalMessage = textResult.structured?.message || textResult.message;
|
|
345
|
-
if (textResult.structured) {
|
|
346
|
-
followUpStructured = textResult.structured;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
logger.debug(`[ToolLoopExecutor] Generated final text response after tool loop:`, {
|
|
350
|
-
hasMessage: !!finalMessage,
|
|
351
|
-
messageLength: finalMessage?.length || 0,
|
|
352
|
-
});
|
|
353
|
-
} catch (error) {
|
|
354
|
-
logger.error(`[ToolLoopExecutor] Failed to generate final text response after tool loop:`, error);
|
|
355
|
-
// finalMessage remains undefined; caller will use original message as fallback
|
|
319
|
+
if (forced.structured) {
|
|
320
|
+
followUpStructured = forced.structured;
|
|
356
321
|
}
|
|
357
322
|
}
|
|
358
323
|
|
|
@@ -377,12 +342,83 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
377
342
|
}
|
|
378
343
|
}
|
|
379
344
|
|
|
345
|
+
/**
|
|
346
|
+
* After tools have executed but the model produced no closing text, make one
|
|
347
|
+
* more LLM call (no tools) that turns the tool results into a user-facing
|
|
348
|
+
* message. Shared by the non-streaming tool loop and the streaming batch so
|
|
349
|
+
* both paths behave identically. Returns an empty object if the call fails
|
|
350
|
+
* or yields nothing, leaving the caller to fall back to its prior message.
|
|
351
|
+
*/
|
|
352
|
+
private async forceFinalTextFromTools(params: {
|
|
353
|
+
history: HistoryItem[];
|
|
354
|
+
toolResultsMap: Map<string, string>;
|
|
355
|
+
toolArgsMap: Map<string, Record<string, unknown>>;
|
|
356
|
+
responsePrompt: string;
|
|
357
|
+
responseSchema?: Record<string, unknown>;
|
|
358
|
+
context: TContext;
|
|
359
|
+
signal?: AbortSignal;
|
|
360
|
+
}): Promise<{ finalMessage?: string; structured?: AgentStructuredResponse }> {
|
|
361
|
+
const { history, toolResultsMap, toolArgsMap, responsePrompt, responseSchema, context, signal } = params;
|
|
362
|
+
|
|
363
|
+
// Reconstruct assistant tool_call + tool result pairs so the follow-up
|
|
364
|
+
// call can see what the tools returned.
|
|
365
|
+
const finalToolResultHistoryItems: HistoryItem[] = [];
|
|
366
|
+
for (const [toolName, toolResult] of toolResultsMap) {
|
|
367
|
+
finalToolResultHistoryItems.push({
|
|
368
|
+
role: "assistant" as const,
|
|
369
|
+
content: null,
|
|
370
|
+
tool_calls: [{
|
|
371
|
+
id: toolName,
|
|
372
|
+
name: toolName,
|
|
373
|
+
arguments: toolArgsMap.get(toolName) || {},
|
|
374
|
+
}],
|
|
375
|
+
});
|
|
376
|
+
finalToolResultHistoryItems.push({
|
|
377
|
+
role: "tool" as const,
|
|
378
|
+
tool_call_id: toolName,
|
|
379
|
+
name: toolName,
|
|
380
|
+
content: toolResult,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const finalHistory = [...history, ...finalToolResultHistoryItems];
|
|
385
|
+
const agentOptions = this.deps.getAgentOptions();
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const textResult = await agentOptions.provider.generateMessage({
|
|
389
|
+
prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
|
|
390
|
+
history: finalHistory,
|
|
391
|
+
context,
|
|
392
|
+
tools: [], // No tools - force text response
|
|
393
|
+
parameters: responseSchema ? {
|
|
394
|
+
jsonSchema: responseSchema,
|
|
395
|
+
schemaName: "tool_final_text",
|
|
396
|
+
} : undefined,
|
|
397
|
+
signal,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const finalMessage = textResult.structured?.message || textResult.message;
|
|
401
|
+
logger.debug(`[ToolLoopExecutor] Generated final text response from tool results:`, {
|
|
402
|
+
hasMessage: !!finalMessage,
|
|
403
|
+
messageLength: finalMessage?.length || 0,
|
|
404
|
+
});
|
|
405
|
+
return { finalMessage, structured: textResult.structured };
|
|
406
|
+
} catch (error) {
|
|
407
|
+
logger.error(`[ToolLoopExecutor] Failed to generate final text response from tool results:`, error);
|
|
408
|
+
// Leave the caller to fall back to its prior message.
|
|
409
|
+
return {};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
380
413
|
/**
|
|
381
414
|
* Execute the streaming path's initial batch of tool calls concurrently
|
|
382
415
|
* via ToolManager.executeWithConcurrency, yielding tool-progress chunks.
|
|
383
|
-
*
|
|
416
|
+
* When tools ran but the model produced no closing text, forces a final
|
|
417
|
+
* text response from the tool results (mirroring `runLoop()`). Falls back
|
|
418
|
+
* to `runLoop()` when concurrent execution fails.
|
|
384
419
|
*
|
|
385
|
-
* Returns the updated session
|
|
420
|
+
* Returns the updated session, final tool calls, and any forced closing
|
|
421
|
+
* message/structured response.
|
|
386
422
|
*/
|
|
387
423
|
async *runStreamingBatch(params: {
|
|
388
424
|
toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
@@ -407,6 +443,9 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
407
443
|
{
|
|
408
444
|
session: SessionState<TData>;
|
|
409
445
|
toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined;
|
|
446
|
+
/** Closing message forced from the tool results, if one was generated. */
|
|
447
|
+
finalMessage?: string;
|
|
448
|
+
structured?: AgentStructuredResponse;
|
|
410
449
|
}
|
|
411
450
|
> {
|
|
412
451
|
const { context, history, selectedFlow, step, accumulated, responsePrompt, availableTools, responseSchema, signal } = params;
|
|
@@ -419,9 +458,17 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
419
458
|
toolName: tc.toolName,
|
|
420
459
|
arguments: tc.arguments,
|
|
421
460
|
}));
|
|
461
|
+
// Map request id -> request so completed results can be attributed back
|
|
462
|
+
// to their tool name/arguments for the forced final-text call.
|
|
463
|
+
const requestById = new Map(toolCallRequests.map((r) => [r.id, r]));
|
|
464
|
+
const toolResultsMap = new Map<string, string>();
|
|
465
|
+
const toolArgsMap = new Map<string, Record<string, unknown>>();
|
|
422
466
|
|
|
423
467
|
const historyEvents = historyToEvents(history);
|
|
424
468
|
|
|
469
|
+
let finalMessage: string | undefined;
|
|
470
|
+
let structured: AgentStructuredResponse | undefined;
|
|
471
|
+
|
|
425
472
|
try {
|
|
426
473
|
for await (const update of this.deps.toolManager.executeWithConcurrency({
|
|
427
474
|
toolCalls: toolCallRequests,
|
|
@@ -450,6 +497,15 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
450
497
|
}
|
|
451
498
|
}
|
|
452
499
|
|
|
500
|
+
// Capture tool results for the forced final-text call
|
|
501
|
+
if (update.result) {
|
|
502
|
+
const req = requestById.get(update.toolCallId);
|
|
503
|
+
if (req) {
|
|
504
|
+
toolResultsMap.set(req.toolName, serializeToolResult(update.result));
|
|
505
|
+
toolArgsMap.set(req.toolName, req.arguments);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
453
509
|
// Yield progress updates immediately
|
|
454
510
|
if (update.progress) {
|
|
455
511
|
yield {
|
|
@@ -465,6 +521,23 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
465
521
|
}
|
|
466
522
|
|
|
467
523
|
logger.debug(`[ToolLoopExecutor] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
|
|
524
|
+
|
|
525
|
+
// Tools ran but streaming produced no post-tool message — force one
|
|
526
|
+
// from the results, mirroring runLoop. (The streaming chunk only ever
|
|
527
|
+
// carries the pre-tool preamble, never a result-aware response.)
|
|
528
|
+
if (toolResultsMap.size > 0) {
|
|
529
|
+
const forced = await this.forceFinalTextFromTools({
|
|
530
|
+
history,
|
|
531
|
+
toolResultsMap,
|
|
532
|
+
toolArgsMap,
|
|
533
|
+
responsePrompt,
|
|
534
|
+
responseSchema,
|
|
535
|
+
context,
|
|
536
|
+
signal,
|
|
537
|
+
});
|
|
538
|
+
finalMessage = forced.finalMessage;
|
|
539
|
+
structured = forced.structured;
|
|
540
|
+
}
|
|
468
541
|
} catch (error) {
|
|
469
542
|
logger.error(`[ToolLoopExecutor] Concurrent tool execution failed, falling back to sequential:`, error);
|
|
470
543
|
// Fall back to the unified tool loop on failure
|
|
@@ -474,9 +547,11 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
474
547
|
});
|
|
475
548
|
session = toolResult.session;
|
|
476
549
|
toolCalls = toolResult.finalToolCalls;
|
|
550
|
+
finalMessage = toolResult.finalMessage;
|
|
551
|
+
structured = toolResult.structured;
|
|
477
552
|
}
|
|
478
553
|
|
|
479
|
-
return { session, toolCalls };
|
|
554
|
+
return { session, toolCalls, finalMessage, structured };
|
|
480
555
|
}
|
|
481
556
|
|
|
482
557
|
/**
|