@memberjunction/ai-agents 6.1.0-edge.6 → 6.1.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/dist/SkillImportExportService.d.ts +17 -1
- package/dist/SkillImportExportService.d.ts.map +1 -1
- package/dist/SkillImportExportService.js +57 -5
- package/dist/SkillImportExportService.js.map +1 -1
- package/dist/SkillMarkdownConverter.d.ts +17 -1
- package/dist/SkillMarkdownConverter.d.ts.map +1 -1
- package/dist/SkillMarkdownConverter.js +40 -5
- package/dist/SkillMarkdownConverter.js.map +1 -1
- package/dist/agent-types/base-agent-type.d.ts +26 -1
- package/dist/agent-types/base-agent-type.d.ts.map +1 -1
- package/dist/agent-types/base-agent-type.js +17 -0
- package/dist/agent-types/base-agent-type.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +16 -0
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js +19 -1
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts +32 -1
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +190 -3
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +186 -3
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +564 -53
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/native-tools/action-tool-builder.d.ts +92 -0
- package/dist/native-tools/action-tool-builder.d.ts.map +1 -0
- package/dist/native-tools/action-tool-builder.js +226 -0
- package/dist/native-tools/action-tool-builder.js.map +1 -0
- package/dist/native-tools/control-tools.d.ts +52 -0
- package/dist/native-tools/control-tools.d.ts.map +1 -0
- package/dist/native-tools/control-tools.js +113 -0
- package/dist/native-tools/control-tools.js.map +1 -0
- package/dist/native-tools/dual-channel.d.ts +16 -0
- package/dist/native-tools/dual-channel.d.ts.map +1 -0
- package/dist/native-tools/dual-channel.js +44 -0
- package/dist/native-tools/dual-channel.js.map +1 -0
- package/dist/native-tools/tool-result-turns.d.ts +32 -0
- package/dist/native-tools/tool-result-turns.d.ts.map +1 -0
- package/dist/native-tools/tool-result-turns.js +29 -0
- package/dist/native-tools/tool-result-turns.js.map +1 -0
- package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
- package/dist/realtime/bridge-realtime-session-factory.js +22 -2
- package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
- package/dist/realtime/realtime-client-session-service.d.ts +104 -17
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +325 -28
- package/dist/realtime/realtime-client-session-service.js.map +1 -1
- package/dist/realtime/realtime-coagent-config.d.ts +61 -1
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
- package/dist/realtime/realtime-coagent-config.js +93 -1
- package/dist/realtime/realtime-coagent-config.js.map +1 -1
- package/dist/realtime/realtime-session-runner.d.ts +10 -8
- package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
- package/dist/realtime/realtime-session-runner.js +11 -9
- package/dist/realtime/realtime-session-runner.js.map +1 -1
- package/dist/realtime/realtime-tool-broker.d.ts +11 -5
- package/dist/realtime/realtime-tool-broker.d.ts.map +1 -1
- package/dist/realtime/realtime-tool-broker.js +19 -7
- package/dist/realtime/realtime-tool-broker.js.map +1 -1
- package/dist/scoped-prompt-config-resolver.d.ts +6 -1
- package/dist/scoped-prompt-config-resolver.d.ts.map +1 -1
- package/dist/scoped-prompt-config-resolver.js.map +1 -1
- package/package.json +18 -18
package/dist/base-agent.js
CHANGED
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
* @since 2.49.0
|
|
12
12
|
*/
|
|
13
13
|
import { FileStorageEngineBase, MJEnvironmentEntityExtended } from '@memberjunction/core-entities';
|
|
14
|
+
import { buildActionToolSet, filterDeclarableActions, sanitizeToolName } from './native-tools/action-tool-builder.js';
|
|
15
|
+
import { buildNativeToolSet, SUB_AGENT_TOOL_PREFIX } from './native-tools/control-tools.js';
|
|
16
|
+
import { buildAssistantToolCallTurn, buildToolResultTurn, compactToolResultContent } from './native-tools/tool-result-turns.js';
|
|
17
|
+
import { looksLikeLoopEnvelope } from './native-tools/dual-channel.js';
|
|
14
18
|
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
|
|
15
19
|
import { AgentRunWatchdog } from './agent-run-watchdog.js';
|
|
16
|
-
import { AIPromptRunner } from '@memberjunction/ai-prompts';
|
|
20
|
+
import { AIPromptRunner, GetToolCallingDecision } from '@memberjunction/ai-prompts';
|
|
17
21
|
import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
|
|
18
22
|
import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
19
23
|
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, SafeExpressionEvaluator, UUIDsEqual, EscapeSQLString } from '@memberjunction/global';
|
|
@@ -143,10 +147,6 @@ export class BaseAgent {
|
|
|
143
147
|
* @private
|
|
144
148
|
*/
|
|
145
149
|
this._generalValidationRetryCount = 0;
|
|
146
|
-
/**
|
|
147
|
-
* Current agent run entity.
|
|
148
|
-
* @private
|
|
149
|
-
*/
|
|
150
150
|
this._agentRun = null;
|
|
151
151
|
/**
|
|
152
152
|
* The task graph this run submitted and is now waiting on, or null.
|
|
@@ -480,6 +480,21 @@ export class BaseAgent {
|
|
|
480
480
|
get AgentTypeInstance() {
|
|
481
481
|
return this._agentTypeInstance;
|
|
482
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Current agent run entity.
|
|
485
|
+
* @private
|
|
486
|
+
*/
|
|
487
|
+
/**
|
|
488
|
+
* System-wide safety net on prompt iterations, overridable per run via
|
|
489
|
+
* `ExecuteAgentParams.absoluteMaxIterations`.
|
|
490
|
+
*
|
|
491
|
+
* A static rather than a local const because two places now depend on the same number: the
|
|
492
|
+
* limit check that STOPS a run, and {@link isFinalPermittedIteration}, which has to predict
|
|
493
|
+
* that stop one turn ahead. Two copies of 5000 would be a silent mismatch the moment either
|
|
494
|
+
* moved — the gate would force `tool_choice: 'none'` on the wrong turn, or fail to force it
|
|
495
|
+
* on the right one, and neither shows up as an error.
|
|
496
|
+
*/
|
|
497
|
+
static { this.DEFAULT_ABSOLUTE_MAX_ITERATIONS = 5000; }
|
|
483
498
|
/**
|
|
484
499
|
* The resolved FileStorageAccount ID for this agent run. Set during Execute()
|
|
485
500
|
* via the hierarchical resolution chain (Runtime → Agent → Category → Type → fallback).
|
|
@@ -2923,6 +2938,296 @@ export class BaseAgent {
|
|
|
2923
2938
|
* @returns {Promise<AIPromptParams>} Configured prompt parameters
|
|
2924
2939
|
* @protected
|
|
2925
2940
|
*/
|
|
2941
|
+
/**
|
|
2942
|
+
* Declares the agent's Actions as native tools on the outgoing request (plan §8.1/§8.3).
|
|
2943
|
+
*
|
|
2944
|
+
* **Supplying tools does not turn native mode on.** It satisfies one of three gate terms; the
|
|
2945
|
+
* prompt runner still requires the model to declare the capability and the configuration to
|
|
2946
|
+
* want it (`ResolveNativeToolCalling`). Because no catalog row declares the capability today,
|
|
2947
|
+
* this is inert — the tools are built, the gate says no, and the run takes the envelope path
|
|
2948
|
+
* exactly as before. That is deliberate: the switch is a metadata change, not a code change.
|
|
2949
|
+
*
|
|
2950
|
+
* `tool_choice` is `'auto'` on a normal turn. §8.3 also specifies `'none'` when the framework
|
|
2951
|
+
* needs a control-flow decision rather than an action — the model must produce the envelope
|
|
2952
|
+
* then, and forcing it is the only way to be sure it can.
|
|
2953
|
+
*/
|
|
2954
|
+
/**
|
|
2955
|
+
* Stamps the step with which tool-calling path it took and how much the model used it (§8.5).
|
|
2956
|
+
*
|
|
2957
|
+
* The mode is derivable through `TargetLogID` -> `AIPromptRun.ToolCallingMode`, but only for
|
|
2958
|
+
* steps whose target is a prompt run and only through a join that returns nothing for every
|
|
2959
|
+
* other step type. The call count is not derivable at all — it lives in the provider response,
|
|
2960
|
+
* which is not persisted per step. Both are recorded here so a comparison of the two paths can
|
|
2961
|
+
* group by them directly instead of reconstructing its own independent variable.
|
|
2962
|
+
*
|
|
2963
|
+
* Instrumentation must never fail a run, so every field is best-effort.
|
|
2964
|
+
*/
|
|
2965
|
+
recordToolCallingInstrumentation(stepEntity, promptResult) {
|
|
2966
|
+
try {
|
|
2967
|
+
const mode = promptResult?.promptRun?.ToolCallingMode;
|
|
2968
|
+
if (mode) {
|
|
2969
|
+
stepEntity.ToolCallingMode = mode;
|
|
2970
|
+
}
|
|
2971
|
+
// Counted only on the native path: null means "envelope", which is different from a
|
|
2972
|
+
// native turn where the model chose not to call anything (0).
|
|
2973
|
+
if (mode === 'Native' || mode === 'NativeFallback' || mode === 'NativeImplicit') {
|
|
2974
|
+
const message = promptResult?.chatResult?.data?.choices?.[0]?.message;
|
|
2975
|
+
const callCount = message?.toolCalls?.length ?? 0;
|
|
2976
|
+
stepEntity.NativeToolCallCount = callCount;
|
|
2977
|
+
// A tool call wins, but a turn that ALSO carried a valid
|
|
2978
|
+
// envelope gave two answers, and the one we discard has to be counted somewhere.
|
|
2979
|
+
stepEntity.NativeDualChannel = callCount > 0 ? looksLikeLoopEnvelope(message?.content) : null;
|
|
2980
|
+
// whether this step's results went back as native tool-result turns.
|
|
2981
|
+
stepEntity.NativeToolResultsSent = GetToolCallingDecision(promptResult?.chatResult)?.toolResults === true;
|
|
2982
|
+
if (stepEntity.NativeDualChannel) {
|
|
2983
|
+
LogStatus(`Agent step answered on both channels: ${callCount} tool call(s) plus a JSON envelope; the envelope was discarded (tool call wins).`);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
if (mode) {
|
|
2987
|
+
this.queueStepSave(stepEntity, (st) => {
|
|
2988
|
+
st.ToolCallingMode = stepEntity.ToolCallingMode;
|
|
2989
|
+
st.NativeToolCallCount = stepEntity.NativeToolCallCount;
|
|
2990
|
+
st.NativeDualChannel = stepEntity.NativeDualChannel;
|
|
2991
|
+
st.NativeToolResultsSent = stepEntity.NativeToolResultsSent;
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
catch (error) {
|
|
2996
|
+
LogError(`Could not record tool-calling instrumentation on agent run step: ${error instanceof Error ? error.message : String(error)}`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* replays the model's own tool-call turn into history once per turn, so the tool-result
|
|
3001
|
+
* turns that follow have a call to answer (every provider requires it; BaseLLM validates it).
|
|
3002
|
+
* A no-op when the turn's results go back as the markdown user message.
|
|
3003
|
+
*/
|
|
3004
|
+
appendNativeAssistantTurn(params, step) {
|
|
3005
|
+
const turn = step.nativeTurn;
|
|
3006
|
+
if (!turn?.sendResultsNatively || this._lastNativeTurnAppended === turn) {
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
params.conversationMessages.push(buildAssistantToolCallTurn(turn));
|
|
3010
|
+
this._lastNativeTurnAppended = turn;
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* The tool_result answering a `payload_change_request` the model made on the SAME turn as its
|
|
3014
|
+
* actions or its delegation.
|
|
3015
|
+
*
|
|
3016
|
+
* The framework applies that change before the rest of the turn runs, so its call has to be
|
|
3017
|
+
* answered alongside them — and inside the same tool turn, since Anthropic requires every
|
|
3018
|
+
* tool_result for an assistant turn to sit in the one message that follows it. Returns an empty
|
|
3019
|
+
* array when the step carried no payload call, so callers can always spread it.
|
|
3020
|
+
*/
|
|
3021
|
+
payloadToolResult(previousDecision) {
|
|
3022
|
+
if (!previousDecision?.payloadToolCallId) {
|
|
3023
|
+
return [];
|
|
3024
|
+
}
|
|
3025
|
+
return [{
|
|
3026
|
+
toolCallId: previousDecision.payloadToolCallId,
|
|
3027
|
+
toolName: 'payload_change_request',
|
|
3028
|
+
content: 'Payload change applied.',
|
|
3029
|
+
isError: false
|
|
3030
|
+
}];
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* action results as ONE tool turn — a tool_result block per call, paired by id — when the
|
|
3034
|
+
* turn's results go back natively; otherwise the markdown user message as before. An action that
|
|
3035
|
+
* cannot be paired with a call id keeps the markdown message for itself.
|
|
3036
|
+
*/
|
|
3037
|
+
appendActionResults(params, summaries, resultsMessage, metadata, previousDecision) {
|
|
3038
|
+
if (!previousDecision.nativeTurn?.sendResultsNatively) {
|
|
3039
|
+
params.conversationMessages.push({ role: 'user', content: resultsMessage, metadata });
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
const unpaired = [...(previousDecision.actions ?? [])];
|
|
3043
|
+
const results = [...this.payloadToolResult(previousDecision)];
|
|
3044
|
+
const orphans = [];
|
|
3045
|
+
for (const summary of summaries) {
|
|
3046
|
+
const at = unpaired.findIndex((a) => a.name === summary.actionName && !!a.toolCallId);
|
|
3047
|
+
const action = at >= 0 ? unpaired.splice(at, 1)[0] : undefined;
|
|
3048
|
+
if (!action?.toolCallId) {
|
|
3049
|
+
orphans.push(summary);
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
results.push({
|
|
3053
|
+
toolCallId: action.toolCallId,
|
|
3054
|
+
toolName: sanitizeToolName(summary.actionName),
|
|
3055
|
+
content: this.formatActionResultsAsMarkdown([summary]),
|
|
3056
|
+
isError: !summary.success
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
if (results.length > 0) {
|
|
3060
|
+
params.conversationMessages.push(buildToolResultTurn(results, metadata));
|
|
3061
|
+
}
|
|
3062
|
+
if (orphans.length > 0) {
|
|
3063
|
+
params.conversationMessages.push({ role: 'user', content: `Action results:\n${this.formatActionResultsAsMarkdown(orphans)}`, metadata });
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Answers any tool call the turn's own result path left dangling, immediately before the
|
|
3068
|
+
* history goes back to the model.
|
|
3069
|
+
*
|
|
3070
|
+
* `appendNativeAssistantTurn` replays the model's call turn for EVERY step that carries one,
|
|
3071
|
+
* but only Actions, Sub-Agents and the payload-only Retry append results for it. An
|
|
3072
|
+
* unknown-tool Retry, a protocol-violation Retry, an `ask_user` Chat, or a parallel dispatch
|
|
3073
|
+
* that could not pair one of its ids therefore leaves calls unanswered — which Anthropic
|
|
3074
|
+
* ("Each `tool_use` block must have a corresponding `tool_result` block in the next message"),
|
|
3075
|
+
* OpenAI ("an assistant message with `tool_calls` must be followed by tool messages
|
|
3076
|
+
* responding to each `tool_call_id`") and Gemini all reject outright. `validateToolConversation`
|
|
3077
|
+
* in BaseLLM cannot catch it: it validates results→calls, never calls→results.
|
|
3078
|
+
*
|
|
3079
|
+
* Reconciling here rather than in each branch means a new step type cannot reintroduce the bug.
|
|
3080
|
+
* The synthetic result states only that the call did not run; the reason travels in whatever
|
|
3081
|
+
* message the branch itself appended (the retry instructions, the chat question).
|
|
3082
|
+
*/
|
|
3083
|
+
reconcileUnansweredToolCalls(params) {
|
|
3084
|
+
const messages = params.conversationMessages;
|
|
3085
|
+
if (!messages?.length) {
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
// Only the most recent assistant call turn can still be open — anything earlier was
|
|
3089
|
+
// answered by its own branch or closed by a previous pass through here.
|
|
3090
|
+
let at = -1;
|
|
3091
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3092
|
+
const candidate = messages[i];
|
|
3093
|
+
if (candidate.role === 'assistant' && candidate.toolCalls?.length) {
|
|
3094
|
+
at = i;
|
|
3095
|
+
break;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
if (at < 0) {
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const answered = new Set();
|
|
3102
|
+
for (let i = at + 1; i < messages.length; i++) {
|
|
3103
|
+
const content = messages[i].content;
|
|
3104
|
+
if (!Array.isArray(content)) {
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
for (const block of content) {
|
|
3108
|
+
if (block.type === 'tool_result' && block.toolCallId) {
|
|
3109
|
+
answered.add(block.toolCallId);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
// A call with no id cannot be paired by any provider, so it cannot be answered here
|
|
3114
|
+
// either — `extractOpenAICompatibleToolCalls` substitutes '' when a host omits the id.
|
|
3115
|
+
const unanswered = (messages[at].toolCalls ?? [])
|
|
3116
|
+
.filter((call) => !!call.id && !answered.has(call.id));
|
|
3117
|
+
if (unanswered.length === 0) {
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
params.conversationMessages.push(buildToolResultTurn(unanswered.map((call) => ({
|
|
3121
|
+
toolCallId: call.id,
|
|
3122
|
+
toolName: call.name,
|
|
3123
|
+
content: 'Not executed — the agent did not run this call on this turn. See the message that follows.',
|
|
3124
|
+
isError: true
|
|
3125
|
+
})), { turnAdded: this._promptTurnCount, messageType: 'action-result' }));
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* a tool turn can never be REMOVED from history — that orphans the assistant call it
|
|
3129
|
+
* answers, which every provider rejects. Expiry and recovery stub its blocks instead.
|
|
3130
|
+
*/
|
|
3131
|
+
stubToolTurn(message, note) {
|
|
3132
|
+
return compactToolResultContent(message, () => note);
|
|
3133
|
+
}
|
|
3134
|
+
applyNativeTools(promptParams, params) {
|
|
3135
|
+
this._nativeToolBindings = undefined;
|
|
3136
|
+
// Only a type that can read the call back may be offered tools — see
|
|
3137
|
+
// BaseAgentType.SupportsNativeToolCalls for what goes wrong otherwise. With no declarations
|
|
3138
|
+
// the gate's `toolsProvided` term is false and the run takes the envelope path unchanged,
|
|
3139
|
+
// whatever the catalog or prompt asked for.
|
|
3140
|
+
if (!this._agentTypeInstance?.SupportsNativeToolCalls) {
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
// A per-AGENT gate on declaration, one level below the agent-type
|
|
3144
|
+
// gate above. A coordinator whose prompt says it never does work itself keeps its Actions in
|
|
3145
|
+
// the prose catalog; with nothing declared the runner's gate resolves Envelope for this run.
|
|
3146
|
+
if (params.agent?.DeclareActionsAsNativeTools === false) {
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
// ...and a per-agent-ACTION gate: rows that opt out are removed before the tool set is built.
|
|
3150
|
+
const actions = filterDeclarableActions(this.getEffectiveActionsForValidation(params.agent.ID), AIEngine.Instance.AgentActions.filter((aa) => UUIDsEqual(aa.AgentID, params.agent.ID)));
|
|
3151
|
+
const subAgents = this.getEffectiveSubAgentsForValidation(params.agent.ID);
|
|
3152
|
+
if (actions.length === 0 && subAgents.length === 0) {
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
try {
|
|
3156
|
+
const actionSet = buildActionToolSet(actions, new Map(actions.map((a) => [a.ID, a.Params.Items])));
|
|
3157
|
+
// Under implicit control flow the agent cannot know which model will answer, so it declares the full
|
|
3158
|
+
// set — Actions plus the control-flow tools (one per sub-agent, payload_change_request,
|
|
3159
|
+
// ask_user) — and NAMES the control ones. The runner keeps them only when the selected
|
|
3160
|
+
// model's LLM.NativeControlFlow resolves to 'implicit'; a hybrid model never sees them.
|
|
3161
|
+
const toolSet = buildNativeToolSet(actionSet, subAgents);
|
|
3162
|
+
promptParams.tools = toolSet.tools;
|
|
3163
|
+
promptParams.controlFlowToolNames = toolSet.controlToolNames;
|
|
3164
|
+
promptParams.toolChoice = this.resolveToolChoiceForTurn(params);
|
|
3165
|
+
this._nativeToolBindings = toolSet.byToolName;
|
|
3166
|
+
}
|
|
3167
|
+
catch (error) {
|
|
3168
|
+
// A tool-name collision is a metadata problem and a hard error at build time — but it
|
|
3169
|
+
// must not take down a run that would otherwise work on the envelope path, which every
|
|
3170
|
+
// agent still does today. Surface it loudly and continue without tools.
|
|
3171
|
+
LogError(`Agent '${params.agent.Name}': could not declare native tools — ${error instanceof Error ? error.message : String(error)}`);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* The `tool_choice` for this turn (§8.3).
|
|
3176
|
+
*
|
|
3177
|
+
* `'auto'` normally, `'none'` on the last turn this run will be allowed.
|
|
3178
|
+
*
|
|
3179
|
+
* **Why the final turn is special.** A tool call is a request to continue: the framework runs
|
|
3180
|
+
* the action, feeds the result back, and the model decides again. On the last permitted
|
|
3181
|
+
* iteration there is no "again" — the limit check fires the moment the turn returns, so the
|
|
3182
|
+
* action is executed, paid for, and its result discarded, and the run ends with no answer for
|
|
3183
|
+
* the user because the model spent its last turn asking a question instead of answering one.
|
|
3184
|
+
* Forcing `'none'` converts that turn into what the framework actually needs from it: a
|
|
3185
|
+
* terminal envelope.
|
|
3186
|
+
*
|
|
3187
|
+
* **What this does NOT fix.** Models call a tool on a measurable share of turns whose right
|
|
3188
|
+
* answer was chat, completion or delegation. Those are not predictable
|
|
3189
|
+
* from framework state — only the model knows the task is finished — so no `tool_choice` can
|
|
3190
|
+
* address them. That belongs to the prompt, and is why the native-mode Actions section names
|
|
3191
|
+
* the cases explicitly.
|
|
3192
|
+
*
|
|
3193
|
+
* Subclasses may narrow this further; the base contract is that a forced `'none'` must never be
|
|
3194
|
+
* relaxed to `'auto'` on a turn the framework has already decided is terminal.
|
|
3195
|
+
*
|
|
3196
|
+
* @param params The run parameters, for the per-run iteration override
|
|
3197
|
+
* @returns `'none'` on the final permitted iteration, otherwise `'auto'`
|
|
3198
|
+
*/
|
|
3199
|
+
resolveToolChoiceForTurn(params) {
|
|
3200
|
+
return this.isFinalPermittedIteration(params) ? 'none' : 'auto';
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Whether the turn currently being prepared is the last one this run will be allowed.
|
|
3204
|
+
*
|
|
3205
|
+
* Reads `TotalPromptIterations`, which the loop increments immediately BEFORE composing the
|
|
3206
|
+
* prompt — so by the time this runs the counter already includes the turn about to go out, and
|
|
3207
|
+
* `iterations >= limit` means "this turn is the last", not "the last one already happened".
|
|
3208
|
+
* That off-by-one is the whole subtlety and is why this lives beside the limit check it mirrors.
|
|
3209
|
+
*
|
|
3210
|
+
* Returns false when no run is in flight: the eval harness composes parameters through
|
|
3211
|
+
* `BaseAgent` without executing a loop, and a composed-but-never-run turn has no iteration
|
|
3212
|
+
* budget to be at the end of.
|
|
3213
|
+
*
|
|
3214
|
+
* Only the ITERATION limits are predicted. Cost, token and time limits also stop a run, but
|
|
3215
|
+
* none can be known before the turn that crosses them — guessing would force `'none'` on turns
|
|
3216
|
+
* that had budget left, which silently disables native tool calling rather than bounding it.
|
|
3217
|
+
*
|
|
3218
|
+
* @param params The run parameters, for `absoluteMaxIterations`
|
|
3219
|
+
* @returns true when the framework will stop the run after this turn
|
|
3220
|
+
*/
|
|
3221
|
+
isFinalPermittedIteration(params) {
|
|
3222
|
+
const iterations = this._agentRun?.TotalPromptIterations;
|
|
3223
|
+
if (!iterations) {
|
|
3224
|
+
return false;
|
|
3225
|
+
}
|
|
3226
|
+
const perAgent = params.agent?.MaxIterationsPerRun;
|
|
3227
|
+
const absolute = params.absoluteMaxIterations ?? BaseAgent.DEFAULT_ABSOLUTE_MAX_ITERATIONS;
|
|
3228
|
+
return (typeof perAgent === 'number' && perAgent > 0 && iterations >= perAgent)
|
|
3229
|
+
|| (absolute > 0 && iterations >= absolute);
|
|
3230
|
+
}
|
|
2926
3231
|
async preparePromptParams(config, payload, params) {
|
|
2927
3232
|
const agentType = config.agentType;
|
|
2928
3233
|
const systemPrompt = config.systemPrompt;
|
|
@@ -2946,8 +3251,11 @@ export class BaseAgent {
|
|
|
2946
3251
|
}
|
|
2947
3252
|
promptParams.data = promptTemplateData;
|
|
2948
3253
|
promptParams.contextUser = params.contextUser;
|
|
3254
|
+
// Last gate before the history goes back to the model: no tool call may be left dangling.
|
|
3255
|
+
this.reconcileUnansweredToolCalls(params);
|
|
2949
3256
|
promptParams.conversationMessages = params.conversationMessages;
|
|
2950
3257
|
promptParams.verbose = params.verbose; // Pass through verbose flag
|
|
3258
|
+
this.applyNativeTools(promptParams, params);
|
|
2951
3259
|
// Apply effortLevel with precedence hierarchy
|
|
2952
3260
|
// 1. params.effortLevel (ExecuteAgentParams - highest priority)
|
|
2953
3261
|
// 2. agent.DefaultPromptEffortLevel (agent default - medium priority)
|
|
@@ -3152,7 +3460,7 @@ export class BaseAgent {
|
|
|
3152
3460
|
async determineNextStep(params, agentType, promptResult, currentPayload) {
|
|
3153
3461
|
// Let the agent type determine the next step
|
|
3154
3462
|
this.logStatus(`🎯 Agent type '${agentType.Name}' determining next step`, true, params);
|
|
3155
|
-
const nextStep = await this.AgentTypeInstance.DetermineNextStep(promptResult, params, currentPayload, this.AgentTypeState);
|
|
3463
|
+
const nextStep = await this.AgentTypeInstance.DetermineNextStep(promptResult, params, currentPayload, this.AgentTypeState, this._nativeToolBindings);
|
|
3156
3464
|
return nextStep;
|
|
3157
3465
|
}
|
|
3158
3466
|
/**
|
|
@@ -3902,8 +4210,7 @@ export class BaseAgent {
|
|
|
3902
4210
|
this.applyTokenStatsToRun(agentRun, this.calculateTokenStats());
|
|
3903
4211
|
}
|
|
3904
4212
|
// Check absolute maximum iterations (safety net to prevent infinite loops)
|
|
3905
|
-
const
|
|
3906
|
-
const absoluteMaxIterations = params.absoluteMaxIterations ?? DEFAULT_ABSOLUTE_MAX_ITERATIONS;
|
|
4213
|
+
const absoluteMaxIterations = params.absoluteMaxIterations ?? BaseAgent.DEFAULT_ABSOLUTE_MAX_ITERATIONS;
|
|
3907
4214
|
if (agentRun.TotalPromptIterations && agentRun.TotalPromptIterations >= absoluteMaxIterations) {
|
|
3908
4215
|
return {
|
|
3909
4216
|
exceeded: true,
|
|
@@ -4355,6 +4662,20 @@ export class BaseAgent {
|
|
|
4355
4662
|
}
|
|
4356
4663
|
// Remove in reverse order to maintain indices
|
|
4357
4664
|
removedIndices.sort((a, b) => b - a).forEach(index => {
|
|
4665
|
+
const target = params.conversationMessages[index];
|
|
4666
|
+
if (target.role === 'tool') {
|
|
4667
|
+
// a tool turn cannot be removed without orphaning the call it answers; stub it.
|
|
4668
|
+
params.conversationMessages[index] = this.stubToolTurn(target, '[result expired — stubbed to recover context]');
|
|
4669
|
+
this.emitMessageLifecycleEvent({
|
|
4670
|
+
type: 'message-compacted',
|
|
4671
|
+
turn: currentStepCount,
|
|
4672
|
+
messageIndex: index,
|
|
4673
|
+
message: params.conversationMessages[index],
|
|
4674
|
+
reason: 'Context recovery - tool turn stubbed (a tool turn cannot be removed)',
|
|
4675
|
+
tokensSaved: this.estimateTokens(target.content)
|
|
4676
|
+
});
|
|
4677
|
+
return;
|
|
4678
|
+
}
|
|
4358
4679
|
const removed = params.conversationMessages.splice(index, 1)[0];
|
|
4359
4680
|
// Emit lifecycle event
|
|
4360
4681
|
this.emitMessageLifecycleEvent({
|
|
@@ -4407,6 +4728,20 @@ export class BaseAgent {
|
|
|
4407
4728
|
const originalContent = typeof originalMessage.content === 'string'
|
|
4408
4729
|
? originalMessage.content
|
|
4409
4730
|
: JSON.stringify(originalMessage.content);
|
|
4731
|
+
if (originalMessage.role === 'tool') {
|
|
4732
|
+
// compact each tool_result block's text; the block structure is what the provider needs.
|
|
4733
|
+
const compacted = compactToolResultContent(originalMessage, (t) => (t.length > 500 ? `${t.slice(0, 500)}… [compacted from ${t.length} chars]` : t));
|
|
4734
|
+
const saved = originalTokens - this.estimateTokens(compacted.content);
|
|
4735
|
+
if (saved > 0) {
|
|
4736
|
+
params.conversationMessages[candidate.index] = {
|
|
4737
|
+
...compacted,
|
|
4738
|
+
metadata: { ...originalMessage.metadata, wasCompacted: true, originalLength: originalContent.length, tokensSaved: saved }
|
|
4739
|
+
};
|
|
4740
|
+
tokensSaved += saved;
|
|
4741
|
+
compactedCount++;
|
|
4742
|
+
}
|
|
4743
|
+
continue;
|
|
4744
|
+
}
|
|
4410
4745
|
// Use smart trim (faster than AI summary, no API cost)
|
|
4411
4746
|
const compactedContent = await this.compactMessage(originalMessage, {
|
|
4412
4747
|
compactMode: 'First N Chars',
|
|
@@ -4648,8 +4983,19 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4648
4983
|
// Check guardrails if next step would continue execution
|
|
4649
4984
|
const guardrailCheckedStep = await this.checkExecutionGuardrails(params, validatedNextStep, currentPayload, this._agentRun, currentStep);
|
|
4650
4985
|
this.logStatus(`📌 Next step determined: ${guardrailCheckedStep.step}${guardrailCheckedStep.terminate ? ' (terminating)' : ''}`, true, params);
|
|
4986
|
+
// the model's own call turn goes into history before anything answers it.
|
|
4987
|
+
this.appendNativeAssistantTurn(params, guardrailCheckedStep);
|
|
4651
4988
|
// if we need to retry make sure we add the retry message to the conversation messages
|
|
4652
|
-
if (guardrailCheckedStep.step === 'Retry' &&
|
|
4989
|
+
if (guardrailCheckedStep.step === 'Retry' && guardrailCheckedStep.payloadToolCallId && guardrailCheckedStep.nativeTurn?.sendResultsNatively) {
|
|
4990
|
+
// the payload-only turn is answered as a tool result for the payload_change_request call.
|
|
4991
|
+
params.conversationMessages.push(buildToolResultTurn([{
|
|
4992
|
+
toolCallId: guardrailCheckedStep.payloadToolCallId,
|
|
4993
|
+
toolName: 'payload_change_request',
|
|
4994
|
+
content: guardrailCheckedStep.retryInstructions || 'Payload change applied.',
|
|
4995
|
+
isError: false
|
|
4996
|
+
}], { turnAdded: this._promptTurnCount, messageType: 'action-result' }));
|
|
4997
|
+
}
|
|
4998
|
+
else if (guardrailCheckedStep.step === 'Retry' && (guardrailCheckedStep.message || guardrailCheckedStep.errorMessage || guardrailCheckedStep.retryInstructions)) {
|
|
4653
4999
|
params.conversationMessages.push({
|
|
4654
5000
|
role: 'user',
|
|
4655
5001
|
content: `Retrying due to: ${guardrailCheckedStep.retryInstructions || guardrailCheckedStep.message || guardrailCheckedStep.errorMessage}`
|
|
@@ -5840,6 +6186,60 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5840
6186
|
throw new Error(`Error executing actions: ${error.message}`);
|
|
5841
6187
|
}
|
|
5842
6188
|
}
|
|
6189
|
+
/**
|
|
6190
|
+
* Makes a SLICED conversation window safe to send on its own.
|
|
6191
|
+
*
|
|
6192
|
+
* A tool turn is only valid immediately after the assistant turn that declared its call ids, so
|
|
6193
|
+
* cutting the parent's history at an arbitrary index can strand either half of a pair. Keeping
|
|
6194
|
+
* the result half throws in `validateToolConversation` before the request is even built; keeping
|
|
6195
|
+
* the call half is accepted there but rejected by every provider. Both halves are repaired here
|
|
6196
|
+
* so the caller's window is internally consistent whatever index it happened to cut on:
|
|
6197
|
+
* an unpairable tool turn is dropped, and an assistant turn whose calls nothing in the window
|
|
6198
|
+
* answers keeps its prose but loses the calls.
|
|
6199
|
+
*
|
|
6200
|
+
* Only the slicing modes need this — 'All' is self-consistent by construction.
|
|
6201
|
+
*/
|
|
6202
|
+
makeToolTurnsSelfConsistent(window) {
|
|
6203
|
+
const declared = new Set();
|
|
6204
|
+
const resolved = new Set();
|
|
6205
|
+
for (const message of window) {
|
|
6206
|
+
const typed = message;
|
|
6207
|
+
if (typed.role === 'assistant') {
|
|
6208
|
+
for (const call of typed.toolCalls ?? []) {
|
|
6209
|
+
if (call.id)
|
|
6210
|
+
declared.add(call.id);
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
if (typed.role === 'tool' && Array.isArray(typed.content)) {
|
|
6214
|
+
for (const block of typed.content) {
|
|
6215
|
+
if (block.type === 'tool_result' && block.toolCallId)
|
|
6216
|
+
resolved.add(block.toolCallId);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
const kept = [];
|
|
6221
|
+
for (const message of window) {
|
|
6222
|
+
const typed = message;
|
|
6223
|
+
if (typed.role === 'tool') {
|
|
6224
|
+
const blocks = Array.isArray(typed.content) ? typed.content : [];
|
|
6225
|
+
// Its assistant turn was cut away — nothing in this window declares these calls.
|
|
6226
|
+
const pairable = blocks.some((b) => b.type === 'tool_result' && b.toolCallId && declared.has(b.toolCallId));
|
|
6227
|
+
if (!pairable)
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
if (typed.role === 'assistant' && typed.toolCalls?.length) {
|
|
6231
|
+
const answered = typed.toolCalls.every((call) => call.id && resolved.has(call.id));
|
|
6232
|
+
if (!answered) {
|
|
6233
|
+
// Demote to prose rather than send a call this window never answers.
|
|
6234
|
+
const { toolCalls: _dropped, ...rest } = typed;
|
|
6235
|
+
kept.push({ ...rest, content: typed.content || '[tool call omitted for context management]' });
|
|
6236
|
+
continue;
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
kept.push(message);
|
|
6240
|
+
}
|
|
6241
|
+
return kept;
|
|
6242
|
+
}
|
|
5843
6243
|
/**
|
|
5844
6244
|
* Prepares conversation messages for sub-agent execution based on database-configured message mode.
|
|
5845
6245
|
*
|
|
@@ -5888,7 +6288,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5888
6288
|
case 'Latest':
|
|
5889
6289
|
// Pass most recent N messages
|
|
5890
6290
|
if (maxMessages && maxMessages > 0) {
|
|
5891
|
-
messages = params.conversationMessages.slice(-maxMessages);
|
|
6291
|
+
messages = this.makeToolTurnsSelfConsistent(params.conversationMessages.slice(-maxMessages));
|
|
5892
6292
|
}
|
|
5893
6293
|
else {
|
|
5894
6294
|
messages = [...params.conversationMessages];
|
|
@@ -5900,14 +6300,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5900
6300
|
const firstTwo = params.conversationMessages.slice(0, 2);
|
|
5901
6301
|
const remaining = params.conversationMessages.slice(-(maxMessages - 2));
|
|
5902
6302
|
const omittedCount = params.conversationMessages.length - maxMessages;
|
|
5903
|
-
messages = [
|
|
6303
|
+
messages = this.makeToolTurnsSelfConsistent([
|
|
5904
6304
|
...firstTwo,
|
|
5905
6305
|
{
|
|
5906
6306
|
role: 'system',
|
|
5907
6307
|
content: `[${omittedCount} messages omitted for context management]`
|
|
5908
6308
|
},
|
|
5909
6309
|
...remaining
|
|
5910
|
-
];
|
|
6310
|
+
]);
|
|
5911
6311
|
}
|
|
5912
6312
|
else {
|
|
5913
6313
|
messages = [...params.conversationMessages];
|
|
@@ -6994,7 +7394,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6994
7394
|
AgentRunID: this._agentRun.ID,
|
|
6995
7395
|
StepNumber: stepNumber,
|
|
6996
7396
|
StepType: params.stepType,
|
|
6997
|
-
StepName: this.formatHierarchicalMessage(params.stepName), //
|
|
7397
|
+
StepName: this.fitStepName(stepEntity, this.formatHierarchicalMessage(params.stepName)), // breadcrumb, trimmed to the column
|
|
6998
7398
|
TargetID: params.targetId,
|
|
6999
7399
|
TargetLogID: params.targetLogId,
|
|
7000
7400
|
ParentID: params.parentId, // Link to parent step (e.g., loop step)
|
|
@@ -7226,6 +7626,21 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7226
7626
|
}
|
|
7227
7627
|
return baseMessage;
|
|
7228
7628
|
}
|
|
7629
|
+
/**
|
|
7630
|
+
* Trims a step name to what `AIAgentRunStep.StepName` can hold. Step names are built from free
|
|
7631
|
+
* text — a termination message quoting a provider's error, a tool name with its arguments, the
|
|
7632
|
+
* hierarchy breadcrumb in front of either — and one longer than the column made the whole row
|
|
7633
|
+
* unsaveable, so the step vanished from the run ("2 step record save(s) failed" — a
|
|
7634
|
+
* 327-character failure reason). The width comes from the entity's field metadata
|
|
7635
|
+
* when the instance carries it, else the column's declared 255 characters.
|
|
7636
|
+
*
|
|
7637
|
+
* @protected
|
|
7638
|
+
*/
|
|
7639
|
+
fitStepName(stepEntity, name) {
|
|
7640
|
+
const declared = stepEntity.EntityInfo?.Fields?.find((f) => f.Name === 'StepName')?.MaxLength;
|
|
7641
|
+
const limit = declared && declared > 0 ? declared : 255;
|
|
7642
|
+
return name.length > limit ? `${name.slice(0, limit - 1)}…` : name;
|
|
7643
|
+
}
|
|
7229
7644
|
/**
|
|
7230
7645
|
* Builds hierarchical step string from parent and current step counts.
|
|
7231
7646
|
*
|
|
@@ -7563,6 +7978,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7563
7978
|
};
|
|
7564
7979
|
// Execute the prompt
|
|
7565
7980
|
const promptResult = await this.executePrompt(promptParams);
|
|
7981
|
+
this.recordToolCallingInstrumentation(stepEntity, promptResult);
|
|
7566
7982
|
// Increment prompt-specific turn counter (used for expiration age calculations)
|
|
7567
7983
|
this._promptTurnCount++;
|
|
7568
7984
|
// Loop and sub-agent results now use the standard expiration/compaction lifecycle
|
|
@@ -7981,10 +8397,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7981
8397
|
// reason as the action record above: the model's real output is the JSON envelope, and
|
|
7982
8398
|
// storing framework prose as an `assistant` turn trains strong in-context models to imitate
|
|
7983
8399
|
// the prose and drift off the required JSON format. See the note at the action-record push.
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
8400
|
+
// a natively-called delegate_to_* is already in the history as the assistant's call turn and
|
|
8401
|
+
// will be answered by a `tool` turn; a user turn in between violates the provider contracts.
|
|
8402
|
+
if (!(previousDecision?.nativeTurn?.sendResultsNatively && subAgentRequest.toolCallId)) {
|
|
8403
|
+
params.conversationMessages.push({
|
|
8404
|
+
role: 'user',
|
|
8405
|
+
content: `[You delegated this task to the "${subAgentRequest.name}" agent. Reason: ${subAgentRequest.message}]`
|
|
8406
|
+
});
|
|
8407
|
+
}
|
|
7988
8408
|
// Prepare input data for the step
|
|
7989
8409
|
const inputData = {
|
|
7990
8410
|
agentName: params.agent.Name,
|
|
@@ -8206,11 +8626,18 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8206
8626
|
subAgentMetadata.compactPromptId = override.compactPromptId;
|
|
8207
8627
|
}
|
|
8208
8628
|
}
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8629
|
+
if (previousDecision?.nativeTurn?.sendResultsNatively && subAgentRequest.toolCallId) {
|
|
8630
|
+
// the delegate_to_* call is answered as a tool result.
|
|
8631
|
+
params.conversationMessages.push(buildToolResultTurn([...this.payloadToolResult(previousDecision), {
|
|
8632
|
+
toolCallId: subAgentRequest.toolCallId,
|
|
8633
|
+
toolName: `${SUB_AGENT_TOOL_PREFIX}${sanitizeToolName(subAgentRequest.name)}`,
|
|
8634
|
+
content: resultMessage,
|
|
8635
|
+
isError: !subAgentResult.success
|
|
8636
|
+
}], subAgentMetadata));
|
|
8637
|
+
}
|
|
8638
|
+
else {
|
|
8639
|
+
params.conversationMessages.push({ role: 'user', content: resultMessage, metadata: subAgentMetadata });
|
|
8640
|
+
}
|
|
8214
8641
|
// Set PayloadAtEnd with the merged payload
|
|
8215
8642
|
if (stepEntity) {
|
|
8216
8643
|
stepEntity.PayloadAtEnd = this.serializePayloadAtEnd(mergedPayload);
|
|
@@ -8405,7 +8832,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8405
8832
|
*
|
|
8406
8833
|
* @private
|
|
8407
8834
|
*/
|
|
8408
|
-
prepareParallelSubAgentDispatch(params, request, stepCount) {
|
|
8835
|
+
prepareParallelSubAgentDispatch(params, request, stepCount, nativeResults = false) {
|
|
8409
8836
|
const resolved = this.resolveSubAgentByName(params, request.name);
|
|
8410
8837
|
if (!resolved) {
|
|
8411
8838
|
this.logError(`Sub-agent '${request.name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
@@ -8429,10 +8856,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8429
8856
|
});
|
|
8430
8857
|
// `user`-role environment annotation (not an `assistant` turn) — see the note on the
|
|
8431
8858
|
// single-delegation push above for why framework prose must not be stored as assistant turns.
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8859
|
+
// skipped when the call is natively recorded and will be answered by a tool result (see the
|
|
8860
|
+
// single-delegation push for why a user turn between call and result must not be inserted).
|
|
8861
|
+
if (!(nativeResults && request.toolCallId)) {
|
|
8862
|
+
params.conversationMessages.push({
|
|
8863
|
+
role: 'user',
|
|
8864
|
+
content: `[You delegated this task to the parallel sub-agent "${request.name}". Reason: ${request.message}]`
|
|
8865
|
+
});
|
|
8866
|
+
}
|
|
8436
8867
|
return { request: request, subAgentEntity, relationship };
|
|
8437
8868
|
}
|
|
8438
8869
|
/**
|
|
@@ -8675,16 +9106,38 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8675
9106
|
async executeParallelSubAgents(params, subAgentRequests, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0) {
|
|
8676
9107
|
const currentPayload = previousDecision.newPayload;
|
|
8677
9108
|
// Synchronous pre-flight — order-stable transcript + progress events.
|
|
8678
|
-
const
|
|
9109
|
+
const nativeResults = previousDecision.nativeTurn?.sendResultsNatively === true;
|
|
9110
|
+
const dispatches = subAgentRequests.map(req => this.prepareParallelSubAgentDispatch(params, req, stepCount, nativeResults));
|
|
8679
9111
|
// Bounded parallel dispatch.
|
|
8680
9112
|
const executions = await this.mapWithConcurrency(dispatches, PARALLEL_SUBAGENT_CONCURRENCY_LIMIT, (dispatch) => this.runSingleParallelSubAgent(params, dispatch, previousDecision, currentPayload, parentStepId, subAgentPayloadOverride, stepCount));
|
|
8681
9113
|
// Sequential merge + per-sibling step finalization.
|
|
8682
9114
|
const { mergedPayload, anyFailure, allExecutions } = await this.mergeParallelExecutionsIntoParent(params, subAgentRequests, executions, currentPayload);
|
|
8683
|
-
// Aggregated summary appended to the parent transcript
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
9115
|
+
// Aggregated summary appended to the parent transcript — or, with native tool results, one `tool` turn whose
|
|
9116
|
+
// tool_result blocks answer each delegate_to_* call by id (every provider requires every call
|
|
9117
|
+
// of an assistant turn to be answered before the next turn).
|
|
9118
|
+
// Pair per execution rather than all-or-nothing: one dispatch that lost its id must not
|
|
9119
|
+
// discard the pairing for the calls that have one, or those calls go unanswered and the
|
|
9120
|
+
// reconciler has to report completed sub-agents as "not executed". Anything unpairable
|
|
9121
|
+
// keeps the markdown user message for itself, as the Actions path already does.
|
|
9122
|
+
const pairable = nativeResults ? allExecutions.filter((e) => !!e.request.toolCallId) : [];
|
|
9123
|
+
const unpairable = allExecutions.filter((e) => !pairable.includes(e));
|
|
9124
|
+
if (pairable.length > 0) {
|
|
9125
|
+
params.conversationMessages.push(buildToolResultTurn([
|
|
9126
|
+
...this.payloadToolResult(previousDecision),
|
|
9127
|
+
...pairable.map((e) => ({
|
|
9128
|
+
toolCallId: e.request.toolCallId,
|
|
9129
|
+
toolName: `${SUB_AGENT_TOOL_PREFIX}${sanitizeToolName(e.request.name)}`,
|
|
9130
|
+
content: this.buildParallelSubAgentSummary([e]),
|
|
9131
|
+
isError: !e.result.success
|
|
9132
|
+
}))
|
|
9133
|
+
], undefined));
|
|
9134
|
+
}
|
|
9135
|
+
if (unpairable.length > 0) {
|
|
9136
|
+
params.conversationMessages.push({
|
|
9137
|
+
role: 'user',
|
|
9138
|
+
content: `Parallel Sub-Agents Completed:\n\n${this.buildParallelSubAgentSummary(unpairable)}`
|
|
9139
|
+
});
|
|
9140
|
+
}
|
|
8688
9141
|
// Termination semantics: matches the single sub-agent path —
|
|
8689
9142
|
// `terminateAfter` triggers parent termination regardless of the child's
|
|
8690
9143
|
// success/failure. The parent's step reflects whether any child failed:
|
|
@@ -8730,10 +9183,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8730
9183
|
// reason as the action record above: the model's real output is the JSON envelope, and
|
|
8731
9184
|
// storing framework prose as an `assistant` turn trains strong in-context models to imitate
|
|
8732
9185
|
// the prose and drift off the required JSON format. See the note at the action-record push.
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
9186
|
+
// a natively-called delegate_to_* is already in the history as the assistant's call turn and
|
|
9187
|
+
// will be answered by a `tool` turn; a user turn in between violates the provider contracts.
|
|
9188
|
+
if (!(previousDecision?.nativeTurn?.sendResultsNatively && subAgentRequest.toolCallId)) {
|
|
9189
|
+
params.conversationMessages.push({
|
|
9190
|
+
role: 'user',
|
|
9191
|
+
content: `[You delegated this task to the "${subAgentRequest.name}" agent. Reason: ${subAgentRequest.message}]`
|
|
9192
|
+
});
|
|
9193
|
+
}
|
|
8737
9194
|
// Prepare input data for the step
|
|
8738
9195
|
const inputData = {
|
|
8739
9196
|
agentName: params.agent.Name,
|
|
@@ -8891,11 +9348,22 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8891
9348
|
relatedMetadata.compactPromptId = override.compactPromptId;
|
|
8892
9349
|
}
|
|
8893
9350
|
}
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
9351
|
+
if (previousDecision.nativeTurn?.sendResultsNatively && subAgentRequest.toolCallId) {
|
|
9352
|
+
// the delegate_to_* call is answered as a tool result (same as the child path).
|
|
9353
|
+
params.conversationMessages.push(buildToolResultTurn([...this.payloadToolResult(previousDecision), {
|
|
9354
|
+
toolCallId: subAgentRequest.toolCallId,
|
|
9355
|
+
toolName: `${SUB_AGENT_TOOL_PREFIX}${sanitizeToolName(subAgentRequest.name)}`,
|
|
9356
|
+
content: relatedResultMessage,
|
|
9357
|
+
isError: !subAgentResult.success
|
|
9358
|
+
}], relatedMetadata));
|
|
9359
|
+
}
|
|
9360
|
+
else {
|
|
9361
|
+
params.conversationMessages.push({
|
|
9362
|
+
role: 'user',
|
|
9363
|
+
content: relatedResultMessage,
|
|
9364
|
+
metadata: relatedMetadata
|
|
9365
|
+
});
|
|
9366
|
+
}
|
|
8899
9367
|
// Update the agent run's current payload
|
|
8900
9368
|
if (this._agentRun) {
|
|
8901
9369
|
this._agentRun.FinalPayloadObject = mergedPayload;
|
|
@@ -9243,10 +9711,17 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9243
9711
|
if (addConversationMessage) {
|
|
9244
9712
|
// Record as a `user`-role environment annotation (no metadata - permanent record).
|
|
9245
9713
|
// See the note above on why this is NOT an `assistant` turn.
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9714
|
+
//
|
|
9715
|
+
// Exception: when the model's own tool-call turn is already in the history (native
|
|
9716
|
+
// tool results), the call and its arguments are the record, and a user turn between that
|
|
9717
|
+
// call and its `tool` result breaks the OpenAI and Anthropic contracts — results must
|
|
9718
|
+
// immediately follow the call. Skip it there.
|
|
9719
|
+
if (!previousDecision.nativeTurn?.sendResultsNatively) {
|
|
9720
|
+
params.conversationMessages.push({
|
|
9721
|
+
role: 'user',
|
|
9722
|
+
content: actionMessage
|
|
9723
|
+
});
|
|
9724
|
+
}
|
|
9250
9725
|
}
|
|
9251
9726
|
const actionEngine = ActionEngineServer.Instance;
|
|
9252
9727
|
// Get the AIAgentAction metadata records for this agent (used for expiration settings)
|
|
@@ -9393,11 +9868,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9393
9868
|
}
|
|
9394
9869
|
if (addConversationMessage) {
|
|
9395
9870
|
// Add user message with results and optional metadata
|
|
9396
|
-
|
|
9397
|
-
role: 'user',
|
|
9398
|
-
content: resultsMessage,
|
|
9399
|
-
metadata: metadata
|
|
9400
|
-
});
|
|
9871
|
+
this.appendActionResults(params, actionSummaries, resultsMessage, metadata, previousDecision); // results as a tool turn or the markdown user message
|
|
9401
9872
|
// Surface explicit AI directives from action results as a separate instruction message.
|
|
9402
9873
|
// Actions that need the AI to follow specific instructions (not just acknowledge data)
|
|
9403
9874
|
// populate AIDirectives on their ActionResultSimple return value.
|
|
@@ -9992,6 +10463,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9992
10463
|
* Because `params` is the same object reference used for the rest of this run, every subsequent
|
|
9993
10464
|
* turn's `gatherPromptTemplateData()` call picks up the change automatically — no extra plumbing.
|
|
9994
10465
|
*
|
|
10466
|
+
* Which actions: the skill's `ExposeToModel` rows only ({@link AIEngine.GetSkillExposedActionIDs}).
|
|
10467
|
+
* A bundled action with the flag off is left OUT of the run: the effective action set is both the
|
|
10468
|
+
* prompt's tool surface and the execution allow-list, so the model neither sees it nor can call it
|
|
10469
|
+
* by name, and no agent path (PreProcessActionStep, a loop's action lookup) can reach it either.
|
|
10470
|
+
* It stays bundled for SKILL.md export and tooling; application code invokes it through the
|
|
10471
|
+
* Actions API. Skill attribution therefore never applies to it — the agent never runs it.
|
|
10472
|
+
*
|
|
9995
10473
|
* Override to change propagation scope (e.g. a subclass that wants skill-granted capabilities
|
|
9996
10474
|
* to cascade to sub-agents could push `scope: 'all-subagents'` instead).
|
|
9997
10475
|
*
|
|
@@ -9999,7 +10477,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9999
10477
|
*/
|
|
10000
10478
|
enableSkillCapabilities(skill, params) {
|
|
10001
10479
|
const activatingAgentIds = [params.agent.ID];
|
|
10002
|
-
|
|
10480
|
+
// Only the actions the skill exposes (AISkillAction.ExposeToModel) join the run. A bundled
|
|
10481
|
+
// action with the flag off is not described to the model and not executable by the agent;
|
|
10482
|
+
// the application invokes it (a menu button in the skill's reply) through the Actions API.
|
|
10483
|
+
const actionIds = AIEngine.Instance.GetSkillExposedActionIDs(skill.ID);
|
|
10003
10484
|
if (actionIds.length > 0) {
|
|
10004
10485
|
if (!params.actionChanges) {
|
|
10005
10486
|
params.actionChanges = [];
|
|
@@ -11620,7 +12101,18 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11620
12101
|
continue;
|
|
11621
12102
|
}
|
|
11622
12103
|
msg.metadata.isExpired = true;
|
|
11623
|
-
if (msg.metadata.expirationMode === 'Remove') {
|
|
12104
|
+
if (msg.metadata.expirationMode === 'Remove' && msg.role === 'tool') {
|
|
12105
|
+
// a tool turn cannot be removed (it answers an assistant call); stub its blocks.
|
|
12106
|
+
params.conversationMessages[i] = this.stubToolTurn(msg, `[result expired after ${turnsAlive} turns]`);
|
|
12107
|
+
this.emitMessageLifecycleEvent({
|
|
12108
|
+
type: 'message-expired',
|
|
12109
|
+
turn: currentTurn,
|
|
12110
|
+
messageIndex: i,
|
|
12111
|
+
message: params.conversationMessages[i],
|
|
12112
|
+
reason: `Expired after ${turnsAlive} turns (limit: ${msg.metadata.expirationTurns}); tool turn stubbed, not removed`
|
|
12113
|
+
});
|
|
12114
|
+
}
|
|
12115
|
+
else if (msg.metadata.expirationMode === 'Remove') {
|
|
11624
12116
|
messagesToRemove.push(i);
|
|
11625
12117
|
this.emitMessageLifecycleEvent({
|
|
11626
12118
|
type: 'message-expired',
|
|
@@ -11653,6 +12145,25 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11653
12145
|
const preserveOriginal = params.messageExpirationOverride?.preserveOriginalContent !== false;
|
|
11654
12146
|
for (const item of messagesToCompact) {
|
|
11655
12147
|
const originalContent = item.message.content;
|
|
12148
|
+
if (item.message.role === 'tool') {
|
|
12149
|
+
// compact per block so the tool turn keeps answering its call.
|
|
12150
|
+
const limit = item.metadata.compactLength || 500;
|
|
12151
|
+
const compactedTurn = compactToolResultContent(item.message, (t) => (t.length > limit ? `${t.slice(0, limit)}… [compacted from ${t.length} chars]` : t));
|
|
12152
|
+
const saved = this.estimateTokens(originalContent) - this.estimateTokens(compactedTurn.content);
|
|
12153
|
+
params.conversationMessages[item.index] = {
|
|
12154
|
+
...compactedTurn,
|
|
12155
|
+
metadata: { ...item.message.metadata, wasCompacted: true, originalContent: preserveOriginal ? originalContent : undefined, originalLength: item.metadata.originalLength, tokensSaved: saved, canExpand: preserveOriginal }
|
|
12156
|
+
};
|
|
12157
|
+
this.emitMessageLifecycleEvent({
|
|
12158
|
+
type: 'message-compacted',
|
|
12159
|
+
turn: currentTurn,
|
|
12160
|
+
messageIndex: item.index,
|
|
12161
|
+
message: params.conversationMessages[item.index],
|
|
12162
|
+
reason: `Compacted tool turn per block (saved ${saved} tokens)`,
|
|
12163
|
+
tokensSaved: saved
|
|
12164
|
+
});
|
|
12165
|
+
continue;
|
|
12166
|
+
}
|
|
11656
12167
|
const compacted = await this.compactMessage(item.message, item.metadata, params);
|
|
11657
12168
|
// Calculate token savings
|
|
11658
12169
|
const originalTokens = this.estimateTokens(originalContent);
|