dsh-search-enhance 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +17 -28
  2. package/README.zh.md +17 -30
  3. package/client/client.js +37 -47
  4. package/client/types/client/locales.d.ts +7 -6
  5. package/client/types/web-config/contracts.d.ts +5 -4
  6. package/cordis.patch.yml +4 -0
  7. package/lib/config.d.ts +2 -10
  8. package/lib/config.js +7 -62
  9. package/lib/contracts/sources.d.ts +0 -5
  10. package/lib/diagnostics/status.js +0 -2
  11. package/lib/diagnostics/types.d.ts +1 -3
  12. package/lib/documentation/service.d.ts +1 -1
  13. package/lib/documentation/service.js +16 -8
  14. package/lib/index.js +18 -104
  15. package/lib/orchestration/orchestrator.js +4 -9
  16. package/lib/orchestration/types.d.ts +2 -6
  17. package/lib/orchestration/types.js +2 -0
  18. package/lib/presentation/render.d.ts +3 -0
  19. package/lib/presentation/render.js +61 -35
  20. package/lib/presentation/web-card.js +16 -6
  21. package/lib/prompt/tool-discovery.d.ts +1 -1
  22. package/lib/prompt/tool-discovery.js +10 -12
  23. package/lib/providers/context7.js +11 -1
  24. package/lib/providers/search-api-request.d.ts +1 -1
  25. package/lib/providers/search-api-request.js +2 -2
  26. package/lib/providers/search-api.d.ts +1 -4
  27. package/lib/providers/search-api.js +1 -4
  28. package/lib/research/bridge.d.ts +29 -0
  29. package/lib/research/bridge.js +106 -0
  30. package/lib/research/child.d.ts +13 -0
  31. package/lib/research/child.js +133 -0
  32. package/lib/research/executor.d.ts +28 -0
  33. package/lib/research/executor.js +94 -0
  34. package/lib/research/model.d.ts +19 -0
  35. package/lib/research/model.js +43 -0
  36. package/lib/research/persona.d.ts +2 -0
  37. package/lib/research/persona.js +14 -0
  38. package/lib/research/profile.d.ts +8 -0
  39. package/lib/research/profile.js +48 -0
  40. package/lib/research/protocol.d.ts +29 -0
  41. package/lib/research/protocol.js +3 -0
  42. package/lib/research/report.d.ts +89 -0
  43. package/lib/research/report.js +51 -0
  44. package/lib/research/sdk.d.ts +45 -0
  45. package/lib/research/sdk.js +66 -0
  46. package/lib/research/session-mirror.d.ts +10 -0
  47. package/lib/research/session-mirror.js +135 -0
  48. package/lib/research/session-observer.d.ts +7 -0
  49. package/lib/research/session-observer.js +57 -0
  50. package/lib/search/index.d.ts +1 -1
  51. package/lib/search/index.js +1 -1
  52. package/lib/search/policy.d.ts +12 -0
  53. package/lib/search/policy.js +35 -0
  54. package/lib/source-storage/domain.js +4 -4
  55. package/lib/source-storage/retention.js +0 -2
  56. package/lib/source-storage/service.d.ts +1 -0
  57. package/lib/source-storage/service.js +3 -0
  58. package/lib/source-storage/store.d.ts +2 -0
  59. package/lib/source-storage/store.js +17 -0
  60. package/lib/tool-discovery/capabilities.d.ts +4 -5
  61. package/lib/tool-discovery/capabilities.js +11 -25
  62. package/lib/tool-discovery/fold.d.ts +14 -6
  63. package/lib/tool-discovery/fold.js +49 -34
  64. package/lib/tool-discovery/manager.d.ts +11 -2
  65. package/lib/tool-discovery/manager.js +81 -16
  66. package/lib/tools/docs-search.js +5 -4
  67. package/lib/tools/index.d.ts +1 -1
  68. package/lib/tools/index.js +1 -1
  69. package/lib/tools/schemas.d.ts +6 -14
  70. package/lib/tools/schemas.js +8 -14
  71. package/lib/tools/services.d.ts +15 -0
  72. package/lib/tools/services.js +96 -0
  73. package/lib/tools/web-research.d.ts +9 -0
  74. package/lib/tools/web-research.js +28 -0
  75. package/lib/tools/web-search.d.ts +1 -1
  76. package/lib/tools/web-search.js +8 -9
  77. package/lib/web-config/contracts.d.ts +5 -4
  78. package/lib/web-config/contracts.js +1 -2
  79. package/lib/web-config/host.js +4 -5
  80. package/package.json +54 -17
  81. package/lib/search/profiles.d.ts +0 -45
  82. package/lib/search/profiles.js +0 -213
  83. package/lib/tools/search-tools.d.ts +0 -62
  84. package/lib/tools/search-tools.js +0 -65
@@ -1,5 +1,5 @@
1
1
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
2
- import type { Config, FallbackMode, MinimumCapabilityProfile, SearchApiProtocol, SearchDepth, SearchProfile, ThinkingLevel } from '../config.js';
2
+ import type { Config, FallbackMode, MinimumCapabilityProfile, SearchApiProtocol, ThinkingLevel } from '../config.js';
3
3
  import type { ProviderErrorKind } from '../provider-runtime/index.js';
4
4
  export declare const DIAGNOSTIC_ACTIONS: readonly ["show", "test"];
5
5
  export type DiagnosticAction = (typeof DIAGNOSTIC_ACTIONS)[number];
@@ -38,8 +38,6 @@ export interface DiagnosticMinimumProfile {
38
38
  }
39
39
  /** Safe deployment facts. Values, refs, endpoints, proxies, and paths do not fit this type. */
40
40
  export interface DiagnosticConfigurationStatus {
41
- readonly defaultProfile: SearchProfile;
42
- readonly defaultDepth: SearchDepth;
43
41
  readonly searchApiProtocol: SearchApiProtocol;
44
42
  readonly searchModelConfigured: boolean;
45
43
  readonly thinkingLevel: ThinkingLevel;
@@ -18,7 +18,7 @@ export interface DocumentationProviderStatus {
18
18
  readonly provider: DocumentationResultProvider;
19
19
  readonly state: DocumentationProviderState;
20
20
  }
21
- export declare const DOCUMENTATION_WARNING_CODES: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "no_results"];
21
+ export declare const DOCUMENTATION_WARNING_CODES: readonly ["provider_failed", "provider_not_configured", "cache_stale", "cache_evicted", "provider_result_truncated", "library_not_found", "collection_limit_reached", "visible_sources_truncated", "no_results"];
22
22
  export type DocumentationWarningCode = (typeof DOCUMENTATION_WARNING_CODES)[number];
23
23
  export declare const DOCUMENTATION_CACHE_PATH_STATES: readonly ["hit", "miss", "refresh", "stale", "skipped", "failed"];
24
24
  export type DocumentationCachePathState = (typeof DOCUMENTATION_CACHE_PATH_STATES)[number];
@@ -15,6 +15,9 @@ export const DOCUMENTATION_WARNING_CODES = [
15
15
  'cache_stale',
16
16
  'cache_evicted',
17
17
  'provider_result_truncated',
18
+ 'library_not_found',
19
+ 'collection_limit_reached',
20
+ 'visible_sources_truncated',
18
21
  'no_results',
19
22
  ];
20
23
  export const DOCUMENTATION_CACHE_PATH_STATES = [
@@ -592,12 +595,18 @@ export class DocumentationSearchService extends Service {
592
595
  const qualitySources = applySourceQuality(validated.query, compatibleSources);
593
596
  const sources = Object.freeze(qualitySources.slice(0, input.maxResults));
594
597
  const snippets = Object.freeze(contextOutcome.snippets.slice(0, input.maxResults));
595
- const truncated = contextOutcome.truncated
596
- || exaOutcome.truncated
597
- || sources.length < qualitySources.length
598
+ const providerTruncated = contextOutcome.truncated || exaOutcome.truncated;
599
+ const visibleSourcesTruncated = sources.length < qualitySources.length;
600
+ const truncated = providerTruncated
601
+ || visibleSourcesTruncated
598
602
  || snippets.length < contextOutcome.snippets.length;
599
- if (truncated)
603
+ if (providerTruncated)
600
604
  warnings.push(fixedWarning('provider_result_truncated'));
605
+ if (visibleSourcesTruncated)
606
+ warnings.push(fixedWarning('visible_sources_truncated'));
607
+ if (exaOutcome.state === 'complete' && exaOutcome.sources.length >= input.maxResults) {
608
+ warnings.push(fixedWarning('collection_limit_reached', { provider: 'exa', count: input.maxResults }));
609
+ }
601
610
  const completedValidPath = contextOutcome.completedValidPath || exaOutcome.completedValidPath;
602
611
  if (sources.length === 0 && snippets.length === 0 && !completedValidPath) {
603
612
  const cause = contextOutcome.error ?? exaOutcome.error ?? new ProviderError({
@@ -613,11 +622,9 @@ export class DocumentationSearchService extends Service {
613
622
  }
614
623
  const cache = contextOutcome.cache ?? cacheReport(cachePath('skipped', 0, 'provider_not_selected'), cachePath('skipped', 0, 'provider_not_selected'));
615
624
  const persistence = Object.freeze({
616
- collectionTruncated: truncated,
617
- depth: 'compact',
618
- profile: 'coding_docs',
625
+ collectionTruncated: exaOutcome.truncated,
619
626
  query: validated.query,
620
- sources,
627
+ sources: Object.freeze(qualitySources),
621
628
  });
622
629
  return Object.freeze({
623
630
  attempts,
@@ -712,6 +719,7 @@ export class DocumentationSearchService extends Service {
712
719
  }
713
720
  if (selectedLibrary?.id === undefined || !isContext7LibraryId(selectedLibrary.id)) {
714
721
  docsReport = cachePath('skipped', 0, 'library_not_found');
722
+ warnings.push(fixedWarning('library_not_found', { provider: 'context7', path: 'resolve' }));
715
723
  attempts.push(skippedAttempt('context7-docs'));
716
724
  return frozenPathOutcome({
717
725
  attempts,
package/lib/index.js CHANGED
@@ -1,116 +1,30 @@
1
1
  import { Config as SearchEnhanceConfig } from './config.js';
2
- import { Context7CachedOperations, DocumentationSearchService, PersistentContext7Cache, } from './documentation/index.js';
3
- import { SearchOrchestrator } from './orchestration/index.js';
2
+ import { ResearchExecutor } from './research/executor.js';
4
3
  import { registerToolDiscoveryGuidance } from './prompt/tool-discovery.js';
5
- import { Context7RemoteClient } from './providers/context7.js';
6
- import { ExaProvider } from './providers/exa.js';
7
- import { FirecrawlSearchProvider } from './providers/firecrawl.js';
8
- import { FirecrawlScrapeProvider } from './providers/firecrawl-scrape.js';
9
- import { DirectFetchProvider } from './providers/direct-fetch.js';
10
- import { ParallelSearchProvider } from './providers/parallel.js';
11
- import { ParallelExtractProvider } from './providers/parallel-extract.js';
12
- import { SearchApiProvider } from './providers/search-api.js';
13
- import { SmartDirectProvider } from './providers/smart-direct.js';
14
- import { TavilySearchProvider } from './providers/tavily.js';
15
- import { TavilyExtractProvider } from './providers/tavily-extract.js';
16
- import { TavilyMapProvider } from './providers/tavily-map.js';
17
- import { SOURCE_RECORD_DOMAIN_SPEC, SOURCE_RECORD_TABLE_NAME, SearchEnhanceSourceService, SourceRecordStore, } from './source-storage/index.js';
18
4
  import { installAgentToolDisclosure } from './tool-discovery/index.js';
19
- import { ForegroundOperationScope, createDocsSearchTool, createSearchSourcesTool, createSearchToolsTool, createWebExtractTool, createWebMapTool, createWebSearchTool, } from './tools/index.js';
20
- import { WebExtractOrchestrator } from './web-extract/orchestrator.js';
5
+ import { createWebResearchTool } from './tools/index.js';
6
+ import { createSearchServices } from './tools/services.js';
21
7
  import { installWebConfigBridge } from './web-config/host.js';
22
8
  export const name = 'search-enhance';
23
- export const inject = ['agents', 'configEditor', 'credentials', 'storageDomain', 'systemPrompt', 'tools'];
9
+ // Display services are discovered per call through reflect.get, never required here.
10
+ export const inject = ['agents', 'configEditor', 'credentials', 'llm', 'storageDomain', 'subagents', 'systemPrompt', 'tools'];
24
11
  export const Config = SearchEnhanceConfig;
25
12
  export async function apply(ctx, config) {
26
- const effective = config;
27
- const domain = await ctx.storageDomain.open(SOURCE_RECORD_DOMAIN_SPEC);
28
- const store = new SourceRecordStore(domain.table(SOURCE_RECORD_TABLE_NAME), {
29
- maxBytes: effective.retention.sourceEventMaxBytes,
30
- maxRecords: effective.retention.sourceStoreMaxRecords,
31
- maxSources: effective.retention.sourceEventMaxSources,
32
- maxPageSize: effective.retention.searchSourcesMaxPageSize,
33
- maxPageBytes: effective.retention.searchSourcesPageMaxBytes,
34
- maxSnippetCharacters: effective.retention.searchSourcesSnippetMaxCharacters,
35
- });
36
- const operations = new ForegroundOperationScope();
37
- // ConfigEditor reconciles edits by rebuilding this plugin in-process; a saved
38
- // configuration is active as soon as the edit request completes.
39
- const getConfig = () => effective;
40
- const providerDependencies = { credentials: ctx.credentials };
41
- const context7Cache = new PersistentContext7Cache(ctx.storageDomain, {
42
- maxEntries: effective.cache.maxEntries,
43
- maxEntryBytes: effective.cache.context7EntryMaxBytes,
44
- });
45
- const cachedContext7 = new Context7CachedOperations(context7Cache);
46
- const context7Remote = new Context7RemoteClient(providerDependencies);
47
- const exa = new ExaProvider(providerDependencies);
48
- let documentation;
49
- // Keep dependent shutdown in one ordered disposer: stop both public-tool and
50
- // service calls, close the lazy docs cache, then drain source writes/domain.
51
- ctx.effect(() => async () => {
52
- await Promise.all([
53
- operations.stop(),
54
- documentation?.stop() ?? Promise.resolve(),
55
- ]);
56
- await context7Cache.close();
57
- store.stop();
58
- await store.drain();
59
- await domain.close();
60
- });
61
- new SearchEnhanceSourceService(ctx, store);
62
- documentation = new DocumentationSearchService(ctx, {
63
- context7: context7Remote,
64
- context7Cache: cachedContext7,
65
- exa,
66
- getConfig,
67
- });
68
- const searchApi = new SearchApiProvider({
69
- credentials: ctx.credentials,
70
- getConfig,
71
- });
72
- const firecrawl = new FirecrawlSearchProvider(providerDependencies);
73
- const tavily = new TavilySearchProvider(providerDependencies);
74
- const parallel = new ParallelSearchProvider(providerDependencies);
75
- const orchestrator = new SearchOrchestrator({
76
- exa,
77
- firecrawl,
78
- getConfig,
79
- mainSearch: searchApi,
80
- parallel,
81
- tavily,
82
- });
83
- const webExtract = new WebExtractOrchestrator({
84
- tavilyExtract: new TavilyExtractProvider(providerDependencies),
85
- firecrawlScrape: new FirecrawlScrapeProvider(providerDependencies),
86
- parallelExtract: new ParallelExtractProvider(providerDependencies),
87
- smartDirect: new SmartDirectProvider(),
88
- direct: new DirectFetchProvider(),
89
- getConfig,
90
- });
91
- const disclosure = installAgentToolDisclosure(ctx, {
92
- mode: effective.toolDiscovery.mode,
93
- webSearchDefinition: createWebSearchTool({
94
- getConfig, operations, orchestrator, sources: ctx.searchEnhanceSources,
95
- }),
96
- deferredTools: {
97
- sources: createSearchSourcesTool({
98
- getConfig, operations, sources: ctx.searchEnhanceSources,
99
- }),
100
- site_map: createWebMapTool({
101
- getConfig, operations, provider: new TavilyMapProvider(providerDependencies),
102
- }),
13
+ const { tools, operations } = await createSearchServices(ctx, config);
14
+ const research = new ResearchExecutor(ctx, () => config);
15
+ ctx.effect(() => async () => { await operations.stop(); research.dispose(); });
16
+ installAgentToolDisclosure(ctx, {
17
+ mode: config.toolDiscovery.mode,
18
+ residentTools: {
19
+ docs_search: tools.docs_search,
20
+ web_extract: tools.web_extract,
21
+ web_research: createWebResearchTool({ research, operations }),
103
22
  },
23
+ canReadSourceRef: (agent, ref) => ctx.searchEnhanceSources.lookup(agent.session, ref).state === 'found',
24
+ sourceRefForCall: (agent, call) => ctx.searchEnhanceSources.sourceRefForCall(agent.session, call),
25
+ webSearchDefinition: tools.web_search,
26
+ deferredTools: { sources: tools.search_sources, site_map: tools.web_map },
104
27
  });
105
- const residentToolDefinitions = [
106
- createDocsSearchTool({
107
- documentation, getConfig, operations, sources: ctx.searchEnhanceSources,
108
- }),
109
- createWebExtractTool({ getConfig, operations, orchestrator: webExtract }),
110
- createSearchToolsTool(disclosure),
111
- ];
112
- for (const definition of residentToolDefinitions)
113
- ctx.tools.register(definition);
114
28
  registerToolDiscoveryGuidance(ctx);
115
29
  installWebConfigBridge(ctx);
116
30
  }
@@ -320,10 +320,7 @@ export class SearchOrchestrator {
320
320
  }
321
321
  let strategy;
322
322
  try {
323
- strategy = resolveSearchStrategy(config, {
324
- ...(input.depth === undefined ? {} : { depth: input.depth }),
325
- ...(input.profile === undefined ? {} : { profile: input.profile }),
326
- });
323
+ strategy = resolveSearchStrategy(config);
327
324
  }
328
325
  catch (error) {
329
326
  throw new ProviderError({
@@ -562,8 +559,10 @@ export class SearchOrchestrator {
562
559
  const collectionTruncated = providerTruncated || mainSourcesTruncated;
563
560
  const visibleSourcesTruncated = visibleSources.length < prioritizedSources.length;
564
561
  const sourcesTruncated = collectionTruncated || visibleSourcesTruncated;
565
- if (sourcesTruncated)
562
+ if (collectionTruncated)
566
563
  warnings.push(warning('sources_truncated'));
564
+ if (visibleSourcesTruncated)
565
+ warnings.push(warning('visible_sources_truncated'));
567
566
  if (mainSucceeded && rawAnswer.trim().length === 0 && prioritizedSources.length === 0) {
568
567
  warnings.push(warning('no_results'));
569
568
  }
@@ -583,10 +582,8 @@ export class SearchOrchestrator {
583
582
  ...slots.map(slot => sourceAttempt(slot)),
584
583
  ]);
585
584
  const routing = Object.freeze({
586
- depth: strategy.depth,
587
585
  supplementalProviders: slots.filter(slot => config.supplementalSearch[slot.key]).map(slot => slot.key),
588
586
  sourcesPerProvider: config.supplementalSearch.maxSourcesPerProvider,
589
- profile: strategy.profile,
590
587
  });
591
588
  const answer = limitedAnswer.text.length === 0 ? undefined : limitedAnswer.text;
592
589
  const candidate = {
@@ -607,8 +604,6 @@ export class SearchOrchestrator {
607
604
  diagnostics: { attempts, routing },
608
605
  persistence: {
609
606
  collectionTruncated,
610
- depth: strategy.depth,
611
- profile: strategy.profile,
612
607
  query,
613
608
  sources: prioritizedSources,
614
609
  },
@@ -1,9 +1,9 @@
1
- import type { Config, SearchDepth, SearchProfile, SupplementalSearchProvider } from '../config.js';
1
+ import type { Config, SupplementalSearchProvider } from '../config.js';
2
2
  import type { CanonicalSource, SourceRecordCandidate } from '../contracts/index.js';
3
3
  import type { ProviderAttemptRecord, ProviderCapability, ProviderErrorKind } from '../provider-runtime/index.js';
4
4
  import type { SearchApiResolvedSearchInput, SearchApiSearchResult } from '../providers/search-api.js';
5
5
  import type { BoundedSourceProvider } from '../providers/types.js';
6
- export declare const SEARCH_WARNING_CODES: readonly ["main_search_failed", "native_search_unconfirmed", "provider_failed", "provider_not_configured", "provider_result_truncated", "cache_stale", "cache_evicted", "answer_truncated", "sources_truncated", "canonical_output_truncated", "no_results"];
6
+ export declare const SEARCH_WARNING_CODES: readonly ["main_search_failed", "native_search_unconfirmed", "provider_failed", "provider_not_configured", "provider_result_truncated", "cache_stale", "cache_evicted", "answer_truncated", "sources_truncated", "visible_sources_truncated", "source_retention_truncated", "canonical_output_truncated", "no_results"];
7
7
  export type SearchWarningCode = (typeof SEARCH_WARNING_CODES)[number];
8
8
  /** Stable, secret-free warning; it deliberately has no arbitrary message field. */
9
9
  export interface SearchWarning {
@@ -14,8 +14,6 @@ export interface SearchWarning {
14
14
  }
15
15
  /** One operation's fixed policy decision, excluding credentials and endpoints. */
16
16
  export interface SearchRoutingDecision {
17
- readonly profile: SearchProfile;
18
- readonly depth: SearchDepth;
19
17
  readonly supplementalProviders: readonly SupplementalSearchProvider[];
20
18
  readonly sourcesPerProvider: number;
21
19
  }
@@ -47,8 +45,6 @@ export interface SearchOrchestrationResult {
47
45
  }
48
46
  export interface SearchOrchestrationInput {
49
47
  readonly query: string;
50
- readonly profile?: unknown;
51
- readonly depth?: unknown;
52
48
  /** Optional caller-owned operation snapshot; otherwise `getConfig()` is read once. */
53
49
  readonly config?: Config;
54
50
  readonly signal: AbortSignal;
@@ -8,6 +8,8 @@ export const SEARCH_WARNING_CODES = [
8
8
  'cache_evicted',
9
9
  'answer_truncated',
10
10
  'sources_truncated',
11
+ 'visible_sources_truncated',
12
+ 'source_retention_truncated',
11
13
  'canonical_output_truncated',
12
14
  'no_results',
13
15
  ];
@@ -11,8 +11,11 @@ export declare function renderWebSearchLimitations(value: WebSearchOutput): stri
11
11
  * Settings, caches, the clock, or network state.
12
12
  */
13
13
  export declare function renderWebSearchText(value: WebSearchOutput): string;
14
+ export declare function isWebSearchModelTextTruncated(value: WebSearchOutput): boolean;
15
+ export declare function renderDocsSearchLimitations(value: DocsSearchOutput): string | undefined;
14
16
  /** Pure Native projection for docs_search; it never reads Settings, cache, storage, or the network. */
15
17
  export declare function renderDocsSearchText(value: DocsSearchOutput): string;
18
+ export declare function isDocsSearchModelTextTruncated(value: DocsSearchOutput): boolean;
16
19
  /** Pure model-text projection for one private-storage source page. */
17
20
  export declare function renderSearchSourcesText(value: SearchSourcesOutput): string;
18
21
  /**
@@ -9,19 +9,24 @@ export function sourceDisplayLabel(source) {
9
9
  return new URL(source.url).hostname;
10
10
  }
11
11
  function renderWithBoundedNotice(text, notice, maximumBytes) {
12
- if (notice === undefined)
13
- return truncateUtf8(text, maximumBytes).text;
14
12
  const separator = '\n\n';
15
- const prefixBudget = maximumBytes - utf8ByteLength(notice) - utf8ByteLength(separator);
13
+ const complete = notice === undefined ? text : `${text}${separator}${notice}`;
14
+ if (utf8ByteLength(complete) <= maximumBytes)
15
+ return complete;
16
+ const marker = '[Model text truncated by model_text_max_bytes.]';
17
+ const suffix = notice === undefined
18
+ ? marker
19
+ : `${marker}\nCounts below describe the canonical result, not fully displayed text.${separator}${notice}`;
20
+ const prefixBudget = maximumBytes - utf8ByteLength(suffix) - utf8ByteLength(separator);
16
21
  if (prefixBudget <= 0)
17
- return truncateUtf8(notice, maximumBytes).text;
22
+ return truncateUtf8(suffix, maximumBytes).text;
18
23
  const prefix = truncateUtf8(text, prefixBudget).text;
19
- return prefix.length === 0 ? notice : `${prefix}${separator}${notice}`;
24
+ return prefix.length === 0 ? suffix : `${prefix}${separator}${suffix}`;
20
25
  }
21
26
  function sourceLoadingNotice(sourceRef) {
22
27
  if (sourceRef === undefined)
23
28
  return undefined;
24
- return `Source reference: ${sourceRef}\n\nFor more sources, call search_tools({ capabilities: ["sources"] }), then call search_sources directly on the next step.`;
29
+ return `Source reference: ${sourceRef}\n\nFor more sources, call search_sources directly on the next step; it becomes available automatically.`;
25
30
  }
26
31
  function warningText(warning) {
27
32
  const subject = [warning.provider, warning.capability].filter(Boolean).join('/');
@@ -45,7 +50,11 @@ function warningText(warning) {
45
50
  case 'answer_truncated':
46
51
  return 'The generated answer was truncated.';
47
52
  case 'sources_truncated':
48
- return 'The source collection or retained source record was truncated.';
53
+ return 'Source collection reached a Provider limit; uncollected sources cannot be recovered by pagination. Rerun with a larger collection limit where supported.';
54
+ case 'visible_sources_truncated':
55
+ return 'Only an inline source prefix is shown; use source_ref to page retained sources.';
56
+ case 'source_retention_truncated':
57
+ return 'Source storage dropped some collected sources at its count/byte limit; pagination cannot recover those entries.';
49
58
  case 'canonical_output_truncated':
50
59
  return 'The canonical search value was truncated to its configured limit.';
51
60
  case 'no_results':
@@ -87,9 +96,8 @@ function sourceSummary(value) {
87
96
  /** Shared model/card notice; Provider failures precede generic truncation information. */
88
97
  export function renderWebSearchLimitations(value) {
89
98
  const lines = value.warnings.map(warningText);
90
- if (value.truncated) {
91
- lines.push('The answer, visible sources, Provider collection, or retained source record was bounded.');
92
- }
99
+ if (value.truncated && lines.length === 0)
100
+ lines.push('The canonical search result was bounded.');
93
101
  if (lines.length === 0)
94
102
  return undefined;
95
103
  return `Limitations\n${lines.map(line => `- ${line}`).join('\n')}`;
@@ -104,15 +112,20 @@ export function renderWebSearchText(value) {
104
112
  renderWebSearchLimitations(value),
105
113
  answerSection(value),
106
114
  sourceSection(value),
107
- sourceSummary(value),
108
- DISCOVERY_NOTICE,
109
115
  ].filter((section) => section !== undefined);
110
116
  const complete = sections.join('\n\n');
111
117
  const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes)
112
118
  && value.model_text_max_bytes > 0
113
119
  ? value.model_text_max_bytes
114
120
  : 0;
115
- return renderWithBoundedNotice(complete, sourceLoadingNotice(value.source_ref), maximumBytes);
121
+ return renderWithBoundedNotice(complete, [sourceSummary(value), DISCOVERY_NOTICE, sourceLoadingNotice(value.source_ref)]
122
+ .filter((section) => section !== undefined).join('\n\n'), maximumBytes);
123
+ }
124
+ export function isWebSearchModelTextTruncated(value) {
125
+ const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes) && value.model_text_max_bytes > 0
126
+ ? value.model_text_max_bytes : 0;
127
+ return utf8ByteLength(renderWebSearchText({ ...value, model_text_max_bytes: Number.MAX_SAFE_INTEGER }))
128
+ > maximumBytes;
116
129
  }
117
130
  function docsWarningText(warning) {
118
131
  const subject = [warning.provider, warning.path].filter(Boolean).join('/');
@@ -131,8 +144,14 @@ function docsWarningText(warning) {
131
144
  return `A documentation Provider bounded its discovery result${detail}.`;
132
145
  case 'no_results':
133
146
  return 'Documentation search completed without matching snippets or sources.';
134
- case 'sources_truncated':
135
- return 'The durable source record retained only a bounded source prefix.';
147
+ case 'library_not_found':
148
+ return 'No Context7 library matched library_name. No unrelated library was selected; check the name or supply an exact library_id.';
149
+ case 'collection_limit_reached':
150
+ return `The Provider returned its requested limit (${warning.count ?? 'max_results'}). Counts cover this request, not all available matches. Increase max_results and rerun to collect more; pagination only reads retained records.`;
151
+ case 'visible_sources_truncated':
152
+ return 'Only an inline source prefix is shown; use source_ref to page retained sources.';
153
+ case 'source_retention_truncated':
154
+ return 'Source storage dropped some collected sources at its count/byte limit; pagination cannot recover those entries.';
136
155
  case 'canonical_output_truncated':
137
156
  return 'The documentation result was shortened to its configured canonical JSON limit.';
138
157
  }
@@ -191,6 +210,12 @@ function docsSourceSection(value) {
191
210
  }
192
211
  return lines.join('\n');
193
212
  }
213
+ export function renderDocsSearchLimitations(value) {
214
+ const lines = value.warnings.map(docsWarningText);
215
+ if (value.truncated && lines.length === 0)
216
+ lines.push('The canonical documentation result was bounded.');
217
+ return lines.length === 0 ? undefined : `Limitations\n${lines.map(line => `- ${line}`).join('\n')}`;
218
+ }
194
219
  /** Pure Native projection for docs_search; it never reads Settings, cache, storage, or the network. */
195
220
  export function renderDocsSearchText(value) {
196
221
  const status = value.state === 'partial' ? 'partial' : 'complete';
@@ -201,35 +226,34 @@ export function renderDocsSearchText(value) {
201
226
  : 'Documentation discovery completed; bounded discovery results follow.';
202
227
  const sections = [
203
228
  `Documentation search (${status})\n${explanation}`,
229
+ renderDocsSearchLimitations(value),
204
230
  docsLibrarySection(value),
205
231
  docsSnippetSection(value),
206
232
  docsSourceSection(value),
207
- [
208
- `Sources shown: ${value.returned_sources}/${value.total_sources}`,
209
- `Snippets shown: ${value.returned_snippets}/${value.total_snippets}`,
210
- ].join('\n'),
211
233
  [
212
234
  docsCachePathText('Context7 resolve cache', value.cache.resolve),
213
235
  docsCachePathText('Context7 docs cache', value.cache.docs),
214
236
  docsProviderSection(value),
215
237
  ].join('\n'),
216
- value.truncated || value.warnings.length > 0
217
- ? [
218
- 'Limitations',
219
- ...(value.truncated
220
- ? ['- Documentation snippets, visible sources, Provider collection, or retained sources were bounded.']
221
- : []),
222
- ...value.warnings.map(warning => `- ${docsWarningText(warning)}`),
223
- ].join('\n')
224
- : undefined,
225
- 'Evidence level: discovery. Documentation snippets and source summaries are discovery metadata, not verified fetched page-body evidence; inspect an exact source page before relying on a claim.',
226
238
  ].filter((section) => section !== undefined);
227
- const complete = sections.join('\n\n');
239
+ const notice = [
240
+ `Sources shown: ${value.returned_sources}/${value.total_sources}`,
241
+ `Snippets shown: ${value.returned_snippets}/${value.total_snippets}`,
242
+ 'Counts cover collected results for this request, not all available matches.',
243
+ 'Evidence level: discovery. Documentation snippets and source summaries are not verified fetched page-body evidence.',
244
+ sourceLoadingNotice(value.source_ref),
245
+ ].filter((section) => section !== undefined).join('\n\n');
228
246
  const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes)
229
247
  && value.model_text_max_bytes > 0
230
248
  ? value.model_text_max_bytes
231
249
  : 0;
232
- return renderWithBoundedNotice(complete, sourceLoadingNotice(value.source_ref), maximumBytes);
250
+ return renderWithBoundedNotice(sections.join('\n\n'), notice, maximumBytes);
251
+ }
252
+ export function isDocsSearchModelTextTruncated(value) {
253
+ const maximumBytes = Number.isSafeInteger(value.model_text_max_bytes) && value.model_text_max_bytes > 0
254
+ ? value.model_text_max_bytes : 0;
255
+ return utf8ByteLength(renderDocsSearchText({ ...value, model_text_max_bytes: Number.MAX_SAFE_INTEGER }))
256
+ > maximumBytes;
233
257
  }
234
258
  function renderPageSources(page) {
235
259
  if (page.sources.length === 0)
@@ -261,13 +285,15 @@ export function renderSearchSourcesText(value) {
261
285
  const sections = [
262
286
  renderPageSources(value),
263
287
  [
264
- `Page returned: ${value.returned}; retained total: ${value.total}; original total: ${value.total_before_retention}.`,
288
+ `Page returned: ${value.returned}; retained total: ${value.total}; collected total before storage: ${value.total_before_retention}.`,
265
289
  value.has_more && value.next_offset !== undefined
266
290
  ? `More sources are available at offset ${value.next_offset}.`
267
291
  : 'No later retained source page is available.',
268
- value.truncated
269
- ? 'The retained source record is truncated.'
270
- : undefined,
292
+ value.total < value.total_before_retention
293
+ ? 'Source storage dropped collected entries; pagination cannot recover them.'
294
+ : value.truncated
295
+ ? 'Upstream source collection was bounded; pagination cannot recover uncollected entries.'
296
+ : undefined,
271
297
  value.page_byte_limited
272
298
  ? 'This page was shortened by its canonical JSON byte limit.'
273
299
  : undefined,
@@ -1,5 +1,5 @@
1
1
  import { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, } from '../documentation/index.js';
2
- import { isWebExtractModelTextTruncated, isWebMapModelTextTruncated, renderWebSearchLimitations, } from './render.js';
2
+ import { isDocsSearchModelTextTruncated, isWebSearchModelTextTruncated, renderDocsSearchLimitations, isWebExtractModelTextTruncated, isWebMapModelTextTruncated, renderWebSearchLimitations, } from './render.js';
3
3
  import { WEB_EXTRACT_EVIDENCE_LEVELS, WEB_EXTRACT_ROUTES, } from '../web-extract/types.js';
4
4
  function isRecord(value) {
5
5
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -49,7 +49,7 @@ export function webSearchPresentationMeta(_args, value) {
49
49
  source_produced: value.source_ref !== undefined,
50
50
  ...(answer === undefined ? {} : { answer }),
51
51
  sources: value.sources.map(source => ({ ...source })),
52
- truncated: value.truncated,
52
+ truncated: value.truncated || isWebSearchModelTextTruncated(value),
53
53
  };
54
54
  }
55
55
  /** Generic pending web-search intent whose title is exactly the query. */
@@ -99,17 +99,19 @@ export function presentWebSearchResult(args, result) {
99
99
  * durable disclosure recovery; cache paths, credentials, and source_ref stay out.
100
100
  */
101
101
  export function docsSearchPresentationMeta(_args, value) {
102
+ const limitations = renderDocsSearchLimitations(value);
102
103
  return {
103
104
  version: 1,
104
105
  type: 'docs_search',
105
106
  source_produced: value.source_ref !== undefined,
107
+ ...(limitations === undefined ? {} : { limitations }),
106
108
  sources: value.sources.map(source => ({ ...source })),
107
109
  snippets: value.snippets.map(snippet => ({ ...snippet })),
108
110
  cache: {
109
111
  resolve: { ...value.cache.resolve },
110
112
  docs: { ...value.cache.docs },
111
113
  },
112
- truncated: value.truncated,
114
+ truncated: value.truncated || isDocsSearchModelTextTruncated(value),
113
115
  };
114
116
  }
115
117
  /** Pending documentation discovery intent; snippets are not presented as fetched pages. */
@@ -155,6 +157,7 @@ function parseDocsSearchMeta(value) {
155
157
  || value.version !== 1
156
158
  || value.type !== 'docs_search'
157
159
  || typeof value.truncated !== 'boolean'
160
+ || !isOptionalString(value.limitations)
158
161
  || !Array.isArray(value.snippets)
159
162
  || !isRecord(value.cache))
160
163
  return undefined;
@@ -173,6 +176,7 @@ function parseDocsSearchMeta(value) {
173
176
  return {
174
177
  version: 1,
175
178
  type: 'docs_search',
179
+ ...(value.limitations === undefined ? {} : { limitations: value.limitations }),
176
180
  sources,
177
181
  snippets,
178
182
  cache: { resolve, docs },
@@ -193,8 +197,11 @@ function docsCacheLabel(meta) {
193
197
  }
194
198
  function docsCardAnswer(meta) {
195
199
  if (meta.snippets.length === 0)
196
- return undefined;
197
- const lines = ['Documentation snippets (discovery metadata; not fetched page bodies):'];
200
+ return meta.limitations;
201
+ const lines = [
202
+ ...(meta.limitations === undefined ? [] : [meta.limitations, '']),
203
+ 'Documentation snippets (discovery metadata; not fetched page bodies):',
204
+ ];
198
205
  for (let index = 0; index < meta.snippets.length; index += 1) {
199
206
  const snippet = meta.snippets[index];
200
207
  if (snippet === undefined)
@@ -216,7 +223,10 @@ export function presentDocsSearchResult(args, result) {
216
223
  const label = docsCacheLabel(meta);
217
224
  const title = `Docs${label === undefined ? '' : ` (${label})`}: ${args.query}`;
218
225
  if (meta.sources.length === 0 && meta.snippets.length === 0) {
219
- return { card: 'generic', title: `No documentation results: ${args.query}` };
226
+ const reason = meta.cache.docs.reason === 'library_not_found'
227
+ ? 'No matching Context7 library; check library_name or supply library_id'
228
+ : 'No documentation results';
229
+ return { card: 'generic', title: `${reason}: ${args.query}` };
220
230
  }
221
231
  const answer = docsCardAnswer(meta);
222
232
  return {
@@ -1,6 +1,6 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  export declare const TOOL_DISCOVERY_GUIDANCE: string;
3
3
  export declare const EVIDENCE_DISCIPLINE_GUIDANCE: string;
4
- /** Register deterministic guidance that never reads Agent state or tool visibility. */
4
+ /** Register main/ordinary-agent guidance; the dedicated research persona owns its own routing. */
5
5
  export declare function registerToolDiscoveryGuidance(ctx: Context): void;
6
6
  //# sourceMappingURL=tool-discovery.d.ts.map
@@ -1,19 +1,17 @@
1
1
  export const TOOL_DISCOVERY_GUIDANCE = [
2
- 'Search Enhance keeps web_search, docs_search, web_extract, and search_tools resident. Optional real tools search_sources and web_map can be loaded per Agent.',
3
- 'For docs_search, pass library_id when an exact /org/project id is known, or library_name when the package/product is known; for example docs_search({ query: "JWT authentication middleware API", library_name: "FastAPI" }). Library resolution is handled internally.',
4
- 'For broad, cross-project, or unknown-library documentation discovery, use docs_search with provider: "auto" and no library identity; this uses Exa instead of guessing a Context7 library.',
5
- 'For webpage bodies (articles, blogs, repository READMEs, and model cards), prefer web_extract with exactly one explicitly selected enabled Provider. Use the host web_fetch, when available, for raw JSON APIs, XML/Atom feeds, or other structured responses.',
6
- 'Use HTTPS URLs directly when available. HTTP 200 with only a title or an app shell is not usable page-body evidence; choose a suitable enabled extraction Provider explicitly rather than repeatedly fetching the same shell. Do not bypass URL safety checks.',
7
- 'Use search_tools({ capabilities: ["sources"] }) only when a source_ref needs pagination, or search_tools({ capabilities: ["site_map"] }) to discover URLs under a known website. Do not load optional tools preemptively.',
8
- 'Newly loaded tools become available on the next model step. Call search_sources or web_map directly using the tool definitions supplied by the host; already loaded tools stay available.',
9
- 'Search source metadata and mapped URLs are discovery, not verified webpage-body evidence.',
2
+ 'For docs_search, use known library identity for library-specific queries; omit it for broad/cross-project or unknown-library discovery (auto uses Exa). Never guess a Context7 library.',
3
+ 'Read webpage bodies with web_extract, choosing exactly one enabled Provider; use host web_fetch (if available) for structured responses such as raw JSON APIs or XML/Atom.',
4
+ 'Prefer HTTPS. A title/app shell alone is not usable page-body evidence; choose another suitable enabled extraction Provider instead of refetching the same shell. Never bypass URL safety checks.',
5
+ 'For multi-step investigations, comparisons or corroboration across sources, delegate to web_research with a self-contained question and all constraints; wait for its URL-and-excerpt report. Use web_search/docs_search/web_extract directly for quick lookups, documentation or a known URL.',
6
+ 'web_research accepts optional timeout_ms for this call\'s total budget, including creation, reasoning, retrieval and reporting: default 600000 ms; integer range 60000–3600000 ms. Choose by task complexity, for example 120000 for a focused comparison or 1200000 for broad corroboration; omission uses the default, and more time does not guarantee better evidence.',
7
+ 'After a successful search returns a readable source_ref, search_sources becomes available automatically on the next step. Call it directly to paginate retained sources; no loader is needed.',
10
8
  ].join('\n');
11
9
  export const EVIDENCE_DISCIPLINE_GUIDANCE = [
12
- 'For current or external factual questions, start with one focused web_search (use docs_search for SDK/API documentation); do not inspect local files, settings, sessions, or credentials unless the user explicitly asks about local state.',
13
- 'Treat web_search/docs_search answers, snippets, and source metadata as discovery, not claim-level evidence.',
14
- 'Before asserting decisive factual or causal conclusions, inspect selected authoritative URLs with web_extract; never present an inferred mechanism as source-stated fact, and label unestablished mechanisms as inference or unconfirmed.',
10
+ 'For current/external facts, use web_research for multi-step investigations; otherwise start with one focused web_search (docs_search for SDK/API docs); do not inspect local files/settings/sessions/credentials unless the user explicitly asks about local state.',
11
+ 'Search answers/snippets, source metadata and mapped URLs are discovery, not claim-level evidence.',
12
+ 'Before decisive factual/causal conclusions, inspect authoritative URLs with web_extract; label unestablished mechanisms as inference or unconfirmed, never source-stated fact.',
15
13
  ].join('\n');
16
- /** Register deterministic guidance that never reads Agent state or tool visibility. */
14
+ /** Register main/ordinary-agent guidance; the dedicated research persona owns its own routing. */
17
15
  export function registerToolDiscoveryGuidance(ctx) {
18
16
  ctx.systemPrompt.section({
19
17
  name: 'search-enhance:tool-discovery',