From b4f30d773ca295e5533d58ca92db633e5257b36b Mon Sep 17 00:00:00 2001 From: "knowgrph-lifecycle[bot]" Date: Thu, 13 Aug 2026 03:55:55 +0800 Subject: [PATCH 1/3] chore(coordination): claim exa-coding-agent-integrations-v2 lease 484 From cafceab91a875eb5f84265f5706d1ed509c8dfab Mon Sep 17 00:00:00 2001 From: "knowgrph-lifecycle[bot]" Date: Thu, 13 Aug 2026 04:08:27 +0800 Subject: [PATCH 2/3] feat(exa): project coding-agent search contract --- .../helpers/mainPanelMcpExpectations.ts | 15 ++ .../mainPanelExaIntegrations.test.tsx | 122 ++++++++++++ .../mainPanelSkillsCommands.test.tsx | 8 +- canvas/src/features/canvas/utils.ts | 1 + .../ExaSearchSkillsCommandsProjection.tsx | 54 +++++ .../features/panels/views/exaMcpApiDocs.ts | 44 +++++ .../features/panels/views/exaSearchApiDocs.ts | 185 ++++++++++++++++++ .../panels/views/settingsView.constants.ts | 7 + .../panels/views/useSettingsView.helpers.ts | 3 + .../features/panels/views/useSettingsView.ts | 15 +- .../FloatingPanelSkillsCommandsView.tsx | 8 +- canvas/src/tests/registry/postParserCases5.ts | 3 + .../knowgrph-mcp/knowgrph-exa-mcp-prd-tad.md | 81 +++++--- docs/runtime-readiness-contract.md | 2 +- grph-shared/package.json | 5 + grph-shared/src/search/exaSearchApiSsot.ts | 107 ++++++++++ 16 files changed, 630 insertions(+), 30 deletions(-) create mode 100644 canvas/src/__tests__/mainPanelExaIntegrations.test.tsx create mode 100644 canvas/src/features/integrations/ExaSearchSkillsCommandsProjection.tsx create mode 100644 canvas/src/features/panels/views/exaSearchApiDocs.ts create mode 100644 grph-shared/src/search/exaSearchApiSsot.ts diff --git a/canvas/src/__tests__/helpers/mainPanelMcpExpectations.ts b/canvas/src/__tests__/helpers/mainPanelMcpExpectations.ts index 79ef2c6ba..004b8d597 100644 --- a/canvas/src/__tests__/helpers/mainPanelMcpExpectations.ts +++ b/canvas/src/__tests__/helpers/mainPanelMcpExpectations.ts @@ -40,6 +40,12 @@ import { EXA_MCP_LOCAL_API_KEY_ENV, EXA_MCP_REMOTE_URL, } from 'grph-shared/search/exaMcpSsot' +import { + EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + EXA_SEARCH_API_DOCS_URL, + EXA_SEARCH_API_ENDPOINT, + EXA_SEARCH_API_INVOCATION_TEXT, +} from 'grph-shared/search/exaSearchApiSsot' import { CLOUDFLARE_AI_GATEWAY_ACCOUNT_HEADER, CLOUDFLARE_AI_GATEWAY_MCP_DOCS_URL, @@ -515,6 +521,11 @@ export function assertMcpHubSurfacesExaMcpConfig(container: Element): void { 'exaMcp.max_results', 'exaMcp.fetch_content_limit', 'exaMcp.require_fetch_review', + 'exaMcp.search_api.endpoint', + 'exaMcp.search_api.default_request', + 'exaMcp.search_api.response_contract', + 'exaMcp.search_api.invocation', + 'exaMcp.search_api.docs_url', 'exaMcp.remote_config.codex', 'exaMcp.remote_config.generic', 'web_search_exa', @@ -535,6 +546,10 @@ export function assertMcpHubSurfacesExaMcpConfig(container: Element): void { EXA_MCP_DOCS_MARKDOWN_URL, EXA_MCP_GITHUB_URL, EXA_MCP_DASHBOARD_URL, + EXA_SEARCH_API_ENDPOINT, + EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + EXA_SEARCH_API_INVOCATION_TEXT, + EXA_SEARCH_API_DOCS_URL, 'restart_mcp_client_after_config_change', 'Open Exa MCP Docs', 'Open FloatingPanel Chat UI', diff --git a/canvas/src/__tests__/mainPanelExaIntegrations.test.tsx b/canvas/src/__tests__/mainPanelExaIntegrations.test.tsx new file mode 100644 index 000000000..56f388cd4 --- /dev/null +++ b/canvas/src/__tests__/mainPanelExaIntegrations.test.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import IntegrationsHubView from '@/features/panels/views/IntegrationsHubView' +import { ExaSearchSkillsCommandsProjection } from '@/features/integrations/ExaSearchSkillsCommandsProjection' +import { initJsdomHarness } from '@/tests/lib/jsdomHarness' +import { initWindowHarness } from '@/tests/lib/windowHarness' +import { MemoryStorage } from '@/tests/lib/memoryStorage' +import { installDeterministicRaf, mountReactRoot, unmountReactRoot } from '@/tests/lib/reactRootHarness' +import { useGraphStore } from '@/hooks/useGraphStore' +import { + EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + EXA_SEARCH_API_DEPRECATED_FIELDS, + EXA_SEARCH_API_DOC_AREA, + EXA_SEARCH_API_DOCS_URL, + EXA_SEARCH_API_ENDPOINT, + EXA_SEARCH_API_INVOCATION_TEXT, + buildExaCodingAgentSearchRequest, +} from 'grph-shared/search/exaSearchApiSsot' + +async function withRendered(element: React.ReactElement, assertions: (container: Element) => void): Promise { + const storage = new MemoryStorage() + const { restore: restoreWindow } = initWindowHarness({ storage }) + const { dom, restore: restoreDom } = initJsdomHarness() + let root: ReturnType | null = null + try { + const anyWindow = dom.window as unknown as { requestAnimationFrame?: (callback: (time: number) => void) => number } + anyWindow.requestAnimationFrame = installDeterministicRaf(dom.window) + useGraphStore.getState().resetAll() + const container = dom.window.document.createElement('section') + dom.window.document.body.appendChild(container) + root = createRoot(container as unknown as HTMLElement) + await mountReactRoot(root, element, { window: dom.window, frames: 4 }) + assertions(container) + } finally { + if (root) await unmountReactRoot(root, { window: dom.window }) + restoreDom() + restoreWindow() + } +} + +const renderedValues = (container: Element): string => Array.from( + container.querySelectorAll('input, textarea, select'), + element => element.value, +).join('\n') + +export async function testIntegrationsHubSurfacesExaCodingAgentSearchContract() { + await withRendered(React.createElement(IntegrationsHubView), container => { + const searchableText = `${container.textContent || ''}\n${renderedValues(container)}` + ;[ + EXA_SEARCH_API_DOC_AREA, + 'exaSearchApi.endpoint', + 'exaSearchApi.request.query', + 'exaSearchApi.request.type', + 'exaSearchApi.request.numResults', + 'exaSearchApi.request.contents.highlights', + 'exaSearchApi.request.structured_output', + 'exaSearchApi.response.contract', + 'exaSearchApi.errors.statuses', + 'exaSearchApi.deprecated.fields', + 'exaSearchApi.invocation', + EXA_SEARCH_API_ENDPOINT, + EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + EXA_SEARCH_API_INVOCATION_TEXT, + EXA_SEARCH_API_DOCS_URL, + 'Open Exa Search API Coding-Agent Guide', + 'Open FloatingPanel Skills & Commands', + ].forEach(token => { + if (!searchableText.includes(token)) { + throw new Error(`expected MainPanel Integrations to include Exa coding-agent token ${JSON.stringify(token)}`) + } + }) + if (!container.querySelector('[data-kg-anchor^="integrations-row-exa-search-"]')) { + throw new Error('expected Exa Search API integration rows to use stable Exa anchors') + } + ;['YOUR_EXA_API_KEY', 'your_api_key', 'exaApiKey=', 'sk_test_', 'sk_live_'].forEach(secret => { + if (searchableText.includes(secret)) throw new Error(`expected Exa integration to omit secret material ${secret}`) + }) + }) +} + +export function testExaCodingAgentSearchRequestUsesCurrentBoundedContract() { + const request = buildExaCodingAgentSearchRequest({ + query: ' inspect current repository issue ', + type: 'unsupported', + numResults: 999, + }) + if ( + request.query !== 'inspect current repository issue' + || request.type !== 'auto' + || request.numResults !== 100 + || request.contents.highlights !== true + ) { + throw new Error(`expected sanitized Exa coding-agent request, got ${JSON.stringify(request)}`) + } + ;['useAutoprompt', 'numSentences', 'tokensNum', 'livecrawl (string)'].forEach(field => { + if (!EXA_SEARCH_API_DEPRECATED_FIELDS.some(candidate => candidate === field)) { + throw new Error(`expected shared Exa contract to reject deprecated field ${field}`) + } + }) +} + +export async function testFloatingSkillsCommandsSurfacesCanonicalExaInvocation() { + await withRendered(React.createElement(ExaSearchSkillsCommandsProjection), container => { + const projection = container.querySelector('[data-kg-exa-search-skills-projection="configuration-only"]') + const invocation = projection?.querySelector('[data-kg-exa-search-invocation="canonical"]') + const text = projection?.textContent || '' + if ( + !projection + || invocation?.getAttribute('data-kg-exa-search-invocation-chip-renderer') !== 'shared-markdown-sigil' + || !text.includes('/tool.catalog') + || !text.includes('#tool-routing') + || !text.includes('@tool-provider') + || !text.includes('no browser-side API key or direct search call') + || projection.querySelector(`a[href="${EXA_SEARCH_API_DOCS_URL}"]`) === null + ) { + throw new Error(`expected FloatingPanel Skills & Commands Exa canonical projection, got ${JSON.stringify(text)}`) + } + if (text.includes('/exa') || text.includes('#exa') || text.includes('@exa')) { + throw new Error(`expected Exa projection to avoid provider-specific invocation aliases, got ${JSON.stringify(text)}`) + } + }) +} diff --git a/canvas/src/__tests__/mainPanelSkillsCommands.test.tsx b/canvas/src/__tests__/mainPanelSkillsCommands.test.tsx index 4a846b1d7..e7dfbf145 100644 --- a/canvas/src/__tests__/mainPanelSkillsCommands.test.tsx +++ b/canvas/src/__tests__/mainPanelSkillsCommands.test.tsx @@ -245,6 +245,8 @@ export async function testFloatingPanelSkillsCommandsViewReusesMediaPanelLayout( const subjectGroupToggle = container.querySelector('[data-kg-skills-commands-grammar-toggle="subject"]') as HTMLButtonElement | null const objectGroupToggle = container.querySelector('[data-kg-skills-commands-grammar-toggle="object"]') as HTMLButtonElement | null const motionCaptureProjection = container.querySelector('[data-kg-motion-capture-projection="skills"]') + const exaSearchProjection = container.querySelector('[data-kg-exa-search-skills-projection="configuration-only"]') + const exaSearchInvocation = exaSearchProjection?.querySelector('[data-kg-exa-search-invocation="canonical"]') const motionCaptureInvocation = motionCaptureProjection?.querySelector('[data-kg-motion-capture-invocation="canonical"]') const motionCaptureWebMcp = motionCaptureProjection?.querySelector('[data-kg-motion-capture-web-mcp="1"]') if ( @@ -271,13 +273,17 @@ export async function testFloatingPanelSkillsCommandsViewReusesMediaPanelLayout( !(sharedSearchToggle instanceof dom.window.HTMLButtonElement) || skillsSearchToggle !== sharedSearchToggle || !motionCaptureProjection || + !exaSearchProjection || + !exaSearchInvocation?.textContent?.includes('/tool.catalog') || + !exaSearchInvocation.textContent.includes('#tool-routing') || + !exaSearchInvocation.textContent.includes('@tool-provider') || motionCaptureProjection.getAttribute('data-kg-motion-capture-runtime-ready') === null || !motionCaptureInvocation?.textContent?.includes('/motion.control') || !motionCaptureInvocation.textContent.includes('@canvas') || !motionCaptureInvocation.textContent.includes('#pose') || !motionCaptureWebMcp?.textContent?.includes('knowgrph.control_local_motion_control') ) { - throw new Error('Expected Skills & Commands to reuse the shared Media layout and canonical Motion Capture invocation projection') + throw new Error('Expected Skills & Commands to reuse the shared Media layout and canonical Exa/Motion Capture invocation projections') } const groupDisclosureActions = container.querySelector('[data-kg-skills-commands-disclosure-actions="header"]') as HTMLElement | null const groupDisclosureButton = groupDisclosureActions?.querySelector('button') as HTMLButtonElement | null diff --git a/canvas/src/features/canvas/utils.ts b/canvas/src/features/canvas/utils.ts index 652bf0594..9c9df7f3e 100644 --- a/canvas/src/features/canvas/utils.ts +++ b/canvas/src/features/canvas/utils.ts @@ -23,6 +23,7 @@ export type PropsPanelOpenEventDetail = { export type FloatingPanelOpenEventDetail = { tab?: | 'inspector' + | 'skillsCommands' | 'node' | 'view' | 'camera' diff --git a/canvas/src/features/integrations/ExaSearchSkillsCommandsProjection.tsx b/canvas/src/features/integrations/ExaSearchSkillsCommandsProjection.tsx new file mode 100644 index 000000000..17638b730 --- /dev/null +++ b/canvas/src/features/integrations/ExaSearchSkillsCommandsProjection.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { Search } from 'lucide-react' +import { renderAgenticOsInvocationKeywordChip } from '@/features/agentic-os/agenticOsInvocationChips' +import { renderMarkdownSigilInlineText } from '@/lib/ui/MarkdownSigilText' +import { UI_INLINE_CHIP_GROUP_CLASSNAME } from '@/lib/ui/textLayout' +import { UI_THEME_TOKENS } from '@/lib/ui/theme-tokens' +import { cn } from '@/lib/utils' +import { + EXA_SEARCH_API_DEFAULT_NUM_RESULTS, + EXA_SEARCH_API_DEFAULT_SEARCH_TYPE, + EXA_SEARCH_API_DOCS_URL, + EXA_SEARCH_API_INVOCATION_TEXT, +} from 'grph-shared/search/exaSearchApiSsot' + +export function ExaSearchSkillsCommandsProjection() { + return ( +
+
+ + + + Coding-agent guide + +
+

+ {EXA_SEARCH_API_DEFAULT_SEARCH_TYPE} · {EXA_SEARCH_API_DEFAULT_NUM_RESULTS} results · highlights · host-owned auth +

+ + {renderMarkdownSigilInlineText(EXA_SEARCH_API_INVOCATION_TEXT, { + renderKeywordChip: ({ value, className }) => renderAgenticOsInvocationKeywordChip({ + value, + className, + sourceLink: false, + }), + })} + +

Reference contract only · no browser-side API key or direct search call

+
+ ) +} diff --git a/canvas/src/features/panels/views/exaMcpApiDocs.ts b/canvas/src/features/panels/views/exaMcpApiDocs.ts index aee235aa1..d10b113d6 100644 --- a/canvas/src/features/panels/views/exaMcpApiDocs.ts +++ b/canvas/src/features/panels/views/exaMcpApiDocs.ts @@ -25,6 +25,14 @@ import { EXA_MCP_TOOL_PROFILES, normalizeExaMcpToolNames, } from 'grph-shared/search/exaMcpSsot' +import { + EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + EXA_SEARCH_API_DEFAULT_SEARCH_TYPE, + EXA_SEARCH_API_DOCS_URL, + EXA_SEARCH_API_ENDPOINT, + EXA_SEARCH_API_INVOCATION_TEXT, + EXA_SEARCH_API_RESPONSE_FIELDS, +} from 'grph-shared/search/exaSearchApiSsot' export { EXA_MCP_DOC_AREA, EXA_MCP_DOCS_URL } @@ -169,6 +177,42 @@ const EXA_MCP_DOC_ROWS: ReadonlyArray = [ tooltipDefaultValue: EXA_MCP_DEFAULT_MAX_RESULTS, searchHints: ['numResults', 'result limit', 'evidence pack'], }, + { + key: 'search_api.endpoint', + typeLabel: 'endpoint', + value: `POST ${EXA_SEARCH_API_ENDPOINT}`, + responsibility: 'Source-owned Search API endpoint behind Exa MCP coding-agent search guidance.', + notes: 'This row is a contract projection only; the browser does not call Exa or store bearer credentials.', + searchHints: ['coding agent search api', EXA_SEARCH_API_ENDPOINT], + }, + { + key: 'search_api.default_request', + typeLabel: 'object', + value: EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + responsibility: 'Shared coding-agent request preview with auto search, bounded results, and highlight content.', + searchHints: ['contents highlights', EXA_SEARCH_API_DEFAULT_SEARCH_TYPE, 'numResults'], + }, + { + key: 'search_api.response_contract', + typeLabel: 'string[]', + value: JSON.stringify(EXA_SEARCH_API_RESPONSE_FIELDS), + responsibility: 'Preserve request identity, evidence, structured output, and upstream cost reporting across MCP handoff.', + searchHints: ['requestId', 'searchType', 'costDollars.total', 'grounded output'], + }, + { + key: 'search_api.invocation', + typeLabel: '/ # @', + value: EXA_SEARCH_API_INVOCATION_TEXT, + responsibility: 'Project the canonical Agentic Canvas OS tool-routing invocation without provider-specific aliases.', + searchHints: ['/tool.catalog', '#tool-routing', '@tool-provider'], + }, + { + key: 'search_api.docs_url', + typeLabel: 'url', + value: EXA_SEARCH_API_DOCS_URL, + responsibility: 'Canonical Exa Search API guide for coding agents used by the shared request contract.', + searchHints: ['Exa Search API coding agent guide', EXA_SEARCH_API_DOCS_URL], + }, { key: 'fetch_content_limit', typeLabel: 'integer', diff --git a/canvas/src/features/panels/views/exaSearchApiDocs.ts b/canvas/src/features/panels/views/exaSearchApiDocs.ts new file mode 100644 index 000000000..219404b84 --- /dev/null +++ b/canvas/src/features/panels/views/exaSearchApiDocs.ts @@ -0,0 +1,185 @@ +import type { FlowDetails, SettingMeta } from '@/features/settings/types' +import type { VirtualSettingsEntry } from './byteplusSharedTextApiDocs' +import { buildSettingsRowAnchorId } from './settingsRowAnchor' +import { + EXA_SEARCH_API_AUTH_HEADER, + EXA_SEARCH_API_AUTH_SCHEME, + EXA_SEARCH_API_CATEGORIES, + EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + EXA_SEARCH_API_DEFAULT_NUM_RESULTS, + EXA_SEARCH_API_DEFAULT_SEARCH_TYPE, + EXA_SEARCH_API_DEPRECATED_FIELDS, + EXA_SEARCH_API_DOC_AREA, + EXA_SEARCH_API_DOCS_MARKDOWN_URL, + EXA_SEARCH_API_DOCS_URL, + EXA_SEARCH_API_ENDPOINT, + EXA_SEARCH_API_ERROR_STATUSES, + EXA_SEARCH_API_INVOCATION_TEXT, + EXA_SEARCH_API_KEY_ENV, + EXA_SEARCH_API_MAX_NUM_RESULTS, + EXA_SEARCH_API_RESPONSE_FIELDS, + EXA_SEARCH_API_SEARCH_TYPES, +} from 'grph-shared/search/exaSearchApiSsot' + +export { EXA_SEARCH_API_DOC_AREA, EXA_SEARCH_API_DOCS_URL } + +type ExaSearchApiDocRow = Readonly<{ + key: string + typeLabel: string + value: string | number | boolean + responsibility: string + notes?: string + searchHints?: readonly string[] +}> + +const rows: ReadonlyArray = [ + { + key: 'endpoint', + typeLabel: 'endpoint', + value: `POST ${EXA_SEARCH_API_ENDPOINT}`, + responsibility: 'Canonical Exa Search endpoint for source-grounded coding-agent research.', + searchHints: ['search endpoint', 'POST', EXA_SEARCH_API_ENDPOINT], + }, + { + key: 'auth.boundary', + typeLabel: 'security note', + value: `${EXA_SEARCH_API_AUTH_HEADER}: ${EXA_SEARCH_API_AUTH_SCHEME} (${EXA_SEARCH_API_KEY_ENV} host reference)`, + responsibility: 'Describe server-owned bearer authentication without accepting or persisting a raw Exa API key in browser state.', + notes: 'The key value belongs in a trusted host or proxy. MainPanel exposes names and boundaries only.', + searchHints: ['authentication', EXA_SEARCH_API_KEY_ENV, EXA_SEARCH_API_AUTH_HEADER], + }, + { + key: 'request.query', + typeLabel: 'string required', + value: 'query', + responsibility: 'Required natural-language search query for the coding task.', + }, + { + key: 'request.type', + typeLabel: 'enum', + value: EXA_SEARCH_API_DEFAULT_SEARCH_TYPE, + responsibility: 'Search strategy selector; auto is the upstream default and chooses an appropriate strategy.', + searchHints: EXA_SEARCH_API_SEARCH_TYPES, + }, + { + key: 'request.numResults', + typeLabel: 'integer', + value: EXA_SEARCH_API_DEFAULT_NUM_RESULTS, + responsibility: `Bound the evidence pack to 1-${EXA_SEARCH_API_MAX_NUM_RESULTS} results; default to ${EXA_SEARCH_API_DEFAULT_NUM_RESULTS}.`, + }, + { + key: 'request.contents.highlights', + typeLabel: 'boolean', + value: true, + responsibility: 'Return focused passages for coding-agent context instead of transferring full page text by default.', + notes: 'The Exa coding-agent guide recommends highlights for concise, relevant evidence.', + }, + { + key: 'request.filters', + typeLabel: 'object', + value: JSON.stringify({ includeDomains: [], excludeDomains: [], category: null }), + responsibility: 'Optional domain, category, and date filters narrow source selection without changing downstream ownership.', + searchHints: ['includeDomains', 'excludeDomains', 'date filters', ...EXA_SEARCH_API_CATEGORIES], + }, + { + key: 'request.freshness', + typeLabel: 'integer optional', + value: 'contents.maxAgeHours', + responsibility: 'Control cache freshness explicitly; zero forces live crawling and can add latency.', + notes: 'Leave maxAgeHours absent unless the task requires a specific freshness boundary.', + searchHints: ['maxAgeHours', 'live crawl', 'cache'], + }, + { + key: 'request.structured_output', + typeLabel: 'object optional', + value: 'outputSchema', + responsibility: 'Request grounded structured output with the upstream schema limits instead of parsing prose downstream.', + notes: 'The guide limits schemas to two nesting levels and ten properties.', + }, + { + key: 'request.stream', + typeLabel: 'boolean', + value: false, + responsibility: 'Opt into SSE only when the caller owns OpenAI-compatible chat-chunk consumption.', + }, + { + key: 'request.coding_agent_default', + typeLabel: 'json', + value: EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON, + responsibility: 'Non-secret request preview shared by MainPanel Integrations, MCP guidance, and Skills & Commands.', + }, + { + key: 'response.contract', + typeLabel: 'string[]', + value: JSON.stringify(EXA_SEARCH_API_RESPONSE_FIELDS), + responsibility: 'Track request identity, selected search type, evidence, structured output, and reported total cost.', + }, + { + key: 'errors.statuses', + typeLabel: 'integer[]', + value: JSON.stringify(EXA_SEARCH_API_ERROR_STATUSES), + responsibility: 'Handle validation, authentication, semantic validation, rate limiting, and service failures explicitly.', + }, + { + key: 'deprecated.fields', + typeLabel: 'string[]', + value: JSON.stringify(EXA_SEARCH_API_DEPRECATED_FIELDS), + responsibility: 'Reject legacy top-level content and retired tuning fields at the shared request boundary.', + }, + { + key: 'invocation', + typeLabel: '/ # @', + value: EXA_SEARCH_API_INVOCATION_TEXT, + responsibility: 'Route Exa through canonical Agentic Canvas OS tool catalog semantics without inventing provider-specific grammar.', + }, + { + key: 'docs.url', + typeLabel: 'url', + value: EXA_SEARCH_API_DOCS_URL, + responsibility: 'Canonical Exa Search API guide for coding agents.', + }, + { + key: 'docs.markdown_url', + typeLabel: 'url', + value: EXA_SEARCH_API_DOCS_MARKDOWN_URL, + responsibility: 'Markdown form of the canonical guide for source verification.', + }, +] + +const toBaseType = (typeLabel: string): SettingMeta['type'] => { + const normalized = typeLabel.toLowerCase() + if (normalized.includes('boolean')) return 'boolean' + if (normalized.includes('object') || normalized.includes('[]') || normalized.includes('json')) return 'json' + if (normalized.includes('integer') || normalized.includes('number')) return 'number' + return 'string' +} + +export function getExaSearchApiRowAnchorId(rowKey: string): string { + return buildSettingsRowAnchorId('integrations-row-exa-search', rowKey) +} + +export const EXA_SEARCH_API_DOC_ENTRIES: ReadonlyArray = rows.map(row => { + const details: FlowDetails = { + area: EXA_SEARCH_API_DOC_AREA, + responsibility: row.responsibility, + notes: row.notes || '', + modules: ['Exa Search API'], + classes: ['CodingAgentSearchContract'], + functions: ['MainPanel Integrations', 'MainPanel MCP', 'FloatingPanel Skills & Commands'], + imports: [], + } + return { + meta: { + key: `exaSearchApi.${row.key}`, + type: toBaseType(row.typeLabel), + source: 'backendEnv', + read: () => row.value, + }, + value: row.value, + typeLabel: row.typeLabel, + tooltipRole: 'Exa Search API', + tooltipDefaultValue: row.value, + searchHints: ['exa search api', 'coding agent search', row.key, ...(row.searchHints || [])], + details, + } +}) diff --git a/canvas/src/features/panels/views/settingsView.constants.ts b/canvas/src/features/panels/views/settingsView.constants.ts index 05ac23518..bf37c2b7f 100644 --- a/canvas/src/features/panels/views/settingsView.constants.ts +++ b/canvas/src/features/panels/views/settingsView.constants.ts @@ -28,6 +28,7 @@ import { OPENAI_MCP_DOC_AREA, OPENAI_MCP_DOCS_URL } from './openaiMcpApiDocs' import { OPERATOR_DEPLOY_MCP_DOC_AREA, OPERATOR_DEPLOY_MCP_DOCS_URL } from './operatorDeployMcpApiDocs' import { OPERATOR_DEPLOY_SETTING_KEYS } from '@/features/settings/operatorDeploySsot' import { EXA_MCP_DOC_AREA, EXA_MCP_DOCS_URL } from './exaMcpApiDocs' +import { EXA_SEARCH_API_DOC_AREA, EXA_SEARCH_API_DOCS_URL } from './exaSearchApiDocs' import { FEISHU_BASE_MCP_DOC_AREA, FEISHU_BASE_MCP_DOCS_URL } from './feishuBaseMcpApiDocs' import { LARK_APP_MCP_DOC_AREA, LARK_APP_MCP_DOCS_URL } from './larkAppMcpApiDocs' import { STRIPE_MCP_DOC_AREA } from './stripeMcpApiDocs' @@ -97,6 +98,12 @@ export const INTEGRATIONS_SECTION_META: Readonly> = panelLabel: 'Open FloatingPanel Props Panel Widget Card', openPanel: () => emitPropsPanelOpen(), }, + [EXA_SEARCH_API_DOC_AREA]: { + docsUrl: EXA_SEARCH_API_DOCS_URL, + docsLabel: 'Open Exa Search API Coding-Agent Guide', + panelLabel: 'Open FloatingPanel Skills & Commands', + openPanel: () => emitFloatingPanelOpen({ tab: 'skillsCommands', open: true }), + }, [OPENAI_CHAT_API_DOC_AREA]: { docsUrl: 'https://developers.openai.com/api/reference/resources/responses', docsLabel: 'Open OpenAI Chat API Docs', diff --git a/canvas/src/features/panels/views/useSettingsView.helpers.ts b/canvas/src/features/panels/views/useSettingsView.helpers.ts index e297050ce..f09d3c729 100644 --- a/canvas/src/features/panels/views/useSettingsView.helpers.ts +++ b/canvas/src/features/panels/views/useSettingsView.helpers.ts @@ -37,6 +37,7 @@ import { CLOUDFLARE_AI_GATEWAY_MCP_DOC_AREA } from './cloudflareAiGatewayMcpApiD import { BYTEPLUS_MODELARK_MCP_DOC_AREA } from './byteplusModelArkMcpApiDocs' import { OPENAI_MCP_DOC_AREA } from './openaiMcpApiDocs' import { EXA_MCP_DOC_AREA } from './exaMcpApiDocs' +import { EXA_SEARCH_API_DOC_AREA } from './exaSearchApiDocs' import { FEISHU_BASE_MCP_DOC_AREA } from './feishuBaseMcpApiDocs' import { LARK_APP_MCP_DOC_AREA } from './larkAppMcpApiDocs' import { STRIPE_MCP_DOC_AREA } from './stripeMcpApiDocs' @@ -117,6 +118,7 @@ const SETTINGS_AREA_ORDER: readonly string[] = [ MARKDOWN_DATA_VIEW_COPY.titleDefault, 'Import / Export', 'Integrations', + EXA_SEARCH_API_DOC_AREA, BYTEPLUS_SHARED_TEXT_API_DOC_AREA, BYTEPLUS_IMAGE_GENERATION_API_DOC_AREA, BYTEPLUS_VIDEO_GENERATION_API_DOC_AREA, @@ -163,6 +165,7 @@ export function isIntegrationsOwnedSetting(key: string, areaRaw: string): boolea if ( area === 'Chat' || area === 'Integrations' + || area === EXA_SEARCH_API_DOC_AREA || area === BYTEPLUS_SHARED_TEXT_API_DOC_AREA || area === BYTEPLUS_IMAGE_GENERATION_API_DOC_AREA || area === BYTEPLUS_VIDEO_GENERATION_API_DOC_AREA diff --git a/canvas/src/features/panels/views/useSettingsView.ts b/canvas/src/features/panels/views/useSettingsView.ts index 0f2a6852d..30b1864f7 100644 --- a/canvas/src/features/panels/views/useSettingsView.ts +++ b/canvas/src/features/panels/views/useSettingsView.ts @@ -131,6 +131,11 @@ import { SENSENOVA_API_DOC_ENTRIES, getSensenovaApiRowAnchorId, } from './sensenovaApiDocs' +import { + EXA_SEARCH_API_DOC_AREA, + EXA_SEARCH_API_DOC_ENTRIES, + getExaSearchApiRowAnchorId, +} from './exaSearchApiDocs' import { MAPS_API_DOC_ENTRIES, getMapsApiRowAnchorId, @@ -189,6 +194,7 @@ const getSettingsSearchHints = (key: string): string[] => { } const INTEGRATION_API_DOC_ENTRIES = [ + ...EXA_SEARCH_API_DOC_ENTRIES, ...BYTEPLUS_SHARED_TEXT_API_REQUEST_DOC_ENTRIES, ...BYTEPLUS_IMAGE_GENERATION_API_REQUEST_DOC_ENTRIES, ...BYTEPLUS_VIDEO_GENERATION_API_REQUEST_DOC_ENTRIES, @@ -1031,7 +1037,9 @@ export function useSettingsView({ const area = normalizeSettingsAreaLabel(entry.details.area) const normalizedDisplayValues = normalizedProviderValuesByArea.get(area) || values const anchorId = - area === BYTEPLUS_SHARED_TEXT_API_DOC_AREA + area === EXA_SEARCH_API_DOC_AREA + ? getExaSearchApiRowAnchorId(entry.meta.key) + : area === BYTEPLUS_SHARED_TEXT_API_DOC_AREA ? getBytePlusSharedTextApiRowAnchorId(entry.meta.key) : area === BYTEPLUS_IMAGE_GENERATION_API_DOC_AREA ? getBytePlusImageGenerationApiRowAnchorId(entry.meta.key) @@ -1199,6 +1207,11 @@ export function useSettingsView({ return area === 'Chat' || area === 'Integrations' }, }, + { + title: EXA_SEARCH_API_DOC_AREA, + searchIndex: normalizeText('Exa Search API coding agents web research highlights structured output cost tool routing FloatingPanel Skills Commands'), + match: entry => normalizeSettingsAreaLabel(entry.details.area) === EXA_SEARCH_API_DOC_AREA, + }, { title: BYTEPLUS_SHARED_TEXT_API_DOC_AREA, searchIndex: normalizeText('BytePlus Shared + Text API BytePlus Chat API ModelArk FloatingPanel Props Panel Widget Card text generation shared auth api key endpoint'), diff --git a/canvas/src/features/toolbar/FloatingPanelSkillsCommandsView.tsx b/canvas/src/features/toolbar/FloatingPanelSkillsCommandsView.tsx index f037f8005..8682c43b2 100644 --- a/canvas/src/features/toolbar/FloatingPanelSkillsCommandsView.tsx +++ b/canvas/src/features/toolbar/FloatingPanelSkillsCommandsView.tsx @@ -19,6 +19,7 @@ import { import { UI_THEME_TOKENS } from '@/lib/ui/theme-tokens' import { cn } from '@/lib/utils' import { MotionCapturePlatformProjection } from '@/features/three/MotionCapturePlatformProjection' +import { ExaSearchSkillsCommandsProjection } from '@/features/integrations/ExaSearchSkillsCommandsProjection' import { useSkillsCommandsMcpTarget } from '@/features/agentic-os/skillsCommandsMcpTarget' const SKILLS_COMMANDS_PREFIX_FILTERS: Array<{ filter: SkillsCommandsPrefixFilter; label: string; Icon: typeof Slash }> = [ @@ -200,7 +201,12 @@ export function FloatingPanelSkillsCommandsView() { Source-backed invocation selected: {targetTokens.join(' ')}

) : null} - {targetingMcpInvocation ? null : } + {targetingMcpInvocation ? null : ( + <> + + + + )} B["Dispatcher: virtual MCP rows"] - B --> C["SSOT normalizer"] - C --> D["Executor: deterministic config builder"] - D --> E["Observer: focused source test"] + A["Integrations / MCP / Skills request"] --> B["Dispatcher: surface projection"] + B --> C["Search API + MCP SSOT"] + C --> D["Executor: deterministic reference/config builder"] + D --> E["Observer: focused contract tests"] E --> F["Operator-owned MCP host"] F -. "provider result; no repo harness evidenced" .-> G["Future evidence validation gate"] ``` @@ -189,7 +203,10 @@ may infer remote readiness from the source test. flowchart TB subgraph Authoring["Authoring lane"] S["exaMcpSsot.ts"] + A["exaSearchApiSsot.ts"] U["exaMcpApiDocs.ts"] + I["exaSearchApiDocs.ts"] + K["ExaSearchSkillsCommandsProjection.tsx"] T["mainPanelMcpExa.test.tsx"] end subgraph Host["Operator host boundary"] @@ -198,6 +215,9 @@ flowchart TB subgraph Upstream["External provider boundary"] P["Hosted Exa service"] end + A --> I + A --> U + A --> K S --> U --> T U -. "copy only" .-> H H -. "host-owned request" .-> P @@ -220,6 +240,8 @@ flowchart TB | `TAD-EXA-SSOT` | Shared SSOT | `TAD-EXA-SSOT-NORMALIZE`; `TAD-EXA-SSOT-URL` (`normalizeExaMcpToolNames`; `buildExaMcpRemoteUrl`) | `VCC-EXA-01`, `VCC-EXA-03` | Three allowed names; default has two. | | `TAD-EXA-CONFIG` | Config builders | `TAD-EXA-CONFIG-BUILD` (`resolveExaMcpEnabledTools`; config builders) | `VCC-EXA-01`, `VCC-EXA-02` | No credential value or header material. | | `TAD-EXA-MAINPANEL` | MainPanel aggregation | `TAD-EXA-MAINPANEL-ROWS` (`EXA_MCP_DOC_ENTRIES`) | `VCC-EXA-04` | No parallel tab or browser MCP client. | +| `TAD-EXA-SEARCH-SSOT` | Coding-agent Search API SSOT | `TAD-EXA-SEARCH-BUILD` (`buildExaCodingAgentSearchRequest`) | `VCC-EXA-07`, `VCC-EXA-08` | Required query; allowed mode; 1-100 results; highlights by default. | +| `TAD-EXA-SKILLS` | Skills & Commands projection | `TAD-EXA-SKILLS-INVOKE` (`EXA_SEARCH_API_INVOCATION_TEXT`) | `VCC-EXA-09` | Canonical dictionary tokens only; no private aliases. | | `TAD-EXA-EVIDENCE` | Evidence boundary (not implemented) | `TAD-EXA-EVIDENCE-VALIDATE` | `VCC-EXA-05` | No direct canvas mutation. | | `TAD-EXA-HARNESS` | Execution harness (not implemented) | `TAD-EXA-HARNESS-EXECUTE` | `VCC-EXA-06` | Activation requires token, quota, and circuit-breaker bounds. | @@ -237,6 +259,9 @@ here. | `PRD-EXA-04` | `TAD-EXA-MAINPANEL` | `TAD-EXA-MAINPANEL-ROWS` | `VCC-EXA-04` | | `PRD-EXA-05` | `TAD-EXA-EVIDENCE` | `TAD-EXA-EVIDENCE-VALIDATE` | `VCC-EXA-05` | | `PRD-EXA-06` | `TAD-EXA-HARNESS` | `TAD-EXA-HARNESS-EXECUTE` | `VCC-EXA-06` | +| `PRD-EXA-07` | `TAD-EXA-SEARCH-SSOT` | `TAD-EXA-SEARCH-BUILD` | `VCC-EXA-07` | +| `PRD-EXA-08` | `TAD-EXA-SEARCH-SSOT` + `TAD-EXA-MAINPANEL` | `TAD-EXA-SEARCH-BUILD` + `TAD-EXA-MAINPANEL-ROWS` | `VCC-EXA-08` | +| `PRD-EXA-09` | `TAD-EXA-SKILLS` | `TAD-EXA-SKILLS-INVOKE` | `VCC-EXA-09` | ### Security and error contract @@ -278,12 +303,16 @@ publication. | VCC | Exact check | Expected end state | Constraint | Evidence Reference | |---|---|---|---|---| -| `VCC-EXA-01` | From `canvas/`: `npm run test:ci:unit -- ui.mainPanel.mcpHub.exaDefaultGeneratedConfigNonSecret` | One registered case runs and the default config uses the source-owned profile. | Require `SUMMARY total=1 ... failed=0`; no network. | None recorded | -| `VCC-EXA-02` | Same exact registered case as `VCC-EXA-01` | Generated text omits secret material. | No real credential fixture. | None recorded | -| `VCC-EXA-03` | From `canvas/`: `npm run test:ci:unit -- ui.mainPanel.mcpHub.exaFiltersUnsupportedTools` | One registered case runs; unknown/duplicate tools are filtered. | Require `SUMMARY total=1 ... failed=0`. | None recorded | +| `VCC-EXA-01` | From `canvas/`: `npm run test:ci:unit -- ui.mainPanel.mcpHub.exa` | Three registered config cases run and the default config uses the source-owned profile. | Require `SUMMARY total=3 ... failed=0`; no network. | Local authoring run, 2026-08-13: `total=3 ok=3 failed=0` | +| `VCC-EXA-02` | Same exact registered filter as `VCC-EXA-01` | Generated text omits secret material. | No real credential fixture. | Local authoring run, 2026-08-13: `total=3 ok=3 failed=0` | +| `VCC-EXA-03` | Same exact registered filter as `VCC-EXA-01` | Unknown/duplicate tools are filtered. | Require `SUMMARY total=3 ... failed=0`. | Local authoring run, 2026-08-13: `total=3 ok=3 failed=0` | | `VCC-EXA-04` | Source-owner review of `exaMcpApiDocs.ts` and its call sites | UI remains configuration/documentation only. | A source review is not delivery evidence. | None recorded | | `VCC-EXA-05` | No invocable Exa evidence-harness case exists. | Provider content cannot mutate app state without validation. | Unsatisfied; no readiness credit. | None recorded | | `VCC-EXA-06` | No invocable Exa execution-harness case exists. | Execution remains disabled until token, quota, and circuit-breaker bounds are specified and checked. | Unsatisfied; no readiness credit. | None recorded | +| `VCC-EXA-07` | From `canvas/`: run `npm run test:ci:unit -- ui.mainPanel.integrationsHub.exaCodingAgentSearchContract` and `npm run test:ci:unit -- integrations.exa.codingAgentSearchRequestBounded` | Both registered cases pass; rows and bounded request contract match the shared SSOT. | No network and no real credential fixture. | Local authoring runs, 2026-08-13: each `total=1 ok=1 failed=0` | +| `VCC-EXA-08` | From `canvas/`: `npm run test:ci:unit -- ui.mainPanel.mcpHub.surfacesExaMcpConfig` | Existing MCP rendering case also checks the coding-agent endpoint, request, response/cost fields, invocation, and guide URL. | Source projection only. | Local authoring run, 2026-08-13: `total=1 ok=1 failed=0` | +| `VCC-EXA-09` | From `canvas/`: separately run `npm run test:ci:unit -- ui.floatingPanel.skillsCommands.exaCanonicalInvocation` and `npm run test:ci:unit -- ui.floatingPanel.skillsCommands.reusesMediaLayout` | Direct and composed FloatingPanel checks pass with canonical `/ # @` tokens and no Exa alias. | Remote catalog remains the dictionary source of truth. | Local authoring runs, 2026-08-13: each `total=1 ok=1 failed=0` | -With no recorded result, lane, commit, and evaluator for these VCCs, the local -rung remains `spec-complete` and the delivered rung remains `undocumented`. +Focused source and TypeScript results raise this authoring lane to +`dev-proven`. No public delivery, live provider call, account quota, or +production evidence is recorded, so the delivered rung remains `undocumented`. diff --git a/docs/runtime-readiness-contract.md b/docs/runtime-readiness-contract.md index a6c6b6a1e..4161fcc9d 100644 --- a/docs/runtime-readiness-contract.md +++ b/docs/runtime-readiness-contract.md @@ -13,7 +13,7 @@ stage_contract: order: ["research", "storyboard", "render", "edit", "publish", "checkout"] docs_dependency: repository: "https://github.com/huijoohwee/agentic-canvas-os.git" - ref: "270dfa50cc6825115d0856ae931047cd5e531b18" + ref: "af931c872c03353ad500ec87a54501cad887e5cd" root_env: "KNOWGRPH_AGENTIC_CANVAS_OS_DOCS_ROOT" default_relative_root: "../agentic-canvas-os/docs" required_files: ["FACTS.md", "DICTIONARY-COMMAND.md", "DICTIONARY-SEMANTIC.md", "DICTIONARY-BINDING.md", "START-WORKFLOW.md", "RELEASE-WORKFLOW.md", "CANONICAL-LIFECYCLE.md", "RUNTIME-PROOF.md", "REPOSITORY-PACKING.md", "LIVE-AGENT-PROVIDER-PROOF.md", "PROGRESSIVE-AGENTS.md", "PROMPT-PRESETS.md", "AGENT-TOOLKIT.md", "APPLICATION-COMPOSITION.md", "SKILL-EVOLUTION.md", "AGENT-TEAM.md", "VOICE-STUDIO.md", "SKILLS.md", "schemas/production-runtime-readiness.v2.schema.json"] diff --git a/grph-shared/package.json b/grph-shared/package.json index e220fcf65..94f2d234f 100644 --- a/grph-shared/package.json +++ b/grph-shared/package.json @@ -348,6 +348,11 @@ "import": "./dist/search/exaMcpSsot.js", "default": "./dist/search/exaMcpSsot.js" }, + "./search/exaSearchApiSsot": { + "types": "./dist/search/exaSearchApiSsot.d.ts", + "import": "./dist/search/exaSearchApiSsot.js", + "default": "./dist/search/exaSearchApiSsot.js" + }, "./search/feishuBaseMcpSsot": { "types": "./dist/search/feishuBaseMcpSsot.d.ts", "import": "./dist/search/feishuBaseMcpSsot.js", diff --git a/grph-shared/src/search/exaSearchApiSsot.ts b/grph-shared/src/search/exaSearchApiSsot.ts new file mode 100644 index 000000000..a45b2b5a0 --- /dev/null +++ b/grph-shared/src/search/exaSearchApiSsot.ts @@ -0,0 +1,107 @@ +export const EXA_SEARCH_API_DOC_AREA = 'Exa Search API for Coding Agents' + +export const EXA_SEARCH_API_DOCS_URL = 'https://exa.ai/docs/reference/search-api-guide-for-coding-agents' + +export const EXA_SEARCH_API_DOCS_MARKDOWN_URL = `${EXA_SEARCH_API_DOCS_URL}.md` + +export const EXA_SEARCH_API_ENDPOINT = 'https://api.exa.ai/search' + +export const EXA_SEARCH_API_KEY_ENV = 'EXA_API_KEY' + +export const EXA_SEARCH_API_AUTH_HEADER = 'Authorization' + +export const EXA_SEARCH_API_AUTH_SCHEME = 'Bearer' + +export const EXA_SEARCH_API_SEARCH_TYPES = [ + 'auto', + 'fast', + 'instant', + 'deep-lite', + 'deep', + 'deep-reasoning', +] as const + +export const EXA_SEARCH_API_DEFAULT_SEARCH_TYPE = 'auto' + +export const EXA_SEARCH_API_DEFAULT_NUM_RESULTS = 10 + +export const EXA_SEARCH_API_MAX_NUM_RESULTS = 100 + +export const EXA_SEARCH_API_CATEGORIES = [ + 'company', + 'people', + 'publication', + 'news', + 'personal site', + 'financial report', +] as const + +export const EXA_SEARCH_API_CODING_AGENT_CONTENTS = Object.freeze({ highlights: true }) + +export const EXA_SEARCH_API_INVOCATION = Object.freeze({ + action: '/tool.catalog', + semantic: '#tool-routing', + binding: '@tool-provider', +}) + +export const EXA_SEARCH_API_RESPONSE_FIELDS = [ + 'requestId', + 'searchType', + 'results', + 'output', + 'costDollars.total', +] as const + +export const EXA_SEARCH_API_ERROR_STATUSES = [400, 401, 422, 429, 500] as const + +export const EXA_SEARCH_API_DEPRECATED_FIELDS = [ + 'useAutoprompt', + 'text (top-level)', + 'highlights (top-level)', + 'summary (top-level)', + 'numSentences', + 'highlightsPerUrl', + 'tokensNum', + 'livecrawl (string)', +] as const + +export type ExaSearchApiSearchType = typeof EXA_SEARCH_API_SEARCH_TYPES[number] + +export type ExaCodingAgentSearchRequest = Readonly<{ + query: string + type: ExaSearchApiSearchType + numResults: number + contents: Readonly<{ highlights: true }> +}> + +const ALLOWED_SEARCH_TYPES = new Set(EXA_SEARCH_API_SEARCH_TYPES) + +export function buildExaCodingAgentSearchRequest(input: { + query: string + type?: string + numResults?: number +}): ExaCodingAgentSearchRequest { + const query = String(input.query || '').trim() + if (!query) throw new Error('Exa search query is required.') + const type = ALLOWED_SEARCH_TYPES.has(String(input.type || '')) + ? input.type as ExaSearchApiSearchType + : EXA_SEARCH_API_DEFAULT_SEARCH_TYPE + const requestedCount = Number.isFinite(input.numResults) + ? Math.trunc(input.numResults as number) + : EXA_SEARCH_API_DEFAULT_NUM_RESULTS + const numResults = Math.min(EXA_SEARCH_API_MAX_NUM_RESULTS, Math.max(1, requestedCount)) + return { + query, + type, + numResults, + contents: EXA_SEARCH_API_CODING_AGENT_CONTENTS, + } +} + +export const EXA_SEARCH_API_CODING_AGENT_REQUEST_JSON = JSON.stringify( + buildExaCodingAgentSearchRequest({ query: 'repository issue and implementation context' }), + null, + 2, +) + +export const EXA_SEARCH_API_INVOCATION_TEXT = Object.values(EXA_SEARCH_API_INVOCATION).join(' ') From c7ba1e8fd08f883637a0babb80d888a96dfee392 Mon Sep 17 00:00:00 2001 From: "knowgrph-lifecycle[bot]" Date: Thu, 13 Aug 2026 07:05:31 +0800 Subject: [PATCH 3/3] chore(coordination): claim exa-coding-agent-integrations-v2 lease 485