mixdog 0.9.19 → 0.9.21
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/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { clean } from './session-text.mjs';
|
|
2
|
+
import { envFlag } from './env.mjs';
|
|
3
|
+
import { normalizeToolMode } from './effort.mjs';
|
|
4
|
+
import {
|
|
5
|
+
toolRow,
|
|
6
|
+
toolSearchMatches,
|
|
7
|
+
sortedNamesByMeasuredUsage,
|
|
8
|
+
selectDeferredTools,
|
|
9
|
+
} from './tool-catalog.mjs';
|
|
10
|
+
|
|
11
|
+
// Turn execution (ask) + session-manage/tool-surface/agent surfaces. Extracted
|
|
12
|
+
// verbatim from the runtime API object; stateless helpers are imported directly
|
|
13
|
+
// and the runtime injects live getters/setters for the mutable session/mode/
|
|
14
|
+
// turn-counter/transcript-writer locals plus the closure callbacks.
|
|
15
|
+
export function createSessionTurnApi(deps) {
|
|
16
|
+
const {
|
|
17
|
+
getSession, setSession, getCurrentCwd, getMode, setMode,
|
|
18
|
+
getActiveTurnCount, setActiveTurnCount, isFirstTurnCompleted, setFirstTurnCompleted,
|
|
19
|
+
getCodeGraphFirstTurnPrewarmDone, setCodeGraphFirstTurnPrewarmDone, codeGraphPrewarmLazy,
|
|
20
|
+
getRemoteEnabled, getCloseRequested,
|
|
21
|
+
getPendingSessionReset, setPendingSessionReset,
|
|
22
|
+
getTranscriptWriter, getTwKey, getLastAppendedAssistant, setLastAppendedAssistant,
|
|
23
|
+
scheduleCodeGraphPrewarm, refreshSessionForCwdIfNeeded, createCurrentSession,
|
|
24
|
+
ensureRemoteTranscriptWriter, channelsEnabled, invokeChannelStart, channels,
|
|
25
|
+
hooks, hookCommonPayload, mgr, notifyFnForSession, bootProfile,
|
|
26
|
+
scheduleProviderWarmup, scheduleProviderModelWarmup, invalidateContextStatusCache,
|
|
27
|
+
agentTool, recreateCurrentSessionIfReady, invalidatePreSessionToolSurface,
|
|
28
|
+
activeToolSurface, applyResolvedCwd, resolveCwdPath, agentStatusState, notificationListeners,
|
|
29
|
+
} = deps;
|
|
30
|
+
return {
|
|
31
|
+
async ask(prompt, options = {}) {
|
|
32
|
+
setActiveTurnCount(getActiveTurnCount() + 1);
|
|
33
|
+
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
34
|
+
// turn, so a likely code lookup hits a warm cache.
|
|
35
|
+
if (codeGraphPrewarmLazy && !getCodeGraphFirstTurnPrewarmDone()) {
|
|
36
|
+
setCodeGraphFirstTurnPrewarmDone(true);
|
|
37
|
+
scheduleCodeGraphPrewarm(0, 'first-turn');
|
|
38
|
+
}
|
|
39
|
+
const startedAt = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
42
|
+
if (!getSession()?.id) await createCurrentSession('turn');
|
|
43
|
+
// Remote outbound: ensure a transcript writer bound to the current
|
|
44
|
+
// session.id + cwd. Gated on remoteEnabled so non-remote sessions write nothing.
|
|
45
|
+
if (getRemoteEnabled()) {
|
|
46
|
+
setLastAppendedAssistant('');
|
|
47
|
+
const prevKey = getTwKey();
|
|
48
|
+
ensureRemoteTranscriptWriter();
|
|
49
|
+
if (getTwKey() && getTwKey() !== prevKey && channelsEnabled() && !envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
50
|
+
void invokeChannelStart()
|
|
51
|
+
.then(() => {
|
|
52
|
+
if (!getRemoteEnabled() || getCloseRequested()) return undefined;
|
|
53
|
+
return channels.execute('activate_channel_bridge', { active: true });
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => bootProfile('channels:turn-rebind-failed', { error: error?.message || String(error) }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const session0 = getSession();
|
|
59
|
+
hooks.emit('turn:start', { sessionId: session0.id, prompt, cwd: getCurrentCwd() });
|
|
60
|
+
// UserPromptSubmit: a hook FAILURE must not block the turn, but blocked===true MUST throw.
|
|
61
|
+
let promptDispatch = null;
|
|
62
|
+
try {
|
|
63
|
+
promptDispatch = await hooks.dispatch('UserPromptSubmit', hookCommonPayload({ session_id: session0.id, prompt }));
|
|
64
|
+
} catch { /* hook failure never blocks the turn */ }
|
|
65
|
+
if (promptDispatch?.blocked === true) {
|
|
66
|
+
throw new Error(`prompt blocked by hook: ${promptDispatch.reason || ''}`);
|
|
67
|
+
}
|
|
68
|
+
const hookContext = Array.isArray(promptDispatch?.additionalContext)
|
|
69
|
+
? promptDispatch.additionalContext.join('\n\n')
|
|
70
|
+
: String(promptDispatch?.additionalContext || '');
|
|
71
|
+
const turnContext = [options.context || '', hookContext]
|
|
72
|
+
.map((part) => String(part || '').trim())
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join('\n\n');
|
|
75
|
+
const result = await mgr.askSession(
|
|
76
|
+
session0.id,
|
|
77
|
+
prompt,
|
|
78
|
+
turnContext || null,
|
|
79
|
+
async (iter, calls) => {
|
|
80
|
+
for (const call of calls || []) {
|
|
81
|
+
hooks.emit('tool:planned', {
|
|
82
|
+
sessionId: session0.id,
|
|
83
|
+
name: call?.name || 'tool',
|
|
84
|
+
callId: call?.id || null,
|
|
85
|
+
});
|
|
86
|
+
if (getRemoteEnabled() && getTranscriptWriter()) {
|
|
87
|
+
try { getTranscriptWriter().appendToolUse(call?.name, call?.input ?? call?.arguments); }
|
|
88
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolCall failed: ${error?.message || error}\n`); }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (typeof options.onToolCall === 'function') {
|
|
92
|
+
return await options.onToolCall(iter, calls);
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
},
|
|
96
|
+
getCurrentCwd(),
|
|
97
|
+
options.prefetch || null,
|
|
98
|
+
{
|
|
99
|
+
onTextDelta: options.onTextDelta,
|
|
100
|
+
onReasoningDelta: options.onReasoningDelta,
|
|
101
|
+
onAssistantText: (text) => {
|
|
102
|
+
if (getRemoteEnabled() && getTranscriptWriter()) {
|
|
103
|
+
try {
|
|
104
|
+
const value = typeof text === 'string' ? text : (text == null ? '' : String(text));
|
|
105
|
+
if (value.trim()) {
|
|
106
|
+
getTranscriptWriter().appendAssistant(value);
|
|
107
|
+
setLastAppendedAssistant(value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: onAssistantText failed: ${error?.message || error}\n`); }
|
|
111
|
+
}
|
|
112
|
+
return options.onAssistantText?.(text);
|
|
113
|
+
},
|
|
114
|
+
onUsageDelta: options.onUsageDelta,
|
|
115
|
+
onToolResult: (message) => {
|
|
116
|
+
if (getRemoteEnabled() && getTranscriptWriter()) {
|
|
117
|
+
try {
|
|
118
|
+
const tur = message?.toolUseResult;
|
|
119
|
+
if (tur && (tur.oldString != null || tur.newString != null)) {
|
|
120
|
+
getTranscriptWriter().appendToolResult({ oldString: tur.oldString ?? '', newString: tur.newString ?? '' });
|
|
121
|
+
}
|
|
122
|
+
} catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolResult failed: ${error?.message || error}\n`); }
|
|
123
|
+
}
|
|
124
|
+
return options.onToolResult?.(message);
|
|
125
|
+
},
|
|
126
|
+
onToolApproval: options.onToolApproval,
|
|
127
|
+
onCompactEvent: options.onCompactEvent,
|
|
128
|
+
onStageChange: options.onStageChange,
|
|
129
|
+
onStreamDelta: options.onStreamDelta,
|
|
130
|
+
drainSteering: options.drainSteering,
|
|
131
|
+
onSteerMessage: options.onSteerMessage,
|
|
132
|
+
notifyFn: notifyFnForSession(session0.id),
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
setSession(mgr.getSession(session0.id) || getSession());
|
|
136
|
+
if (getRemoteEnabled() && getTranscriptWriter()) {
|
|
137
|
+
try {
|
|
138
|
+
const finalText = result?.content != null ? String(result.content) : '';
|
|
139
|
+
if (finalText.trim() && finalText !== getLastAppendedAssistant()) {
|
|
140
|
+
getTranscriptWriter().appendAssistant(finalText);
|
|
141
|
+
setLastAppendedAssistant(finalText);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
process.stderr.write(`mixdog: transcript-writer: final append failed: ${error?.message || error}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
hooks.emit('turn:end', { sessionId: session0.id, elapsedMs: Date.now() - startedAt });
|
|
148
|
+
try {
|
|
149
|
+
await hooks.dispatch('Stop', hookCommonPayload({ session_id: session0.id }));
|
|
150
|
+
} catch { /* best-effort: Stop hook must never break the turn */ }
|
|
151
|
+
return { result, session: getSession() };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
hooks.emit('turn:error', { sessionId: getSession()?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
154
|
+
try {
|
|
155
|
+
const msg = String(error?.message || error || '').toLowerCase();
|
|
156
|
+
const errorType = /rate.?limit|429|too many requests/.test(msg) ? 'rate_limit'
|
|
157
|
+
: /overloaded|529/.test(msg) ? 'overloaded'
|
|
158
|
+
: /authenticat|unauthorized|401|invalid.*api.?key/.test(msg) ? 'authentication_failed'
|
|
159
|
+
: /server.?error|5\d\d|internal error/.test(msg) ? 'server_error'
|
|
160
|
+
: 'unknown';
|
|
161
|
+
void hooks.dispatch('StopFailure', hookCommonPayload({ session_id: getSession()?.id || null, error_type: errorType }));
|
|
162
|
+
} catch { /* best-effort: StopFailure hook must never break teardown */ }
|
|
163
|
+
throw error;
|
|
164
|
+
} finally {
|
|
165
|
+
setActiveTurnCount(Math.max(0, getActiveTurnCount() - 1));
|
|
166
|
+
if (!isFirstTurnCompleted()) {
|
|
167
|
+
setFirstTurnCompleted(true);
|
|
168
|
+
scheduleProviderWarmup();
|
|
169
|
+
scheduleProviderModelWarmup();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
async clear(options = {}) {
|
|
174
|
+
const session = getSession();
|
|
175
|
+
if (!session?.id) return false;
|
|
176
|
+
const cleared = await mgr.clearSessionMessages(session.id, options);
|
|
177
|
+
if (!cleared) return false;
|
|
178
|
+
setSession(typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session));
|
|
179
|
+
if (options.recoverAgent === true) {
|
|
180
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: getSession()?.clientHostPid || process.pid }); } catch {}
|
|
181
|
+
}
|
|
182
|
+
invalidateContextStatusCache();
|
|
183
|
+
return true;
|
|
184
|
+
},
|
|
185
|
+
// session_manage tool handoff: the engine polls this at turn end and, if
|
|
186
|
+
// set, runs the same clear path the idle auto-clear uses. One-shot read.
|
|
187
|
+
consumePendingSessionReset() {
|
|
188
|
+
const pending = getPendingSessionReset();
|
|
189
|
+
setPendingSessionReset(null);
|
|
190
|
+
if (!pending) return null;
|
|
191
|
+
const session = getSession();
|
|
192
|
+
// Session changed since scheduling (resume / new session) — drop it.
|
|
193
|
+
if (!session?.id || pending.sessionId !== session.id) return null;
|
|
194
|
+
return pending.action;
|
|
195
|
+
},
|
|
196
|
+
async compact(options = {}) {
|
|
197
|
+
const session = getSession();
|
|
198
|
+
if (!session?.id) return null;
|
|
199
|
+
if (getActiveTurnCount() > 0) {
|
|
200
|
+
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
201
|
+
}
|
|
202
|
+
// Manual compact bypasses loop.mjs, so its PreCompact/PostCompact never
|
|
203
|
+
// fire here — dispatch them explicitly via the session-property hooks.
|
|
204
|
+
try { await session.preCompactHook?.({ trigger: 'manual' }); }
|
|
205
|
+
catch { /* best-effort: PreCompact hook must never break manual compact */ }
|
|
206
|
+
const result = await mgr.compactSessionMessages(session.id);
|
|
207
|
+
try { await session.postCompactHook?.({ trigger: 'manual' }); }
|
|
208
|
+
catch { /* best-effort: PostCompact hook must never break manual compact */ }
|
|
209
|
+
setSession(mgr.getSession(session.id) || session);
|
|
210
|
+
if (options.recoverAgent === true) {
|
|
211
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: getSession()?.clientHostPid || process.pid }); } catch {}
|
|
212
|
+
}
|
|
213
|
+
invalidateContextStatusCache();
|
|
214
|
+
return result;
|
|
215
|
+
},
|
|
216
|
+
async setToolMode(nextMode) {
|
|
217
|
+
const mode = normalizeToolMode(nextMode);
|
|
218
|
+
setMode(mode);
|
|
219
|
+
invalidatePreSessionToolSurface();
|
|
220
|
+
const session = getSession();
|
|
221
|
+
if (session?.id) mgr.closeSession(session.id, 'cli-mode-switch');
|
|
222
|
+
await recreateCurrentSessionIfReady();
|
|
223
|
+
return mode;
|
|
224
|
+
},
|
|
225
|
+
agentStatus() {
|
|
226
|
+
return agentStatusState();
|
|
227
|
+
},
|
|
228
|
+
agentControl(args = {}) {
|
|
229
|
+
const session = getSession();
|
|
230
|
+
const callerSessionId = session?.id || null;
|
|
231
|
+
return agentTool.execute(args, {
|
|
232
|
+
callerCwd: getCurrentCwd(),
|
|
233
|
+
invocationSource: 'user-command',
|
|
234
|
+
callerSessionId,
|
|
235
|
+
clientHostPid: session?.clientHostPid || process.pid,
|
|
236
|
+
notifyFn: notifyFnForSession(callerSessionId),
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
onNotification(listener) {
|
|
240
|
+
if (typeof listener !== 'function') return () => {};
|
|
241
|
+
notificationListeners.add(listener);
|
|
242
|
+
return () => notificationListeners.delete(listener);
|
|
243
|
+
},
|
|
244
|
+
toolsStatus(query = '') {
|
|
245
|
+
const surface = activeToolSurface();
|
|
246
|
+
const catalog = Array.isArray(surface?.deferredToolCatalog)
|
|
247
|
+
? surface.deferredToolCatalog
|
|
248
|
+
: (Array.isArray(surface?.tools) ? surface.tools : []);
|
|
249
|
+
const activeNames = new Set((surface?.tools || []).map((tool) => tool?.name).filter(Boolean));
|
|
250
|
+
const needle = clean(query).toLowerCase();
|
|
251
|
+
const rows = catalog.map((tool) => toolRow(tool, activeNames)).filter((row) => row.name);
|
|
252
|
+
const tools = needle
|
|
253
|
+
? rows.filter((row) => toolSearchMatches(row, needle))
|
|
254
|
+
: rows;
|
|
255
|
+
return {
|
|
256
|
+
mode: getMode(),
|
|
257
|
+
count: rows.length,
|
|
258
|
+
activeCount: rows.filter((row) => row.active).length,
|
|
259
|
+
tools,
|
|
260
|
+
activeTools: sortedNamesByMeasuredUsage(activeNames),
|
|
261
|
+
discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
selectTools(names) {
|
|
265
|
+
const list = Array.isArray(names) ? names : String(names || '').split(/[,\s]+/);
|
|
266
|
+
const result = selectDeferredTools(activeToolSurface(), list, getMode());
|
|
267
|
+
return { ...result, status: this.toolsStatus() };
|
|
268
|
+
},
|
|
269
|
+
setCwd(path) {
|
|
270
|
+
applyResolvedCwd(resolveCwdPath(path));
|
|
271
|
+
return getCurrentCwd();
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -98,56 +98,6 @@ const DEFERRED_SELECT_ALIASES = {
|
|
|
98
98
|
code: ['code_graph'],
|
|
99
99
|
shell: ['shell', 'task'],
|
|
100
100
|
};
|
|
101
|
-
const TOOL_SEARCH_SAFE_AUTO_ALIASES = new Set([
|
|
102
|
-
'shell',
|
|
103
|
-
'web',
|
|
104
|
-
'search',
|
|
105
|
-
'agent',
|
|
106
|
-
'memory',
|
|
107
|
-
]);
|
|
108
|
-
const TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES = new Set([
|
|
109
|
-
'status',
|
|
110
|
-
'state',
|
|
111
|
-
'info',
|
|
112
|
-
'list',
|
|
113
|
-
'show',
|
|
114
|
-
'config',
|
|
115
|
-
]);
|
|
116
|
-
const TOOL_SEARCH_STOP_WORDS = new Set([
|
|
117
|
-
'a',
|
|
118
|
-
'an',
|
|
119
|
-
'and',
|
|
120
|
-
'are',
|
|
121
|
-
'as',
|
|
122
|
-
'for',
|
|
123
|
-
'from',
|
|
124
|
-
'how',
|
|
125
|
-
'i',
|
|
126
|
-
'in',
|
|
127
|
-
'is',
|
|
128
|
-
'me',
|
|
129
|
-
'need',
|
|
130
|
-
'of',
|
|
131
|
-
'on',
|
|
132
|
-
'please',
|
|
133
|
-
'the',
|
|
134
|
-
'to',
|
|
135
|
-
'tool',
|
|
136
|
-
'tools',
|
|
137
|
-
'use',
|
|
138
|
-
'using',
|
|
139
|
-
'with',
|
|
140
|
-
]);
|
|
141
|
-
const TOOL_SEARCH_ROW_ALIASES = Object.freeze({
|
|
142
|
-
agent: ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer'],
|
|
143
|
-
cwd: ['cwd', 'working directory', 'current directory', 'project root', 'folder'],
|
|
144
|
-
memory: ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status'],
|
|
145
|
-
recall: ['recall', 'previous work', 'past work', 'prior context', 'history', 'resume context'],
|
|
146
|
-
search: ['web search', 'internet search', 'current info', 'latest info', 'online search', 'docs search'],
|
|
147
|
-
shell: ['run command', 'execute command', 'terminal', 'powershell', 'bash', 'run tests', 'test command', 'build command', 'npm', 'node', 'git'],
|
|
148
|
-
task: ['background task', 'async task', 'wait task', 'cancel task', 'task status'],
|
|
149
|
-
web_fetch: ['fetch url', 'fetch page', 'open url', 'web page', 'read url', 'docs page'],
|
|
150
|
-
});
|
|
151
101
|
|
|
152
102
|
export function toolKind(tool) {
|
|
153
103
|
const name = clean(tool?.name);
|
|
@@ -326,189 +276,14 @@ function toolSearchNativePayload(catalog, names, provider = '') {
|
|
|
326
276
|
};
|
|
327
277
|
}
|
|
328
278
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.filter(Boolean);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function toolSearchMeaningfulTokens(value) {
|
|
336
|
-
return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function toolSearchText(row) {
|
|
340
|
-
const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
|
|
341
|
-
return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function toolSearchRowAliases(name) {
|
|
345
|
-
return TOOL_SEARCH_ROW_ALIASES[clean(name)] || [];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function toolSearchRank(row, query) {
|
|
349
|
-
const raw = clean(query).toLowerCase();
|
|
350
|
-
if (!raw) return { score: 0, reasons: [] };
|
|
351
|
-
const name = clean(row?.name).toLowerCase();
|
|
352
|
-
const prettyName = name.replace(/_/g, ' ');
|
|
353
|
-
const haystack = toolSearchText(row);
|
|
354
|
-
const aliases = toolSearchRowAliases(name);
|
|
355
|
-
const aliasText = aliases.join(' ').toLowerCase();
|
|
356
|
-
const queryTokens = toolSearchMeaningfulTokens(raw);
|
|
357
|
-
const nameTokens = new Set(toolSearchTokens(`${name} ${prettyName}`));
|
|
358
|
-
const aliasTokens = new Set(toolSearchTokens(aliasText));
|
|
359
|
-
let score = 0;
|
|
360
|
-
const reasons = [];
|
|
361
|
-
if (raw === name || raw === prettyName) {
|
|
362
|
-
score += 120;
|
|
363
|
-
reasons.push('exact-name');
|
|
364
|
-
}
|
|
365
|
-
if (aliases.some((alias) => raw === alias || raw === alias.replace(/[_-]+/g, ' '))) {
|
|
366
|
-
score += 100;
|
|
367
|
-
reasons.push('exact-alias');
|
|
368
|
-
}
|
|
369
|
-
// Stop-word-only queries (e.g. "tool", "to") have zero meaningful tokens.
|
|
370
|
-
// Without an exact name/alias hit, they must not fall through to the raw
|
|
371
|
-
// substring/haystack checks below — every row description likely contains
|
|
372
|
-
// "tool" somewhere, producing a noisy pseudo-match list instead of "no
|
|
373
|
-
// results". Exact name/alias matches stay intact even when the name is
|
|
374
|
-
// itself a stop word.
|
|
375
|
-
if (!queryTokens.length && !reasons.length) {
|
|
376
|
-
return { score: 0, reasons: [] };
|
|
377
|
-
}
|
|
378
|
-
if (haystack.includes(raw)) {
|
|
379
|
-
score += 34;
|
|
380
|
-
reasons.push('phrase');
|
|
381
|
-
}
|
|
382
|
-
for (const alias of aliases) {
|
|
383
|
-
const normalizedAlias = alias.toLowerCase();
|
|
384
|
-
if (normalizedAlias && raw.includes(normalizedAlias)) {
|
|
385
|
-
score += 58;
|
|
386
|
-
reasons.push('alias-phrase');
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
let matchedTokens = 0;
|
|
391
|
-
for (const token of queryTokens) {
|
|
392
|
-
if (nameTokens.has(token) || name.includes(token)) {
|
|
393
|
-
score += 24;
|
|
394
|
-
matchedTokens += 1;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
if (aliasTokens.has(token) || aliasText.includes(token)) {
|
|
398
|
-
score += 18;
|
|
399
|
-
matchedTokens += 1;
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (haystack.includes(token)) {
|
|
403
|
-
score += 7;
|
|
404
|
-
matchedTokens += 1;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (queryTokens.length && matchedTokens === queryTokens.length) {
|
|
408
|
-
score += Math.min(18, queryTokens.length * 6);
|
|
409
|
-
reasons.push('all-tokens');
|
|
410
|
-
}
|
|
411
|
-
if (clean(row?.kind).toLowerCase() === raw) score += 10;
|
|
412
|
-
if (score > 0) score += Math.min(6, Math.floor(measuredToolUsage(name) / 150));
|
|
413
|
-
return { score, reasons };
|
|
414
|
-
}
|
|
415
|
-
|
|
279
|
+
// Plain case-insensitive substring filter over name + description. No scores,
|
|
280
|
+
// no aliases, no auto-selection: tool_search only lists and (via select)
|
|
281
|
+
// loads. Empty query matches every row.
|
|
416
282
|
export function toolSearchMatches(row, query) {
|
|
417
283
|
const raw = clean(query).toLowerCase();
|
|
418
284
|
if (!raw) return true;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
function rankedToolSearchRows(rows, query) {
|
|
423
|
-
const raw = clean(query).toLowerCase();
|
|
424
|
-
if (!raw) return rows;
|
|
425
|
-
return rows
|
|
426
|
-
.map((row) => {
|
|
427
|
-
const ranked = toolSearchRank(row, raw);
|
|
428
|
-
return { ...row, score: ranked.score, reasons: ranked.reasons };
|
|
429
|
-
})
|
|
430
|
-
.filter((row) => row.score > 0)
|
|
431
|
-
.sort((a, b) => {
|
|
432
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
433
|
-
if (a.active !== b.active) return a.active ? 1 : -1;
|
|
434
|
-
if (b.usage !== a.usage) return b.usage - a.usage;
|
|
435
|
-
return String(a.name).localeCompare(String(b.name));
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function isAsciiWordChar(ch) {
|
|
440
|
-
return !!ch && /[A-Za-z0-9]/.test(ch);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Matches `phrase` inside `text` at a word boundary. ASCII letters/digits on
|
|
444
|
-
// either side of the match block it (so "webhook" does not match "web" and
|
|
445
|
-
// "prune" does not match "run"); non-ASCII characters (e.g. Korean, where
|
|
446
|
-
// words are not space-separated) never count as word chars, so Korean
|
|
447
|
-
// substring phrases keep matching as before.
|
|
448
|
-
function phraseMatchesAsWords(text, phrase) {
|
|
449
|
-
if (!phrase) return false;
|
|
450
|
-
let idx = text.indexOf(phrase);
|
|
451
|
-
while (idx !== -1) {
|
|
452
|
-
const before = idx > 0 ? text[idx - 1] : '';
|
|
453
|
-
const after = idx + phrase.length < text.length ? text[idx + phrase.length] : '';
|
|
454
|
-
if (!isAsciiWordChar(before) && !isAsciiWordChar(after)) return true;
|
|
455
|
-
idx = text.indexOf(phrase, idx + 1);
|
|
456
|
-
}
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function queryHasAnyPhrase(query, phrases) {
|
|
461
|
-
const text = clean(query).toLowerCase().replace(/[_-]+/g, ' ');
|
|
462
|
-
return phrases.some((phrase) => phraseMatchesAsWords(text, phrase));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const TOOL_SEARCH_AUTO_CATEGORY_BRANCHES = [
|
|
466
|
-
{
|
|
467
|
-
names: ['memory'],
|
|
468
|
-
phrases: ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status', '기억 저장', '메모리 저장'],
|
|
469
|
-
},
|
|
470
|
-
{
|
|
471
|
-
names: ['shell', 'task'],
|
|
472
|
-
phrases: ['run', 'execute', 'test', 'tests', 'build', 'terminal', 'command', 'powershell', 'bash', 'shell', 'npm', 'node', 'git', '실행', '테스트', '빌드', '터미널', '쉘'],
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
names: ['search', 'web_fetch'],
|
|
476
|
-
phrases: ['web', 'internet', 'online', 'current info', 'latest', 'news', 'browse', 'docs', 'documentation', '웹', '인터넷', '최신', '뉴스', '문서'],
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
names: ['recall'],
|
|
480
|
-
phrases: ['recall', 'remember', 'memory previous', 'previous', 'history', 'past', 'prior', 'earlier', 'resume', '이전', '기억', '히스토리'],
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
names: ['agent'],
|
|
484
|
-
phrases: ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer', '에이전트', '워커', '병렬'],
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
names: ['cwd'],
|
|
488
|
-
phrases: ['working directory', 'project root', 'current directory', 'cwd'],
|
|
489
|
-
},
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
function autoToolSelectionNames(query, rows) {
|
|
493
|
-
const raw = clean(query).toLowerCase();
|
|
494
|
-
if (!raw || TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES.has(raw)) return [];
|
|
495
|
-
if (TOOL_SEARCH_SAFE_AUTO_ALIASES.has(raw) && DEFERRED_SELECT_ALIASES[raw]) {
|
|
496
|
-
return DEFERRED_SELECT_ALIASES[raw];
|
|
497
|
-
}
|
|
498
|
-
const matchedBranches = TOOL_SEARCH_AUTO_CATEGORY_BRANCHES.filter((branch) =>
|
|
499
|
-
queryHasAnyPhrase(raw, branch.phrases)
|
|
500
|
-
);
|
|
501
|
-
if (matchedBranches.length === 1) return matchedBranches[0].names;
|
|
502
|
-
// Ambiguous (0 or 2+ category branches matched): never auto-load a
|
|
503
|
-
// category's tools off a possibly-wrong guess. Fall through to the ranked
|
|
504
|
-
// exact-name/alias path below; that path returns [] on its own when there
|
|
505
|
-
// is no exact match, so an ambiguous query still safely resolves to [].
|
|
506
|
-
const ranked = rankedToolSearchRows(rows, raw);
|
|
507
|
-
const top = ranked[0];
|
|
508
|
-
if (!top) return [];
|
|
509
|
-
const reasons = new Set(top.reasons || []);
|
|
510
|
-
if (reasons.has('exact-name') || reasons.has('exact-alias')) return [top.name];
|
|
511
|
-
return [];
|
|
285
|
+
const haystack = `${clean(row?.name)} ${clean(row?.description)}`.toLowerCase();
|
|
286
|
+
return haystack.includes(raw);
|
|
512
287
|
}
|
|
513
288
|
|
|
514
289
|
function expandSelectionNames(names) {
|
|
@@ -694,39 +469,18 @@ export function renderToolSearch(args = {}, session, mode = 'full') {
|
|
|
694
469
|
const forcedSelectedNames = explicitSelectedNames.length ? explicitSelectedNames : querySelectedNames;
|
|
695
470
|
const query = querySelectedNames.length ? '' : rawQuery.toLowerCase();
|
|
696
471
|
const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
: rows;
|
|
710
|
-
// Query hits must become loadable without a second explicit select turn:
|
|
711
|
-
// if the query produced ranked matches but the forced/auto path above
|
|
712
|
-
// loaded nothing (no exact-name/alias/category hit), fall back to
|
|
713
|
-
// auto-loading the ranked match names themselves (deferred/non-active
|
|
714
|
-
// only, capped at `limit`). Ambiguous queries stay excluded so a guess
|
|
715
|
-
// never silently loads a wrong category.
|
|
716
|
-
const STRONG_MATCH_REASONS = new Set(['exact-name', 'exact-alias', 'alias-phrase', 'phrase', 'all-tokens']);
|
|
717
|
-
const isStrongMatch = (row) => row.score >= 24 || (row.reasons || []).some((reason) => STRONG_MATCH_REASONS.has(reason));
|
|
718
|
-
const queryLoadedNothing = !toolSelection
|
|
719
|
-
|| (!toolSelection.added.length && !toolSelection.already.length);
|
|
720
|
-
if (query && !TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES.has(query) && matches.length && queryLoadedNothing) {
|
|
721
|
-
const fallbackNames = matches.filter((row) => !row.active && isStrongMatch(row)).map((row) => row.name).slice(0, limit);
|
|
722
|
-
if (fallbackNames.length) {
|
|
723
|
-
toolSelection = selectDeferredTools(session, fallbackNames, mode);
|
|
724
|
-
selectionMode = 'query';
|
|
725
|
-
nextActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
726
|
-
rows = catalog.map((tool) => toolRow(tool, nextActiveNames)).filter((row) => row.name);
|
|
727
|
-
matches = query ? rankedToolSearchRows(rows, query) : rows;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
472
|
+
// Explicit loader only: select names/aliases (or query "select:a,b") load
|
|
473
|
+
// deferred tools. A plain query is a case-insensitive substring filter over
|
|
474
|
+
// name+description for listing — it never loads. No scores, no ranking, no
|
|
475
|
+
// category auto-load: deferred tools are called directly (they auto-promote
|
|
476
|
+
// via deferred call-through) and are advertised in the system-prompt manifest.
|
|
477
|
+
const toolSelection = forcedSelectedNames.length
|
|
478
|
+
? selectDeferredTools(session, forcedSelectedNames, mode)
|
|
479
|
+
: null;
|
|
480
|
+
const selectionMode = forcedSelectedNames.length ? 'select' : null;
|
|
481
|
+
const nextActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
482
|
+
const rows = catalog.map((tool) => toolRow(tool, nextActiveNames)).filter((row) => row.name);
|
|
483
|
+
const matches = query ? rows.filter((row) => toolSearchMatches(row, query)) : rows;
|
|
730
484
|
const selected = toolSelection
|
|
731
485
|
? {
|
|
732
486
|
mode: selectionMode,
|
|
@@ -743,6 +497,6 @@ export function renderToolSearch(args = {}, session, mode = 'full') {
|
|
|
743
497
|
matches: matches.slice(0, limit),
|
|
744
498
|
activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
|
|
745
499
|
discoveredTools: sortedNamesByMeasuredUsage(session?.deferredDiscoveredTools || []),
|
|
746
|
-
note: '
|
|
500
|
+
note: 'Deferred tools listed in the system-prompt manifest can be called directly by name (they auto-load on first call). This tool only lists (query = case-insensitive substring over name+description; never loads) or loads exact tools when you pass select names/aliases or query "select:a,b".',
|
|
747
501
|
}, null, 2);
|
|
748
502
|
}
|
|
@@ -13,11 +13,11 @@ export const TOOL_SEARCH_TOOL = {
|
|
|
13
13
|
openWorldHint: false,
|
|
14
14
|
agentHidden: true,
|
|
15
15
|
},
|
|
16
|
-
description: '
|
|
16
|
+
description: 'List deferred tools by name+description substring, or load exact tools via select. Deferred tools can also be called directly by name.',
|
|
17
17
|
inputSchema: {
|
|
18
18
|
type: 'object',
|
|
19
19
|
properties: {
|
|
20
|
-
query: { type: 'string', description: '
|
|
20
|
+
query: { type: 'string', description: 'Case-insensitive substring filter over deferred tool name+description (lists only, no load). Use select:a,b to load exact names.' },
|
|
21
21
|
select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Force exact tool names.' },
|
|
22
22
|
limit: { type: 'number', description: 'Max matches.' },
|
|
23
23
|
},
|