mixdog 0.9.47 → 0.9.50
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/README.md +6 -6
- package/package.json +19 -9
- package/scripts/agent-parallel-smoke.mjs +4 -1
- package/scripts/agent-route-batch-test.mjs +40 -0
- package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
- package/scripts/code-graph-description-contract.mjs +185 -0
- package/scripts/code-graph-disk-hit-test.mjs +55 -0
- package/scripts/code-graph-dispatch-test.mjs +96 -0
- package/scripts/compact-pressure-test.mjs +40 -0
- package/scripts/deferred-tool-loading-test.mjs +233 -0
- package/scripts/embedding-worker-exit-test.mjs +76 -0
- package/scripts/execution-completion-dedup-test.mjs +48 -0
- package/scripts/explore-prompt-policy-test.mjs +88 -3
- package/scripts/live-worker-smoke.mjs +68 -16
- package/scripts/memory-core-input-test.mjs +33 -13
- package/scripts/native-edit-wire-test.mjs +152 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
- package/scripts/patch-binary-cache-test.mjs +181 -0
- package/scripts/prompt-immediate-render-test.mjs +89 -0
- package/scripts/provider-toolcall-test.mjs +280 -15
- package/scripts/shell-failure-diagnostics-test.mjs +211 -0
- package/scripts/smoke-loop.mjs +9 -3
- package/scripts/smoke.mjs +5 -106
- package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
- package/scripts/streaming-tail-window-test.mjs +29 -0
- package/scripts/tool-failures.mjs +21 -3
- package/scripts/tool-smoke.mjs +263 -38
- package/scripts/tool-tui-presentation-test.mjs +17 -1
- package/scripts/tui-perf-run.ps1 +26 -0
- package/scripts/tui-transcript-perf-test.mjs +7 -1
- package/scripts/verify-release-assets-test.mjs +281 -0
- package/scripts/verify-release-assets.mjs +293 -0
- package/scripts/windows-hide-spawn-options-test.mjs +19 -0
- package/src/cli.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
- package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
- package/src/runtime/memory/tool-defs.mjs +1 -2
- package/src/session-runtime/context-status.mjs +25 -16
- package/src/session-runtime/model-route-api.mjs +2 -0
- package/src/session-runtime/runtime-core.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +4 -1
- package/src/session-runtime/tool-catalog.mjs +113 -19
- package/src/session-runtime/tool-defs.mjs +1 -0
- package/src/standalone/agent-tool/helpers.mjs +25 -6
- package/src/standalone/agent-tool.mjs +75 -41
- package/src/tui/App.jsx +4 -0
- package/src/tui/app/input-parsers.mjs +8 -9
- package/src/tui/components/Markdown.jsx +6 -1
- package/src/tui/components/Message.jsx +11 -2
- package/src/tui/components/PromptInput.jsx +19 -21
- package/src/tui/components/Spinner.jsx +4 -4
- package/src/tui/components/StatusLine.jsx +6 -6
- package/src/tui/components/TranscriptItem.jsx +2 -2
- package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
- package/src/tui/dist/index.mjs +130 -45
- package/src/tui/engine/agent-job-feed.mjs +21 -2
- package/src/tui/engine/turn.mjs +12 -1
- package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +20 -0
- package/src/ui/statusline.mjs +5 -5
- package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
package/scripts/tool-smoke.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
shouldRecordObservedForProvider,
|
|
31
31
|
} from '../src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs';
|
|
32
32
|
import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
|
|
33
|
+
import { executeFuzzyFindTool } from '../src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs';
|
|
33
34
|
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, validateBuiltinArgs } from '../src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs';
|
|
34
35
|
import { normaliseReadLineWindowArgs } from '../src/runtime/agent/orchestrator/tools/builtin/read-args.mjs';
|
|
35
36
|
import { BUILTIN_TOOLS } from '../src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs';
|
|
@@ -44,10 +45,16 @@ import { TOOL_DEFS as SEARCH_TOOL_DEFS } from '../src/runtime/search/tool-defs.m
|
|
|
44
45
|
import { TOOL_DEFS as CHANNEL_TOOL_DEFS } from '../src/runtime/channels/tool-defs.mjs';
|
|
45
46
|
import { AGENT_OWNER } from '../src/runtime/agent/orchestrator/agent-owner.mjs';
|
|
46
47
|
import { recursiveWrapperToolNameForPublicAgent } from '../src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs';
|
|
48
|
+
import {
|
|
49
|
+
applyDeferredToolSurface,
|
|
50
|
+
reconcileDeferredMcpToolCatalog,
|
|
51
|
+
} from '../src/session-runtime/tool-catalog.mjs';
|
|
52
|
+
import { prepareDeferredToolCallThrough } from '../src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs';
|
|
47
53
|
import { composeSystemPrompt } from '../src/runtime/agent/orchestrator/context/collect.mjs';
|
|
48
54
|
import { setInternalToolsProvider } from '../src/runtime/agent/orchestrator/internal-tools.mjs';
|
|
49
55
|
import { prepareAgentSession } from '../src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
50
56
|
import { resolveHiddenRoleSchemaAllowedTools } from '../src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
|
|
57
|
+
import { assertCodeGraphDescriptionContract } from './code-graph-description-contract.mjs';
|
|
51
58
|
import { getHiddenAgent, resolveAgentSessionPermission } from '../src/runtime/agent/orchestrator/internal-agents.mjs';
|
|
52
59
|
|
|
53
60
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -492,8 +499,8 @@ const findOut = await executeBuiltinTool('find', {
|
|
|
492
499
|
}, root);
|
|
493
500
|
assertOk('find', findOut, /scripts[\\/]tool-smoke\.mjs/i);
|
|
494
501
|
|
|
495
|
-
// find query[] batch: fan-out must emit one section per query in
|
|
496
|
-
//
|
|
502
|
+
// End-to-end find query[] batch: fan-out must emit one section per query in
|
|
503
|
+
// caller order.
|
|
497
504
|
const findBatchOut = await executeBuiltinTool('find', {
|
|
498
505
|
query: ['tool smoke', 'smoke'],
|
|
499
506
|
path: '.',
|
|
@@ -514,6 +521,48 @@ const findBatchOut = await executeBuiltinTool('find', {
|
|
|
514
521
|
}
|
|
515
522
|
}
|
|
516
523
|
|
|
524
|
+
// Exercise the test-only rg seam directly so this verifies the in-flight
|
|
525
|
+
// single-flight rather than merely observing cached output from the real tree.
|
|
526
|
+
// TTL=0 rules out persistent broad-enumeration cache reuse; the delayed listing
|
|
527
|
+
// keeps both exact-name workers concurrent while the first sweep is in flight.
|
|
528
|
+
{
|
|
529
|
+
const previousFindEnumCacheTtl = process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS;
|
|
530
|
+
const firstQuery = 'tool-smoke-single-flight-first.mjs';
|
|
531
|
+
const secondQuery = 'tool-smoke-single-flight-second.mjs';
|
|
532
|
+
const firstPath = `fixtures/${firstQuery}`;
|
|
533
|
+
const secondPath = `fixtures/${secondQuery}`;
|
|
534
|
+
let broadEnumerationCalls = 0;
|
|
535
|
+
process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS = '0';
|
|
536
|
+
try {
|
|
537
|
+
const singleFlightOut = await executeFuzzyFindTool({
|
|
538
|
+
query: [firstQuery, secondQuery],
|
|
539
|
+
path: '.',
|
|
540
|
+
head_limit: 5,
|
|
541
|
+
}, root, {
|
|
542
|
+
__runRg: async () => {
|
|
543
|
+
broadEnumerationCalls += 1;
|
|
544
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
545
|
+
return `${firstPath}\n${secondPath}\n`;
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
const s = String(singleFlightOut);
|
|
549
|
+
const iA = s.indexOf(`# find ${firstQuery}`);
|
|
550
|
+
const iB = s.indexOf(`# find ${secondQuery}`);
|
|
551
|
+
if (broadEnumerationCalls !== 1) {
|
|
552
|
+
throw new Error(`find query[] must share exactly one broad enumeration, got ${broadEnumerationCalls}`);
|
|
553
|
+
}
|
|
554
|
+
if (iA < 0 || iB < 0 || !(iA < iB)) {
|
|
555
|
+
throw new Error(`find query[] seam must preserve section order:\n${s}`);
|
|
556
|
+
}
|
|
557
|
+
if (!s.slice(iA, iB).includes(firstPath) || !s.slice(iB).includes(secondPath)) {
|
|
558
|
+
throw new Error(`find query[] seam must retain both exact-name bodies:\n${s}`);
|
|
559
|
+
}
|
|
560
|
+
} finally {
|
|
561
|
+
if (previousFindEnumCacheTtl === undefined) delete process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS;
|
|
562
|
+
else process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS = previousFindEnumCacheTtl;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
517
566
|
const readOut = await executeBuiltinTool('read', {
|
|
518
567
|
path: 'scripts/smoke.mjs',
|
|
519
568
|
offset: 0,
|
|
@@ -1922,6 +1971,9 @@ const memoryProps = memoryTool?.inputSchema?.properties || {};
|
|
|
1922
1971
|
if (!/mutation/i.test(memoryTool?.description || '') || !/Exact confirmation phrase/i.test(memoryProps.confirm?.description || '')) {
|
|
1923
1972
|
throw new Error('memory schema must preserve mutation/destructive confirmation guidance');
|
|
1924
1973
|
}
|
|
1974
|
+
if (memoryProps.category || /category/i.test(memoryTool?.description || '')) {
|
|
1975
|
+
throw new Error('memory mutation schema must not expose category');
|
|
1976
|
+
}
|
|
1925
1977
|
const searchTool = SEARCH_TOOL_DEFS.find((tool) => tool.name === 'search');
|
|
1926
1978
|
const searchProps = searchTool?.inputSchema?.properties || {};
|
|
1927
1979
|
if (!/Runs synchronously/i.test(searchTool?.description || '') || searchProps.mode || searchProps.action || searchProps.task_id || !searchProps.query?.anyOf || !/array for fan-out/i.test(searchProps.query?.description || '')) {
|
|
@@ -1940,7 +1992,8 @@ if (!/offset/i.test(webFetchProps.startIndex?.description || '') || !/Maximum ch
|
|
|
1940
1992
|
}
|
|
1941
1993
|
if (!/deferred tools/i.test(TOOL_SEARCH_TOOL.description || '')
|
|
1942
1994
|
|| !TOOL_SEARCH_TOOL.inputSchema?.properties?.names
|
|
1943
|
-
|| !TOOL_SEARCH_TOOL.inputSchema?.properties?.select
|
|
1995
|
+
|| !TOOL_SEARCH_TOOL.inputSchema?.properties?.select
|
|
1996
|
+
|| TOOL_SEARCH_TOOL.inputSchema?.additionalProperties !== false) {
|
|
1944
1997
|
throw new Error('load_tool schema must preserve loader guidance plus names + legacy select fields');
|
|
1945
1998
|
}
|
|
1946
1999
|
const toolSearchSession = {
|
|
@@ -1994,6 +2047,7 @@ if (!Array.isArray(toolSearchSession.deferredDiscoveredTools) || !toolSearchSess
|
|
|
1994
2047
|
throw new Error('tool_search must persist discovered tool state on the session');
|
|
1995
2048
|
}
|
|
1996
2049
|
const nativeToolSearchSession = {
|
|
2050
|
+
provider: 'openai-oauth',
|
|
1997
2051
|
tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
|
|
1998
2052
|
deferredToolCatalog: smokeCatalog.slice(),
|
|
1999
2053
|
deferredSelectedTools: [...fullDefaults],
|
|
@@ -2001,21 +2055,111 @@ const nativeToolSearchSession = {
|
|
|
2001
2055
|
deferredProviderMode: 'native',
|
|
2002
2056
|
deferredNativeTools: true,
|
|
2003
2057
|
};
|
|
2058
|
+
nativeToolSearchSession.deferredCallableTools = nativeToolSearchSession.tools.map((tool) => tool.name);
|
|
2059
|
+
const nativeBaseToolsJson = JSON.stringify(nativeToolSearchSession.tools);
|
|
2060
|
+
const nativeBaseRequest = buildRequestBody(
|
|
2061
|
+
[{ role: 'user', content: 'load shell' }],
|
|
2062
|
+
'gpt-5.4',
|
|
2063
|
+
nativeToolSearchSession.tools,
|
|
2064
|
+
{ sessionId: 'deferred-stability', session: nativeToolSearchSession },
|
|
2065
|
+
);
|
|
2004
2066
|
const nativeSelectResult = JSON.parse(__renderToolSearchForTest({ select: 'shell,recall' }, nativeToolSearchSession, 'full'));
|
|
2005
|
-
if (nativeSelectResult.activeTools.includes('shell') || nativeSelectResult.activeTools.includes('recall')) {
|
|
2006
|
-
throw new Error(`native tool_search must not mutate active tool schemas: ${JSON.stringify(nativeSelectResult)}`);
|
|
2007
|
-
}
|
|
2008
2067
|
for (const name of ['shell', 'task', 'recall']) {
|
|
2009
|
-
if (!nativeSelectResult.
|
|
2010
|
-
throw new Error(`native
|
|
2068
|
+
if (!nativeSelectResult.activeTools.includes(name)) {
|
|
2069
|
+
throw new Error(`native load_tool must register ${name} as callable: ${JSON.stringify(nativeSelectResult)}`);
|
|
2011
2070
|
}
|
|
2012
2071
|
}
|
|
2072
|
+
if (JSON.stringify(nativeToolSearchSession.tools) !== nativeBaseToolsJson
|
|
2073
|
+
|| nativeToolSearchSession.tools.some((tool) => tool?.name === 'shell')) {
|
|
2074
|
+
throw new Error(`native load_tool must keep the base tools array byte-stable: ${JSON.stringify(nativeToolSearchSession.tools)}`);
|
|
2075
|
+
}
|
|
2013
2076
|
if (!nativeSelectResult.nativeToolSearch?.openaiTools?.some((tool) => tool?.name === 'shell' && tool?.defer_loading === true)) {
|
|
2014
2077
|
throw new Error(`native tool_search must return OpenAI loadable deferred tools: ${JSON.stringify(nativeSelectResult.nativeToolSearch)}`);
|
|
2015
2078
|
}
|
|
2016
2079
|
if (!nativeSelectResult.nativeToolSearch?.toolReferences?.includes('shell')) {
|
|
2017
2080
|
throw new Error(`native tool_search must return Anthropic tool references: ${JSON.stringify(nativeSelectResult.nativeToolSearch)}`);
|
|
2018
2081
|
}
|
|
2082
|
+
const nativeToolCountAfterFirstLoad = nativeToolSearchSession.tools.length;
|
|
2083
|
+
const nativeRepeatResult = JSON.parse(__renderToolSearchForTest({ select: 'shell,recall' }, nativeToolSearchSession, 'full'));
|
|
2084
|
+
if (nativeRepeatResult.loaded.length
|
|
2085
|
+
|| !['shell', 'task', 'recall'].every((name) => nativeRepeatResult.alreadyActive.includes(name))
|
|
2086
|
+
|| nativeRepeatResult.nativeToolSearch?.toolReferences?.length
|
|
2087
|
+
|| nativeRepeatResult.nativeToolSearch?.openaiTools?.length
|
|
2088
|
+
|| nativeToolSearchSession.tools.length !== nativeToolCountAfterFirstLoad) {
|
|
2089
|
+
throw new Error(`repeated native load_tool must report already-active without reinjection: ${JSON.stringify(nativeRepeatResult)}`);
|
|
2090
|
+
}
|
|
2091
|
+
const nativeHistory = [
|
|
2092
|
+
{ role: 'user', content: 'load shell' },
|
|
2093
|
+
{
|
|
2094
|
+
role: 'assistant',
|
|
2095
|
+
content: '',
|
|
2096
|
+
toolCalls: [{ id: 'search-1', name: 'load_tool', arguments: { names: ['shell'] }, nativeType: 'tool_search_call' }],
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
role: 'tool',
|
|
2100
|
+
toolCallId: 'search-1',
|
|
2101
|
+
content: nativeSelectResult.nativeToolSearch.summary,
|
|
2102
|
+
nativeToolSearch: nativeSelectResult.nativeToolSearch,
|
|
2103
|
+
},
|
|
2104
|
+
];
|
|
2105
|
+
const nativeFollowupRequest = buildRequestBody(
|
|
2106
|
+
nativeHistory,
|
|
2107
|
+
'gpt-5.4',
|
|
2108
|
+
nativeToolSearchSession.tools,
|
|
2109
|
+
{ sessionId: 'deferred-stability', session: nativeToolSearchSession },
|
|
2110
|
+
);
|
|
2111
|
+
if (JSON.stringify(nativeFollowupRequest.tools) !== JSON.stringify(nativeBaseRequest.tools)
|
|
2112
|
+
|| nativeFollowupRequest.prompt_cache_key !== nativeBaseRequest.prompt_cache_key) {
|
|
2113
|
+
throw new Error('OpenAI native loading must not change tools or prompt_cache_key');
|
|
2114
|
+
}
|
|
2115
|
+
const nativeOutput = nativeFollowupRequest.input.find((item) => item?.type === 'tool_search_output');
|
|
2116
|
+
if (!nativeOutput?.tools?.some((tool) => tool?.name === 'shell')
|
|
2117
|
+
|| nativeFollowupRequest.tools.some((tool) => tool?.name === 'shell')) {
|
|
2118
|
+
throw new Error(`OpenAI loaded schemas must exist only in tool_search_output history: ${JSON.stringify(nativeFollowupRequest)}`);
|
|
2119
|
+
}
|
|
2120
|
+
const directMcpSession = {
|
|
2121
|
+
provider: 'openai-oauth',
|
|
2122
|
+
toolSpec: 'full',
|
|
2123
|
+
tools: [{ name: 'load_tool', inputSchema: { type: 'object', properties: {} } }],
|
|
2124
|
+
deferredToolCatalog: [
|
|
2125
|
+
{ name: 'load_tool', inputSchema: { type: 'object', properties: {} } },
|
|
2126
|
+
{ name: 'mcp__demo__ping', annotations: { readOnlyHint: true }, inputSchema: { type: 'object', properties: {} } },
|
|
2127
|
+
],
|
|
2128
|
+
deferredCallableTools: ['load_tool', 'mcp__demo__ping'],
|
|
2129
|
+
deferredProviderMode: 'native',
|
|
2130
|
+
deferredNativeTools: true,
|
|
2131
|
+
};
|
|
2132
|
+
prepareDeferredToolCallThrough(directMcpSession, 'mcp__demo__ping', {});
|
|
2133
|
+
if (directMcpSession.tools.some((tool) => tool?.name === 'mcp__demo__ping')
|
|
2134
|
+
|| !directMcpSession.deferredCallableTools.includes('mcp__demo__ping')) {
|
|
2135
|
+
throw new Error('subsequent native MCP calls must use the callable registry without session.tools promotion');
|
|
2136
|
+
}
|
|
2137
|
+
const readonlyReportingSession = {
|
|
2138
|
+
tools: [TOOL_SEARCH_TOOL],
|
|
2139
|
+
deferredToolCatalog: smokeCatalog.slice(),
|
|
2140
|
+
deferredSelectedTools: ['load_tool'],
|
|
2141
|
+
};
|
|
2142
|
+
const readonlyReportingResult = JSON.parse(__renderToolSearchForTest(
|
|
2143
|
+
{ names: ['shell', 'definitely_missing_tool'] },
|
|
2144
|
+
readonlyReportingSession,
|
|
2145
|
+
'readonly',
|
|
2146
|
+
{
|
|
2147
|
+
mcpStatus: () => ({
|
|
2148
|
+
servers: [
|
|
2149
|
+
{ name: 'connecting-mcp', status: 'disconnected' },
|
|
2150
|
+
{ name: 'failed-mcp', status: 'failed' },
|
|
2151
|
+
],
|
|
2152
|
+
}),
|
|
2153
|
+
},
|
|
2154
|
+
));
|
|
2155
|
+
if (!readonlyReportingResult.blocked?.some((entry) => entry?.name === 'shell' && entry?.reason === 'readonly mode')
|
|
2156
|
+
|| !readonlyReportingResult.missing.includes('definitely_missing_tool')
|
|
2157
|
+
|| !readonlyReportingResult.pendingMcpServers?.includes('connecting-mcp')
|
|
2158
|
+
|| !readonlyReportingResult.failedMcpServers?.includes('failed-mcp')
|
|
2159
|
+
|| !/retry next turn/i.test(readonlyReportingResult.note || '')
|
|
2160
|
+
|| !/unavailable/i.test(readonlyReportingResult.note || '')) {
|
|
2161
|
+
throw new Error(`load_tool must preserve readonly and MCP status reporting: ${JSON.stringify(readonlyReportingResult)}`);
|
|
2162
|
+
}
|
|
2019
2163
|
const nativePatchSearchSession = {
|
|
2020
2164
|
provider: 'openai-oauth',
|
|
2021
2165
|
tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name) && tool?.name !== 'apply_patch'),
|
|
@@ -2033,24 +2177,17 @@ if (nativePatchTool?.type !== 'custom' || nativePatchTool?.format?.syntax !== 'l
|
|
|
2033
2177
|
if (nativePatchTool.defer_loading === true || nativePatchTool.parameters) {
|
|
2034
2178
|
throw new Error(`native tool_search custom apply_patch must not be downgraded to deferred function schema: ${JSON.stringify(nativePatchTool)}`);
|
|
2035
2179
|
}
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
};
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
if (nativeGrokPatchTool?.type !== 'function' || nativeGrokPatchTool?.format || nativeGrokPatchTool?.defer_loading !== true) {
|
|
2048
|
-
throw new Error(`Grok native tool_search apply_patch must use JSON function schema, not OpenAI custom: ${JSON.stringify(nativeGrokPatchTool)}`);
|
|
2049
|
-
}
|
|
2050
|
-
if (nativeGrokPatchTool.parameters?.properties?.patch?.type !== 'string') {
|
|
2051
|
-
throw new Error(`Grok native tool_search apply_patch must preserve patch JSON schema: ${JSON.stringify(nativeGrokPatchTool)}`);
|
|
2052
|
-
}
|
|
2053
|
-
// Native query-select discovers (without mutating active schemas); aliases expand.
|
|
2180
|
+
const grokCanonicalSession = { provider: 'grok-oauth', tools: [], messages: [] };
|
|
2181
|
+
applyDeferredToolSurface(grokCanonicalSession, 'full', smokeCatalog, { provider: 'grok-oauth' });
|
|
2182
|
+
const grokCanonicalJson = JSON.stringify(grokCanonicalSession.tools);
|
|
2183
|
+
const grokLoadResult = JSON.parse(__renderToolSearchForTest({ names: ['apply_patch'] }, grokCanonicalSession, 'full'));
|
|
2184
|
+
if (grokCanonicalSession.deferredNativeTools
|
|
2185
|
+
|| grokLoadResult.nativeToolSearch
|
|
2186
|
+
|| JSON.stringify(grokCanonicalSession.tools) !== grokCanonicalJson
|
|
2187
|
+
|| !grokLoadResult.alreadyActive.includes('apply_patch')) {
|
|
2188
|
+
throw new Error(`Grok must use a fixed canonical ordinary-function surface: ${JSON.stringify(grokLoadResult)}`);
|
|
2189
|
+
}
|
|
2190
|
+
// Native query-select explicitly loads onto the active surface; aliases expand.
|
|
2054
2191
|
const nativeSelectQuerySession = {
|
|
2055
2192
|
tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
|
|
2056
2193
|
deferredToolCatalog: smokeCatalog.slice(),
|
|
@@ -2061,13 +2198,10 @@ const nativeSelectQuerySession = {
|
|
|
2061
2198
|
};
|
|
2062
2199
|
const nativeSelectQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'select:search' }, nativeSelectQuerySession, 'full'));
|
|
2063
2200
|
for (const name of ['search', 'web_fetch']) {
|
|
2064
|
-
if (!nativeSelectQueryResult.
|
|
2065
|
-
throw new Error(`native tool_search query-select should
|
|
2201
|
+
if (!nativeSelectQueryResult.activeTools.includes(name)) {
|
|
2202
|
+
throw new Error(`native tool_search query-select should load ${name}: ${JSON.stringify(nativeSelectQueryResult)}`);
|
|
2066
2203
|
}
|
|
2067
2204
|
}
|
|
2068
|
-
if (nativeSelectQueryResult.activeTools.includes('search') || nativeSelectQueryResult.activeTools.includes('web_fetch')) {
|
|
2069
|
-
throw new Error(`native tool_search must not mutate active schemas: ${JSON.stringify(nativeSelectQueryResult)}`);
|
|
2070
|
-
}
|
|
2071
2205
|
if (!nativeSelectQueryResult.nativeToolSearch?.toolReferences?.includes('search')) {
|
|
2072
2206
|
throw new Error(`native query-select must return nativeToolSearch payload: ${JSON.stringify(nativeSelectQueryResult.nativeToolSearch)}`);
|
|
2073
2207
|
}
|
|
@@ -2083,6 +2217,9 @@ const nativeLateMcpSearchSession = {
|
|
|
2083
2217
|
deferredNativeTools: true,
|
|
2084
2218
|
};
|
|
2085
2219
|
const nativeLateMcpSelectResult = JSON.parse(__renderToolSearchForTest({ names: ['mcp__late__ping'] }, nativeLateMcpSearchSession, 'full'));
|
|
2220
|
+
if (nativeLateMcpSearchSession.tools.some((tool) => tool?.name === 'mcp__late__ping')) {
|
|
2221
|
+
throw new Error(`native late MCP load must not promote its schema onto session.tools: ${JSON.stringify(nativeLateMcpSearchSession.tools)}`);
|
|
2222
|
+
}
|
|
2086
2223
|
if (!nativeLateMcpSelectResult.nativeToolSearch?.toolReferences?.includes('mcp__late__ping')) {
|
|
2087
2224
|
throw new Error(`native late MCP load must include nativeToolSearch payload: ${JSON.stringify(nativeLateMcpSelectResult)}`);
|
|
2088
2225
|
}
|
|
@@ -2104,6 +2241,22 @@ for (const q of ['run tests', 'web docs', 'memory previous', 'status']) {
|
|
|
2104
2241
|
throw new Error(`native tool_search plain query "${q}" must not auto-load/discover: ${JSON.stringify(r)}`);
|
|
2105
2242
|
}
|
|
2106
2243
|
}
|
|
2244
|
+
const geminiManifestSession = { provider: 'gemini', tools: [], messages: [] };
|
|
2245
|
+
const manifestBase = [
|
|
2246
|
+
{ name: 'load_tool', inputSchema: { type: 'object', properties: {} } },
|
|
2247
|
+
{ name: 'read', annotations: { readOnlyHint: true }, inputSchema: { type: 'object', properties: {} } },
|
|
2248
|
+
];
|
|
2249
|
+
applyDeferredToolSurface(geminiManifestSession, 'full', manifestBase, { provider: 'gemini' });
|
|
2250
|
+
const geminiTurnManifest = JSON.stringify(geminiManifestSession.tools);
|
|
2251
|
+
const geminiLate = { name: 'mcp__gemini__late', inputSchema: { type: 'object', properties: {} } };
|
|
2252
|
+
// Continuations use the same array; only the next user-turn reconciliation may replace it.
|
|
2253
|
+
if (JSON.stringify(geminiManifestSession.tools) !== geminiTurnManifest) {
|
|
2254
|
+
throw new Error('Gemini manifest changed within a user turn');
|
|
2255
|
+
}
|
|
2256
|
+
reconcileDeferredMcpToolCatalog(geminiManifestSession, [geminiLate]);
|
|
2257
|
+
if (!geminiManifestSession.tools.some((tool) => tool.name === 'mcp__gemini__late')) {
|
|
2258
|
+
throw new Error('Gemini must adopt the complete ordered live manifest at the next user turn');
|
|
2259
|
+
}
|
|
2107
2260
|
// Skill-style deferred manifest: `- name: description` lines, `<`/`>` sanitized,
|
|
2108
2261
|
// bare names allowed, header instructs direct calls, empty pool → ''.
|
|
2109
2262
|
const manifestText = buildDeferredToolManifest([
|
|
@@ -2158,8 +2311,38 @@ const grepHeadLimitDescription = grepTool?.inputSchema?.properties?.head_limit?.
|
|
|
2158
2311
|
if (!/pattern\[\] batches variants/i.test(grepPatternDescription) || !/File\/dir scope/i.test(grepPathDescription)) {
|
|
2159
2312
|
throw new Error('grep schema must keep compact pattern/path guidance');
|
|
2160
2313
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2314
|
+
const grepPredicateNegator = /\b(?:no|not|never|cannot|can(?:not|'t)|does(?:\s+not|n't)|do(?:\s+not|n't)|did(?:\s+not|n't)|unable(?:\s+|-)to|fail(?:s|ed|ing)?(?:\s*[-–]\s*|\s+)to|without|forbid(?:s|den)?|prohibit(?:s|ed|ing)?)(?:\s+\w+){0,2}\s+/i;
|
|
2315
|
+
const hasPositiveGrepSequence = (description, pattern, negatedPredicate) => {
|
|
2316
|
+
const match = description.match(pattern);
|
|
2317
|
+
return Boolean(match) && !(negatedPredicate?.test(match[0]));
|
|
2318
|
+
};
|
|
2319
|
+
const grepRoutingVerb = '(?:us(?:e|es|ed|ing)|rout(?:e|es|ed|ing)|target(?:s|ed|ing)?)';
|
|
2320
|
+
const grepRouteRelation = `(?:\\s*→\\s*|\\s+\\b${grepRoutingVerb}\\b(?:\\s+(?:to|via|with))?\\s+)\\bgrep\\b`;
|
|
2321
|
+
const grepGroupedRoutePattern = new RegExp(`\\bliteral\\b\\s*(?:or|and|/)\\s*\\bregex\\b(?:(?![.;]|→|\\b${grepRoutingVerb}\\b)[\\s\\S]){0,80}?${grepRouteRelation}`, 'i');
|
|
2322
|
+
const grepRoutePattern = (subject, alternateSubject) => new RegExp(`\\b${subject}\\b(?:(?![.;]|→|\\b(?:${alternateSubject}|${grepRoutingVerb})\\b)[\\s\\S]){0,80}?${grepRouteRelation}`, 'i');
|
|
2323
|
+
const grepNegatedRoute = new RegExp(`${grepPredicateNegator.source}\\bgrep\\b`, 'i');
|
|
2324
|
+
const hasGrepDescriptionContract = (description) => (
|
|
2325
|
+
(hasPositiveGrepSequence(description, grepGroupedRoutePattern, grepNegatedRoute)
|
|
2326
|
+
|| (hasPositiveGrepSequence(description, grepRoutePattern('literal', 'regex'), grepNegatedRoute)
|
|
2327
|
+
&& hasPositiveGrepSequence(description, grepRoutePattern('regex', 'literal'), grepNegatedRoute)))
|
|
2328
|
+
&& hasPositiveGrepSequence(description, /\bnonzero\b[\s\S]{0,80}?\bcontent_with_context\b[\s\S]{0,80}?\bresolve\w*\b/i, new RegExp(`${grepPredicateNegator.source}\\bresolve\\w*\\b`, 'i'))
|
|
2329
|
+
&& hasPositiveGrepSequence(description, /\bonly\b[\s\S]{0,80}?\bzero(?:\s*\/\s*|\s+or\s+)error\b[\s\S]{0,80}?\bchange\w*\b[\s\S]{0,80}?\btoken(?:s)?\b[\s\S]{0,80}?\bscope\b/i, new RegExp(`${grepPredicateNegator.source}\\bchange\\w*\\b`, 'i'))
|
|
2330
|
+
&& hasPositiveGrepSequence(description, /\bfiles_with_matches(?:\s*\/\s*|\s+and\s+)count\b[\s\S]{0,80}?\bexistence\b[\s\S]{0,80}?\bmodes?\b/i, new RegExp(`${grepPredicateNegator.source}\\bexistence\\s+modes?\\b`, 'i'))
|
|
2331
|
+
);
|
|
2332
|
+
const grepDescriptionProbe = 'literal and regex → grep; nonzero content_with_context resolves; only zero/error results may change tokens and scope; files_with_matches/count are existence modes.';
|
|
2333
|
+
const grepEquivalentPositiveProbe = 'literal and regex use grep; nonzero content_with_context returns not an empty result and resolves; only zero/error results may change tokens and scope; files_with_matches/count are existence modes.';
|
|
2334
|
+
const grepSeparateClausePositiveProbe = 'literal uses grep; regex routes to grep; nonzero content_with_context resolves; only zero/error results may change tokens and scope; files_with_matches/count are existence modes.';
|
|
2335
|
+
const grepPolarityMutations = [
|
|
2336
|
+
grepDescriptionProbe.replace('literal and regex → grep', 'literal → read; regex → grep'),
|
|
2337
|
+
grepDescriptionProbe.replace('literal and regex → grep', 'literal targets read, while regex uses grep'),
|
|
2338
|
+
grepDescriptionProbe.replace('literal and regex → grep', 'literal and regex do not use grep'),
|
|
2339
|
+
grepDescriptionProbe.replace('resolves', 'does not resolve'),
|
|
2340
|
+
grepDescriptionProbe.replace('resolves', 'fails to resolve'),
|
|
2341
|
+
grepDescriptionProbe.replace('may change', 'cannot change'),
|
|
2342
|
+
grepDescriptionProbe.replace('are existence modes', 'are never existence modes'),
|
|
2343
|
+
];
|
|
2344
|
+
if (!hasGrepDescriptionContract(grepTool?.description || '') || !hasGrepDescriptionContract(grepEquivalentPositiveProbe) || !hasGrepDescriptionContract(grepSeparateClausePositiveProbe) || grepPolarityMutations.some(hasGrepDescriptionContract)) {
|
|
2345
|
+
throw new Error('grep description must state its literal/regex locator, resolution qualifiers, and result-mode contract');
|
|
2163
2346
|
}
|
|
2164
2347
|
if (!/Glob filter/i.test(grepGlobDescription)) {
|
|
2165
2348
|
throw new Error('grep glob schema must describe scope narrowing');
|
|
@@ -2179,15 +2362,57 @@ const listTool = BUILTIN_TOOLS.find((tool) => tool.name === 'list');
|
|
|
2179
2362
|
if (!/exact glob patterns/i.test(globTool?.description || '')) {
|
|
2180
2363
|
throw new Error('glob description must route exact-pattern unknown paths before read/grep/list');
|
|
2181
2364
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2365
|
+
const findClauseNegator = '(?:not|never|(?:do|does|did)\\s+not|(?:do|does|did)n\'t|(?:is|are|was|were)\\s+not|(?:is|are|was|were)n\'t|should\\s+not|shouldn\'t|must\\s+not|mustn\'t|can(?:not|\\s+not)|can\'t|could\\s+not|couldn\'t|will\\s+not|won\'t|fail(?:s|ed|ing)?\\s+to)';
|
|
2366
|
+
const findPredicateModifiers = '(?:\\s+(?:necessarily|generally|strictly|really|simply|always|still|currently|typically|ordinarily|be|been|being|considered|deemed|regarded)){0,3}';
|
|
2367
|
+
const hasPositiveFindClause = (description, positive, negated) => positive.test(description) && !negated.test(description);
|
|
2368
|
+
const findUnknownPathsPattern = /\b(?:fuzzy\s+)?(?:lookup|find)\b(?:\s+[\w']+){0,3}\s+only\s+for\s+unknown\s+partial\s+paths\/names\b/i;
|
|
2369
|
+
const findUnknownPathsNegation = new RegExp(`\\b${findClauseNegator}${findPredicateModifiers}\\s+(?:use\\s+)?(?:fuzzy\\s+)?(?:lookup|find)\\b(?:\\s+[\\w']+){0,3}\\s+only\\s+for\\s+unknown\\s+partial\\s+paths\\/names|\\b(?:fuzzy\\s+)?(?:lookup|find)\\b(?:\\s+[\\w']+){0,3}\\s+${findClauseNegator}${findPredicateModifiers}\\s+only\\s+for\\s+unknown\\s+partial\\s+paths\\/names`, 'i');
|
|
2370
|
+
const findRootExclusionPattern = /\b(?:not\s+for|excludes?)\s+(?:the\s+)?project\s+root\s+(?:or|and)\s+already-verified\s+roots\b|\b(?:the\s+)?project\s+root\s+(?:or|and)\s+already-verified\s+roots\s+(?:are\s+)?excluded\b|\balready-verified\s+roots\s+(?:or|and)\s+(?:the\s+)?project\s+root\s+(?:are\s+)?excluded\b/i;
|
|
2371
|
+
const findRootExclusionNegation = new RegExp(`\\b${findClauseNegator}${findPredicateModifiers}\\s+exclude\\s+(?:the\\s+)?(?:project\\s+root|already-verified\\s+roots)\\b|\\b(?:project\\s+root\\s+(?:or|and)\\s+already-verified\\s+roots|already-verified\\s+roots\\s+(?:or|and)\\s+project\\s+root|project\\s+root|already-verified\\s+roots)\\s+(?:(?:is|are|was|were)\\s+)?${findClauseNegator}${findPredicateModifiers}\\s+excluded\\b`, 'i');
|
|
2372
|
+
const findVerifiedPathsPattern = /\b(?:output|returned)\s+paths\b(?:\s+[\w']+){0,4}\s+verified\s+(?:downstream|for\s+downstream\s+use)\b|\bdownstream\s+paths\b(?:\s+[\w']+){0,4}\s+verified\b|\bpaths\b(?:\s+[\w']+){0,4}\s+verified\s+(?:downstream|for\s+downstream\s+use)\b/i;
|
|
2373
|
+
const findVerifiedPathsNegation = new RegExp(`\\b(?:output|returned|downstream)\\s+paths\\b(?:\\s+[\\w']+){0,4}\\s+${findClauseNegator}${findPredicateModifiers}\\s+verified\\b|\\bpaths\\b(?:\\s+[\\w']+){0,4}\\s+${findClauseNegator}${findPredicateModifiers}\\s+verified\\b|\\b(?:output|returned|downstream)\\s+paths\\b(?:\\s+[\\w']+){0,4}\\s+unverified\\b`, 'i');
|
|
2374
|
+
const findFileContentsPattern = /\b(?:not\s+for|excludes?)\s+file\s+contents\b|\bfile\s+contents\b(?:\s+[\w']+){0,3}\s+excluded\b/i;
|
|
2375
|
+
const findFileContentsNegation = new RegExp(`\\b${findClauseNegator}${findPredicateModifiers}\\s+exclude\\s+file\\s+contents\\b|\\bfile\\s+contents\\s+(?:(?:is|are|was|were)\\s+)?${findClauseNegator}${findPredicateModifiers}\\s+excluded\\b`, 'i');
|
|
2376
|
+
const hasFindDescriptionContract = (description) => (
|
|
2377
|
+
hasPositiveFindClause(description, findUnknownPathsPattern, findUnknownPathsNegation)
|
|
2378
|
+
&& hasPositiveFindClause(description, findRootExclusionPattern, findRootExclusionNegation)
|
|
2379
|
+
&& hasPositiveFindClause(description, findVerifiedPathsPattern, findVerifiedPathsNegation)
|
|
2380
|
+
&& hasPositiveFindClause(description, findFileContentsPattern, findFileContentsNegation)
|
|
2381
|
+
);
|
|
2382
|
+
const findDescriptionProbe = 'Fuzzy lookup only for unknown partial paths/names; excludes project root and already-verified roots. Output paths are verified downstream. Not for file contents.';
|
|
2383
|
+
const findEquivalentPositiveProbe = 'File contents are excluded. Paths verified downstream are returned. Already-verified roots and project root are excluded. Lookup is only for unknown partial paths/names.';
|
|
2384
|
+
const findPolarityMutations = [
|
|
2385
|
+
findDescriptionProbe.replace('Fuzzy lookup only', 'Do not use fuzzy lookup only'),
|
|
2386
|
+
findDescriptionProbe.replace('excludes project root', 'does not exclude project root'),
|
|
2387
|
+
findDescriptionProbe.replace('Output paths are verified downstream', 'Downstream paths not verified'),
|
|
2388
|
+
findDescriptionProbe.replace('Not for file contents', 'Does not exclude file contents'),
|
|
2389
|
+
findDescriptionProbe.replace('Fuzzy lookup only', 'Fuzzy lookup is not only'),
|
|
2390
|
+
findDescriptionProbe.replace('excludes project root', 'should not exclude project root'),
|
|
2391
|
+
findDescriptionProbe.replace('excludes project root', 'must not exclude project root'),
|
|
2392
|
+
findDescriptionProbe.replace('excludes project root', 'can not exclude project root'),
|
|
2393
|
+
findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths cannot be verified downstream'),
|
|
2394
|
+
findDescriptionProbe.replace('Not for file contents', 'File contents must not be excluded'),
|
|
2395
|
+
findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths are verified'),
|
|
2396
|
+
findDescriptionProbe.replace('Fuzzy lookup only', 'Fuzzy lookup isn\'t only'),
|
|
2397
|
+
findDescriptionProbe.replace('excludes project root and already-verified roots', 'project root and already-verified roots shouldn\'t be excluded'),
|
|
2398
|
+
findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths can\'t be verified downstream'),
|
|
2399
|
+
findDescriptionProbe.replace('Not for file contents', 'File contents aren\'t excluded'),
|
|
2400
|
+
findDescriptionProbe.replace('Fuzzy lookup only', 'Lookup isn\'t necessarily only'),
|
|
2401
|
+
findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths shouldn\'t be considered verified downstream'),
|
|
2402
|
+
];
|
|
2403
|
+
if (!hasFindDescriptionContract(findTool?.description || '') || !hasFindDescriptionContract(findEquivalentPositiveProbe) || findPolarityMutations.some(hasFindDescriptionContract)) {
|
|
2404
|
+
throw new Error('find description must limit lookup to unknown partial paths/names, exclude roots and file contents, and return verified paths');
|
|
2184
2405
|
}
|
|
2185
2406
|
if (!/List directory entries/i.test(listTool?.description || '') || !/path\[\]/i.test(listTool?.inputSchema?.properties?.path?.description || '')) {
|
|
2186
2407
|
throw new Error('list description must require verified directories and locator-first unknown dirs');
|
|
2187
2408
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2409
|
+
const codeGraphModeDescription = codeGraphProps.mode?.description || '';
|
|
2410
|
+
const codeGraphSymbolsDescription = codeGraphProps.symbols?.description || '';
|
|
2411
|
+
assertCodeGraphDescriptionContract({
|
|
2412
|
+
description: codeGraphDescription,
|
|
2413
|
+
modeDescription: codeGraphModeDescription,
|
|
2414
|
+
symbolsDescription: codeGraphSymbolsDescription,
|
|
2415
|
+
});
|
|
2191
2416
|
|
|
2192
2417
|
const longToolSearchText = compactToolSearchDescription(`${patchDescription}\n${patchDescription}`);
|
|
2193
2418
|
if (longToolSearchText.length > 220 || /\n/.test(longToolSearchText)) {
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
agentArgsWithResultMetadata,
|
|
25
25
|
parseAgentJob,
|
|
26
26
|
} from '../src/tui/engine/agent-envelope.mjs';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
agentActionTitle,
|
|
29
|
+
toolSearchLoadedSummary,
|
|
30
|
+
} from '../src/tui/components/tool-execution/surface-detail.mjs';
|
|
28
31
|
|
|
29
32
|
test('tool action copy keeps Add/Delete patch verbs and human read offsets', () => {
|
|
30
33
|
assert.equal(
|
|
@@ -38,6 +41,19 @@ test('tool action copy keeps Add/Delete patch verbs and human read offsets', ()
|
|
|
38
41
|
assert.match(summarizeToolArgs('read', { path: 'a.mjs', offset: 0, limit: 10 }), /lines 1-10/);
|
|
39
42
|
});
|
|
40
43
|
|
|
44
|
+
test('load_tool presentation distinguishes first load from already-active repeats', () => {
|
|
45
|
+
assert.equal(
|
|
46
|
+
toolSearchLoadedSummary('Loaded deferred tools: shell\nAlready active: task'),
|
|
47
|
+
'Loaded: shell · Already active: task',
|
|
48
|
+
);
|
|
49
|
+
assert.equal(
|
|
50
|
+
toolSearchLoadedSummary(JSON.stringify({
|
|
51
|
+
selected: { tools: { added: [], already: ['shell', 'task'] } },
|
|
52
|
+
})),
|
|
53
|
+
'Already active: shell, task',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
41
57
|
test('Agent aggregation is batch-scoped and keeps category action wording', () => {
|
|
42
58
|
const entry = aggregateToolCategoryEntry('agent', { type: 'spawn' }, 'Agent');
|
|
43
59
|
assert.equal(formatAggregateHeader({ [entry.key]: { ...entry, count: 2 } }, { pending: false }), 'Called 2 agents');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# TUI typing-lag perf measurement launcher.
|
|
2
|
+
#
|
|
3
|
+
# Enables the render/event-loop probes with tuned thresholds, routes probe
|
|
4
|
+
# output to a DEDICATED fresh log, then launches mixdog exactly like `mixdog`.
|
|
5
|
+
# Reproduce the lag (long transcript + streaming, type continuously), then
|
|
6
|
+
# /exit. The log is then read to pin the bottleneck.
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
$repo = Split-Path -Parent $PSScriptRoot
|
|
9
|
+
$log = Join-Path $repo 'tui-perf.log'
|
|
10
|
+
|
|
11
|
+
# Probe gates. Thresholds lowered below the defaults (80ms stall / 120ms gap)
|
|
12
|
+
# because the synthetic measurement put real stalls at ~40-60ms, under the
|
|
13
|
+
# defaults, so they would never be logged.
|
|
14
|
+
$env:MIXDOG_TUI_PERF = '1'
|
|
15
|
+
$env:MIXDOG_TUI_PERF_STALL_MS = '40'
|
|
16
|
+
$env:MIXDOG_TUI_PERF_RENDER_GAP_MS = '50'
|
|
17
|
+
$env:MIXDOG_TUI_LOOP_PROBE = '1'
|
|
18
|
+
$env:MIXDOG_TUI_STDERR_LOG = $log
|
|
19
|
+
|
|
20
|
+
# Start from a clean log so the read is unambiguous.
|
|
21
|
+
if (Test-Path $log) { Remove-Item $log -Force }
|
|
22
|
+
|
|
23
|
+
Write-Host "perf log -> $log" -ForegroundColor Cyan
|
|
24
|
+
Write-Host 'Reproduce: long transcript + streaming, type continuously to trigger lag, then /exit.' -ForegroundColor Cyan
|
|
25
|
+
|
|
26
|
+
node (Join-Path $repo 'src\cli.mjs') @args
|
|
@@ -8,7 +8,7 @@ import { buildTranscriptRowIndexIncremental } from '../src/tui/app/transcript-wi
|
|
|
8
8
|
|
|
9
9
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
10
|
|
|
11
|
-
function makeTurnHarness(ask, stateOverrides = {}) {
|
|
11
|
+
function makeTurnHarness(ask, stateOverrides = {}, bagOverrides = {}) {
|
|
12
12
|
let seq = 0;
|
|
13
13
|
let state = {
|
|
14
14
|
items: [],
|
|
@@ -84,18 +84,22 @@ function makeTurnHarness(ask, stateOverrides = {}) {
|
|
|
84
84
|
flushDeferredExecutionPendingResumeKick: () => {},
|
|
85
85
|
drain: async () => {},
|
|
86
86
|
drainPendingSteering: () => [],
|
|
87
|
+
...bagOverrides,
|
|
87
88
|
};
|
|
88
89
|
return { runTurn: createRunTurn(bag), getState: () => state };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
test('successful mid-turn compact trims history but preserves live turn references', async () => {
|
|
92
93
|
let preservedTail = false;
|
|
94
|
+
let contextSyncs = 0;
|
|
93
95
|
const harness = makeTurnHarness(async (_text, options) => {
|
|
94
96
|
options.onTextDelta('before compact\n');
|
|
95
97
|
await wait(30);
|
|
96
98
|
options.onCompactEvent({ status: 'compacted', trigger: 'reactive' });
|
|
99
|
+
assert.equal(contextSyncs, 1, 'compact event must refresh context before returning');
|
|
97
100
|
preservedTail = harness.getState().streamingTail?.text === 'before compact\n';
|
|
98
101
|
options.onCompactEvent({ status: 'compacted', trigger: 'reactive' });
|
|
102
|
+
assert.equal(contextSyncs, 2, 'each compact event refreshes context immediately');
|
|
99
103
|
options.onTextDelta('after compact\n');
|
|
100
104
|
return { result: { content: 'before compact\nafter compact\n' }, session: { messages: [] } };
|
|
101
105
|
}, {
|
|
@@ -103,6 +107,8 @@ test('successful mid-turn compact trims history but preserves live turn referenc
|
|
|
103
107
|
{ id: 'old', kind: 'assistant', text: 'old history' },
|
|
104
108
|
{ id: 'current-user', kind: 'user', text: 'go' },
|
|
105
109
|
],
|
|
110
|
+
}, {
|
|
111
|
+
syncContextStats: () => { contextSyncs += 1; },
|
|
106
112
|
});
|
|
107
113
|
|
|
108
114
|
assert.equal(await harness.runTurn('go', { submittedIds: ['current-user'] }), 'done');
|