@memberjunction/ai-agents 5.39.0 → 5.40.1
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/AgentRunner.d.ts +23 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +81 -26
- package/dist/AgentRunner.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +12 -1
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +16 -0
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +85 -15
- package/dist/base-agent.js.map +1 -1
- package/package.json +17 -17
package/dist/base-agent.js
CHANGED
|
@@ -292,6 +292,22 @@ export class BaseAgent {
|
|
|
292
292
|
* @private
|
|
293
293
|
*/
|
|
294
294
|
static { this.MAX_CONSECUTIVE_FAILED_STEPS = 10; }
|
|
295
|
+
/**
|
|
296
|
+
* Maximum consecutive *unproductive* retry steps before forcing termination.
|
|
297
|
+
*
|
|
298
|
+
* An unproductive retry is a 'Retry' next-step that carries an errorMessage — i.e. one
|
|
299
|
+
* produced by {@link BaseAgentType.createRetryStep} because the model's output could not
|
|
300
|
+
* be parsed or failed structural validation (e.g. the LLM returned conversational prose
|
|
301
|
+
* instead of the required JSON envelope). These do NOT count as 'Failed' steps, so they
|
|
302
|
+
* bypass {@link MAX_CONSECUTIVE_FAILED_STEPS} entirely and — without this guard — loop
|
|
303
|
+
* until the far-higher absolute iteration cap (effectively forever, burning time and tokens).
|
|
304
|
+
*
|
|
305
|
+
* Legitimate yield/await retries (pipeline / client-tools / sub-agent re-entry) are created
|
|
306
|
+
* via createNextStep('Retry', …) WITHOUT an errorMessage, so they do not increment this
|
|
307
|
+
* counter. Any productive (non-unproductive-retry) step resets it.
|
|
308
|
+
* @private
|
|
309
|
+
*/
|
|
310
|
+
static { this.MAX_CONSECUTIVE_UNPRODUCTIVE_RETRIES = 10; }
|
|
295
311
|
/**
|
|
296
312
|
* Returns the active metadata provider for this agent run. Subclasses MUST
|
|
297
313
|
* use this getter (rather than `new Metadata()` or `Metadata.Provider`) so
|
|
@@ -1216,6 +1232,7 @@ export class BaseAgent {
|
|
|
1216
1232
|
let currentNextStep = null;
|
|
1217
1233
|
let stepCount = 0;
|
|
1218
1234
|
let consecutiveFailedSteps = 0;
|
|
1235
|
+
let consecutiveUnproductiveRetries = 0;
|
|
1219
1236
|
while (continueExecution) {
|
|
1220
1237
|
// Check for cancellation before each step
|
|
1221
1238
|
if (params.cancellationToken?.aborted) {
|
|
@@ -1255,6 +1272,37 @@ export class BaseAgent {
|
|
|
1255
1272
|
else if (nextStep.step !== 'Failed') {
|
|
1256
1273
|
consecutiveFailedSteps = 0;
|
|
1257
1274
|
}
|
|
1275
|
+
// Track consecutive *unproductive* retries to prevent infinite loops that the
|
|
1276
|
+
// consecutive-failed-steps net above cannot catch. A model that repeatedly returns
|
|
1277
|
+
// output we can't parse/validate (e.g. conversational prose instead of the required
|
|
1278
|
+
// JSON envelope) yields a stream of 'Retry' steps — never 'Failed' — so the failed-step
|
|
1279
|
+
// counter resets every turn and never trips. Such retries are produced via
|
|
1280
|
+
// createRetryStep(), which always sets an errorMessage; legitimate yield/await retries
|
|
1281
|
+
// (pipeline / client-tools / sub-agent re-entry) carry no errorMessage and are exempt.
|
|
1282
|
+
const isUnproductiveRetry = nextStep.step === 'Retry' && !nextStep.terminate && !!nextStep.errorMessage;
|
|
1283
|
+
if (isUnproductiveRetry) {
|
|
1284
|
+
consecutiveUnproductiveRetries++;
|
|
1285
|
+
if (consecutiveUnproductiveRetries >= BaseAgent.MAX_CONSECUTIVE_UNPRODUCTIVE_RETRIES) {
|
|
1286
|
+
this.logError(`⛔ Agent '${params.agent.Name}' reached maximum consecutive unproductive retries ` +
|
|
1287
|
+
`(${BaseAgent.MAX_CONSECUTIVE_UNPRODUCTIVE_RETRIES}). The model is repeatedly returning output ` +
|
|
1288
|
+
`that cannot be parsed or validated. Forcing termination to prevent infinite loop.`, {
|
|
1289
|
+
agent: params.agent,
|
|
1290
|
+
category: 'ExecutionSafetyNet',
|
|
1291
|
+
metadata: {
|
|
1292
|
+
consecutiveUnproductiveRetries,
|
|
1293
|
+
lastError: nextStep.errorMessage
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
nextStep.step = 'Failed';
|
|
1297
|
+
nextStep.terminate = true;
|
|
1298
|
+
nextStep.errorMessage = `Agent terminated after ${consecutiveUnproductiveRetries} consecutive unproductive retries ` +
|
|
1299
|
+
`(model repeatedly returned output that could not be parsed or validated). ` +
|
|
1300
|
+
`Last error: ${nextStep.errorMessage || 'Unknown'}`;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
consecutiveUnproductiveRetries = 0;
|
|
1305
|
+
}
|
|
1258
1306
|
// Check if we should continue or terminate
|
|
1259
1307
|
if (nextStep.terminate) {
|
|
1260
1308
|
continueExecution = false;
|
|
@@ -5628,9 +5676,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5628
5676
|
}
|
|
5629
5677
|
});
|
|
5630
5678
|
// Add assistant message indicating we're executing a sub-agent
|
|
5679
|
+
// Recorded as a `user`-role environment annotation (not an `assistant` turn) for the same
|
|
5680
|
+
// reason as the action record above: the model's real output is the JSON envelope, and
|
|
5681
|
+
// storing framework prose as an `assistant` turn trains strong in-context models to imitate
|
|
5682
|
+
// the prose and drift off the required JSON format. See the note at the action-record push.
|
|
5631
5683
|
params.conversationMessages.push({
|
|
5632
|
-
role: '
|
|
5633
|
-
content: `
|
|
5684
|
+
role: 'user',
|
|
5685
|
+
content: `[You delegated this task to the "${subAgentRequest.name}" agent. Reason: ${subAgentRequest.message}]`
|
|
5634
5686
|
});
|
|
5635
5687
|
// Prepare input data for the step
|
|
5636
5688
|
const inputData = {
|
|
@@ -6030,9 +6082,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6030
6082
|
hierarchicalStep: this.buildHierarchicalStep(stepCount + 1, this._parentStepCounts)
|
|
6031
6083
|
}
|
|
6032
6084
|
});
|
|
6085
|
+
// `user`-role environment annotation (not an `assistant` turn) — see the note on the
|
|
6086
|
+
// single-delegation push above for why framework prose must not be stored as assistant turns.
|
|
6033
6087
|
params.conversationMessages.push({
|
|
6034
|
-
role: '
|
|
6035
|
-
content: `
|
|
6088
|
+
role: 'user',
|
|
6089
|
+
content: `[You delegated this task to the parallel sub-agent "${request.name}". Reason: ${request.message}]`
|
|
6036
6090
|
});
|
|
6037
6091
|
return { request: request, subAgentEntity, relationship };
|
|
6038
6092
|
}
|
|
@@ -6326,9 +6380,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6326
6380
|
}
|
|
6327
6381
|
});
|
|
6328
6382
|
// Add assistant message indicating we're executing a related sub-agent
|
|
6383
|
+
// Recorded as a `user`-role environment annotation (not an `assistant` turn) for the same
|
|
6384
|
+
// reason as the action record above: the model's real output is the JSON envelope, and
|
|
6385
|
+
// storing framework prose as an `assistant` turn trains strong in-context models to imitate
|
|
6386
|
+
// the prose and drift off the required JSON format. See the note at the action-record push.
|
|
6329
6387
|
params.conversationMessages.push({
|
|
6330
|
-
role: '
|
|
6331
|
-
content: `
|
|
6388
|
+
role: 'user',
|
|
6389
|
+
content: `[You delegated this task to the "${subAgentRequest.name}" agent. Reason: ${subAgentRequest.message}]`
|
|
6332
6390
|
});
|
|
6333
6391
|
// Prepare input data for the step
|
|
6334
6392
|
const inputData = {
|
|
@@ -6767,12 +6825,23 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6767
6825
|
},
|
|
6768
6826
|
displayMode: 'live' // Only show in live mode
|
|
6769
6827
|
});
|
|
6770
|
-
// Build detailed action
|
|
6771
|
-
// This
|
|
6828
|
+
// Build a detailed record of the action(s) invoked, with parameters, in markdown.
|
|
6829
|
+
// This is a permanent, lightweight memory of what was requested.
|
|
6830
|
+
//
|
|
6831
|
+
// IMPORTANT — this record is injected as a `user`-role environment annotation, NOT an
|
|
6832
|
+
// `assistant` turn. The model's actual output is the JSON envelope, but we don't store
|
|
6833
|
+
// that raw JSON; we store this human-readable summary instead. If it were recorded as an
|
|
6834
|
+
// `assistant` turn, then after a few action-heavy turns the model's entire visible
|
|
6835
|
+
// assistant history would be prose like "I'm executing the X action with parameters: …",
|
|
6836
|
+
// and strong in-context learners (e.g. Gemini Flash) imitate that demonstrated pattern
|
|
6837
|
+
// over the system-prompt instruction — drifting into prose and breaking JSON parsing,
|
|
6838
|
+
// which (pre-guardrail) looped forever. Phrasing it in second person under the `user`
|
|
6839
|
+
// role keeps the memory while removing the false assistant-prose exemplar. The
|
|
6840
|
+
// human-facing narration is emitted separately via onProgress above.
|
|
6772
6841
|
let actionMessage;
|
|
6773
6842
|
if (actions.length === 1) {
|
|
6774
6843
|
const aa = actions[0];
|
|
6775
|
-
actionMessage = `
|
|
6844
|
+
actionMessage = `[You invoked the **${aa.name}** action`;
|
|
6776
6845
|
// Add parameters if they exist
|
|
6777
6846
|
if (aa.params && Object.keys(aa.params).length > 0) {
|
|
6778
6847
|
const paramsList = Object.entries(aa.params)
|
|
@@ -6781,14 +6850,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6781
6850
|
return `• **${key}**: ${displayValue}`;
|
|
6782
6851
|
})
|
|
6783
6852
|
.join('\n');
|
|
6784
|
-
actionMessage += ` with parameters:\n${paramsList}`;
|
|
6853
|
+
actionMessage += ` with parameters:\n${paramsList}\n]`;
|
|
6785
6854
|
}
|
|
6786
6855
|
else {
|
|
6787
|
-
actionMessage += '.';
|
|
6856
|
+
actionMessage += '.]';
|
|
6788
6857
|
}
|
|
6789
6858
|
}
|
|
6790
6859
|
else {
|
|
6791
|
-
actionMessage = `
|
|
6860
|
+
actionMessage = `[You invoked **${actions.length} actions** in parallel:\n\n` + actions.map((aa, index) => {
|
|
6792
6861
|
let actionText = `${index + 1}. **${aa.name}**`;
|
|
6793
6862
|
// Add parameters if they exist
|
|
6794
6863
|
if (aa.params && Object.keys(aa.params).length > 0) {
|
|
@@ -6801,12 +6870,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6801
6870
|
actionText += `\n${paramsList}`;
|
|
6802
6871
|
}
|
|
6803
6872
|
return actionText;
|
|
6804
|
-
}).join('\n\n');
|
|
6873
|
+
}).join('\n\n') + '\n]';
|
|
6805
6874
|
}
|
|
6806
6875
|
if (addConversationMessage) {
|
|
6807
|
-
//
|
|
6876
|
+
// Record as a `user`-role environment annotation (no metadata - permanent record).
|
|
6877
|
+
// See the note above on why this is NOT an `assistant` turn.
|
|
6808
6878
|
params.conversationMessages.push({
|
|
6809
|
-
role: '
|
|
6879
|
+
role: 'user',
|
|
6810
6880
|
content: actionMessage
|
|
6811
6881
|
});
|
|
6812
6882
|
}
|