mixdog 0.9.0 → 0.9.2
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/package.json +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -2,27 +2,34 @@ import { classifyResultKind } from './result-classification.mjs';
|
|
|
2
2
|
import { executeMcpTool, isMcpTool, mcpToolHasField } from '../mcp/client.mjs';
|
|
3
3
|
import { canonicalizeBuiltinToolName, executeBuiltinTool, formatUnknownBuiltinToolMessage, isBuiltinTool } from '../tools/builtin.mjs';
|
|
4
4
|
import { executeBashSessionTool } from '../tools/bash-session.mjs';
|
|
5
|
-
import { executePatchTool } from '../tools/patch.mjs';
|
|
5
|
+
import { executePatchTool, takeApplyPatchUiDiff } from '../tools/patch.mjs';
|
|
6
6
|
import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
|
|
7
|
-
import { collectSkillsCached,
|
|
8
|
-
import {
|
|
7
|
+
import { collectSkillsCached, loadSkillResource, buildSkillToolEnvelope } from '../context/collect.mjs';
|
|
8
|
+
import { normalizeToolEnvelope, makeToolEnvelope } from './tool-envelope.mjs';
|
|
9
|
+
import { traceAgentLoop, traceAgentTool, traceAgentToolFailure, traceAgentCompact, estimateProviderPayloadBytes, messagePrefixHash, appendAgentTrace } from '../agent-trace.mjs';
|
|
10
|
+
import { resolveSessionMaxLoopIterations } from '../agent-runtime/agent-loop-policy.mjs';
|
|
9
11
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
10
12
|
import { markSessionToolCall, updateSessionStage, SessionClosedError, getSessionAbortSignal, enqueuePendingMessage, bumpUsageMetricsEpoch } from './manager.mjs';
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
estimateMessagesTokens,
|
|
15
|
+
estimateRequestReserveTokens,
|
|
16
|
+
sanitizeToolPairs,
|
|
17
|
+
resolveCompactBufferRatio,
|
|
18
|
+
resolveCompactBufferTokens,
|
|
19
|
+
} from './context-utils.mjs';
|
|
12
20
|
import {
|
|
13
21
|
recallFastTrackCompactMessages,
|
|
14
22
|
pruneToolOutputs,
|
|
23
|
+
pruneToolOutputsUnanchored,
|
|
15
24
|
semanticCompactMessages,
|
|
25
|
+
effectiveBudget as compactEffectiveBudget,
|
|
16
26
|
compactTypeIsRecallFastTrack,
|
|
17
27
|
compactTypeIsSemantic,
|
|
18
28
|
normalizeCompactType,
|
|
19
29
|
DEFAULT_COMPACT_TYPE,
|
|
20
|
-
DEFAULT_COMPACTION_BUFFER_TOKENS,
|
|
21
|
-
DEFAULT_COMPACTION_BUFFER_RATIO,
|
|
22
30
|
DEFAULT_COMPACTION_KEEP_TOKENS,
|
|
23
|
-
compactionBufferTokensForBoundary,
|
|
24
|
-
normalizeCompactionBufferRatio,
|
|
25
31
|
drainSessionCycle1,
|
|
32
|
+
countRawPendingRows,
|
|
26
33
|
} from './compact.mjs';
|
|
27
34
|
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
28
35
|
import { stripSoftWarns } from '../tool-loop-guard.mjs';
|
|
@@ -126,9 +133,9 @@ function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
|
126
133
|
if (_agentOwned && _controlPlaneTool) {
|
|
127
134
|
return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
|
|
128
135
|
}
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
return `Error: tool "${name}" is not available in
|
|
136
|
+
const noToolAgent = sessionRef?.agent === 'cycle1-agent' || sessionRef?.agent === 'cycle2-agent';
|
|
137
|
+
if (noToolAgent) {
|
|
138
|
+
return `Error: tool "${name}" is not available in agent "${sessionRef.agent}". Re-emit the answer as pipe-separated text per the agent's output format (first character a digit, NO tool_use blocks, NO JSON, NO prose, NO apology).`;
|
|
132
139
|
}
|
|
133
140
|
return null;
|
|
134
141
|
}
|
|
@@ -151,6 +158,27 @@ function estimateMessagesTokensSafe(messages) {
|
|
|
151
158
|
catch { return null; }
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
function compactDebugEnabled() {
|
|
162
|
+
return String(process.env.MIXDOG_COMPACT_DEBUG || '').trim() === '1';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compactDiagnosticError(err) {
|
|
166
|
+
if (!err) return null;
|
|
167
|
+
const text = String(err?.message || err);
|
|
168
|
+
return text.length > 500 ? `${text.slice(0, 499)}…` : text;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function compactByteLength(text) {
|
|
172
|
+
try { return Buffer.byteLength(String(text || ''), 'utf8'); }
|
|
173
|
+
catch { return String(text || '').length; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function compactDebugLog(scope, details = {}) {
|
|
177
|
+
if (!compactDebugEnabled()) return;
|
|
178
|
+
try { process.stderr.write(`[compact] ${scope} ${JSON.stringify(details)}\n`); }
|
|
179
|
+
catch { /* best-effort diagnostics only */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
function steeringContentText(content) {
|
|
155
183
|
if (typeof content === 'string') return content;
|
|
156
184
|
if (Array.isArray(content)) {
|
|
@@ -255,21 +283,20 @@ function agentContextOverflowError({ stage, sessionId, sessionRef, model, budget
|
|
|
255
283
|
// transcript is compacted (see the trim block below): a long task that keeps
|
|
256
284
|
// compacting can proceed past this count, while a tight NON-compacting loop
|
|
257
285
|
// still stops here and returns the accumulated transcript.
|
|
258
|
-
const MAX_LOOP_ITERATIONS = 200;
|
|
259
286
|
// Consecutive identical-AND-failing tool calls (same name+args, error result)
|
|
260
287
|
// tolerated across iterations before the loop refuses to re-execute and steers
|
|
261
288
|
// the model to change approach. Distinct from the hard iteration cap above:
|
|
262
289
|
// this catches tight deterministic-failure loops (e.g. a command that errors
|
|
263
290
|
// the same way every time) far earlier than 100 iterations.
|
|
264
291
|
const REPEAT_FAIL_LIMIT = 3;
|
|
265
|
-
const
|
|
266
|
-
let
|
|
267
|
-
function
|
|
268
|
-
if (
|
|
292
|
+
const _AGENTS_JSON = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../defaults/agents.json');
|
|
293
|
+
let _hiddenAgentsCache = null;
|
|
294
|
+
function _getHiddenAgents() {
|
|
295
|
+
if (_hiddenAgentsCache) return _hiddenAgentsCache;
|
|
269
296
|
try {
|
|
270
|
-
|
|
271
|
-
} catch {
|
|
272
|
-
return
|
|
297
|
+
_hiddenAgentsCache = JSON.parse(_readFileSync(_AGENTS_JSON, 'utf8'));
|
|
298
|
+
} catch { _hiddenAgentsCache = { agents: [] }; }
|
|
299
|
+
return _hiddenAgentsCache;
|
|
273
300
|
}
|
|
274
301
|
// Transcript pairing guard. Anthropic 400-rejects when an assistant message
|
|
275
302
|
// ends with tool_use blocks and the next message isn't tool results for
|
|
@@ -443,6 +470,27 @@ function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
|
|
|
443
470
|
|
|
444
471
|
async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
445
472
|
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
473
|
+
const startedAt = Date.now();
|
|
474
|
+
const diagnostics = {
|
|
475
|
+
hydrateLimit: null,
|
|
476
|
+
ingestMs: null,
|
|
477
|
+
ingestSkipped: false,
|
|
478
|
+
ingestError: null,
|
|
479
|
+
initialDumpMs: null,
|
|
480
|
+
initialDumpBytes: null,
|
|
481
|
+
initialDumpChars: null,
|
|
482
|
+
initialRawPending: null,
|
|
483
|
+
cycle1Ms: null,
|
|
484
|
+
cycle1Skipped: false,
|
|
485
|
+
cycle1SkipReason: null,
|
|
486
|
+
cycle1Passes: null,
|
|
487
|
+
cycle1RawRemaining: null,
|
|
488
|
+
cycle1TextBytes: null,
|
|
489
|
+
cycle1Error: null,
|
|
490
|
+
finalRecallBytes: null,
|
|
491
|
+
finalRecallChars: null,
|
|
492
|
+
totalMs: null,
|
|
493
|
+
};
|
|
446
494
|
const query = `session:${sessionId}:all-chunks`;
|
|
447
495
|
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
448
496
|
const callerCtx = {
|
|
@@ -454,6 +502,8 @@ async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTo
|
|
|
454
502
|
};
|
|
455
503
|
const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
|
|
456
504
|
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
505
|
+
diagnostics.hydrateLimit = hydrateLimit;
|
|
506
|
+
let t0 = Date.now();
|
|
457
507
|
try {
|
|
458
508
|
await executeInternalTool('memory', {
|
|
459
509
|
action: 'ingest_session',
|
|
@@ -463,7 +513,11 @@ async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTo
|
|
|
463
513
|
limit: hydrateLimit,
|
|
464
514
|
}, callerCtx);
|
|
465
515
|
} catch (err) {
|
|
516
|
+
diagnostics.ingestSkipped = true;
|
|
517
|
+
diagnostics.ingestError = compactDiagnosticError(err);
|
|
466
518
|
try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
519
|
+
} finally {
|
|
520
|
+
diagnostics.ingestMs = Date.now() - t0;
|
|
467
521
|
}
|
|
468
522
|
const dumpArgs = {
|
|
469
523
|
action: 'dump_session_roots',
|
|
@@ -472,10 +526,16 @@ async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTo
|
|
|
472
526
|
limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
|
|
473
527
|
};
|
|
474
528
|
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
529
|
+
t0 = Date.now();
|
|
475
530
|
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
531
|
+
diagnostics.initialDumpMs = Date.now() - t0;
|
|
532
|
+
diagnostics.initialDumpChars = String(recallText || '').length;
|
|
533
|
+
diagnostics.initialDumpBytes = compactByteLength(recallText);
|
|
534
|
+
diagnostics.initialRawPending = countRawPendingRows(recallText);
|
|
476
535
|
let cycle1Text = '';
|
|
477
536
|
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
478
537
|
if (hasRawRows) {
|
|
538
|
+
t0 = Date.now();
|
|
479
539
|
try {
|
|
480
540
|
// Drain this session's cycle1 in window×concurrency units until no
|
|
481
541
|
// raw rows remain, so the injected root is fully chunked rather than
|
|
@@ -496,19 +556,32 @@ async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTo
|
|
|
496
556
|
});
|
|
497
557
|
recallText = drained.recallText;
|
|
498
558
|
cycle1Text = drained.cycle1Text;
|
|
559
|
+
diagnostics.cycle1Passes = drained.passes;
|
|
560
|
+
diagnostics.cycle1RawRemaining = drained.rawRemaining;
|
|
561
|
+
diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
|
|
499
562
|
if (drained.rawRemaining > 0) {
|
|
500
563
|
try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
|
|
501
564
|
}
|
|
502
565
|
} catch (err) {
|
|
566
|
+
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
503
567
|
try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
568
|
+
} finally {
|
|
569
|
+
diagnostics.cycle1Ms = Date.now() - t0;
|
|
504
570
|
}
|
|
505
571
|
} else {
|
|
572
|
+
diagnostics.cycle1Skipped = true;
|
|
573
|
+
diagnostics.cycle1SkipReason = 'session chunks already hydrated';
|
|
574
|
+
diagnostics.cycle1Passes = 0;
|
|
575
|
+
diagnostics.cycle1RawRemaining = 0;
|
|
506
576
|
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
507
577
|
}
|
|
508
|
-
|
|
578
|
+
const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
|
|
579
|
+
diagnostics.finalRecallChars = combinedRecallText.length;
|
|
580
|
+
diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
|
|
581
|
+
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
509
582
|
reserveTokens: compactPolicy.reserveTokens,
|
|
510
583
|
force: true,
|
|
511
|
-
recallText:
|
|
584
|
+
recallText: combinedRecallText,
|
|
512
585
|
query,
|
|
513
586
|
querySha,
|
|
514
587
|
allowEmptyRecall: true,
|
|
@@ -516,97 +589,15 @@ async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTo
|
|
|
516
589
|
keepTokens: compactPolicy.keepTokens,
|
|
517
590
|
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
518
591
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
function _resolveBufferRatioCandidate(percentInputs, ratioInputs) {
|
|
526
|
-
for (const raw of percentInputs) {
|
|
527
|
-
const n = Number(raw);
|
|
528
|
-
if (Number.isFinite(n) && n > 0) return Math.min(1, n / 100);
|
|
529
|
-
}
|
|
530
|
-
for (const raw of ratioInputs) {
|
|
531
|
-
const n = Number(raw);
|
|
532
|
-
if (Number.isFinite(n) && n > 0) return n > 1 ? n / 100 : n;
|
|
533
|
-
}
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
function resolveCompactBufferRatio(cfg = {}) {
|
|
537
|
-
const resolved = _resolveBufferRatioCandidate(
|
|
538
|
-
[cfg.bufferPercent, cfg.bufferPct, process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT],
|
|
539
|
-
[cfg.bufferRatio, cfg.bufferFraction, process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO],
|
|
540
|
-
);
|
|
541
|
-
return normalizeCompactionBufferRatio(resolved, DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
542
|
-
}
|
|
543
|
-
const LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
544
|
-
function isPersistedZeroBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
545
|
-
const boundary = positiveTokenInt(boundaryTokens);
|
|
546
|
-
if (!boundary) return false;
|
|
547
|
-
if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
|
|
548
|
-
for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
|
|
549
|
-
const n = Number(process.env[envName]);
|
|
550
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
551
|
-
}
|
|
552
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
553
|
-
const n = Number(cfg?.[key]);
|
|
554
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
555
|
-
}
|
|
556
|
-
const ratio = Number(cfg?.bufferRatio);
|
|
557
|
-
if (Number.isFinite(ratio) && ratio > 0) return false;
|
|
558
|
-
const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
|
|
559
|
-
if (!Number.isFinite(explicitTokens) || explicitTokens !== 0) return false;
|
|
560
|
-
return true;
|
|
561
|
-
}
|
|
562
|
-
function isLegacyDefaultBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
563
|
-
const boundary = positiveTokenInt(boundaryTokens);
|
|
564
|
-
if (!boundary) return false;
|
|
565
|
-
if (envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')) return false;
|
|
566
|
-
for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
|
|
567
|
-
const n = Number(process.env[envName]);
|
|
568
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
569
|
-
}
|
|
570
|
-
// Percent/fraction-named fields are operator config. The legacy default
|
|
571
|
-
// telemetry persisted bufferTokens + bufferRatio after a check/compact pass.
|
|
572
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
573
|
-
const n = Number(cfg?.[key]);
|
|
574
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
575
|
-
}
|
|
576
|
-
const explicitTokens = positiveTokenInt(cfg?.bufferTokens ?? cfg?.buffer);
|
|
577
|
-
const ratio = Number(cfg?.bufferRatio);
|
|
578
|
-
if (!explicitTokens || !Number.isFinite(ratio) || Math.abs(ratio - LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO) > 1e-9) return false;
|
|
579
|
-
const expectedTokens = Math.floor(boundary * LEGACY_DEFAULT_COMPACTION_BUFFER_RATIO);
|
|
580
|
-
const cfgBoundary = positiveTokenInt(cfg?.boundaryTokens);
|
|
581
|
-
const cfgTrigger = positiveTokenInt(cfg?.triggerTokens);
|
|
582
|
-
return explicitTokens === expectedTokens
|
|
583
|
-
|| (cfgBoundary === boundary && cfgTrigger > 0 && explicitTokens === Math.max(0, boundary - cfgTrigger));
|
|
584
|
-
}
|
|
585
|
-
function compactBufferConfigForBoundary(cfg = {}, boundaryTokens = 0) {
|
|
586
|
-
const base = cfg || {};
|
|
587
|
-
if (!isLegacyDefaultBufferTelemetry(base, boundaryTokens)
|
|
588
|
-
&& !isPersistedZeroBufferTelemetry(base, boundaryTokens)) {
|
|
589
|
-
return base;
|
|
592
|
+
diagnostics.totalMs = Date.now() - startedAt;
|
|
593
|
+
if (result && typeof result === 'object') {
|
|
594
|
+
result.diagnostics = {
|
|
595
|
+
...(result.diagnostics || {}),
|
|
596
|
+
pipeline: diagnostics,
|
|
597
|
+
};
|
|
590
598
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
bufferTokens: null,
|
|
594
|
-
buffer: null,
|
|
595
|
-
bufferRatio: null,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
function resolveCompactBufferTokens(boundaryTokens, cfg = {}) {
|
|
599
|
-
const boundary = positiveTokenInt(boundaryTokens);
|
|
600
|
-
const effectiveCfg = compactBufferConfigForBoundary(cfg, boundary);
|
|
601
|
-
const configured = positiveTokenInt(effectiveCfg.bufferTokens ?? effectiveCfg.buffer)
|
|
602
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_BUFFER_TOKENS')
|
|
603
|
-
|| 0;
|
|
604
|
-
if (!boundary) return configured || DEFAULT_COMPACTION_BUFFER_TOKENS;
|
|
605
|
-
return compactionBufferTokensForBoundary(boundary, {
|
|
606
|
-
explicitTokens: configured,
|
|
607
|
-
ratio: resolveCompactBufferRatio(effectiveCfg),
|
|
608
|
-
maxRatio: COMPACT_BUFFER_MAX_WINDOW_FRACTION,
|
|
609
|
-
});
|
|
599
|
+
compactDebugLog('recall-fasttrack pipeline', diagnostics);
|
|
600
|
+
return result;
|
|
610
601
|
}
|
|
611
602
|
const COMPACT_TARGET_RATIO = 0.02;
|
|
612
603
|
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
@@ -947,9 +938,12 @@ function buildSkillsListResponse(cwd) {
|
|
|
947
938
|
}
|
|
948
939
|
function viewSkill(cwd, name) {
|
|
949
940
|
if (!name) return 'Error: skill name is required';
|
|
950
|
-
const
|
|
951
|
-
if (!
|
|
952
|
-
|
|
941
|
+
const res = loadSkillResource(name, cwd);
|
|
942
|
+
if (!res) return `Error: skill "${name}" not found`;
|
|
943
|
+
// Return the general tool envelope: the model-visible tool_result is the
|
|
944
|
+
// short stub (`Loaded skill: <name>`) and the full SKILL.md body is
|
|
945
|
+
// delivered ONCE as a separate injected role:'user' message (newMessages).
|
|
946
|
+
return buildSkillToolEnvelope(name, res.content, res.dir);
|
|
953
947
|
}
|
|
954
948
|
|
|
955
949
|
/** Normalize PostToolUse hook override values (legacy MCP text envelopes only). */
|
|
@@ -1238,7 +1232,7 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1238
1232
|
}
|
|
1239
1233
|
if (name === 'apply_patch') {
|
|
1240
1234
|
const patchArgs = typeof args === 'string' ? { patch: args } : args;
|
|
1241
|
-
return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId });
|
|
1235
|
+
return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId, toolCallId: executeOpts.toolCallId || null });
|
|
1242
1236
|
}
|
|
1243
1237
|
if (isBuiltinTool(name)) {
|
|
1244
1238
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
@@ -1257,7 +1251,14 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1257
1251
|
toolCallId: executeOpts.toolCallId || null,
|
|
1258
1252
|
result: __result,
|
|
1259
1253
|
});
|
|
1260
|
-
|
|
1254
|
+
// Envelope-aware hook override: a PostToolUse hook may override the
|
|
1255
|
+
// model-VISIBLE tool output (the envelope's `result` / stub), but it
|
|
1256
|
+
// must NEVER drop the `newMessages` channel. Split first, apply the
|
|
1257
|
+
// override to `result` only, then re-wrap so newMessages survive.
|
|
1258
|
+
const { result: __res, newMessages: __nm } = normalizeToolEnvelope(__result);
|
|
1259
|
+
const __overridden = resolveToolResultAfterHook(__res, hookResult);
|
|
1260
|
+
if (__nm.length) return makeToolEnvelope(__overridden, __nm);
|
|
1261
|
+
return __overridden;
|
|
1261
1262
|
} catch {
|
|
1262
1263
|
// PostToolUse hooks are best-effort; never let one break the tool result.
|
|
1263
1264
|
}
|
|
@@ -1275,11 +1276,11 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1275
1276
|
* wrapper can propagate a clean cancellation.
|
|
1276
1277
|
* - `onStageChange(stage)` / `onStreamDelta()` — forwarded to provider.send for heartbeats
|
|
1277
1278
|
*/
|
|
1278
|
-
// Source of truth: defaults/
|
|
1279
|
-
// above). Build the name Set eagerly at module load so
|
|
1279
|
+
// Source of truth: defaults/agents.json (loaded via _getHiddenAgents
|
|
1280
|
+
// above). Build the name Set eagerly at module load so HIDDEN_AGENT_NAMES
|
|
1280
1281
|
// stays in sync with the declarative registry — no hardcoded duplicate.
|
|
1281
|
-
const
|
|
1282
|
-
(
|
|
1282
|
+
const HIDDEN_AGENT_NAMES = new Set(
|
|
1283
|
+
(_getHiddenAgents().agents || []).map((r) => r && r.agent).filter((n) => typeof n === 'string' && n.length > 0)
|
|
1283
1284
|
);
|
|
1284
1285
|
|
|
1285
1286
|
// Stop reasons that signal the turn was cut short mid-synthesis (token cap,
|
|
@@ -1315,6 +1316,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1315
1316
|
let response;
|
|
1316
1317
|
let contractNudges = 0;
|
|
1317
1318
|
let contextOverflowRetryUsed = false;
|
|
1319
|
+
// Set when the hard iteration-cap break below fires. Consumed at the final
|
|
1320
|
+
// return to tag terminationReason='iteration_cap' so a worker that exhausts
|
|
1321
|
+
// the loop without a final answer surfaces to Lead as an explicit error
|
|
1322
|
+
// instead of a silent empty "completed".
|
|
1323
|
+
let terminatedByCap = false;
|
|
1318
1324
|
// Set when a provider context-overflow refusal triggers the in-turn
|
|
1319
1325
|
// reactive compact retry below; consumed by the next pre-send compact pass
|
|
1320
1326
|
// so its telemetry/events carry trigger:'reactive' (distinct from the
|
|
@@ -1323,7 +1329,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1323
1329
|
const opts = sendOpts || {};
|
|
1324
1330
|
const sessionId = opts.sessionId || null;
|
|
1325
1331
|
const signal = opts.signal || null;
|
|
1326
|
-
const
|
|
1332
|
+
const sessionAgent = opts.session?.agent;
|
|
1327
1333
|
const forcedFirstTool = opts.forcedFirstTool ?? null;
|
|
1328
1334
|
const forcedFirstToolDef = forcedFirstTool
|
|
1329
1335
|
? tools.find(tool => tool?.name === forcedFirstTool)
|
|
@@ -1396,9 +1402,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1396
1402
|
});
|
|
1397
1403
|
return true;
|
|
1398
1404
|
};
|
|
1399
|
-
const maxLoopIterations =
|
|
1400
|
-
? sessionRef.maxLoopIterations
|
|
1401
|
-
: MAX_LOOP_ITERATIONS;
|
|
1405
|
+
const maxLoopIterations = resolveSessionMaxLoopIterations(sessionRef);
|
|
1402
1406
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
1403
1407
|
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
1404
1408
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
@@ -1408,6 +1412,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1408
1412
|
throwIfAborted();
|
|
1409
1413
|
if (iterations >= maxLoopIterations) {
|
|
1410
1414
|
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); stopping loop.\n`);
|
|
1415
|
+
terminatedByCap = true;
|
|
1411
1416
|
break;
|
|
1412
1417
|
}
|
|
1413
1418
|
// Drain queued steering/prompts BEFORE the
|
|
@@ -1570,6 +1575,51 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1570
1575
|
}
|
|
1571
1576
|
summaryChanged = messagesArrayChanged(compactInputMessages, compacted);
|
|
1572
1577
|
} catch (compactErr) {
|
|
1578
|
+
// Anchor-independent prune safety net. When SEMANTIC compact
|
|
1579
|
+
// throws (e.g. a degenerate single-turn transcript, or a
|
|
1580
|
+
// summary that cannot fit), attempt one non-LLM prune that
|
|
1581
|
+
// needs no user anchor: middle-truncate the oldest oversized
|
|
1582
|
+
// tool_result bodies until the transcript fits the budget.
|
|
1583
|
+
// If it shrinks the transcript we continue with that result
|
|
1584
|
+
// instead of escalating to overflow. Structure/pairing is
|
|
1585
|
+
// preserved (only string content shrinks) and the result is
|
|
1586
|
+
// re-reconciled inside the helper.
|
|
1587
|
+
//
|
|
1588
|
+
// GATED to the non-recall path: a recall-fasttrack failure
|
|
1589
|
+
// must NOT be silently recovered by this prune (that would
|
|
1590
|
+
// change the type-2 path's contract by shipping a pruned
|
|
1591
|
+
// transcript with no recall output). When recallFastTrackError
|
|
1592
|
+
// is set the fallback is skipped and the original overflow
|
|
1593
|
+
// escalation runs unchanged.
|
|
1594
|
+
if (!recallFastTrackError) {
|
|
1595
|
+
try {
|
|
1596
|
+
// Accept only if the pruned transcript fits the SAME
|
|
1597
|
+
// effective budget the prune targets (compactBudgetTokens
|
|
1598
|
+
// minus the request reserve) — comparing against the raw
|
|
1599
|
+
// compactBudgetTokens would accept a result with no
|
|
1600
|
+
// reserve headroom and overflow on the very next send.
|
|
1601
|
+
const acceptThreshold = compactEffectiveBudget(compactBudgetTokens, {
|
|
1602
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
1603
|
+
});
|
|
1604
|
+
const salvaged = pruneToolOutputsUnanchored(messages, compactBudgetTokens, {
|
|
1605
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
1606
|
+
});
|
|
1607
|
+
if (messagesArrayChanged(messages, salvaged)
|
|
1608
|
+
&& estimateMessagesTokensSafe(salvaged) <= acceptThreshold) {
|
|
1609
|
+
compacted = salvaged;
|
|
1610
|
+
pruneCount = countPrunedToolOutputs(messages, salvaged);
|
|
1611
|
+
summaryChanged = true;
|
|
1612
|
+
}
|
|
1613
|
+
} catch { /* fall through to overflow escalation */ }
|
|
1614
|
+
}
|
|
1615
|
+
if (compacted !== undefined) {
|
|
1616
|
+
try {
|
|
1617
|
+
process.stderr.write(
|
|
1618
|
+
`[loop] compact fallback prune recovered (sess=${sessionId || 'unknown'}): ` +
|
|
1619
|
+
`${compactErr?.message || compactErr}\n`,
|
|
1620
|
+
);
|
|
1621
|
+
} catch { /* best-effort */ }
|
|
1622
|
+
} else {
|
|
1573
1623
|
const compactFailMsg = compactErr && compactErr.message ? compactErr.message : String(compactErr);
|
|
1574
1624
|
const semanticFailMsg = semanticCompactError?.message || null;
|
|
1575
1625
|
const recallFailMsg = recallFastTrackError?.message || null;
|
|
@@ -1593,6 +1643,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1593
1643
|
iteration: iterations + 1,
|
|
1594
1644
|
stage: 'pre_send',
|
|
1595
1645
|
trigger: compactTrigger,
|
|
1646
|
+
compact_type: compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE,
|
|
1596
1647
|
prune_count: pruneCount,
|
|
1597
1648
|
compact_changed: false,
|
|
1598
1649
|
input_prefix_hash: messagePrefixHash(messages),
|
|
@@ -1602,13 +1653,23 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1602
1653
|
after_bytes: getBeforeBytes(),
|
|
1603
1654
|
context_window: compactPolicy.contextWindow,
|
|
1604
1655
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
1656
|
+
boundary_tokens: compactPolicy.boundaryTokens,
|
|
1605
1657
|
target_budget_tokens: compactBudgetTokens,
|
|
1606
1658
|
reserve_tokens: compactPolicy.reserveTokens,
|
|
1659
|
+
pressure_tokens: pressureTokens,
|
|
1660
|
+
trigger_tokens: compactPolicy.triggerTokens,
|
|
1607
1661
|
message_tokens_est: messageTokensEst,
|
|
1662
|
+
duration_ms: Date.now() - compactStartedAt,
|
|
1608
1663
|
provider: sessionRef.provider,
|
|
1609
1664
|
model: sessionRef.model || model,
|
|
1610
1665
|
error: compactFailMsg,
|
|
1611
1666
|
error_code: compactFailCode,
|
|
1667
|
+
details: {
|
|
1668
|
+
semantic: semanticCompactResult?.diagnostics || null,
|
|
1669
|
+
recallFastTrack: recallFastTrackResult?.diagnostics || null,
|
|
1670
|
+
semanticError: semanticFailMsg,
|
|
1671
|
+
recallFastTrackError: recallFailMsg,
|
|
1672
|
+
},
|
|
1612
1673
|
});
|
|
1613
1674
|
emitCompactEvent(opts, {
|
|
1614
1675
|
sessionId,
|
|
@@ -1640,6 +1701,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1640
1701
|
reserveTokens: compactPolicy.reserveTokens,
|
|
1641
1702
|
messageTokensEst,
|
|
1642
1703
|
}, compactErr);
|
|
1704
|
+
}
|
|
1643
1705
|
}
|
|
1644
1706
|
try { opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
|
|
1645
1707
|
const compactChanged = messagesArrayChanged(messages, compacted);
|
|
@@ -1684,6 +1746,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1684
1746
|
iteration: iterations + 1,
|
|
1685
1747
|
stage: 'pre_send',
|
|
1686
1748
|
trigger: compactTrigger,
|
|
1749
|
+
compact_type: compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE,
|
|
1687
1750
|
prune_count: pruneCount,
|
|
1688
1751
|
compact_changed: compactChanged || summaryChanged,
|
|
1689
1752
|
input_prefix_hash: messagePrefixHash(messages),
|
|
@@ -1693,11 +1756,19 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1693
1756
|
after_bytes: afterBytes,
|
|
1694
1757
|
context_window: compactPolicy.contextWindow,
|
|
1695
1758
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
1759
|
+
boundary_tokens: compactPolicy.boundaryTokens,
|
|
1696
1760
|
target_budget_tokens: compactBudgetTokens,
|
|
1697
1761
|
reserve_tokens: compactPolicy.reserveTokens,
|
|
1762
|
+
pressure_tokens: pressureTokens,
|
|
1763
|
+
trigger_tokens: compactPolicy.triggerTokens,
|
|
1698
1764
|
message_tokens_est: messageTokensEst,
|
|
1765
|
+
duration_ms: compactDurationMs,
|
|
1699
1766
|
provider: sessionRef.provider,
|
|
1700
1767
|
model: sessionRef.model || model,
|
|
1768
|
+
details: {
|
|
1769
|
+
semantic: semanticCompactResult?.diagnostics || null,
|
|
1770
|
+
recallFastTrack: recallFastTrackResult?.diagnostics || null,
|
|
1771
|
+
},
|
|
1701
1772
|
});
|
|
1702
1773
|
emitCompactEvent(opts, {
|
|
1703
1774
|
sessionId,
|
|
@@ -1780,7 +1851,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1780
1851
|
_eagerInFlightSigs.set(_sig, call.id);
|
|
1781
1852
|
entry.promise = (async () => {
|
|
1782
1853
|
try {
|
|
1783
|
-
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval }) };
|
|
1854
|
+
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: nextIteration }) };
|
|
1784
1855
|
} catch (error) {
|
|
1785
1856
|
return { ok: false, error };
|
|
1786
1857
|
}
|
|
@@ -1810,10 +1881,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1810
1881
|
// calls (they return null above), so those `continue`-before-
|
|
1811
1882
|
// execution stub paths can never early-notify (contract #5).
|
|
1812
1883
|
try {
|
|
1884
|
+
// UI-only: surface the model-VISIBLE result (envelope
|
|
1885
|
+
// stub for envelope returns), never the envelope object
|
|
1886
|
+
// or its injected newMessages body — no [object Object],
|
|
1887
|
+
// no full skill body in the tool card.
|
|
1888
|
+
const _earlyVisible = settled && settled.ok
|
|
1889
|
+
? normalizeToolEnvelope(settled.value).result
|
|
1890
|
+
: null;
|
|
1813
1891
|
const _earlyContent = settled && settled.ok
|
|
1814
|
-
? (typeof
|
|
1815
|
-
?
|
|
1816
|
-
: (
|
|
1892
|
+
? (typeof _earlyVisible === 'string'
|
|
1893
|
+
? _earlyVisible
|
|
1894
|
+
: (_earlyVisible == null ? '' : String(_earlyVisible)))
|
|
1817
1895
|
: `Error: ${settled && settled.error instanceof Error ? settled.error.message : String(settled && settled.error)}`;
|
|
1818
1896
|
opts.onToolResult?.({
|
|
1819
1897
|
role: 'tool',
|
|
@@ -1870,6 +1948,88 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1870
1948
|
try {
|
|
1871
1949
|
response = await provider.send(messages, model, sendTools.length ? sendTools : undefined, opts);
|
|
1872
1950
|
} catch (sendErr) {
|
|
1951
|
+
// Partial-final recovery (owner-notify fix): the recurring "worker
|
|
1952
|
+
// finished but the task hung / no result delivered" case is a FINAL,
|
|
1953
|
+
// no-tool summary stream that wedges (ping-only) AFTER all real tool
|
|
1954
|
+
// work completed in earlier iterations. The provider attaches its
|
|
1955
|
+
// partial stream state to the StreamStalledError. When the stall
|
|
1956
|
+
// carries streamed assistant text, has NO pending tool_use, and did
|
|
1957
|
+
// NOT emit a tool call this iteration, accept the partial as a
|
|
1958
|
+
// successful terminal response (deliver the summary we have) instead
|
|
1959
|
+
// of throwing — which would strand/notify-as-failure a turn whose
|
|
1960
|
+
// work actually succeeded. A stall WITH a pending/emitted tool call
|
|
1961
|
+
// is NOT recoverable (a tool whose input never completed must never
|
|
1962
|
+
// look done) and falls through to the normal error path.
|
|
1963
|
+
if (
|
|
1964
|
+
sendErr?.streamStalled === true
|
|
1965
|
+
&& sendErr.pendingToolUse !== true
|
|
1966
|
+
&& sendErr.unsafeToRetry !== true
|
|
1967
|
+
&& typeof sendErr.partialContent === 'string'
|
|
1968
|
+
&& sendErr.partialContent.trim().length > 0
|
|
1969
|
+
&& !(Array.isArray(sendErr.partialToolCalls) && sendErr.partialToolCalls.length > 0)
|
|
1970
|
+
) {
|
|
1971
|
+
try {
|
|
1972
|
+
process.stderr.write(
|
|
1973
|
+
`[loop] final stream stalled with partial text (sess=${sessionId || 'unknown'} `
|
|
1974
|
+
+ `iter=${nextIteration} len=${sendErr.partialContent.length}); `
|
|
1975
|
+
+ `accepting as partial-final success\n`,
|
|
1976
|
+
);
|
|
1977
|
+
} catch { /* best-effort */ }
|
|
1978
|
+
response = {
|
|
1979
|
+
content: sendErr.partialContent,
|
|
1980
|
+
model: sendErr.partialModel || model,
|
|
1981
|
+
toolCalls: undefined,
|
|
1982
|
+
usage: sendErr.partialUsage || undefined,
|
|
1983
|
+
stopReason: sendErr.partialStopReason || 'end_turn',
|
|
1984
|
+
hasThinkingContent: sendErr.partialHasThinking === true,
|
|
1985
|
+
partialFinal: true,
|
|
1986
|
+
};
|
|
1987
|
+
} else
|
|
1988
|
+
// Partial tool-call recovery (agent-hang fix): a stream that stalls
|
|
1989
|
+
// AFTER fully-parsed tool calls were emitted used to lose the whole
|
|
1990
|
+
// turn — unsafeToRetry blocks the mid-stream replay (correct: a
|
|
1991
|
+
// replay would re-run side-effecting tools) and the old code threw,
|
|
1992
|
+
// discarding tool work that had ALREADY completed via eager dispatch.
|
|
1993
|
+
// But the parsed calls are complete (pendingToolUse false ⇒ no
|
|
1994
|
+
// half-streamed tool input), so instead of replaying the request we
|
|
1995
|
+
// accept the partial as a normal tool-call turn and fall through to
|
|
1996
|
+
// the standard execution path: eager-dispatched (read-only) calls
|
|
1997
|
+
// resolve from the pending map without re-running, side-effecting
|
|
1998
|
+
// calls were never started during streaming and execute exactly
|
|
1999
|
+
// once. providerState stays undefined so the next iteration resends
|
|
2000
|
+
// a full frame on a fresh stream.
|
|
2001
|
+
if (
|
|
2002
|
+
sendErr?.streamStalled === true
|
|
2003
|
+
&& sendErr.pendingToolUse !== true
|
|
2004
|
+
&& Array.isArray(sendErr.partialToolCalls)
|
|
2005
|
+
&& sendErr.partialToolCalls.length > 0
|
|
2006
|
+
) {
|
|
2007
|
+
try {
|
|
2008
|
+
process.stderr.write(
|
|
2009
|
+
`[loop] stream stalled after ${sendErr.partialToolCalls.length} complete tool call(s) `
|
|
2010
|
+
+ `(sess=${sessionId || 'unknown'} iter=${nextIteration}); `
|
|
2011
|
+
+ `recovering as tool-call turn instead of failing\n`,
|
|
2012
|
+
);
|
|
2013
|
+
} catch { /* best-effort */ }
|
|
2014
|
+
try {
|
|
2015
|
+
appendAgentTrace({
|
|
2016
|
+
kind: 'stall_tool_recovery',
|
|
2017
|
+
sessionId: sessionId || null,
|
|
2018
|
+
iteration: nextIteration,
|
|
2019
|
+
toolCalls: sendErr.partialToolCalls.length,
|
|
2020
|
+
partialContentLen: typeof sendErr.partialContent === 'string' ? sendErr.partialContent.length : 0,
|
|
2021
|
+
});
|
|
2022
|
+
} catch { /* best-effort */ }
|
|
2023
|
+
response = {
|
|
2024
|
+
content: typeof sendErr.partialContent === 'string' ? sendErr.partialContent : '',
|
|
2025
|
+
model: sendErr.partialModel || model,
|
|
2026
|
+
toolCalls: sendErr.partialToolCalls.slice(),
|
|
2027
|
+
usage: sendErr.partialUsage || undefined,
|
|
2028
|
+
stopReason: 'tool_use',
|
|
2029
|
+
hasThinkingContent: sendErr.partialHasThinking === true,
|
|
2030
|
+
partialToolRecovery: true,
|
|
2031
|
+
};
|
|
2032
|
+
} else
|
|
1873
2033
|
// Context-window-exceeded is a deterministic refusal from the API.
|
|
1874
2034
|
// Recover context overflow reactively by compacting and retrying
|
|
1875
2035
|
// in the same active turn. MixDog's proactive estimator can miss a
|
|
@@ -1948,6 +2108,37 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1948
2108
|
// Provider may have returned despite an abort (SDKs that don't honour
|
|
1949
2109
|
// signal) — bail before processing any of its output.
|
|
1950
2110
|
throwIfAborted();
|
|
2111
|
+
// P1 audit fix (Step4): a text-only turn truncated by the provider's
|
|
2112
|
+
// max-output limit (response.truncated, set by the provider layer
|
|
2113
|
+
// when stopReason==='length' AND content is non-empty) used to look
|
|
2114
|
+
// identical to a clean completion — the model's answer could be
|
|
2115
|
+
// silently cut mid-sentence with zero signal to the operator. Surface
|
|
2116
|
+
// it as a one-line stderr warning + trace event WITHOUT failing the
|
|
2117
|
+
// turn (the partial content is still usable and the loop's own
|
|
2118
|
+
// isIncompleteStop nudge below already re-prompts when content is
|
|
2119
|
+
// empty).
|
|
2120
|
+
if (response?.truncated === true) {
|
|
2121
|
+
try {
|
|
2122
|
+
process.stderr.write(
|
|
2123
|
+
`[loop] provider output truncated at max-output limit (sess=${sessionId || 'unknown'} `
|
|
2124
|
+
+ `iter=${iterations} stopReason=${response.stopReason ?? response.stop_reason ?? 'length'} `
|
|
2125
|
+
+ `contentLen=${typeof response.content === 'string' ? response.content.length : 0}); `
|
|
2126
|
+
+ `answer may be cut off mid-sentence.\n`,
|
|
2127
|
+
);
|
|
2128
|
+
} catch { /* best-effort */ }
|
|
2129
|
+
try {
|
|
2130
|
+
appendAgentTrace({
|
|
2131
|
+
sessionId,
|
|
2132
|
+
iteration: iterations,
|
|
2133
|
+
kind: 'output_truncated',
|
|
2134
|
+
payload: {
|
|
2135
|
+
stop_reason: response.stopReason ?? response.stop_reason ?? 'length',
|
|
2136
|
+
content_len: typeof response.content === 'string' ? response.content.length : 0,
|
|
2137
|
+
agent: sessionAgent || null,
|
|
2138
|
+
},
|
|
2139
|
+
});
|
|
2140
|
+
} catch { /* best-effort */ }
|
|
2141
|
+
}
|
|
1951
2142
|
// Incremental metric persistence (fix A): push per-iteration token delta
|
|
1952
2143
|
// immediately so watchdog / agent type=list sees live totals mid-turn.
|
|
1953
2144
|
if (sessionId && opts.onUsageDelta && response.usage) {
|
|
@@ -1994,7 +2185,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1994
2185
|
// reminders waste turns and fragment the working context, so
|
|
1995
2186
|
// the second empty turn is accepted as terminal.
|
|
1996
2187
|
const hasContent = typeof response.content === 'string' && response.content.trim().length > 0;
|
|
1997
|
-
const isHidden =
|
|
2188
|
+
const isHidden = HIDDEN_AGENT_NAMES.has(sessionAgent);
|
|
1998
2189
|
const stopReason = response.stopReason ?? response.stop_reason ?? null;
|
|
1999
2190
|
const isIncompleteStop = stopReason && INCOMPLETE_STOP_REASONS.has(stopReason);
|
|
2000
2191
|
// A user/schedule notification can arrive while provider.send() is
|
|
@@ -2091,6 +2282,14 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2091
2282
|
}
|
|
2092
2283
|
// R15: per-turn scalar read-count Map. Lifetime = this turn's tool-call batch.
|
|
2093
2284
|
// Declared between the duplicate-detection block and the for-loop so it resets
|
|
2285
|
+
// Per-batch buffer for the general `newMessages` tool-result channel.
|
|
2286
|
+
// A tool MAY return a `{ __toolEnvelope, result, newMessages }` envelope;
|
|
2287
|
+
// its newMessages (e.g. the Skill SKILL.md body as a role:'user' message)
|
|
2288
|
+
// are collected here across EVERY call in this assistant turn and flushed
|
|
2289
|
+
// ONCE, AFTER the batch's last tool_result is pushed — never interleaved
|
|
2290
|
+
// between two tool results of the same multi-tool turn (which would put a
|
|
2291
|
+
// user message between tool(A) and tool(B) and break provider pairing).
|
|
2292
|
+
const _batchNewMessages = [];
|
|
2094
2293
|
for (let callIndex = 0; callIndex < calls.length; callIndex += 1) {
|
|
2095
2294
|
const call = calls[callIndex];
|
|
2096
2295
|
if (isBuiltinTool(call.name)) {
|
|
@@ -2227,7 +2426,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2227
2426
|
toolEndedAt = Date.now();
|
|
2228
2427
|
_resultKind = 'error';
|
|
2229
2428
|
} else {
|
|
2230
|
-
result = await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval });
|
|
2429
|
+
result = await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: iterations });
|
|
2231
2430
|
toolEndedAt = Date.now();
|
|
2232
2431
|
// Boundary: tool-return string convention → structural kind.
|
|
2233
2432
|
// The only prefix check in this codebase; downstream layers
|
|
@@ -2249,6 +2448,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2249
2448
|
result = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
2250
2449
|
_resultKind = 'error';
|
|
2251
2450
|
}
|
|
2451
|
+
// CENTRAL ENVELOPE NORMALIZE (general newMessages channel).
|
|
2452
|
+
// executeTool (serial + eager) and cache/error paths above all
|
|
2453
|
+
// funnel into `result`. Split ONCE here: downstream post-processing
|
|
2454
|
+
// (classifyResultKind / maybeOffloadToolResult / compressToolResult /
|
|
2455
|
+
// traceAgentTool / cache writes / messages.push) sees ONLY the
|
|
2456
|
+
// model-visible `result`; the `newMessages` ride a per-batch buffer
|
|
2457
|
+
// flushed after the batch's last tool_result (never interleaved).
|
|
2458
|
+
{
|
|
2459
|
+
const _env = normalizeToolEnvelope(result);
|
|
2460
|
+
result = _env.result;
|
|
2461
|
+
if (_env.newMessages.length) _batchNewMessages.push(..._env.newMessages);
|
|
2462
|
+
}
|
|
2252
2463
|
// Update the cross-iteration repeat-failure guard with this call's
|
|
2253
2464
|
// outcome: bump the consecutive-failure count for an identical
|
|
2254
2465
|
// signature, or clear it the moment the same call succeeds.
|
|
@@ -2400,7 +2611,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2400
2611
|
toolKind,
|
|
2401
2612
|
toolMs: toolEndedAt - toolStartedAt,
|
|
2402
2613
|
toolArgs: call.arguments,
|
|
2403
|
-
|
|
2614
|
+
agent: sessionRef?.agent || null,
|
|
2404
2615
|
model: sessionRef?.model || null,
|
|
2405
2616
|
resultKind: _resultKind,
|
|
2406
2617
|
resultText: result,
|
|
@@ -2432,12 +2643,22 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2432
2643
|
setReadCached({ sessionId, args: call.arguments, cwd, content: result, toolUseId: call.id });
|
|
2433
2644
|
}
|
|
2434
2645
|
}
|
|
2646
|
+
// UI-only: apply_patch stashes the standard unified diff keyed
|
|
2647
|
+
// by tool_use id (never in the model-visible result). Attach it
|
|
2648
|
+
// here as a side-channel field so the TUI's expanded (ctrl+o)
|
|
2649
|
+
// raw view renders a colored +/- diff. The provider lowering
|
|
2650
|
+
// (anthropic/openai/etc.) never reads `uiDiff`, so the model
|
|
2651
|
+
// sees only `content` (the compact summary) — no token bloat.
|
|
2652
|
+
const _applyPatchUiDiff = _stripMcpPrefix(call.name) === 'apply_patch'
|
|
2653
|
+
? takeApplyPatchUiDiff(call.id)
|
|
2654
|
+
: null;
|
|
2435
2655
|
pushToolResultMessage({
|
|
2436
2656
|
role: 'tool',
|
|
2437
2657
|
content: result,
|
|
2438
2658
|
toolCallId: call.id,
|
|
2439
2659
|
toolKind: _resultKind,
|
|
2440
2660
|
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
2661
|
+
...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
|
|
2441
2662
|
});
|
|
2442
2663
|
} catch (postErr) {
|
|
2443
2664
|
_postProcessOk = false;
|
|
@@ -2453,7 +2674,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2453
2674
|
toolKind,
|
|
2454
2675
|
toolMs: toolEndedAt && toolStartedAt ? toolEndedAt - toolStartedAt : null,
|
|
2455
2676
|
toolArgs: call.arguments,
|
|
2456
|
-
|
|
2677
|
+
agent: sessionRef?.agent || null,
|
|
2457
2678
|
model: sessionRef?.model || null,
|
|
2458
2679
|
cwd,
|
|
2459
2680
|
resultText: _postMsg,
|
|
@@ -2477,6 +2698,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2477
2698
|
// discard the rest of the batch and skip the next provider.send.
|
|
2478
2699
|
throwIfAborted();
|
|
2479
2700
|
}
|
|
2701
|
+
// Flush the per-batch newMessages channel. All tool_results for this
|
|
2702
|
+
// assistant turn are now pushed; appending the injected role:'user'
|
|
2703
|
+
// messages here (AFTER the last tool_result, BEFORE the next provider
|
|
2704
|
+
// send) keeps provider pairing valid — no user message is interleaved
|
|
2705
|
+
// between tool(A) and tool(B). pre-send repairTranscriptBeforeProviderSend
|
|
2706
|
+
// normalizes any residual ordering. The injected messages carry their
|
|
2707
|
+
// own meta flag (e.g. meta:'skill') so compaction's latest-human-prompt
|
|
2708
|
+
// selection does not mistake them for the user's request.
|
|
2709
|
+
for (const _nm of _batchNewMessages) {
|
|
2710
|
+
if (!_nm || _nm.role !== 'user' || typeof _nm.content !== 'string' || !_nm.content) continue;
|
|
2711
|
+
messages.push({ role: 'user', content: _nm.content, ...(_nm.meta ? { meta: _nm.meta } : {}) });
|
|
2712
|
+
}
|
|
2480
2713
|
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
2481
2714
|
// AFTER any auto-compact pass. Draining here would put the steering
|
|
2482
2715
|
// user turn after the fresh tool results before compaction runs; then
|
|
@@ -2485,6 +2718,33 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2485
2718
|
// About to re-send with tool results — transition back to connecting for the next turn.
|
|
2486
2719
|
if (sessionId) updateSessionStage(sessionId, 'connecting');
|
|
2487
2720
|
}
|
|
2721
|
+
// Classify WHY the loop ended so agent-tool can promote an empty/abnormal
|
|
2722
|
+
// finish to an explicit Lead-facing error instead of a silent empty
|
|
2723
|
+
// "completed". Determine "has content" exactly the way the no-tool-call
|
|
2724
|
+
// branch above does (trimmed string content, or any reasoning content).
|
|
2725
|
+
const _finalHasContent = (typeof response?.content === 'string' && response.content.trim().length > 0)
|
|
2726
|
+
|| (typeof response?.reasoningContent === 'string' && response.reasoningContent.trim().length > 0);
|
|
2727
|
+
const _finalStopReason = response?.stopReason ?? response?.stop_reason ?? null;
|
|
2728
|
+
const _finalIncompleteStop = _finalStopReason && INCOMPLETE_STOP_REASONS.has(_finalStopReason);
|
|
2729
|
+
const _finalIsHidden = HIDDEN_AGENT_NAMES.has(sessionAgent);
|
|
2730
|
+
let terminationReason;
|
|
2731
|
+
if (terminatedByCap) {
|
|
2732
|
+
// Real problem regardless of hidden/public: the loop never terminated
|
|
2733
|
+
// on its own contract.
|
|
2734
|
+
terminationReason = 'iteration_cap';
|
|
2735
|
+
} else if (!_finalHasContent && _finalIncompleteStop) {
|
|
2736
|
+
// Cut short mid-synthesis (token cap / provider pause). Real problem
|
|
2737
|
+
// for hidden agents too.
|
|
2738
|
+
terminationReason = 'truncated';
|
|
2739
|
+
} else if (!_finalHasContent && !_finalIsHidden) {
|
|
2740
|
+
// Empty terminal turn. Only public agents violate their contract by
|
|
2741
|
+
// finishing empty — hidden agents (explorer/cycle/…) legitimately emit
|
|
2742
|
+
// text-only/empty terminal turns per their own role contract, so leave
|
|
2743
|
+
// terminationReason undefined for them.
|
|
2744
|
+
terminationReason = 'empty';
|
|
2745
|
+
} else {
|
|
2746
|
+
terminationReason = undefined;
|
|
2747
|
+
}
|
|
2488
2748
|
return {
|
|
2489
2749
|
...response,
|
|
2490
2750
|
usage: lastUsage || response.usage,
|
|
@@ -2493,5 +2753,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2493
2753
|
iterations,
|
|
2494
2754
|
toolCallsTotal,
|
|
2495
2755
|
providerState,
|
|
2756
|
+
terminationReason,
|
|
2757
|
+
maxLoopIterations,
|
|
2496
2758
|
};
|
|
2497
2759
|
}
|