banana-code 1.3.1 → 1.4.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/banana.js +219 -31
- package/lib/agenticRunner.js +236 -10
- package/lib/claudeCodeProvider.js +540 -0
- package/lib/config.js +49 -15
- package/lib/contextBuilder.js +11 -4
- package/lib/fileManager.js +9 -11
- package/lib/fsUtils.js +30 -0
- package/lib/historyManager.js +3 -5
- package/lib/interactivePicker.js +2 -2
- package/lib/modelRegistry.js +3 -2
- package/lib/providerManager.js +7 -1
- package/lib/providerStore.js +38 -4
- package/lib/streamHandler.js +25 -4
- package/package.json +48 -43
- package/prompts/base.md +33 -23
- package/prompts/code-agent-qwen.md +1 -0
- package/prompts/code-agent.md +157 -70
package/lib/agenticRunner.js
CHANGED
|
@@ -549,6 +549,11 @@ const READ_ONLY_TOOLS = TOOLS.filter(t => READ_ONLY_TOOL_NAMES.has(t.function.na
|
|
|
549
549
|
|
|
550
550
|
const IGNORE_PATTERNS = ['node_modules', '.git', '.next', 'dist', 'build', '.banana'];
|
|
551
551
|
const MAX_ITERATIONS = 50;
|
|
552
|
+
const MAX_TOOL_CALLS_PER_TURN = 24;
|
|
553
|
+
const MAX_IDENTICAL_TOOL_CALLS_PER_TURN = 1;
|
|
554
|
+
const MAX_TOOL_CALLS_BY_NAME_PER_TURN = {
|
|
555
|
+
list_files: 6
|
|
556
|
+
};
|
|
552
557
|
const WRITE_TOOL_NAMES = new Set(['create_file', 'edit_file', 'run_command']);
|
|
553
558
|
const CONTEXT_TRIM_THRESHOLD = 0.60; // 60% of context limit - start trimming early
|
|
554
559
|
const CONTEXT_TRIM_KEEP_RECENT = 6; // Keep last N messages intact
|
|
@@ -885,9 +890,50 @@ function executeEditFile(projectDir, filePath, content) {
|
|
|
885
890
|
}
|
|
886
891
|
}
|
|
887
892
|
|
|
893
|
+
function classifyCommandVerification(command) {
|
|
894
|
+
const lowerCommand = String(command || '').trim().toLowerCase();
|
|
895
|
+
const gitMutationRe = /\bgit\s+(pull|checkout|switch|reset|merge|rebase|cherry-pick|restore|clean|stash\s+(pop|apply|drop)|apply|commit|push)\b/;
|
|
896
|
+
const fsMutationRe = /\b(copy|move|ren|rename|mkdir|rmdir|del|erase|xcopy|robocopy|attrib)\b/;
|
|
897
|
+
const gitReadOnlyRe = /\bgit\s+(status|rev-parse|branch|log|diff|show|ls-files|show-ref)\b/;
|
|
898
|
+
const fsReadOnlyRe = /\b(dir|type|findstr|where)\b/;
|
|
899
|
+
const verificationEvidenceFor = [];
|
|
900
|
+
|
|
901
|
+
if (gitReadOnlyRe.test(lowerCommand)) verificationEvidenceFor.push('git_state');
|
|
902
|
+
if (fsReadOnlyRe.test(lowerCommand)) verificationEvidenceFor.push('filesystem_state');
|
|
903
|
+
|
|
904
|
+
if (gitMutationRe.test(lowerCommand)) {
|
|
905
|
+
return {
|
|
906
|
+
requiresVerification: true,
|
|
907
|
+
category: 'git_state',
|
|
908
|
+
verificationHint: 'Before claiming success, run a read-only git check such as `git status --short`, `git rev-parse HEAD`, or compare `HEAD` to `@{u}`.',
|
|
909
|
+
verificationEvidenceFor,
|
|
910
|
+
readOnlyCommand: false
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (fsMutationRe.test(lowerCommand)) {
|
|
915
|
+
return {
|
|
916
|
+
requiresVerification: true,
|
|
917
|
+
category: 'filesystem_state',
|
|
918
|
+
verificationHint: 'Before claiming success, run a read-only check such as `dir`, `type`, or `findstr` to confirm the change is actually present.',
|
|
919
|
+
verificationEvidenceFor,
|
|
920
|
+
readOnlyCommand: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
requiresVerification: false,
|
|
926
|
+
category: null,
|
|
927
|
+
verificationHint: null,
|
|
928
|
+
verificationEvidenceFor,
|
|
929
|
+
readOnlyCommand: verificationEvidenceFor.length > 0
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
888
933
|
async function executeRunCommand(projectDir, command, options = {}) {
|
|
889
934
|
const signal = options.signal;
|
|
890
935
|
const timeoutMs = options.timeoutMs ?? 30000;
|
|
936
|
+
const verificationMeta = classifyCommandVerification(command);
|
|
891
937
|
|
|
892
938
|
// Basic safety check - block destructive commands
|
|
893
939
|
const dangerous = /\b(rm\s+-rf|del\s+\/[sqf]|format\s+[a-z]:)\b/i;
|
|
@@ -946,6 +992,13 @@ async function executeRunCommand(projectDir, command, options = {}) {
|
|
|
946
992
|
const limit = 15000;
|
|
947
993
|
finish(resolve, {
|
|
948
994
|
success: true,
|
|
995
|
+
command,
|
|
996
|
+
outcome: 'completed',
|
|
997
|
+
requiresVerification: verificationMeta.requiresVerification,
|
|
998
|
+
verificationCategory: verificationMeta.category,
|
|
999
|
+
verificationHint: verificationMeta.verificationHint,
|
|
1000
|
+
verificationEvidenceFor: verificationMeta.verificationEvidenceFor,
|
|
1001
|
+
readOnlyCommand: verificationMeta.readOnlyCommand,
|
|
949
1002
|
output: output.substring(0, limit),
|
|
950
1003
|
...(output.length > limit ? { truncated: true, totalLength: output.length } : {})
|
|
951
1004
|
});
|
|
@@ -953,6 +1006,8 @@ async function executeRunCommand(projectDir, command, options = {}) {
|
|
|
953
1006
|
const limit = 10000;
|
|
954
1007
|
finish(resolve, {
|
|
955
1008
|
error: `Command failed with exit code ${code}`,
|
|
1009
|
+
command,
|
|
1010
|
+
outcome: code === 124 ? 'timed_out' : 'failed',
|
|
956
1011
|
output: output.substring(0, limit),
|
|
957
1012
|
exitCode: code,
|
|
958
1013
|
...(output.length > limit ? { truncated: true, totalLength: output.length } : {})
|
|
@@ -967,6 +1022,7 @@ async function executeRunCommand(projectDir, command, options = {}) {
|
|
|
967
1022
|
finish(resolve, {
|
|
968
1023
|
error: `Command timed out after ${timeoutMs}ms`,
|
|
969
1024
|
output: raw.substring(0, 10000),
|
|
1025
|
+
outcome: 'timed_out',
|
|
970
1026
|
exitCode: 124,
|
|
971
1027
|
...(raw.length > 10000 ? { truncated: true, totalLength: raw.length } : {})
|
|
972
1028
|
});
|
|
@@ -1124,6 +1180,77 @@ function stripControlTokens(text) {
|
|
|
1124
1180
|
return cleaned.replace(/^\s+$/, '');
|
|
1125
1181
|
}
|
|
1126
1182
|
|
|
1183
|
+
function stableStringify(value) {
|
|
1184
|
+
if (Array.isArray(value)) {
|
|
1185
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
1186
|
+
}
|
|
1187
|
+
if (value && typeof value === 'object') {
|
|
1188
|
+
const keys = Object.keys(value).sort();
|
|
1189
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
1190
|
+
}
|
|
1191
|
+
return JSON.stringify(value);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function parseToolArgs(rawArgs) {
|
|
1195
|
+
if (typeof rawArgs !== 'string') return {};
|
|
1196
|
+
try {
|
|
1197
|
+
return JSON.parse(rawArgs);
|
|
1198
|
+
} catch {
|
|
1199
|
+
return {};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function sanitizeToolCalls(toolCalls) {
|
|
1204
|
+
const kept = [];
|
|
1205
|
+
const dropped = [];
|
|
1206
|
+
const signatureCounts = new Map();
|
|
1207
|
+
const toolNameCounts = new Map();
|
|
1208
|
+
|
|
1209
|
+
for (const toolCall of toolCalls || []) {
|
|
1210
|
+
const functionName = toolCall?.function?.name;
|
|
1211
|
+
if (!functionName) {
|
|
1212
|
+
dropped.push({ reason: 'invalid', toolCall });
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const args = parseToolArgs(toolCall.function.arguments);
|
|
1217
|
+
const signature = `${functionName}:${stableStringify(args)}`;
|
|
1218
|
+
const seenCount = signatureCounts.get(signature) || 0;
|
|
1219
|
+
const sameToolCount = toolNameCounts.get(functionName) || 0;
|
|
1220
|
+
|
|
1221
|
+
if (seenCount >= MAX_IDENTICAL_TOOL_CALLS_PER_TURN) {
|
|
1222
|
+
dropped.push({ reason: 'duplicate', toolCall, signature });
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const perToolLimit = MAX_TOOL_CALLS_BY_NAME_PER_TURN[functionName];
|
|
1227
|
+
if (perToolLimit && sameToolCount >= perToolLimit) {
|
|
1228
|
+
dropped.push({ reason: 'per_tool_overflow', toolCall, signature });
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (kept.length >= MAX_TOOL_CALLS_PER_TURN) {
|
|
1233
|
+
dropped.push({ reason: 'overflow', toolCall, signature });
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
signatureCounts.set(signature, seenCount + 1);
|
|
1238
|
+
toolNameCounts.set(functionName, sameToolCount + 1);
|
|
1239
|
+
kept.push(toolCall);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return {
|
|
1243
|
+
toolCalls: kept,
|
|
1244
|
+
dropped,
|
|
1245
|
+
summary: {
|
|
1246
|
+
invalid: dropped.filter(item => item.reason === 'invalid').length,
|
|
1247
|
+
duplicate: dropped.filter(item => item.reason === 'duplicate').length,
|
|
1248
|
+
perToolOverflow: dropped.filter(item => item.reason === 'per_tool_overflow').length,
|
|
1249
|
+
overflow: dropped.filter(item => item.reason === 'overflow').length
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1127
1254
|
// ─── Repetition Detection ─────────────────────────────────────────────────────
|
|
1128
1255
|
|
|
1129
1256
|
/**
|
|
@@ -1155,6 +1282,8 @@ async function consumeStream(response, onToken) {
|
|
|
1155
1282
|
let thinkBuffer = ''; // accumulates text inside a think block
|
|
1156
1283
|
let inThink = false;
|
|
1157
1284
|
let repetitionDetected = false;
|
|
1285
|
+
let doneSignalReceived = false;
|
|
1286
|
+
let warning = null;
|
|
1158
1287
|
|
|
1159
1288
|
const flush = (text) => {
|
|
1160
1289
|
const clean = stripControlTokens(text);
|
|
@@ -1184,7 +1313,11 @@ async function consumeStream(response, onToken) {
|
|
|
1184
1313
|
|
|
1185
1314
|
for (const line of lines) {
|
|
1186
1315
|
const trimmed = line.trim();
|
|
1187
|
-
if (!trimmed
|
|
1316
|
+
if (!trimmed) continue;
|
|
1317
|
+
if (trimmed === 'data: [DONE]') {
|
|
1318
|
+
doneSignalReceived = true;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1188
1321
|
if (!trimmed.startsWith('data: ')) continue;
|
|
1189
1322
|
|
|
1190
1323
|
try {
|
|
@@ -1224,7 +1357,16 @@ async function consumeStream(response, onToken) {
|
|
|
1224
1357
|
}
|
|
1225
1358
|
}
|
|
1226
1359
|
|
|
1227
|
-
|
|
1360
|
+
if (!doneSignalReceived) {
|
|
1361
|
+
warning = 'Warning: final stream ended without an explicit completion signal. The response may be incomplete.';
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return {
|
|
1365
|
+
content: fullContent,
|
|
1366
|
+
completed: doneSignalReceived,
|
|
1367
|
+
warning,
|
|
1368
|
+
repetitionDetected
|
|
1369
|
+
};
|
|
1228
1370
|
}
|
|
1229
1371
|
|
|
1230
1372
|
// ─── Agentic Loop ───────────────────────────────────────────────────────────
|
|
@@ -1252,6 +1394,7 @@ class AgenticRunner {
|
|
|
1252
1394
|
this.lastTurnMessagesEstimate = 0;
|
|
1253
1395
|
this.totalCacheReadTokens = 0;
|
|
1254
1396
|
this.totalCacheCreationTokens = 0;
|
|
1397
|
+
this.lastRunOutcome = { status: 'running', phase: 'start', warning: null };
|
|
1255
1398
|
}
|
|
1256
1399
|
|
|
1257
1400
|
/**
|
|
@@ -1302,9 +1445,12 @@ class AgenticRunner {
|
|
|
1302
1445
|
let iterations = 0;
|
|
1303
1446
|
const toolCallHistory = []; // Track tool calls for loop detection
|
|
1304
1447
|
const failedMcpTools = new Set(); // Track MCP tools that returned "Unknown tool" errors
|
|
1448
|
+
const pendingCommandVerifications = new Map(); // category -> verification hint
|
|
1305
1449
|
let readOnlyStreak = 0; // Consecutive iterations with only read-only tool calls
|
|
1306
1450
|
let loopWarningCount = 0; // How many times loop detection has fired
|
|
1307
1451
|
|
|
1452
|
+
let verificationReminderCount = 0; // How many times we had to demand verification before finalizing
|
|
1453
|
+
|
|
1308
1454
|
// Model-tier-aware read-only thresholds: smarter models get more research leeway
|
|
1309
1455
|
// options.model is the raw model ID (e.g. "claude-sonnet-4-6-20250514", "gpt-4o", "silverback")
|
|
1310
1456
|
const modelId = (options.model || '').toLowerCase();
|
|
@@ -1483,12 +1629,27 @@ class AgenticRunner {
|
|
|
1483
1629
|
// Some models use finish_reason "tool_calls", others use "stop" or "function_call"
|
|
1484
1630
|
// but still include tool_calls in the message. Check for the array itself.
|
|
1485
1631
|
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
|
|
1486
|
-
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1632
|
+
const originalToolCallCount = assistantMessage.tool_calls.length;
|
|
1633
|
+
const sanitizedBatch = sanitizeToolCalls(assistantMessage.tool_calls);
|
|
1634
|
+
assistantMessage.tool_calls = sanitizedBatch.toolCalls;
|
|
1635
|
+
|
|
1636
|
+
if (sanitizedBatch.dropped.length > 0) {
|
|
1637
|
+
appendDebugLog(
|
|
1638
|
+
` [tool batch sanitized] original=${originalToolCallCount} kept=${assistantMessage.tool_calls.length} ` +
|
|
1639
|
+
`duplicate=${sanitizedBatch.summary.duplicate} per_tool_overflow=${sanitizedBatch.summary.perToolOverflow} ` +
|
|
1640
|
+
`overflow=${sanitizedBatch.summary.overflow} invalid=${sanitizedBatch.summary.invalid}\n`
|
|
1641
|
+
);
|
|
1642
|
+
this.onWarning(
|
|
1643
|
+
`Trimmed a noisy tool batch from ${originalToolCallCount} calls to ${assistantMessage.tool_calls.length}.`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (assistantMessage.tool_calls.length === 0) {
|
|
1648
|
+
messages.push({
|
|
1649
|
+
role: 'system',
|
|
1650
|
+
content: 'Your previous tool batch was invalid or excessively repetitive. Do NOT emit more tools right now. Answer the user directly with what you already know, or explain what specific missing context is still needed.'
|
|
1651
|
+
});
|
|
1652
|
+
continue;
|
|
1492
1653
|
}
|
|
1493
1654
|
|
|
1494
1655
|
// Add assistant message to history, preserving the reasoning field
|
|
@@ -1568,6 +1729,14 @@ class AgenticRunner {
|
|
|
1568
1729
|
|
|
1569
1730
|
// Track command execution for hooks
|
|
1570
1731
|
if (functionName === 'run_command' && !result.error) {
|
|
1732
|
+
if (result.requiresVerification && result.verificationCategory) {
|
|
1733
|
+
pendingCommandVerifications.set(result.verificationCategory, result.verificationHint || 'Run a read-only verification command before claiming success.');
|
|
1734
|
+
}
|
|
1735
|
+
if (Array.isArray(result.verificationEvidenceFor)) {
|
|
1736
|
+
for (const category of result.verificationEvidenceFor) {
|
|
1737
|
+
pendingCommandVerifications.delete(category);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1571
1740
|
if (this.onCommandComplete) this.onCommandComplete(args.command, result);
|
|
1572
1741
|
}
|
|
1573
1742
|
|
|
@@ -1673,6 +1842,12 @@ class AgenticRunner {
|
|
|
1673
1842
|
nudgeParts.push(`Non-existent MCP tools (do NOT retry): ${[...failedMcpTools].join(', ')}`);
|
|
1674
1843
|
}
|
|
1675
1844
|
|
|
1845
|
+
if (pendingCommandVerifications.size > 0) {
|
|
1846
|
+
nudgeParts.push(
|
|
1847
|
+
`State-changing commands are still UNVERIFIED. Before telling the user the task is done, run a read-only verification step. ${[...pendingCommandVerifications.values()].join(' ')}`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1676
1851
|
if (nudgeParts.length > 0) {
|
|
1677
1852
|
messages.push({
|
|
1678
1853
|
role: 'system',
|
|
@@ -1680,6 +1855,17 @@ class AgenticRunner {
|
|
|
1680
1855
|
});
|
|
1681
1856
|
}
|
|
1682
1857
|
|
|
1858
|
+
if (sanitizedBatch.dropped.length > 0) {
|
|
1859
|
+
messages.push({
|
|
1860
|
+
role: 'system',
|
|
1861
|
+
content:
|
|
1862
|
+
`Your previous response tried to call too many or duplicate tools. ` +
|
|
1863
|
+
`Dropped: ${sanitizedBatch.summary.duplicate} duplicate, ${sanitizedBatch.summary.perToolOverflow} excessive same-tool calls, ` +
|
|
1864
|
+
`${sanitizedBatch.summary.overflow} overflow, ${sanitizedBatch.summary.invalid} invalid. ` +
|
|
1865
|
+
`Next turn, use fewer tools and avoid repeating the same call with identical arguments.`
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1683
1869
|
// Track read-only streaks (iterations with no writes or commands)
|
|
1684
1870
|
// Skip streak tracking in plan mode - plan mode is inherently read-only
|
|
1685
1871
|
const thisIterToolNames = assistantMessage.tool_calls.map(t => t.function.name);
|
|
@@ -1752,6 +1938,7 @@ class AgenticRunner {
|
|
|
1752
1938
|
this._lastWrittenFiles = [...writtenFiles];
|
|
1753
1939
|
logRunTotals('loop-break');
|
|
1754
1940
|
const loopResponse = finalContent || 'I got stuck in a loop and could not complete the task. Please try rephrasing your request.';
|
|
1941
|
+
this.lastRunOutcome = { status: 'completed_with_warnings', phase: 'loop-break', warning: 'Loop breaker forced finalization.' };
|
|
1755
1942
|
await this.emitStreaming(loopResponse);
|
|
1756
1943
|
this.onContent(loopResponse);
|
|
1757
1944
|
return loopResponse;
|
|
@@ -1787,6 +1974,7 @@ class AgenticRunner {
|
|
|
1787
1974
|
this._lastWrittenFiles = [...writtenFiles];
|
|
1788
1975
|
logRunTotals('no-progress-break');
|
|
1789
1976
|
const npResponse = npContent || 'I spent too many iterations researching without making progress. Please try a more specific request.';
|
|
1977
|
+
this.lastRunOutcome = { status: 'completed_with_warnings', phase: 'no-progress-break', warning: 'No-progress breaker forced finalization.' };
|
|
1790
1978
|
await this.emitStreaming(npResponse);
|
|
1791
1979
|
this.onContent(npResponse);
|
|
1792
1980
|
return npResponse;
|
|
@@ -1801,6 +1989,18 @@ class AgenticRunner {
|
|
|
1801
1989
|
// Final response - no more tool calls.
|
|
1802
1990
|
// The non-streaming chat() call already returned content. Use it directly
|
|
1803
1991
|
// instead of making a redundant streaming call that may return empty/truncated.
|
|
1992
|
+
if (pendingCommandVerifications.size > 0 && verificationReminderCount < 1) {
|
|
1993
|
+
verificationReminderCount++;
|
|
1994
|
+
messages.push({
|
|
1995
|
+
role: 'system',
|
|
1996
|
+
content:
|
|
1997
|
+
`STOP. You are about to answer, but you still have unverified state-changing command results. ` +
|
|
1998
|
+
`Before claiming completion, run at least one read-only verification step for these categories: ${[...pendingCommandVerifications.keys()].join(', ')}. ` +
|
|
1999
|
+
`${[...pendingCommandVerifications.values()].join(' ')}`
|
|
2000
|
+
});
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1804
2004
|
let existingContent = stripControlTokens(assistantMessage.content || '');
|
|
1805
2005
|
|
|
1806
2006
|
// Extract inline <think>/<thinking> blocks from content (Qwen3.5 embeds reasoning in content)
|
|
@@ -1835,11 +2035,23 @@ class AgenticRunner {
|
|
|
1835
2035
|
|
|
1836
2036
|
const reasoning = assistantMessage.reasoning || assistantMessage.reasoning_content || inlineReasoning;
|
|
1837
2037
|
|
|
2038
|
+
if (pendingCommandVerifications.size > 0) {
|
|
2039
|
+
const verificationWarning = `Warning: the requested command effects were not independently verified. ${[...pendingCommandVerifications.values()].join(' ')}`;
|
|
2040
|
+
existingContent = existingContent
|
|
2041
|
+
? `${verificationWarning}\n\n${existingContent}`
|
|
2042
|
+
: verificationWarning;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1838
2045
|
// If the model already produced content in this iteration, use it directly
|
|
1839
2046
|
if (existingContent) {
|
|
1840
2047
|
if (reasoning) {
|
|
1841
2048
|
this.onReasoning(stripControlTokens(reasoning));
|
|
1842
2049
|
}
|
|
2050
|
+
this.lastRunOutcome = {
|
|
2051
|
+
status: pendingCommandVerifications.size > 0 ? 'completed_with_warnings' : 'completed',
|
|
2052
|
+
phase: 'final-content',
|
|
2053
|
+
warning: pendingCommandVerifications.size > 0 ? 'Completion claims were not fully verified.' : null
|
|
2054
|
+
};
|
|
1843
2055
|
await this.emitStreaming(existingContent);
|
|
1844
2056
|
this.onContent(existingContent);
|
|
1845
2057
|
logRunTotals('final-content');
|
|
@@ -1853,6 +2065,7 @@ class AgenticRunner {
|
|
|
1853
2065
|
// Some models put the actual answer in reasoning when content is empty.
|
|
1854
2066
|
// Return a minimal acknowledgment rather than an empty response.
|
|
1855
2067
|
const fallback = '(Response was in reasoning only - see thinking output above)';
|
|
2068
|
+
this.lastRunOutcome = { status: 'completed_with_warnings', phase: 'final-reasoning-fallback', warning: 'Model returned reasoning without visible content.' };
|
|
1856
2069
|
await this.emitStreaming(fallback);
|
|
1857
2070
|
this.onContent(fallback);
|
|
1858
2071
|
logRunTotals('final-reasoning-fallback');
|
|
@@ -1877,6 +2090,7 @@ class AgenticRunner {
|
|
|
1877
2090
|
const content = stripControlTokens(thinkMsg?.content || '');
|
|
1878
2091
|
|
|
1879
2092
|
if (thinkReasoning) this.onReasoning(stripControlTokens(thinkReasoning));
|
|
2093
|
+
this.lastRunOutcome = { status: 'completed', phase: 'final-think-pass', warning: null };
|
|
1880
2094
|
await this.emitStreaming(content);
|
|
1881
2095
|
this.onContent(content);
|
|
1882
2096
|
logRunTotals('final-think-pass');
|
|
@@ -1895,10 +2109,21 @@ class AgenticRunner {
|
|
|
1895
2109
|
signal: options.signal
|
|
1896
2110
|
});
|
|
1897
2111
|
|
|
1898
|
-
const
|
|
2112
|
+
const streamResult = await consumeStream(streamResponse, (token) => {
|
|
1899
2113
|
this.onToken(token);
|
|
1900
2114
|
});
|
|
2115
|
+
const content = streamResult.completed || !streamResult.warning
|
|
2116
|
+
? streamResult.content
|
|
2117
|
+
: `${streamResult.warning}\n\n${streamResult.content}`.trim();
|
|
1901
2118
|
|
|
2119
|
+
if (!streamResult.completed && streamResult.warning) {
|
|
2120
|
+
this.onWarning(streamResult.warning);
|
|
2121
|
+
}
|
|
2122
|
+
this.lastRunOutcome = {
|
|
2123
|
+
status: streamResult.completed ? 'completed' : 'completed_with_warnings',
|
|
2124
|
+
phase: 'final-stream',
|
|
2125
|
+
warning: streamResult.warning
|
|
2126
|
+
};
|
|
1902
2127
|
this.onContent(content);
|
|
1903
2128
|
logRunTotals('final-stream');
|
|
1904
2129
|
return content;
|
|
@@ -1906,9 +2131,10 @@ class AgenticRunner {
|
|
|
1906
2131
|
}
|
|
1907
2132
|
|
|
1908
2133
|
this.onWarning('Max tool iterations reached');
|
|
2134
|
+
this.lastRunOutcome = { status: 'failed', phase: 'max-iterations', warning: 'Max tool iterations reached.' };
|
|
1909
2135
|
logRunTotals('max-iterations');
|
|
1910
2136
|
return '';
|
|
1911
2137
|
}
|
|
1912
2138
|
}
|
|
1913
2139
|
|
|
1914
|
-
module.exports = { AgenticRunner, TOOLS, READ_ONLY_TOOLS, executeTool, setMcpClient };
|
|
2140
|
+
module.exports = { AgenticRunner, TOOLS, READ_ONLY_TOOLS, executeTool, setMcpClient, sanitizeToolCalls, classifyCommandVerification };
|