@illuma-ai/agents 1.0.96 → 1.0.98
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/common/constants.cjs +25 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +32 -142
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +8 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/utils/contextPressure.cjs +154 -0
- package/dist/cjs/utils/contextPressure.cjs.map +1 -0
- package/dist/esm/common/constants.mjs +24 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +32 -142
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/utils/contextPressure.mjs +148 -0
- package/dist/esm/utils/contextPressure.mjs.map +1 -0
- package/dist/types/common/constants.d.ts +14 -0
- package/dist/types/utils/contextPressure.d.ts +72 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/common/constants.ts +26 -0
- package/src/graphs/Graph.ts +46 -170
- package/src/graphs/contextManagement.e2e.test.ts +28 -20
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +7 -7
- package/src/specs/agent-handoffs.test.ts +36 -36
- package/src/specs/thinking-handoff.test.ts +10 -10
- package/src/utils/contextPressure.test.ts +247 -0
- package/src/utils/contextPressure.ts +188 -0
- package/src/utils/index.ts +1 -0
|
@@ -19,7 +19,32 @@ const MIN_THINKING_BUDGET = 1024;
|
|
|
19
19
|
* compounding across multi-tool conversations (e.g., 10 tool calls).
|
|
20
20
|
*/
|
|
21
21
|
const TOOL_TURN_THINKING_BUDGET = 1024;
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// CONTEXT OVERFLOW MANAGEMENT
|
|
24
|
+
//
|
|
25
|
+
// Context overflow is handled mechanically — no token budget numbers are
|
|
26
|
+
// exposed to the LLM. The system uses: pruning (Graph), summarization
|
|
27
|
+
// (summarizeCallback), and auto-continuation (client.js max_tokens detection).
|
|
28
|
+
//
|
|
29
|
+
// See: docs/context-overflow-architecture.md
|
|
30
|
+
// ============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Minimum number of attached documents before the multi-document delegation
|
|
33
|
+
* hint is injected. Below this threshold, the agent processes documents
|
|
34
|
+
* directly within its own context.
|
|
35
|
+
*/
|
|
36
|
+
const MULTI_DOCUMENT_THRESHOLD = 3;
|
|
37
|
+
/**
|
|
38
|
+
* Context utilization safety buffer multiplier (0-1).
|
|
39
|
+
* Applied as: effectiveMax = (maxContextTokens - maxOutputTokens) * CONTEXT_SAFETY_BUFFER
|
|
40
|
+
*
|
|
41
|
+
* Reserves headroom so the LLM doesn't hit hard token limits mid-generation.
|
|
42
|
+
* 0.9 = 10% reserved for safety.
|
|
43
|
+
*/
|
|
44
|
+
const CONTEXT_SAFETY_BUFFER = 0.9;
|
|
22
45
|
|
|
46
|
+
exports.CONTEXT_SAFETY_BUFFER = CONTEXT_SAFETY_BUFFER;
|
|
23
47
|
exports.MIN_THINKING_BUDGET = MIN_THINKING_BUDGET;
|
|
48
|
+
exports.MULTI_DOCUMENT_THRESHOLD = MULTI_DOCUMENT_THRESHOLD;
|
|
24
49
|
exports.TOOL_TURN_THINKING_BUDGET = TOOL_TURN_THINKING_BUDGET;
|
|
25
50
|
//# sourceMappingURL=constants.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.cjs","sources":["../../../src/common/constants.ts"],"sourcesContent":["// src/common/constants.ts\n\n/**\n * Minimum thinking budget allowed by the Anthropic API.\n * Extended thinking requires at least 1024 budget_tokens.\n * @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking\n */\nexport const MIN_THINKING_BUDGET = 1024;\n\n/**\n * Reduced thinking budget for subsequent ReAct iterations (tool-result turns).\n *\n * In a ReAct agent loop, the first LLM call processes the user's query and\n * may need deep reasoning. Subsequent iterations (after tool results return)\n * typically only need to decide \"call next tool\" or \"generate final response\"\n * — 1024 tokens is sufficient for this routing logic.\n *\n * This reduces wall-clock time per iteration from ~20-30s to ~5-10s,\n * compounding across multi-tool conversations (e.g., 10 tool calls).\n */\nexport const TOOL_TURN_THINKING_BUDGET = 1024;\n"],"names":[],"mappings":";;AAAA;AAEA;;;;AAIG;AACI,MAAM,mBAAmB,GAAG;AAEnC;;;;;;;;;;AAUG;AACI,MAAM,yBAAyB,GAAG
|
|
1
|
+
{"version":3,"file":"constants.cjs","sources":["../../../src/common/constants.ts"],"sourcesContent":["// src/common/constants.ts\n\n/**\n * Minimum thinking budget allowed by the Anthropic API.\n * Extended thinking requires at least 1024 budget_tokens.\n * @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking\n */\nexport const MIN_THINKING_BUDGET = 1024;\n\n/**\n * Reduced thinking budget for subsequent ReAct iterations (tool-result turns).\n *\n * In a ReAct agent loop, the first LLM call processes the user's query and\n * may need deep reasoning. Subsequent iterations (after tool results return)\n * typically only need to decide \"call next tool\" or \"generate final response\"\n * — 1024 tokens is sufficient for this routing logic.\n *\n * This reduces wall-clock time per iteration from ~20-30s to ~5-10s,\n * compounding across multi-tool conversations (e.g., 10 tool calls).\n */\nexport const TOOL_TURN_THINKING_BUDGET = 1024;\n\n// ============================================================================\n// CONTEXT OVERFLOW MANAGEMENT\n//\n// Context overflow is handled mechanically — no token budget numbers are\n// exposed to the LLM. The system uses: pruning (Graph), summarization\n// (summarizeCallback), and auto-continuation (client.js max_tokens detection).\n//\n// See: docs/context-overflow-architecture.md\n// ============================================================================\n\n/**\n * Minimum number of attached documents before the multi-document delegation\n * hint is injected. Below this threshold, the agent processes documents\n * directly within its own context.\n */\nexport const MULTI_DOCUMENT_THRESHOLD = 3;\n\n/**\n * Context utilization safety buffer multiplier (0-1).\n * Applied as: effectiveMax = (maxContextTokens - maxOutputTokens) * CONTEXT_SAFETY_BUFFER\n *\n * Reserves headroom so the LLM doesn't hit hard token limits mid-generation.\n * 0.9 = 10% reserved for safety.\n */\nexport const CONTEXT_SAFETY_BUFFER = 0.9;\n"],"names":[],"mappings":";;AAAA;AAEA;;;;AAIG;AACI,MAAM,mBAAmB,GAAG;AAEnC;;;;;;;;;;AAUG;AACI,MAAM,yBAAyB,GAAG;AAEzC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;;;;AAIG;AACI,MAAM,wBAAwB,GAAG;AAExC;;;;;;AAMG;AACI,MAAM,qBAAqB,GAAG;;;;;;;"}
|
|
@@ -24,6 +24,7 @@ require('ai-tokenizer');
|
|
|
24
24
|
require('../utils/toonFormat.cjs');
|
|
25
25
|
var contextAnalytics = require('../utils/contextAnalytics.cjs');
|
|
26
26
|
require('zod-to-json-schema');
|
|
27
|
+
var contextPressure = require('../utils/contextPressure.cjs');
|
|
27
28
|
var providers = require('../llm/providers.cjs');
|
|
28
29
|
var ToolNode = require('../tools/ToolNode.cjs');
|
|
29
30
|
var index = require('../llm/openai/index.cjs');
|
|
@@ -950,36 +951,12 @@ class StandardGraph extends Graph {
|
|
|
950
951
|
let messagesToUse = messages$1;
|
|
951
952
|
// ====================================================================
|
|
952
953
|
// PRE-PRUNING DELEGATION CHECK
|
|
953
|
-
// Before pruning strips messages (losing context), check if we should
|
|
954
|
-
// delegate instead. If context would be pruned AND the agent has the
|
|
955
|
-
// task tool, inject a delegation hint and SKIP pruning — preserving
|
|
956
|
-
// the content for the LLM to understand what to delegate.
|
|
957
954
|
// ====================================================================
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return toolName === 'task';
|
|
964
|
-
});
|
|
965
|
-
if (hasTaskToolPrePrune === true &&
|
|
966
|
-
agentContext.tokenCounter &&
|
|
967
|
-
agentContext.maxContextTokens != null) {
|
|
968
|
-
// Estimate total tokens in messages BEFORE pruning
|
|
969
|
-
let prePruneTokens = 0;
|
|
970
|
-
for (const msg of messages$1) {
|
|
971
|
-
prePruneTokens += agentContext.tokenCounter(msg);
|
|
972
|
-
}
|
|
973
|
-
// Add instruction tokens (system prompt)
|
|
974
|
-
prePruneTokens += agentContext.instructionTokens;
|
|
975
|
-
const prePruneUtilization = (prePruneTokens / agentContext.maxContextTokens) * 100;
|
|
976
|
-
if (prePruneUtilization > 70) {
|
|
977
|
-
console.warn(`[Graph] PRE-PRUNE delegation check: ${prePruneUtilization.toFixed(1)}% utilization ` +
|
|
978
|
-
`(${prePruneTokens}/${agentContext.maxContextTokens} tokens). ` +
|
|
979
|
-
'Injecting delegation hint INSTEAD of pruning.');
|
|
980
|
-
delegationInjectedPrePrune = true;
|
|
981
|
-
}
|
|
982
|
-
}
|
|
955
|
+
// Context management is now fully mechanical:
|
|
956
|
+
// - Pruning always runs when needed (no delegation-based skip)
|
|
957
|
+
// - Auto-continuation in client.js handles max_tokens finish reason
|
|
958
|
+
// - LLM never sees raw token numbers (prevents voluntary bail-out)
|
|
959
|
+
// ====================================================================
|
|
983
960
|
if (!agentContext.pruneMessages &&
|
|
984
961
|
agentContext.tokenCounter &&
|
|
985
962
|
agentContext.maxContextTokens != null &&
|
|
@@ -1002,7 +979,7 @@ class StandardGraph extends Graph {
|
|
|
1002
979
|
indexTokenCountMap: agentContext.indexTokenCountMap,
|
|
1003
980
|
});
|
|
1004
981
|
}
|
|
1005
|
-
if (agentContext.pruneMessages
|
|
982
|
+
if (agentContext.pruneMessages) {
|
|
1006
983
|
console.debug(`[Graph:ContextMgmt] Pruning messages | inputCount=${messages$1.length} | maxTokens=${agentContext.maxContextTokens}`);
|
|
1007
984
|
const { context, indexTokenCountMap, messagesToRefine } = agentContext.pruneMessages({
|
|
1008
985
|
messages: messages$1,
|
|
@@ -1013,12 +990,14 @@ class StandardGraph extends Graph {
|
|
|
1013
990
|
messagesToUse = context;
|
|
1014
991
|
console.debug(`[Graph:ContextMgmt] Pruned | kept=${context.length} | discarded=${messagesToRefine.length} | originalCount=${messages$1.length}`);
|
|
1015
992
|
// Summarize discarded messages if callback provided
|
|
993
|
+
let hasSummary = false;
|
|
1016
994
|
if (messagesToRefine.length > 0 && agentContext.summarizeCallback) {
|
|
1017
995
|
console.debug(`[Graph:ContextMgmt] Summarizing ${messagesToRefine.length} discarded messages`);
|
|
1018
996
|
try {
|
|
1019
997
|
const summary = await agentContext.summarizeCallback(messagesToRefine);
|
|
1020
998
|
console.debug(`[Graph:ContextMgmt] Summary received | len=${summary?.length ?? 0} | hasContent=${summary != null && summary !== ''}`);
|
|
1021
999
|
if (summary != null && summary !== '') {
|
|
1000
|
+
hasSummary = true;
|
|
1022
1001
|
const summaryMsg = new messages.SystemMessage(`[Conversation Summary]\n${summary}`);
|
|
1023
1002
|
// Insert after system message (if present), before conversation messages
|
|
1024
1003
|
const systemIdx = messagesToUse[0]?.getType() === 'system' ? 1 : 0;
|
|
@@ -1034,9 +1013,15 @@ class StandardGraph extends Graph {
|
|
|
1034
1013
|
console.error('[Graph] Summarization callback failed:', err);
|
|
1035
1014
|
}
|
|
1036
1015
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1016
|
+
// Post-prune context note: inform the LLM that context was compressed
|
|
1017
|
+
// without exposing token numbers (prevents voluntary bail-out)
|
|
1018
|
+
if (messagesToRefine.length > 0 && contextPressure.hasTaskTool(agentContext.tools)) {
|
|
1019
|
+
const postPruneNote = contextPressure.buildPostPruneNote(messagesToRefine.length, hasSummary);
|
|
1020
|
+
if (postPruneNote) {
|
|
1021
|
+
messagesToUse = [...messagesToUse, new messages.SystemMessage(postPruneNote)];
|
|
1022
|
+
console.debug(`[Graph:ContextMgmt] Post-prune note injected | hasSummary=${hasSummary} | discarded=${messagesToRefine.length}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1040
1025
|
}
|
|
1041
1026
|
let finalMessages = messagesToUse;
|
|
1042
1027
|
if (agentContext.useLegacyContent) {
|
|
@@ -1149,125 +1134,30 @@ class StandardGraph extends Graph {
|
|
|
1149
1134
|
analytics: contextAnalytics$1,
|
|
1150
1135
|
}, config);
|
|
1151
1136
|
// ====================================================================
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
// Two triggers for delegation hints:
|
|
1155
|
-
// 1. DOCUMENT COUNT: When 3+ documents are detected in the conversation,
|
|
1156
|
-
// inject a delegation hint on the FIRST iteration (before the LLM
|
|
1157
|
-
// has called any tools). This ensures the agent delegates upfront
|
|
1158
|
-
// rather than trying to process all documents itself.
|
|
1159
|
-
// 2. TOKEN UTILIZATION: At EVERY iteration, if context is filling up
|
|
1160
|
-
// (70%/85%), inject escalating hints to delegate remaining work.
|
|
1137
|
+
// MULTI-DOCUMENT DELEGATION (task-driven, not budget-driven)
|
|
1161
1138
|
//
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1139
|
+
// Token-based pressure hints have been removed — the LLM never sees
|
|
1140
|
+
// raw token numbers. Context overflow is handled mechanically by
|
|
1141
|
+
// pruning (Graph) + auto-continuation (client.js max_tokens detection).
|
|
1142
|
+
// See: docs/context-overflow-architecture.md
|
|
1164
1143
|
// ====================================================================
|
|
1165
|
-
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (hasTaskToolInContext === true &&
|
|
1172
|
-
contextAnalytics$1.utilizationPercent != null &&
|
|
1173
|
-
contextAnalytics$1.maxContextTokens != null) {
|
|
1174
|
-
const utilization = contextAnalytics$1.utilizationPercent;
|
|
1175
|
-
const totalTokens = contextAnalytics$1.totalTokens;
|
|
1176
|
-
const maxTokens = contextAnalytics$1.maxContextTokens;
|
|
1177
|
-
const remainingTokens = maxTokens - totalTokens;
|
|
1178
|
-
// Count attached documents by scanning for document patterns in HumanMessages:
|
|
1179
|
-
// 1. # "filename" headers in "Attached document(s):" blocks (text content)
|
|
1180
|
-
// 2. **filename1, filename2** in "The user has attached:" blocks (embedded files)
|
|
1181
|
-
// 3. Filenames in file_search tool results
|
|
1182
|
-
let documentCount = 0;
|
|
1183
|
-
const documentNames = [];
|
|
1184
|
-
for (const msg of finalMessages) {
|
|
1185
|
-
const content = typeof msg.content === 'string'
|
|
1186
|
-
? msg.content
|
|
1187
|
-
: Array.isArray(msg.content)
|
|
1188
|
-
? msg.content
|
|
1189
|
-
.map((p) => {
|
|
1190
|
-
const part = p;
|
|
1191
|
-
return String(part.text ?? part.content ?? '');
|
|
1192
|
-
})
|
|
1193
|
-
.join(' ')
|
|
1194
|
-
: '';
|
|
1195
|
-
// Pattern 1: # "filename" headers in attached document blocks
|
|
1196
|
-
const docMatches = content.match(/# "([^"]+)"/g);
|
|
1197
|
-
if (docMatches) {
|
|
1198
|
-
for (const match of docMatches) {
|
|
1199
|
-
const name = match.replace(/# "/, '').replace(/"$/, '');
|
|
1200
|
-
if (!documentNames.includes(name)) {
|
|
1201
|
-
documentNames.push(name);
|
|
1202
|
-
documentCount++;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
// Pattern 2: "The user has attached: **file1, file2**" (embedded files)
|
|
1207
|
-
const attachedMatch = content.match(/user has attached:\s*\*\*([^*]+)\*\*/i);
|
|
1208
|
-
if (attachedMatch) {
|
|
1209
|
-
const names = attachedMatch[1]
|
|
1210
|
-
.split(',')
|
|
1211
|
-
.map((n) => n.trim())
|
|
1212
|
-
.filter(Boolean);
|
|
1213
|
-
for (const name of names) {
|
|
1214
|
-
if (!documentNames.includes(name)) {
|
|
1215
|
-
documentNames.push(name);
|
|
1216
|
-
documentCount++;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1144
|
+
if (contextPressure.hasTaskTool(agentContext.tools)) {
|
|
1145
|
+
const { count: documentCount, names: documentNames } = contextPressure.detectDocuments(finalMessages);
|
|
1146
|
+
// Observability log (no token numbers exposed to LLM)
|
|
1147
|
+
if (contextAnalytics$1.utilizationPercent != null) {
|
|
1148
|
+
console.debug(`[Graph] Context utilization: ${contextAnalytics$1.utilizationPercent.toFixed(1)}% | ` +
|
|
1149
|
+
`messages: ${finalMessages.length} | docs: ${documentCount}`);
|
|
1220
1150
|
}
|
|
1221
|
-
//
|
|
1222
|
-
console.debug(`[Graph] Context utilization: ${utilization.toFixed(1)}% ` +
|
|
1223
|
-
`(${totalTokens}/${maxTokens} tokens, ${remainingTokens} remaining) | ` +
|
|
1224
|
-
`hasTaskTool: true | messages: ${finalMessages.length} | docs: ${documentCount}`);
|
|
1225
|
-
// TRIGGER 1: Multi-document delegation (3+ documents detected)
|
|
1226
|
-
// Only inject on first iteration (no AI messages yet = agent hasn't responded)
|
|
1151
|
+
// Multi-document delegation: first iteration only (before AI has responded)
|
|
1227
1152
|
const hasAiResponse = finalMessages.some((m) => m._getType() === 'ai' || m._getType() === 'tool');
|
|
1228
|
-
if (documentCount
|
|
1153
|
+
if (contextPressure.shouldInjectMultiDocHint(documentCount, hasAiResponse)) {
|
|
1229
1154
|
const pressureMsg = new messages.HumanMessage({
|
|
1230
|
-
content:
|
|
1231
|
-
`Documents: ${documentNames.join(', ')}\n\n` +
|
|
1232
|
-
`You have ${documentCount} documents attached. For thorough analysis, use the "task" tool ` +
|
|
1233
|
-
'to delegate each document (or group of related documents) to a sub-agent.\n' +
|
|
1234
|
-
'Each sub-agent has its own fresh context window and can use file_search to retrieve the full document content.\n' +
|
|
1235
|
-
'After all sub-agents complete, synthesize their results into a comprehensive response.\n\n' +
|
|
1236
|
-
'This approach ensures each document gets full attention without context limitations.',
|
|
1155
|
+
content: contextPressure.buildMultiDocHintContent(documentCount, documentNames),
|
|
1237
1156
|
});
|
|
1238
1157
|
finalMessages = [...finalMessages, pressureMsg];
|
|
1239
1158
|
console.info(`[Graph] Multi-document delegation hint injected for ${documentCount} documents: ` +
|
|
1240
1159
|
`${documentNames.join(', ')}`);
|
|
1241
1160
|
}
|
|
1242
|
-
// TRIGGER 2: Token utilization thresholds (mid-chain safety net)
|
|
1243
|
-
// Also fires when we skipped pruning due to delegationInjectedPrePrune
|
|
1244
|
-
if (utilization > 85 ||
|
|
1245
|
-
(delegationInjectedPrePrune && utilization > 50)) {
|
|
1246
|
-
// CRITICAL: Context is high — MANDATE delegation
|
|
1247
|
-
const pressureMsg = new messages.HumanMessage({
|
|
1248
|
-
content: `[CONTEXT BUDGET CRITICAL — ${utilization.toFixed(0)}% used]\n` +
|
|
1249
|
-
`You have used ${totalTokens} of ${maxTokens} tokens (${remainingTokens} remaining).\n` +
|
|
1250
|
-
'Your context is very large. You MUST use the "task" tool to delegate work to sub-agents.\n' +
|
|
1251
|
-
'Each sub-agent runs in its own fresh context window and can use file_search to access documents.\n' +
|
|
1252
|
-
'Do NOT attempt to process documents directly — delegate each document to a sub-agent, then synthesize results.',
|
|
1253
|
-
});
|
|
1254
|
-
finalMessages = [...finalMessages, pressureMsg];
|
|
1255
|
-
console.warn(`[Graph] Context pressure CRITICAL (${utilization.toFixed(0)}%): ` +
|
|
1256
|
-
`Injected mandatory delegation hint. ${remainingTokens} tokens remaining. ` +
|
|
1257
|
-
`prePruneSkipped: ${delegationInjectedPrePrune}`);
|
|
1258
|
-
}
|
|
1259
|
-
else if (utilization > 70) {
|
|
1260
|
-
// WARNING: Context filling up — suggest delegation
|
|
1261
|
-
const pressureMsg = new messages.HumanMessage({
|
|
1262
|
-
content: `[CONTEXT BUDGET WARNING — ${utilization.toFixed(0)}% used]\n` +
|
|
1263
|
-
`You have used ${totalTokens} of ${maxTokens} tokens (${remainingTokens} remaining).\n` +
|
|
1264
|
-
'Your context is filling up. Consider using the "task" tool to delegate complex operations to sub-agents.\n' +
|
|
1265
|
-
"Sub-agents run in fresh context windows and won't consume your remaining budget.",
|
|
1266
|
-
});
|
|
1267
|
-
finalMessages = [...finalMessages, pressureMsg];
|
|
1268
|
-
console.info(`[Graph] Context pressure WARNING (${utilization.toFixed(0)}%): ` +
|
|
1269
|
-
`Injected delegation suggestion. ${remainingTokens} tokens remaining.`);
|
|
1270
|
-
}
|
|
1271
1161
|
}
|
|
1272
1162
|
// Structured output mode: when the agent has NO tools, produce structured JSON immediately.
|
|
1273
1163
|
// When the agent HAS tools, we defer structured output until after tool use completes
|