mixdog 0.9.2 → 0.9.3
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 +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
import { classifyResultKind } from './result-classification.mjs';
|
|
2
2
|
import { executeMcpTool, isMcpTool, mcpToolHasField } from '../mcp/client.mjs';
|
|
3
|
-
import { canonicalizeBuiltinToolName, executeBuiltinTool, formatUnknownBuiltinToolMessage, isBuiltinTool } from '../tools/builtin.mjs';
|
|
3
|
+
import { canonicalizeBuiltinToolName, executeBuiltinTool, formatUnknownBuiltinToolMessage, isBuiltinTool, isExternalAdapterTool } from '../tools/builtin.mjs';
|
|
4
4
|
import { executeBashSessionTool } from '../tools/bash-session.mjs';
|
|
5
5
|
import { executePatchTool, takeApplyPatchUiDiff } from '../tools/patch.mjs';
|
|
6
6
|
import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
|
|
7
|
-
import { collectSkillsCached, loadSkillResource, buildSkillToolEnvelope } from '../context/collect.mjs';
|
|
8
7
|
import { normalizeToolEnvelope, makeToolEnvelope } from './tool-envelope.mjs';
|
|
9
8
|
import { traceAgentLoop, traceAgentTool, traceAgentToolFailure, traceAgentCompact, estimateProviderPayloadBytes, messagePrefixHash, appendAgentTrace } from '../agent-trace.mjs';
|
|
10
9
|
import { resolveSessionMaxLoopIterations } from '../agent-runtime/agent-loop-policy.mjs';
|
|
11
10
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
12
11
|
import { markSessionToolCall, updateSessionStage, SessionClosedError, getSessionAbortSignal, enqueuePendingMessage, bumpUsageMetricsEpoch } from './manager.mjs';
|
|
13
|
-
import {
|
|
14
|
-
estimateMessagesTokens,
|
|
15
|
-
estimateRequestReserveTokens,
|
|
16
|
-
sanitizeToolPairs,
|
|
17
|
-
resolveCompactBufferRatio,
|
|
18
|
-
resolveCompactBufferTokens,
|
|
19
|
-
} from './context-utils.mjs';
|
|
20
12
|
import {
|
|
21
13
|
recallFastTrackCompactMessages,
|
|
22
14
|
pruneToolOutputs,
|
|
23
15
|
pruneToolOutputsUnanchored,
|
|
24
16
|
semanticCompactMessages,
|
|
25
17
|
effectiveBudget as compactEffectiveBudget,
|
|
26
|
-
compactTypeIsRecallFastTrack,
|
|
27
|
-
compactTypeIsSemantic,
|
|
28
|
-
normalizeCompactType,
|
|
29
18
|
DEFAULT_COMPACT_TYPE,
|
|
30
|
-
DEFAULT_COMPACTION_KEEP_TOKENS,
|
|
31
19
|
drainSessionCycle1,
|
|
32
20
|
countRawPendingRows,
|
|
33
21
|
} from './compact.mjs';
|
|
@@ -40,26 +28,15 @@ import { modelVisibleToolCompletionMessage } from '../../../shared/tool-executio
|
|
|
40
28
|
import { createHash } from 'crypto';
|
|
41
29
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
function _isMutationTool(name) {
|
|
54
|
-
const n = _stripMcpPrefix(name);
|
|
55
|
-
return n === 'apply_patch';
|
|
56
|
-
}
|
|
57
|
-
const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
58
|
-
'code_graph',
|
|
59
|
-
'grep',
|
|
60
|
-
'list',
|
|
61
|
-
'glob',
|
|
62
|
-
]);
|
|
31
|
+
import {
|
|
32
|
+
_stripMcpPrefix,
|
|
33
|
+
_isReadTool,
|
|
34
|
+
_isMutationTool,
|
|
35
|
+
_isScopedCacheableTool,
|
|
36
|
+
_isShellTool,
|
|
37
|
+
_intraTurnSig,
|
|
38
|
+
} from './loop/tool-classify.mjs';
|
|
39
|
+
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
63
40
|
let codeGraphRuntimePromise = null;
|
|
64
41
|
async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
|
|
65
42
|
codeGraphRuntimePromise ??= import('../tools/code-graph.mjs');
|
|
@@ -67,218 +44,72 @@ async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options
|
|
|
67
44
|
if (typeof mod.executeCodeGraphTool !== 'function') throw new Error('code_graph runtime is not available');
|
|
68
45
|
return mod.executeCodeGraphTool(name, args, cwd, signal, options);
|
|
69
46
|
}
|
|
70
|
-
function _isScopedCacheableTool(name) {
|
|
71
|
-
const n = _stripMcpPrefix(name);
|
|
72
|
-
return SCOPED_CACHEABLE_TOOLS.has(n);
|
|
73
|
-
}
|
|
74
|
-
function _isShellTool(name) {
|
|
75
|
-
const n = _stripMcpPrefix(name);
|
|
76
|
-
return n === 'shell' || n === 'bash_session';
|
|
77
|
-
}
|
|
78
47
|
|
|
79
48
|
// classifyResultKind is imported from result-classification.mjs at the top of
|
|
80
49
|
// this file; import it from there directly rather than via this module.
|
|
81
|
-
|
|
82
|
-
// Canonical signature for intra-turn duplicate detection. Sorting keys
|
|
83
|
-
// produces a stable hash regardless of arg-object key order. Anything
|
|
84
|
-
// non-serializable falls back to String(args) — still deterministic for
|
|
85
|
-
// the model's typical structured-arg shape.
|
|
86
|
-
function _canonicalArgs(args) {
|
|
87
|
-
if (args == null || typeof args !== 'object') {
|
|
88
|
-
try { return JSON.stringify(args); } catch { return String(args); }
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const keys = Object.keys(args).sort();
|
|
92
|
-
const sorted = {};
|
|
93
|
-
for (const k of keys) sorted[k] = args[k];
|
|
94
|
-
return JSON.stringify(sorted);
|
|
95
|
-
} catch { return String(args); }
|
|
96
|
-
}
|
|
97
|
-
function _intraTurnSig(name, args) {
|
|
98
|
-
return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Shared pre-dispatch deny — single source of truth for the remaining
|
|
102
|
-
// control-plane / role scoping rejects. Called by BOTH the eager dispatch
|
|
103
|
-
// path (startEagerTool) and the serial dispatch path (executeTool body).
|
|
104
|
-
// Returns null when the call is allowed to proceed; otherwise returns the
|
|
105
|
-
// Error string the serial path would emit. The eager caller ignores the
|
|
106
|
-
// message body and just treats non-null as "do not start eager".
|
|
107
|
-
//
|
|
108
|
-
// This is NOT a permission gate — runtime permission enforcement was removed
|
|
109
|
-
// (every tool call is trusted). What remains is architectural scoping:
|
|
110
|
-
// agent workers are sandboxed to code/research tools. They must never reach
|
|
111
|
-
// owner/host control surfaces: session management, the ENTIRE channels module
|
|
112
|
-
// (Discord messaging, schedules, webhook/config, channel-bridge toggle,
|
|
113
|
-
// command injection), or host input injection. Explicit name list (no imports)
|
|
114
|
-
// keeps this hot-path gate dependency-free; add new owner/channel tools here.
|
|
115
|
-
const WORKER_DENIED_TOOLS = new Set([
|
|
116
|
-
// session control-plane — unified into the single `agent` tool
|
|
117
|
-
// (type=spawn|send|close|list). Denying the one name blocks all worker
|
|
118
|
-
// session control.
|
|
119
|
-
'agent',
|
|
120
|
-
// channels module (owner/Discord-facing)
|
|
121
|
-
'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
|
|
122
|
-
'schedule_status', 'trigger_schedule', 'schedule_control',
|
|
123
|
-
'activate_channel_bridge', 'reload_config', 'inject_command',
|
|
124
|
-
// host input injection
|
|
125
|
-
'inject_input',
|
|
126
|
-
]);
|
|
127
|
-
function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
128
|
-
const name = call?.name;
|
|
129
|
-
if (typeof name !== 'string' || !name) return null;
|
|
130
|
-
const _agentOwned = sessionRef?.scope?.startsWith?.('agent:')
|
|
131
|
-
|| isAgentOwner(sessionRef);
|
|
132
|
-
const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
|
|
133
|
-
if (_agentOwned && _controlPlaneTool) {
|
|
134
|
-
return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
|
|
135
|
-
}
|
|
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).`;
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
/** Exported for smoke tests — same runtime deny as the agent loop. */
|
|
143
|
-
export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin') {
|
|
144
|
-
return _preDispatchDeny(call, toolKind, sessionRef);
|
|
145
|
-
}
|
|
146
50
|
import { compressToolResult, recordToolBatch } from '../tools/result-compression.mjs';
|
|
147
51
|
|
|
148
52
|
|
|
149
|
-
import {
|
|
150
|
-
import {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return String(content ?? '');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function normalizeSteeringEntry(entry) {
|
|
196
|
-
if (typeof entry === 'string') {
|
|
197
|
-
const text = entry.trim();
|
|
198
|
-
return text ? { content: text, text } : null;
|
|
199
|
-
}
|
|
200
|
-
if (!entry || typeof entry !== 'object') return null;
|
|
201
|
-
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : entry;
|
|
202
|
-
const text = typeof entry.text === 'string' ? entry.text.trim() : steeringContentText(content).trim();
|
|
203
|
-
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
204
|
-
if (typeof content === 'string') {
|
|
205
|
-
const value = content.trim();
|
|
206
|
-
return value ? { content: value, text: text || value } : null;
|
|
207
|
-
}
|
|
208
|
-
const fallback = steeringContentText(content).trim();
|
|
209
|
-
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function mergeSteeringEntries(entries) {
|
|
213
|
-
const normalized = (Array.isArray(entries) ? entries : [])
|
|
214
|
-
.map(normalizeSteeringEntry)
|
|
215
|
-
.filter(Boolean);
|
|
216
|
-
if (normalized.length === 0) return null;
|
|
217
|
-
const displayText = normalized.map((entry) => entry.text || steeringContentText(entry.content))
|
|
218
|
-
.filter((text) => String(text || '').trim())
|
|
219
|
-
.join('\n');
|
|
220
|
-
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
221
|
-
return {
|
|
222
|
-
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
223
|
-
text: displayText,
|
|
224
|
-
count: normalized.length,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const parts = [];
|
|
228
|
-
for (const entry of normalized) {
|
|
229
|
-
if (typeof entry.content === 'string') {
|
|
230
|
-
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
231
|
-
} else if (Array.isArray(entry.content)) {
|
|
232
|
-
parts.push(...entry.content);
|
|
233
|
-
} else {
|
|
234
|
-
const text = steeringContentText(entry.content);
|
|
235
|
-
if (text.trim()) parts.push({ type: 'text', text });
|
|
236
|
-
}
|
|
237
|
-
parts.push({ type: 'text', text: '\n' });
|
|
238
|
-
}
|
|
239
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
240
|
-
return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
class AgentContextOverflowError extends Error {
|
|
244
|
-
constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
245
|
-
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
246
|
-
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
247
|
-
super(
|
|
248
|
-
`agent context overflow (${target}, stage=${stage || 'compact'}): ` +
|
|
249
|
-
`latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
|
|
250
|
-
`reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
|
|
251
|
-
`messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
|
|
252
|
-
);
|
|
253
|
-
this.name = 'AgentContextOverflowError';
|
|
254
|
-
this.code = 'AGENT_CONTEXT_OVERFLOW';
|
|
255
|
-
this.sessionId = sessionId || null;
|
|
256
|
-
this.provider = provider || null;
|
|
257
|
-
this.model = model || null;
|
|
258
|
-
this.contextWindow = contextWindow ?? null;
|
|
259
|
-
this.budgetTokens = budgetTokens ?? null;
|
|
260
|
-
this.reserveTokens = reserveTokens ?? null;
|
|
261
|
-
this.messageTokensEst = messageTokensEst ?? null;
|
|
262
|
-
if (cause) this.cause = cause;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
53
|
+
import { resolve as resolvePath, isAbsolute } from 'path';
|
|
54
|
+
import {
|
|
55
|
+
estimateMessagesTokensSafe,
|
|
56
|
+
compactDiagnosticError,
|
|
57
|
+
compactByteLength,
|
|
58
|
+
compactDebugLog,
|
|
59
|
+
} from './loop/compact-debug.mjs';
|
|
60
|
+
import { mergeSteeringEntries, steeringContentText } from './loop/steering.mjs';
|
|
61
|
+
import { agentContextOverflowError } from './loop/context-overflow.mjs';
|
|
62
|
+
import { positiveTokenInt } from './loop/env.mjs';
|
|
63
|
+
import { normalizeUsage, addUsage } from './loop/usage.mjs';
|
|
64
|
+
import { HIDDEN_AGENT_NAMES } from './loop/hidden-agents.mjs';
|
|
65
|
+
import {
|
|
66
|
+
resolveWorkerCompactPolicy,
|
|
67
|
+
compactionTelemetryPressureTokens,
|
|
68
|
+
compactTargetBudget,
|
|
69
|
+
shouldCompactForSession,
|
|
70
|
+
countPrunedToolOutputs,
|
|
71
|
+
rememberCompactTelemetry,
|
|
72
|
+
emitCompactEvent,
|
|
73
|
+
compactEventType,
|
|
74
|
+
} from './loop/compact-policy.mjs';
|
|
75
|
+
import {
|
|
76
|
+
isEagerDispatchable,
|
|
77
|
+
messagesArrayChanged,
|
|
78
|
+
getToolKind,
|
|
79
|
+
buildSkillsListResponse,
|
|
80
|
+
viewSkill,
|
|
81
|
+
normalizeHookUpdatedToolOutput,
|
|
82
|
+
resolveToolResultAfterHook,
|
|
83
|
+
parseNativeToolSearchPayload,
|
|
84
|
+
extractBashSessionId,
|
|
85
|
+
buildAgentBashSessionArgs,
|
|
86
|
+
formatMissingToolApprovalUiDenial,
|
|
87
|
+
resolvePreToolAskApproval,
|
|
88
|
+
approvalGranted,
|
|
89
|
+
approvalReason,
|
|
90
|
+
} from './loop/tool-helpers.mjs';
|
|
91
|
+
import {
|
|
92
|
+
compactToolCallsForHistory,
|
|
93
|
+
restoreToolCallBodyForId,
|
|
94
|
+
} from './loop/stored-tool-args.mjs';
|
|
95
|
+
import { repairTranscriptBeforeProviderSend } from './loop/transcript-repair.mjs';
|
|
265
96
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
97
|
+
// Facade re-exports: these symbols moved to split modules under ./loop/ but
|
|
98
|
+
// remain part of loop.mjs's public surface (imported by scripts/tests and other
|
|
99
|
+
// runtime modules). Re-export the already-imported local bindings so every
|
|
100
|
+
// existing import path keeps working (no duplicate module binding).
|
|
101
|
+
export {
|
|
102
|
+
preDispatchDenyForSession,
|
|
103
|
+
repairTranscriptBeforeProviderSend,
|
|
104
|
+
normalizeHookUpdatedToolOutput,
|
|
105
|
+
resolveToolResultAfterHook,
|
|
106
|
+
buildAgentBashSessionArgs,
|
|
107
|
+
formatMissingToolApprovalUiDenial,
|
|
108
|
+
resolvePreToolAskApproval,
|
|
109
|
+
approvalGranted,
|
|
110
|
+
approvalReason,
|
|
111
|
+
};
|
|
278
112
|
|
|
279
|
-
// Cache-hit results always inline the cached body. The earlier size-gated
|
|
280
|
-
// `[cache-hit-ref]` branch confused agents whose context did not
|
|
281
|
-
// contain the referenced prior tool_result, triggering shell-cat detours.
|
|
282
113
|
// Hard iteration ceiling for every agent loop. Reset to 0 whenever the
|
|
283
114
|
// transcript is compacted (see the trim block below): a long task that keeps
|
|
284
115
|
// compacting can proceed past this count, while a tight NON-compacting loop
|
|
@@ -289,185 +120,6 @@ function agentContextOverflowError({ stage, sessionId, sessionRef, model, budget
|
|
|
289
120
|
// this catches tight deterministic-failure loops (e.g. a command that errors
|
|
290
121
|
// the same way every time) far earlier than 100 iterations.
|
|
291
122
|
const REPEAT_FAIL_LIMIT = 3;
|
|
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;
|
|
296
|
-
try {
|
|
297
|
-
_hiddenAgentsCache = JSON.parse(_readFileSync(_AGENTS_JSON, 'utf8'));
|
|
298
|
-
} catch { _hiddenAgentsCache = { agents: [] }; }
|
|
299
|
-
return _hiddenAgentsCache;
|
|
300
|
-
}
|
|
301
|
-
// Transcript pairing guard. Anthropic 400-rejects when an assistant message
|
|
302
|
-
// ends with tool_use blocks and the next message isn't tool results for
|
|
303
|
-
// those exact ids. abort/timeout/error race in the loop body can leave a
|
|
304
|
-
// dangling assistant tool_use at the tail (e.g. the structure_probe loop
|
|
305
|
-
// running 12 deep then aborting between push-assistant and push-tool).
|
|
306
|
-
// Strip any trailing assistant tool_use that has no matching tool result
|
|
307
|
-
// so provider.send sees a valid transcript instead of leaking the 400 to
|
|
308
|
-
// the user. Repair runs every iteration but is a no-op on healthy paths.
|
|
309
|
-
function _ensureTranscriptPairing(msgs, sessionId) {
|
|
310
|
-
// Walk backwards to find the last assistant message that emitted
|
|
311
|
-
// tool_use, then validate that every id has a matching tool result
|
|
312
|
-
// inside the CONTIGUOUS tool-message block immediately following it.
|
|
313
|
-
// Earlier guard splice'd the entire tail — which silently deleted any
|
|
314
|
-
// user prompt appended after the dangling assistant by manager.mjs:
|
|
315
|
-
// when the guard fired with shape
|
|
316
|
-
// [..., assistant{a,b}, tool{a}, user{new prompt}]
|
|
317
|
-
// the splice removed user{new prompt} along with the orphan suffix.
|
|
318
|
-
// Fix: remove only assistant + the contiguous tool block; preserve
|
|
319
|
-
// anything past it (user / system / next assistant) untouched.
|
|
320
|
-
let popped = 0;
|
|
321
|
-
while (msgs.length > 0) {
|
|
322
|
-
let lastAssistantIdx = -1;
|
|
323
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
324
|
-
const m = msgs[i];
|
|
325
|
-
if (m?.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
326
|
-
lastAssistantIdx = i;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (lastAssistantIdx === -1) break;
|
|
331
|
-
// Collect the contiguous tool messages directly after this assistant.
|
|
332
|
-
// Anything past that block is unrelated (next user prompt, system
|
|
333
|
-
// marker, etc.) and must survive the repair.
|
|
334
|
-
let toolBlockEnd = lastAssistantIdx + 1;
|
|
335
|
-
while (toolBlockEnd < msgs.length && msgs[toolBlockEnd]?.role === 'tool') {
|
|
336
|
-
toolBlockEnd += 1;
|
|
337
|
-
}
|
|
338
|
-
const toolBlock = msgs.slice(lastAssistantIdx + 1, toolBlockEnd);
|
|
339
|
-
const ids = msgs[lastAssistantIdx].toolCalls.map(c => c.id);
|
|
340
|
-
const matched = ids.every(id => toolBlock.some(m => m.toolCallId === id));
|
|
341
|
-
if (matched) break;
|
|
342
|
-
const removed = toolBlockEnd - lastAssistantIdx;
|
|
343
|
-
msgs.splice(lastAssistantIdx, removed);
|
|
344
|
-
popped += removed;
|
|
345
|
-
}
|
|
346
|
-
// Second sweep — catch dangling tool results that survived the
|
|
347
|
-
// contiguous-block splice. Anthropic strict spec requires every
|
|
348
|
-
// tool result to sit in a contiguous block right after the
|
|
349
|
-
// assistant whose toolCalls produced it; a `[..., assistant{a,b},
|
|
350
|
-
// tool{a}, user, tool{b}]` shape leaves tool{b} orphaned even
|
|
351
|
-
// after assistant + tool{a} are repaired by the loop above.
|
|
352
|
-
// Walk back from each tool message to the nearest non-tool
|
|
353
|
-
// ancestor; if it is not an assistant whose toolCalls include
|
|
354
|
-
// this id, drop the orphan.
|
|
355
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
356
|
-
const m = msgs[i];
|
|
357
|
-
if (m?.role !== 'tool') continue;
|
|
358
|
-
if (!m.toolCallId) {
|
|
359
|
-
msgs.splice(i, 1);
|
|
360
|
-
popped += 1;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
let prevIdx = i - 1;
|
|
364
|
-
while (prevIdx >= 0 && msgs[prevIdx]?.role === 'tool') prevIdx--;
|
|
365
|
-
const anchor = prevIdx >= 0 ? msgs[prevIdx] : null;
|
|
366
|
-
const anchorOk = anchor?.role === 'assistant'
|
|
367
|
-
&& Array.isArray(anchor.toolCalls)
|
|
368
|
-
&& anchor.toolCalls.some(c => c.id === m.toolCallId);
|
|
369
|
-
if (!anchorOk) {
|
|
370
|
-
msgs.splice(i, 1);
|
|
371
|
-
popped += 1;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (popped > 0 && sessionId) {
|
|
375
|
-
try { process.stderr.write(`[transcript-repair] sess=${sessionId} popped=${popped} dangling assistant tool_use\n`); } catch {}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Pre-provider transcript repair for the agent loop. Reattach valid tool
|
|
381
|
-
* results (non-destructive) before any destructive orphan pairing cleanup.
|
|
382
|
-
* Mutates `messages` in place to preserve the session array reference.
|
|
383
|
-
*/
|
|
384
|
-
export function repairTranscriptBeforeProviderSend(messages, sessionId = null) {
|
|
385
|
-
if (!Array.isArray(messages)) return messages;
|
|
386
|
-
const sanitized = sanitizeToolPairs(messages);
|
|
387
|
-
if (sanitized !== messages) {
|
|
388
|
-
messages.length = 0;
|
|
389
|
-
messages.push(...sanitized);
|
|
390
|
-
}
|
|
391
|
-
_ensureTranscriptPairing(messages, sessionId);
|
|
392
|
-
return messages;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
396
|
-
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
397
|
-
// stream. Writes, bash, MCP and skills stay serial after send() returns.
|
|
398
|
-
function isEagerDispatchable(name, tools) {
|
|
399
|
-
if (!Array.isArray(tools)) return false;
|
|
400
|
-
const def = tools.find(t => t?.name === name);
|
|
401
|
-
return def?.annotations?.readOnlyHint === true;
|
|
402
|
-
}
|
|
403
|
-
function messagesArrayChanged(before, after) {
|
|
404
|
-
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
405
|
-
if (before.length !== after.length) return true;
|
|
406
|
-
for (let i = 0; i < before.length; i += 1) {
|
|
407
|
-
if (before[i] !== after[i]) return true;
|
|
408
|
-
}
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
function normalizeUsage(usage) {
|
|
412
|
-
if (!usage) return null;
|
|
413
|
-
const costUsd = Number(usage.costUsd);
|
|
414
|
-
return {
|
|
415
|
-
inputTokens: usage.inputTokens || 0,
|
|
416
|
-
outputTokens: usage.outputTokens || 0,
|
|
417
|
-
cachedTokens: usage.cachedTokens || 0,
|
|
418
|
-
cacheWriteTokens: usage.cacheWriteTokens || 0,
|
|
419
|
-
promptTokens: usage.promptTokens || 0,
|
|
420
|
-
...(Number.isFinite(costUsd) ? { costUsd } : {}),
|
|
421
|
-
raw: usage.raw,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
function addUsage(total, usage) {
|
|
425
|
-
const delta = normalizeUsage(usage);
|
|
426
|
-
if (!delta) return total;
|
|
427
|
-
if (!total) return { ...delta };
|
|
428
|
-
const next = {
|
|
429
|
-
...total,
|
|
430
|
-
inputTokens: (total.inputTokens || 0) + delta.inputTokens,
|
|
431
|
-
outputTokens: (total.outputTokens || 0) + delta.outputTokens,
|
|
432
|
-
cachedTokens: (total.cachedTokens || 0) + delta.cachedTokens,
|
|
433
|
-
cacheWriteTokens: (total.cacheWriteTokens || 0) + delta.cacheWriteTokens,
|
|
434
|
-
promptTokens: (total.promptTokens || 0) + delta.promptTokens,
|
|
435
|
-
};
|
|
436
|
-
if (delta.costUsd != null || total.costUsd != null) {
|
|
437
|
-
next.costUsd = (total.costUsd || 0) + (delta.costUsd || 0);
|
|
438
|
-
}
|
|
439
|
-
return next;
|
|
440
|
-
}
|
|
441
|
-
function positiveTokenInt(value) {
|
|
442
|
-
const n = Number(value);
|
|
443
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
444
|
-
}
|
|
445
|
-
function envFlag(name, fallback = false) {
|
|
446
|
-
const v = process.env[name];
|
|
447
|
-
if (v === undefined) return fallback;
|
|
448
|
-
return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
|
|
449
|
-
}
|
|
450
|
-
function envTokenInt(name) {
|
|
451
|
-
return positiveTokenInt(process.env[name]);
|
|
452
|
-
}
|
|
453
|
-
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
454
|
-
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
455
|
-
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
456
|
-
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
|
|
457
|
-
// The compact type is already explicit (`semantic` by default). Honor it
|
|
458
|
-
// directly instead of substituting another compaction path.
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
|
|
463
|
-
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
464
|
-
?? process.env.MIXDOG_COMPACT_TYPE
|
|
465
|
-
?? cfg.type
|
|
466
|
-
?? cfg.compactType
|
|
467
|
-
?? cfg.compact_type;
|
|
468
|
-
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
123
|
async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
472
124
|
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
473
125
|
const startedAt = Date.now();
|
|
@@ -599,473 +251,6 @@ async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTo
|
|
|
599
251
|
compactDebugLog('recall-fasttrack pipeline', diagnostics);
|
|
600
252
|
return result;
|
|
601
253
|
}
|
|
602
|
-
const COMPACT_TARGET_RATIO = 0.02;
|
|
603
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
604
|
-
const COMPACT_TARGET_MAX_TOKENS = 16_000;
|
|
605
|
-
function resolveCompactTargetRatio(cfg = {}) {
|
|
606
|
-
const raw = cfg.targetPercent
|
|
607
|
-
?? cfg.targetPct
|
|
608
|
-
?? cfg.targetRatio
|
|
609
|
-
?? cfg.targetFraction
|
|
610
|
-
?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
611
|
-
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
612
|
-
?? COMPACT_TARGET_RATIO;
|
|
613
|
-
const n = Number(raw);
|
|
614
|
-
if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
|
|
615
|
-
return n > 1 ? n / 100 : n;
|
|
616
|
-
}
|
|
617
|
-
function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
618
|
-
const boundary = positiveTokenInt(boundaryTokens);
|
|
619
|
-
if (!boundary) return null;
|
|
620
|
-
const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
|
|
621
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_TOKENS')
|
|
622
|
-
|| envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
|
|
623
|
-
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
624
|
-
const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
|
|
625
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MIN_TOKENS')
|
|
626
|
-
|| envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
|
|
627
|
-
|| COMPACT_TARGET_MIN_TOKENS);
|
|
628
|
-
const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
|
|
629
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MAX_TOKENS')
|
|
630
|
-
|| envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
|
|
631
|
-
|| COMPACT_TARGET_MAX_TOKENS);
|
|
632
|
-
const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
|
|
633
|
-
return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
|
|
634
|
-
}
|
|
635
|
-
function resolveCompactKeepTokens(cfg = {}) {
|
|
636
|
-
return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
|
|
637
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
|
|
638
|
-
|| DEFAULT_COMPACTION_KEEP_TOKENS;
|
|
639
|
-
}
|
|
640
|
-
function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
641
|
-
if (!sessionRef) return null;
|
|
642
|
-
const cfg = sessionRef.compaction || {};
|
|
643
|
-
const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
644
|
-
if (!auto) return { auto: false };
|
|
645
|
-
const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
|
|
646
|
-
const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
|
|
647
|
-
const autoLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit ?? cfg.autoCompactTokenLimit);
|
|
648
|
-
const boundaryTokens = explicitBoundary && contextWindow
|
|
649
|
-
? Math.min(explicitBoundary, contextWindow)
|
|
650
|
-
: (explicitBoundary || contextWindow || autoLimit);
|
|
651
|
-
if (!boundaryTokens) return null;
|
|
652
|
-
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
653
|
-
// Only an explicit auto-compact limit STRICTLY BELOW the boundary acts as
|
|
654
|
-
// the trigger. A persisted value == boundary (legacy derived full-window
|
|
655
|
-
// autoCompactTokenLimit) would set autoTriggerTokens == boundary and
|
|
656
|
-
// collapse/override the default trigger, so it is ignored in favor of the
|
|
657
|
-
// default boundary trigger.
|
|
658
|
-
const autoTriggerTokens = autoLimit && autoLimit < compactBoundaryTokens ? Math.max(1, autoLimit) : null;
|
|
659
|
-
// Sanitized explicit limit: only a sub-boundary value is a real auto-compact
|
|
660
|
-
// limit. Anything >= boundary is a legacy derived full-window artifact and
|
|
661
|
-
// is reported as null so rememberCompactTelemetry does not re-persist it
|
|
662
|
-
// back onto the session and re-collapse the buffer on the next turn.
|
|
663
|
-
const explicitAutoCompactTokenLimit = autoTriggerTokens;
|
|
664
|
-
const bufferTokens = autoTriggerTokens
|
|
665
|
-
? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
|
|
666
|
-
: resolveCompactBufferTokens(compactBoundaryTokens, cfg);
|
|
667
|
-
const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
|
|
668
|
-
const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
|
|
669
|
-
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
670
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
671
|
-
|| 0;
|
|
672
|
-
const requestReserve = estimateRequestReserveTokens(tools);
|
|
673
|
-
const keepTokens = resolveCompactKeepTokens(cfg);
|
|
674
|
-
const compactType = resolveCompactTypeSetting(sessionRef, cfg);
|
|
675
|
-
return {
|
|
676
|
-
auto: true,
|
|
677
|
-
type: compactType,
|
|
678
|
-
compactType,
|
|
679
|
-
prune: cfg.prune === true || envFlag('MIXDOG_AGENT_COMPACT_PRUNE', false),
|
|
680
|
-
boundaryTokens: compactBoundaryTokens,
|
|
681
|
-
triggerTokens,
|
|
682
|
-
bufferTokens,
|
|
683
|
-
bufferRatio,
|
|
684
|
-
contextWindow,
|
|
685
|
-
rawContextWindow: positiveTokenInt(sessionRef.rawContextWindow ?? cfg.rawContextWindow) || contextWindow,
|
|
686
|
-
effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
|
|
687
|
-
? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
|
|
688
|
-
: null,
|
|
689
|
-
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
690
|
-
semantic: compactTypeIsSemantic(compactType) && resolveSemanticCompactSetting(sessionRef, cfg),
|
|
691
|
-
recallFastTrack: compactTypeIsRecallFastTrack(compactType),
|
|
692
|
-
semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
|
|
693
|
-
tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
|
|
694
|
-
keepTokens,
|
|
695
|
-
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
|
|
696
|
-
reserveTokens: requestReserve + configuredReserve,
|
|
697
|
-
requestReserveTokens: requestReserve,
|
|
698
|
-
configuredReserveTokens: configuredReserve,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
/** Transcript + request reserve only (never provider lastContextTokens). */
|
|
702
|
-
function compactPressureTokens(messageTokensEst, policy) {
|
|
703
|
-
if (messageTokensEst === null) return 0;
|
|
704
|
-
return Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/** Telemetry pressure when a reactive overflow retry forces the next compact. */
|
|
708
|
-
function compactionTelemetryPressureTokens(messageTokensEst, policy, { reactivePending = false } = {}) {
|
|
709
|
-
const base = compactPressureTokens(messageTokensEst, policy);
|
|
710
|
-
if (!reactivePending) return base;
|
|
711
|
-
const floor = positiveTokenInt(policy?.triggerTokens) || positiveTokenInt(policy?.boundaryTokens) || 0;
|
|
712
|
-
return floor ? Math.max(base, floor) : base;
|
|
713
|
-
}
|
|
714
|
-
function compactTargetBudget(policy) {
|
|
715
|
-
const boundary = positiveTokenInt(policy?.boundaryTokens);
|
|
716
|
-
if (!boundary) return null;
|
|
717
|
-
const reserve = Math.max(0, Number(policy?.reserveTokens) || 0);
|
|
718
|
-
const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
|
|
719
|
-
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
720
|
-
}
|
|
721
|
-
function shouldCompactForSession(messageTokensEst, policy, { forceReactive = false } = {}) {
|
|
722
|
-
if (!policy?.auto || !policy.boundaryTokens) return false;
|
|
723
|
-
if (forceReactive) return true;
|
|
724
|
-
if (messageTokensEst === null) return true;
|
|
725
|
-
return compactPressureTokens(messageTokensEst, policy) >= (policy.triggerTokens || policy.boundaryTokens);
|
|
726
|
-
}
|
|
727
|
-
function countPrunedToolOutputs(before, after) {
|
|
728
|
-
if (!Array.isArray(before) || !Array.isArray(after)) return 0;
|
|
729
|
-
let count = 0;
|
|
730
|
-
const n = Math.min(before.length, after.length);
|
|
731
|
-
for (let i = 0; i < n; i += 1) {
|
|
732
|
-
if (before[i]?.role !== 'tool' || after[i]?.role !== 'tool') continue;
|
|
733
|
-
if (before[i]?.content !== after[i]?.content && after[i]?.compactedKind === 'tool_output_prune') count += 1;
|
|
734
|
-
}
|
|
735
|
-
return count;
|
|
736
|
-
}
|
|
737
|
-
function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
738
|
-
if (!sessionRef || !policy) return;
|
|
739
|
-
const prev = sessionRef.compaction && typeof sessionRef.compaction === 'object'
|
|
740
|
-
? sessionRef.compaction
|
|
741
|
-
: {};
|
|
742
|
-
const changed = meta.compactChanged === true || meta.pruneCount > 0;
|
|
743
|
-
sessionRef.compaction = {
|
|
744
|
-
...prev,
|
|
745
|
-
auto: policy.auto !== false,
|
|
746
|
-
prune: policy.prune === true,
|
|
747
|
-
reservedTokens: policy.configuredReserveTokens || prev.reservedTokens || null,
|
|
748
|
-
requestReserveTokens: policy.requestReserveTokens || 0,
|
|
749
|
-
reserveTokens: policy.reserveTokens || 0,
|
|
750
|
-
boundaryTokens: policy.boundaryTokens || null,
|
|
751
|
-
triggerTokens: policy.triggerTokens || null,
|
|
752
|
-
bufferTokens: policy.bufferTokens || 0,
|
|
753
|
-
bufferRatio: policy.bufferRatio ?? prev.bufferRatio ?? null,
|
|
754
|
-
contextWindow: policy.contextWindow || null,
|
|
755
|
-
rawContextWindow: policy.rawContextWindow || null,
|
|
756
|
-
effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
|
|
757
|
-
autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
|
|
758
|
-
type: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
759
|
-
compactType: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
760
|
-
semantic: policy.semantic === true ? 'auto' : false,
|
|
761
|
-
recallFastTrack: policy.recallFastTrack === true,
|
|
762
|
-
semanticModel: policy.semanticModel || null,
|
|
763
|
-
semanticTimeoutMs: policy.semanticTimeoutMs || null,
|
|
764
|
-
tailTurns: policy.tailTurns || null,
|
|
765
|
-
keepTokens: policy.keepTokens || null,
|
|
766
|
-
preserveRecentTokens: policy.preserveRecentTokens || null,
|
|
767
|
-
lastCheckedAt: Date.now(),
|
|
768
|
-
lastBeforeTokens: meta.beforeTokens ?? null,
|
|
769
|
-
lastAfterTokens: meta.afterTokens ?? null,
|
|
770
|
-
lastPressureTokens: meta.pressureTokens ?? null,
|
|
771
|
-
currentEstimatedTokens: meta.pressureTokens ?? prev.currentEstimatedTokens ?? null,
|
|
772
|
-
lastApiRequestTokens: positiveTokenInt(sessionRef?.lastContextTokens) || prev.lastApiRequestTokens || null,
|
|
773
|
-
lastStage: meta.stage || prev.lastStage || null,
|
|
774
|
-
lastChanged: changed,
|
|
775
|
-
lastTrigger: meta.trigger || prev.lastTrigger || null,
|
|
776
|
-
lastSemantic: meta.semanticCompact === true,
|
|
777
|
-
lastSemanticError: Object.hasOwn(meta, 'semanticError')
|
|
778
|
-
? (meta.semanticError ?? null)
|
|
779
|
-
: (prev.lastSemanticError ?? null),
|
|
780
|
-
lastRecallFastTrack: meta.recallFastTrack === true,
|
|
781
|
-
lastRecallFastTrackError: Object.hasOwn(meta, 'recallFastTrackError')
|
|
782
|
-
? (meta.recallFastTrackError ?? null)
|
|
783
|
-
: (prev.lastRecallFastTrackError ?? null),
|
|
784
|
-
lastError: Object.hasOwn(meta, 'compactError') || Object.hasOwn(meta, 'lastError')
|
|
785
|
-
? (meta.compactError ?? meta.lastError ?? null)
|
|
786
|
-
: (prev.lastError ?? null),
|
|
787
|
-
lastPruneCount: meta.pruneCount || 0,
|
|
788
|
-
lastDurationMs: meta.durationMs != null && Number.isFinite(Number(meta.durationMs))
|
|
789
|
-
? Math.max(0, Math.round(Number(meta.durationMs)))
|
|
790
|
-
: null,
|
|
791
|
-
compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
|
|
792
|
-
};
|
|
793
|
-
if (changed) {
|
|
794
|
-
const changedAt = Date.now();
|
|
795
|
-
sessionRef.compaction.lastChangedAt = changedAt;
|
|
796
|
-
sessionRef.compaction.lastCompactAt = changedAt;
|
|
797
|
-
sessionRef.lastContextTokensStaleAfterCompact = true;
|
|
798
|
-
}
|
|
799
|
-
sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
|
|
800
|
-
sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
|
|
801
|
-
sessionRef.compactBoundaryTokens = policy.boundaryTokens || sessionRef.compactBoundaryTokens || null;
|
|
802
|
-
// Persist only the sanitized (sub-boundary) explicit limit. policy.autoCompactTokenLimit
|
|
803
|
-
// is already null for legacy derived full-window values, so a stale
|
|
804
|
-
// boundary-sized autoCompactTokenLimit on the session is cleared here rather
|
|
805
|
-
// than carried forward to re-collapse the buffer next turn.
|
|
806
|
-
{
|
|
807
|
-
const _boundary = positiveTokenInt(sessionRef.compactBoundaryTokens);
|
|
808
|
-
const _prevLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit);
|
|
809
|
-
const _keepPrev = _prevLimit && (!_boundary || _prevLimit < _boundary) ? _prevLimit : null;
|
|
810
|
-
sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || _keepPrev || null;
|
|
811
|
-
}
|
|
812
|
-
if (policy.effectiveContextWindowPercent !== null) {
|
|
813
|
-
sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function emitCompactEvent(opts, event = {}) {
|
|
818
|
-
if (!opts || typeof opts.onCompactEvent !== 'function') return;
|
|
819
|
-
try { opts.onCompactEvent({ ts: Date.now(), ...event }); }
|
|
820
|
-
catch { /* best-effort UI/log hook */ }
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function compactEventType(policy, fallback = DEFAULT_COMPACT_TYPE) {
|
|
824
|
-
return policy?.compactType || policy?.type || fallback;
|
|
825
|
-
}
|
|
826
|
-
const SKILL_TOOL_NAMES = new Set(['Skill', 'skills_list', 'skill_view']);
|
|
827
|
-
const SPECIAL_TOOL_NAMES = new Set(['bash_session', 'apply_patch', 'code_graph']);
|
|
828
|
-
const BASH_SESSION_HEADER_RE = /\[session: ([^\]\r\n]+)\]/;
|
|
829
|
-
const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
|
|
830
|
-
const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
|
|
831
|
-
const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
|
|
832
|
-
const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
|
|
833
|
-
const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
|
|
834
|
-
const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
|
|
835
|
-
|
|
836
|
-
function compactStoredToolArgString(value, key = '') {
|
|
837
|
-
if (typeof value !== 'string') return value;
|
|
838
|
-
const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
|
|
839
|
-
const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
|
|
840
|
-
const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
|
|
841
|
-
if (value.length <= limit) return value;
|
|
842
|
-
const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
843
|
-
const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
|
|
844
|
-
const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
|
|
845
|
-
return `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function compactStoredToolArgValue(value, key = '', depth = 0) {
|
|
849
|
-
if (value === null || value === undefined) return value;
|
|
850
|
-
if (typeof value === 'string') return compactStoredToolArgString(value, key);
|
|
851
|
-
if (typeof value !== 'object') return value;
|
|
852
|
-
if (depth >= 6) return Array.isArray(value) ? `[${value.length} items]` : '{...}';
|
|
853
|
-
if (Array.isArray(value)) {
|
|
854
|
-
return value.map((item) => compactStoredToolArgValue(item, key, depth + 1));
|
|
855
|
-
}
|
|
856
|
-
const out = {};
|
|
857
|
-
for (const [k, v] of Object.entries(value)) {
|
|
858
|
-
out[k] = compactStoredToolArgValue(v, k, depth + 1);
|
|
859
|
-
}
|
|
860
|
-
return out;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function compactToolCallsForHistory(calls) {
|
|
864
|
-
if (!Array.isArray(calls)) return calls;
|
|
865
|
-
return calls.map((call) => {
|
|
866
|
-
if (!call || typeof call !== 'object') return call;
|
|
867
|
-
return {
|
|
868
|
-
...call,
|
|
869
|
-
arguments: compactStoredToolArgValue(call.arguments),
|
|
870
|
-
};
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Restore the FULL body of ONE tool call inside a history assistant message
|
|
875
|
-
// whose toolCalls were compacted at push time. Used for a failed edit call so
|
|
876
|
-
// the model sees the original patch/old_string on retry instead of a
|
|
877
|
-
// `[mixdog compacted …]` placeholder it cannot act on. Must run BEFORE the
|
|
878
|
-
// message is first transmitted so it never mutates an already-cached prefix
|
|
879
|
-
// (the prompt cache is content-prefix matched).
|
|
880
|
-
//
|
|
881
|
-
// Only the compactable body/long keys (patch, old_string, new_string, content,
|
|
882
|
-
// rewrite, command, script) are restored, and at ANY depth — compaction is
|
|
883
|
-
// recursive (compactStoredToolArgValue), so batch shapes like edits[].old_string
|
|
884
|
-
// or writes[].content carry nested compacted bodies too. Every other field
|
|
885
|
-
// (e.g. `path`, which a tool may mutate in place during execution) is taken from
|
|
886
|
-
// the compacted snapshot captured at push time, before any mutation. The
|
|
887
|
-
// compacted args tree is built fresh by compactToolCallsForHistory and is not
|
|
888
|
-
// shared with originalCalls, so rebuilding it here is safe.
|
|
889
|
-
function restoreToolCallBodyForId(assistantMsg, originalCalls, callId) {
|
|
890
|
-
if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
|
|
891
|
-
if (!Array.isArray(originalCalls)) return;
|
|
892
|
-
const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
|
|
893
|
-
const orig = originalCalls.find((c) => c && c.id === callId);
|
|
894
|
-
if (!tc || !orig) return;
|
|
895
|
-
if (!tc.arguments || typeof tc.arguments !== 'object'
|
|
896
|
-
|| !orig.arguments || typeof orig.arguments !== 'object') return;
|
|
897
|
-
tc.arguments = _restoreCompactedBodies(tc.arguments, orig.arguments, '');
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Recursively rebuild a compacted args tree: replace ONLY compactable body/long
|
|
901
|
-
// string fields (matched by key at any depth) with their full originals, and
|
|
902
|
-
// keep every other field from the compacted snapshot. tcVal and origVal share
|
|
903
|
-
// the same structure (compaction only shortens body strings), so the walk
|
|
904
|
-
// descends them in parallel; a missing or non-object origVal falls back to the
|
|
905
|
-
// compacted value rather than throwing.
|
|
906
|
-
function _restoreCompactedBodies(tcVal, origVal, key) {
|
|
907
|
-
if ((STORED_TOOL_ARG_BODY_KEY_RE.test(key) || STORED_TOOL_ARG_LONG_KEY_RE.test(key))
|
|
908
|
-
&& typeof origVal === 'string') {
|
|
909
|
-
return origVal;
|
|
910
|
-
}
|
|
911
|
-
if (Array.isArray(tcVal) && Array.isArray(origVal)) {
|
|
912
|
-
return tcVal.map((item, i) => _restoreCompactedBodies(item, origVal[i], key));
|
|
913
|
-
}
|
|
914
|
-
if (tcVal && typeof tcVal === 'object' && origVal && typeof origVal === 'object') {
|
|
915
|
-
const out = {};
|
|
916
|
-
for (const k of Object.keys(tcVal)) {
|
|
917
|
-
out[k] = (k in origVal) ? _restoreCompactedBodies(tcVal[k], origVal[k], k) : tcVal[k];
|
|
918
|
-
}
|
|
919
|
-
return out;
|
|
920
|
-
}
|
|
921
|
-
return tcVal;
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Execute a single tool call — routes to MCP or builtin.
|
|
925
|
-
*/
|
|
926
|
-
function getToolKind(name) {
|
|
927
|
-
if (SKILL_TOOL_NAMES.has(name)) return 'skill';
|
|
928
|
-
if (SPECIAL_TOOL_NAMES.has(name)) return 'builtin';
|
|
929
|
-
if (isMcpTool(name)) return 'mcp';
|
|
930
|
-
if (isInternalTool(name)) return 'internal';
|
|
931
|
-
if (isBuiltinTool(name)) return 'builtin';
|
|
932
|
-
return 'builtin';
|
|
933
|
-
}
|
|
934
|
-
function buildSkillsListResponse(cwd) {
|
|
935
|
-
const skills = collectSkillsCached(cwd);
|
|
936
|
-
const entries = skills.map(s => ({ name: s.name, description: s.description || '' }));
|
|
937
|
-
return JSON.stringify({ skills: entries });
|
|
938
|
-
}
|
|
939
|
-
function viewSkill(cwd, name) {
|
|
940
|
-
if (!name) return 'Error: skill name is required';
|
|
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);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/** Normalize PostToolUse hook override values (legacy MCP text envelopes only). */
|
|
950
|
-
export function normalizeHookUpdatedToolOutput(value) {
|
|
951
|
-
if (typeof value === 'string') return value;
|
|
952
|
-
if (value == null) return '';
|
|
953
|
-
if (typeof value === 'object' && Array.isArray(value.content)) {
|
|
954
|
-
const hasNonText = value.content.some((c) => c && typeof c === 'object' && c.type && c.type !== 'text');
|
|
955
|
-
if (hasNonText) return value;
|
|
956
|
-
return value.content
|
|
957
|
-
.map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
|
|
958
|
-
.join('\n');
|
|
959
|
-
}
|
|
960
|
-
return value;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
export function resolveToolResultAfterHook(originalResult, hookResult) {
|
|
964
|
-
if (!hookResult || typeof hookResult !== 'object' || hookResult.updatedToolOutput === undefined) {
|
|
965
|
-
return originalResult;
|
|
966
|
-
}
|
|
967
|
-
const updated = normalizeHookUpdatedToolOutput(hookResult.updatedToolOutput);
|
|
968
|
-
return updated === undefined ? originalResult : updated;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function parseNativeToolSearchPayload(toolName, result) {
|
|
972
|
-
if (toolName !== 'tool_search' || typeof result !== 'string') return null;
|
|
973
|
-
try {
|
|
974
|
-
const parsed = JSON.parse(result);
|
|
975
|
-
const native = parsed?.nativeToolSearch;
|
|
976
|
-
if (!native || typeof native !== 'object') return null;
|
|
977
|
-
const toolReferences = Array.isArray(native.toolReferences)
|
|
978
|
-
? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
979
|
-
: [];
|
|
980
|
-
const openaiTools = Array.isArray(native.openaiTools)
|
|
981
|
-
? native.openaiTools.filter((tool) => tool && typeof tool === 'object')
|
|
982
|
-
: [];
|
|
983
|
-
if (!toolReferences.length && !openaiTools.length) return null;
|
|
984
|
-
return {
|
|
985
|
-
toolReferences,
|
|
986
|
-
openaiTools,
|
|
987
|
-
summary: typeof native.summary === 'string' && native.summary
|
|
988
|
-
? native.summary
|
|
989
|
-
: `Loaded deferred tools: ${toolReferences.join(', ') || openaiTools.map((tool) => tool.name).filter(Boolean).join(', ')}`,
|
|
990
|
-
};
|
|
991
|
-
} catch {
|
|
992
|
-
return null;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
function extractBashSessionId(result) {
|
|
996
|
-
if (typeof result !== 'string') return null;
|
|
997
|
-
const match = BASH_SESSION_HEADER_RE.exec(result);
|
|
998
|
-
return match ? match[1] : null;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
export function buildAgentBashSessionArgs(args, sessionRef) {
|
|
1002
|
-
if (!isAgentOwner(sessionRef)) return null;
|
|
1003
|
-
// run_in_background is a detached one-shot job, incompatible with the
|
|
1004
|
-
// persistent bash session. Fall through to the background-job path
|
|
1005
|
-
// (executeBuiltinTool -> startBackgroundShellJob) so the worker gets a
|
|
1006
|
-
// task_id that task control can resolve — otherwise the persistent
|
|
1007
|
-
// session returns a [session: ...] header and task control reports "task not found".
|
|
1008
|
-
if (args?.run_in_background === true) return null;
|
|
1009
|
-
const routedArgs = { ...(args || {}) };
|
|
1010
|
-
const explicitSessionId = typeof routedArgs.session_id === 'string' && routedArgs.session_id.trim()
|
|
1011
|
-
? routedArgs.session_id.trim()
|
|
1012
|
-
: null;
|
|
1013
|
-
const wantsPersistent = routedArgs.persistent === true || !!explicitSessionId;
|
|
1014
|
-
if (!wantsPersistent) return null;
|
|
1015
|
-
if (!explicitSessionId && sessionRef?.implicitBashSessionId) {
|
|
1016
|
-
routedArgs.session_id = sessionRef.implicitBashSessionId;
|
|
1017
|
-
} else if (explicitSessionId) {
|
|
1018
|
-
routedArgs.session_id = explicitSessionId;
|
|
1019
|
-
}
|
|
1020
|
-
delete routedArgs.persistent;
|
|
1021
|
-
return routedArgs;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export function formatMissingToolApprovalUiDenial(toolName, askReason) {
|
|
1025
|
-
const reason = String(askReason || 'approval requested by hook').trim();
|
|
1026
|
-
const name = String(toolName || 'tool');
|
|
1027
|
-
return `Error: tool "${name}" denied by hook: approval required but no approval UI is available${reason ? ` (${reason})` : ''}`;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* Resolve PreToolUse `{ action: 'ask' }` against an optional approval callback.
|
|
1032
|
-
* Returns `{ denial }` when the tool must not run; otherwise `{ approval }`.
|
|
1033
|
-
*/
|
|
1034
|
-
export async function resolvePreToolAskApproval({
|
|
1035
|
-
toolName,
|
|
1036
|
-
args,
|
|
1037
|
-
cwd,
|
|
1038
|
-
sessionId,
|
|
1039
|
-
toolCallId,
|
|
1040
|
-
askReason,
|
|
1041
|
-
toolApprovalHook,
|
|
1042
|
-
}) {
|
|
1043
|
-
const name = String(toolName || 'tool');
|
|
1044
|
-
const reason = String(askReason || 'approval requested by hook').trim();
|
|
1045
|
-
if (typeof toolApprovalHook !== 'function') {
|
|
1046
|
-
return { denial: formatMissingToolApprovalUiDenial(name, reason) };
|
|
1047
|
-
}
|
|
1048
|
-
let approval;
|
|
1049
|
-
try {
|
|
1050
|
-
approval = await toolApprovalHook({
|
|
1051
|
-
name,
|
|
1052
|
-
args,
|
|
1053
|
-
cwd,
|
|
1054
|
-
sessionId,
|
|
1055
|
-
toolCallId: toolCallId || null,
|
|
1056
|
-
reason,
|
|
1057
|
-
});
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
const detail = error?.message || String(error || 'approval failed');
|
|
1060
|
-
return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
|
|
1061
|
-
}
|
|
1062
|
-
if (!approvalGranted(approval)) {
|
|
1063
|
-
const detail = approvalReason(approval, reason || 'not approved');
|
|
1064
|
-
return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
|
|
1065
|
-
}
|
|
1066
|
-
return { approval };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
254
|
function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
|
|
1070
255
|
if (executeOpts.scopedCacheOutcome) {
|
|
1071
256
|
if (sessionRef && toolCallId) {
|
|
@@ -1239,6 +424,12 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1239
424
|
// the bash branch above (see resolveJobOwnerHostPid).
|
|
1240
425
|
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1241
426
|
}
|
|
427
|
+
if (isExternalAdapterTool(name)) {
|
|
428
|
+
// Foreign-CLI tool names (StrReplace/Write/bash variants) adapt to a
|
|
429
|
+
// native execution inside executeBuiltinTool's default: case; on a
|
|
430
|
+
// shape mismatch it falls back to the redirect guidance message.
|
|
431
|
+
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
432
|
+
}
|
|
1242
433
|
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
1243
434
|
})();
|
|
1244
435
|
if (typeof afterToolHook === 'function') {
|
|
@@ -1276,13 +467,6 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1276
467
|
* wrapper can propagate a clean cancellation.
|
|
1277
468
|
* - `onStageChange(stage)` / `onStreamDelta()` — forwarded to provider.send for heartbeats
|
|
1278
469
|
*/
|
|
1279
|
-
// Source of truth: defaults/agents.json (loaded via _getHiddenAgents
|
|
1280
|
-
// above). Build the name Set eagerly at module load so HIDDEN_AGENT_NAMES
|
|
1281
|
-
// stays in sync with the declarative registry — no hardcoded duplicate.
|
|
1282
|
-
const HIDDEN_AGENT_NAMES = new Set(
|
|
1283
|
-
(_getHiddenAgents().agents || []).map((r) => r && r.agent).filter((n) => typeof n === 'string' && n.length > 0)
|
|
1284
|
-
);
|
|
1285
|
-
|
|
1286
470
|
// Stop reasons that signal the turn was cut short mid-synthesis (token cap,
|
|
1287
471
|
// provider pause). Empty content + one of these reasons means the worker
|
|
1288
472
|
// was not done — re-prompt instead of accepting empty as final.
|
|
@@ -1292,22 +476,6 @@ const INCOMPLETE_STOP_REASONS = new Set([
|
|
|
1292
476
|
'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
|
|
1293
477
|
]);
|
|
1294
478
|
|
|
1295
|
-
export function approvalGranted(value) {
|
|
1296
|
-
if (value === true) return true;
|
|
1297
|
-
if (!value || typeof value !== 'object') return false;
|
|
1298
|
-
if (value.approved === true || value.allow === true || value.allowed === true) return true;
|
|
1299
|
-
const decision = String(value.decision || value.action || value.result || '').trim().toLowerCase();
|
|
1300
|
-
return decision === 'approve' || decision === 'approved' || decision === 'allow' || decision === 'yes';
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
export function approvalReason(value, fallback = '') {
|
|
1304
|
-
if (value && typeof value === 'object') {
|
|
1305
|
-
const reason = String(value.reason || value.message || '').trim();
|
|
1306
|
-
if (reason) return reason;
|
|
1307
|
-
}
|
|
1308
|
-
return fallback;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
479
|
export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
|
|
1312
480
|
let iterations = 0;
|
|
1313
481
|
let toolCallsTotal = 0;
|
|
@@ -1465,6 +633,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1465
633
|
// them apart, then clear the one-shot flag.
|
|
1466
634
|
const compactTrigger = reactiveOverflowRetryPending ? 'reactive' : 'auto';
|
|
1467
635
|
reactiveOverflowRetryPending = false;
|
|
636
|
+
// PreCompact: bridge to the standard hook bus before compaction
|
|
637
|
+
// runs. session-property hook (manager/loop have no bus access).
|
|
638
|
+
// { trigger } normalized to 'auto'|'manual'. Best-effort.
|
|
639
|
+
{
|
|
640
|
+
const _preCompactHook = typeof opts.preCompactHook === 'function'
|
|
641
|
+
? opts.preCompactHook
|
|
642
|
+
: sessionRef?.preCompactHook;
|
|
643
|
+
if (typeof _preCompactHook === 'function') {
|
|
644
|
+
try { await _preCompactHook({ sessionId, cwd, trigger: compactTrigger === 'manual' ? 'manual' : 'auto' }); }
|
|
645
|
+
catch { /* best-effort: PreCompact hook must never break compaction */ }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
1468
648
|
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1469
649
|
stage: 'compacting',
|
|
1470
650
|
beforeTokens: messageTokensEst,
|
|
@@ -1792,6 +972,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1792
972
|
durationMs: compactDurationMs,
|
|
1793
973
|
});
|
|
1794
974
|
}
|
|
975
|
+
// PostCompact: bridge to the standard hook bus after compaction
|
|
976
|
+
// completes. session-property hook; { trigger } 'auto'|'manual'.
|
|
977
|
+
{
|
|
978
|
+
const _postCompactHook = typeof opts.postCompactHook === 'function'
|
|
979
|
+
? opts.postCompactHook
|
|
980
|
+
: sessionRef?.postCompactHook;
|
|
981
|
+
if (typeof _postCompactHook === 'function') {
|
|
982
|
+
try { await _postCompactHook({ sessionId, cwd, trigger: compactTrigger === 'manual' ? 'manual' : 'auto' }); }
|
|
983
|
+
catch { /* best-effort: PostCompact hook must never break the loop */ }
|
|
984
|
+
}
|
|
985
|
+
}
|
|
1795
986
|
}
|
|
1796
987
|
const nextIteration = iterations + 1;
|
|
1797
988
|
opts.iteration = nextIteration;
|
|
@@ -1846,7 +1037,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1846
1037
|
// Shared pre-dispatch deny: identical predicate runs in the
|
|
1847
1038
|
// serial path below. If any role/permission guard would reject
|
|
1848
1039
|
// this call there, never start it eagerly here.
|
|
1849
|
-
if (
|
|
1040
|
+
if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
|
|
1850
1041
|
const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: _mutationEpoch };
|
|
1851
1042
|
_eagerInFlightSigs.set(_sig, call.id);
|
|
1852
1043
|
entry.promise = (async () => {
|
|
@@ -2082,13 +1273,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2082
1273
|
// the stateless contract for providers that don't use continuation).
|
|
2083
1274
|
providerState = response?.providerState ?? undefined;
|
|
2084
1275
|
iterations = nextIteration;
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
1276
|
+
// Payload byte estimate serializes the FULL messages+tools array —
|
|
1277
|
+
// only pay that cost when verbose loop tracing is actually enabled
|
|
1278
|
+
// (traceAgentLoop is a no-op otherwise).
|
|
1279
|
+
if (process.env.MIXDOG_AGENT_TRACE_VERBOSE === '1') {
|
|
1280
|
+
traceAgentLoop({
|
|
1281
|
+
sessionId,
|
|
1282
|
+
iteration: iterations,
|
|
1283
|
+
sendMs: Date.now() - sendStartedAt,
|
|
1284
|
+
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
1285
|
+
bodyBytesEst: estimateProviderPayloadBytes(messages, model, sendTools),
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
2092
1288
|
// Accumulate usage across iterations — every billable slot, not just
|
|
2093
1289
|
// input/output. Anthropic cache_read/cache_write typically stay 0 on
|
|
2094
1290
|
// the first iteration and surge on later ones (warm prefix reuse),
|
|
@@ -2330,8 +1526,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2330
1526
|
let toolStartedAt;
|
|
2331
1527
|
let toolEndedAt;
|
|
2332
1528
|
const toolKind = getToolKind(call.name);
|
|
2333
|
-
// Cross-turn read dedup
|
|
2334
|
-
// fileReadCache.ts: if the path's stat tuple (mtime/size/ino/dev)
|
|
1529
|
+
// Cross-turn read dedup: if the path's stat tuple (mtime/size/ino/dev)
|
|
2335
1530
|
// is unchanged since a prior read in THIS session, return the cached
|
|
2336
1531
|
// body instead of executing. Both scalar and array/object-array path
|
|
2337
1532
|
// forms are cached — keyed by (abs, offset, limit, mode, n) per entry.
|
|
@@ -2415,12 +1610,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2415
1610
|
// Runtime pre-dispatch deny. Schema profiles may hide
|
|
2416
1611
|
// tools for routing efficiency, but this remains the
|
|
2417
1612
|
// control-plane boundary for any tool_use that still
|
|
2418
|
-
// reaches the loop.
|
|
1613
|
+
// reaches the loop. preDispatchDenyForSession is the SHARED helper
|
|
2419
1614
|
// used by both the eager dispatch path (startEagerTool)
|
|
2420
1615
|
// and this serial path — keeps the agent-owned control-
|
|
2421
1616
|
// plane reject and no-tool role guards consistent across
|
|
2422
1617
|
// both paths.
|
|
2423
|
-
const _denyMsg =
|
|
1618
|
+
const _denyMsg = preDispatchDenyForSession(sessionRef, call, toolKind);
|
|
2424
1619
|
if (_denyMsg !== null) {
|
|
2425
1620
|
result = _denyMsg;
|
|
2426
1621
|
toolEndedAt = Date.now();
|
|
@@ -2460,6 +1655,36 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2460
1655
|
result = _env.result;
|
|
2461
1656
|
if (_env.newMessages.length) _batchNewMessages.push(..._env.newMessages);
|
|
2462
1657
|
}
|
|
1658
|
+
// Bounded-map cleanup: a scoped-cache outcome recorded for this call.id
|
|
1659
|
+
// (via _scopedCacheOutcomeForCall) is only ever consumed/deleted on the
|
|
1660
|
+
// success path below (_executeOk && _resultKind==='normal'). A failed or
|
|
1661
|
+
// errored call would otherwise leak its entry in
|
|
1662
|
+
// sessionRef._scopedCacheOutcomeByCallId forever — reclaim it here.
|
|
1663
|
+
if (sessionRef?._scopedCacheOutcomeByCallId && call?.id && (!_executeOk || _resultKind === 'error')) {
|
|
1664
|
+
sessionRef._scopedCacheOutcomeByCallId.delete(call.id);
|
|
1665
|
+
}
|
|
1666
|
+
// PostToolUseFailure: a tool that resolved to a failure (thrown-error
|
|
1667
|
+
// path -> `Error:` string, or an is_error result classified as
|
|
1668
|
+
// 'error') fires the optional session failure hook. Same shape as
|
|
1669
|
+
// afterToolHook; `result` carries the error text. Best-effort — a
|
|
1670
|
+
// hook error must never wedge the tool loop.
|
|
1671
|
+
if (!_executeOk || _resultKind === 'error') {
|
|
1672
|
+
const _afterToolFailureHook = typeof opts.afterToolFailureHook === 'function'
|
|
1673
|
+
? opts.afterToolFailureHook
|
|
1674
|
+
: sessionRef?.afterToolFailureHook;
|
|
1675
|
+
if (typeof _afterToolFailureHook === 'function') {
|
|
1676
|
+
try {
|
|
1677
|
+
await _afterToolFailureHook({
|
|
1678
|
+
name: call.name,
|
|
1679
|
+
args: call.arguments,
|
|
1680
|
+
cwd,
|
|
1681
|
+
sessionId,
|
|
1682
|
+
toolCallId: call.id,
|
|
1683
|
+
result: typeof result === 'string' ? result : String(result ?? ''),
|
|
1684
|
+
});
|
|
1685
|
+
} catch { /* best-effort: PostToolUseFailure hook must never break the loop */ }
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
2463
1688
|
// Update the cross-iteration repeat-failure guard with this call's
|
|
2464
1689
|
// outcome: bump the consecutive-failure count for an identical
|
|
2465
1690
|
// signature, or clear it the moment the same call succeeds.
|
|
@@ -2710,6 +1935,32 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2710
1935
|
if (!_nm || _nm.role !== 'user' || typeof _nm.content !== 'string' || !_nm.content) continue;
|
|
2711
1936
|
messages.push({ role: 'user', content: _nm.content, ...(_nm.meta ? { meta: _nm.meta } : {}) });
|
|
2712
1937
|
}
|
|
1938
|
+
// PostToolBatch: the full parallel batch of tool calls for this
|
|
1939
|
+
// assistant turn has resolved and all tool_results are pushed. Fire the
|
|
1940
|
+
// optional session hook before the next model call. No matcher event.
|
|
1941
|
+
// Block support: if the hook returns blocked===true, inject its reason
|
|
1942
|
+
// as a system-note user message for the next send (natural mechanism —
|
|
1943
|
+
// same channel the newMessages flush just used). Best-effort otherwise.
|
|
1944
|
+
{
|
|
1945
|
+
const _afterToolBatchHook = typeof opts.afterToolBatchHook === 'function'
|
|
1946
|
+
? opts.afterToolBatchHook
|
|
1947
|
+
: sessionRef?.afterToolBatchHook;
|
|
1948
|
+
if (typeof _afterToolBatchHook === 'function' && calls.length > 0) {
|
|
1949
|
+
try {
|
|
1950
|
+
const _batchDecision = await _afterToolBatchHook({
|
|
1951
|
+
sessionId,
|
|
1952
|
+
cwd,
|
|
1953
|
+
toolCount: calls.length,
|
|
1954
|
+
});
|
|
1955
|
+
if (_batchDecision?.blocked === true) {
|
|
1956
|
+
const _reason = String(_batchDecision.reason || 'PostToolBatch hook blocked continuation').trim();
|
|
1957
|
+
if (_reason) {
|
|
1958
|
+
messages.push({ role: 'user', content: `<system-reminder>\n${_reason}\n</system-reminder>`, meta: 'hook' });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
} catch { /* best-effort: PostToolBatch hook must never break the loop */ }
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
2713
1964
|
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
2714
1965
|
// AFTER any auto-compact pass. Draining here would put the steering
|
|
2715
1966
|
// user turn after the fresh tool results before compaction runs; then
|