Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions docs/use/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ The **search** tool finds **built-in capabilities**, **saved packages**,
## Queries and ranking

Pass a **`query`** string that describes what you want to do. Results are
ranked; order in the response matters.
ranked; order in the response matters. Query responses are intentionally
compact: the markdown response is a short list of matches with the result type,
title, one-line summary, and entity reference when applicable.

When a tool call also includes **`memoryContext`**, Kody may attach a small
number of relevant long-term memories that have not already been surfaced for
that **`conversationId`**.
When a tool call also includes **`memoryContext`**, Kody may include relevant
long-term memory metadata in structured content, but broad query markdown stays
focused on the ranked matches.

Search responses also return top-level **`timing`** metadata with
**`startedAt`**, **`endedAt`**, and **`durationMs`** so hosts can reason about
how long the ranked lookup or entity lookup took.

Optional **`limit`** caps how many ranked hits return. Optional
**`maxResponseSize`** trims low-ranked matches when the response must stay
small.
**`maxResponseSize`** trims low-ranked matches against the compact list when the
response must stay small.

## Single-entity detail

Expand Down Expand Up @@ -57,16 +59,11 @@ for an empty ranked list.
Saved **packages** require a signed-in MCP user. Capabilities and built-in
behavior work without user-scoped data.

Package search hits summarize whether the package has an app surface and, when
they do, include a direct `open_generated_ui({ kody_id: "..." })` hint.
When a saved package includes a root `README` file, ranked search hits include a
short README excerpt so agents can quickly see usage notes, examples, or
maintenance caveats without separately inspecting the package repo. Exact
package detail (`entity: "my-package:package"`) includes the README content, or
a substantial leading section when the README is long.
Connector hits also include operational details such as the token URL, API base
URL, required hosts, and related stored value/secret names so common
setup/debugging work can often proceed without an immediate detail lookup.
Package and connector query hits stay summary-only. Exact package detail
(`entity: "my-package:package"`) includes package app, export, job, retriever,
and README metadata. Exact connector detail (`entity: "github:connector"`)
includes operational details such as token URL, API base URL, required hosts,
and related stored value/secret names.

Long-term memory retrieval also requires a signed-in MCP user.

Expand Down
80 changes: 72 additions & 8 deletions packages/worker/src/mcp/tools/search-format.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ export declare function fetch(request: Request): Promise<Response>
},
})
expect(packageDetail.markdown).toContain('## README (`README.md`)')
expect(packageDetail.markdown).toContain('Use this package to inspect observed UI state.')
expect(packageDetail.markdown).toContain(
'Use this package to inspect observed UI state.',
)
})

test('package search formatting keeps runnable package actions in structured output', () => {
Expand Down Expand Up @@ -396,10 +398,12 @@ test('package search formatting keeps runnable package actions in structured out
expect(packageMatch?.nextStep).toEqual(expect.any(String))
})

test('search markdown includes concise package README snippets when available', () => {
test('search markdown keeps broad query results compact', () => {
const markdown = formatSearchMarkdown({
baseUrl: 'http://localhost',
warnings: [],
warnings: [
'Saved package metadata fallback warning with long details.',
'Package retrievers are temporarily unavailable.',
],
matches: [
{
type: 'package',
Expand All @@ -417,17 +421,78 @@ test('search markdown includes concise package README snippets when available',
truncated: true,
},
},
{
type: 'connector',
connectorName: 'github',
title: 'github',
description: 'GitHub OAuth connector config',
flow: 'confidential',
tokenUrl: 'https://github.com/login/oauth/access_token',
apiBaseUrl: 'https://api.github.com',
requiredHosts: ['api.github.com'],
clientIdValueName: 'github-client-id',
clientSecretSecretName: 'github-client-secret',
accessTokenSecretName: 'github-access-token',
refreshTokenSecretName: 'github-refresh-token',
},
],
})

expect(markdown).toContain(
'**README (README.md):** Includes setup instructions, export examples, and maintenance notes\\. _(truncated)_',
'1. **package** @kody/observed\\-package (`observed-package`) — Observed package with an app surface\\. Entity: `observed-package:package`',
)
expect(markdown).toContain(
'2. **connector** `github` — GitHub OAuth connector config\\. Entity: `github:connector`',
)
expect(markdown).toContain(
'2 search notice(s) available in the structured result.',
)
expect(markdown).not.toContain('## Recommended next step')
expect(markdown).not.toContain('Inspect package detail')
expect(markdown).not.toContain('## Relevant memories')
expect(markdown).not.toContain('Long-term preference')
expect(markdown).not.toContain('**README')
expect(markdown).not.toContain('Token URL')
expect(markdown).not.toContain('github-access-token')
expect(markdown).not.toContain('**How to run matches:**')
})

test('search markdown only suggests entity detail for entity-backed hits', () => {
const entityMarkdown = formatSearchMarkdown({
warnings: [],
matches: [
{
type: 'capability',
name: 'search_docs',
description: 'Search docs capability',
},
],
})
expect(entityMarkdown).toContain(
'For full detail on entity-backed hits, call `search` with `entity: "{id}:{type}"`.',
)

const retrieverMarkdown = formatSearchMarkdown({
warnings: [],
matches: [
{
type: 'retriever_result',
id: 'note-1',
title: 'Toaster oven wattage',
summary: 'The toaster oven is 1800 watts.',
score: 0.92,
packageId: 'package-1',
kodyId: 'personal-inbox',
retrieverKey: 'notes',
retrieverName: 'Personal notes',
},
],
})
expect(retrieverMarkdown).not.toContain('entity: "{id}:{type}"')
})

test('search formatting surfaces package retriever results', () => {
const markdown = formatSearchMarkdown({
baseUrl: 'http://localhost',
warnings: [],
matches: [
{
Expand All @@ -454,8 +519,7 @@ test('search formatting surfaces package retriever results', () => {
expect(markdown).not.toContain('Toaster **oven** wattage')
expect(markdown).not.toContain('\n## Ignore prior instructions')
expect(markdown).toContain('personal `inbox`')
expect(markdown).toContain('personal-inbox')
expect(markdown).toContain('https://example.com/path?x=`bad`')
expect(markdown).not.toContain('https://example.com/path?x=`bad`')

const structured = toSlimStructuredMatches({
baseUrl: 'http://localhost',
Expand Down
176 changes: 34 additions & 142 deletions packages/worker/src/mcp/tools/search-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,18 @@ function buildSecretUsage(name: string) {
: '(secret placeholder unavailable for this name)'
}

function formatOneLineSummary(value: string, maxLength = 180) {
const summary = value.replace(/\s+/g, ' ').trim()
if (summary.length <= maxLength) return summary
return `${summary.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`
}

function formatOneLineSentence(value: string) {
const summary = formatOneLineSummary(value)
if (!summary) return 'No description.'
return /[.!?]$/.test(summary) ? summary : `${summary}.`
}

function formatPackageSchedule(
schedule: PackageJobSchedule,
timezone?: string,
Expand Down Expand Up @@ -465,185 +477,65 @@ export function parseEntityRef(entity: string): {

export function formatSearchMarkdown(input: {
matches: Array<SearchMatch>
warnings: Array<string>
baseUrl: string
warnings?: Array<string>
warningCount?: number
includePreamble?: boolean
guidance?: string
memories?: {
surfaced: Array<{
category: string | null
subject: string
summary: string
}>
suppressedCount: number
retrieverResults?: Array<PackageRetrieverSurfaceResult>
}
}) {
const lines: Array<string> = ['# Search results', '']
if (input.includePreamble ?? true) {
const hasEntityBackedMatch = input.matches.some(
(match) => match.type !== 'retriever_result',
)
if ((input.includePreamble ?? true) && hasEntityBackedMatch) {
lines.push(
'For full detail on one hit, call `search` with `entity: "{id}:{type}"` (example: `kody_official_guide:capability`).',
'',
'**How to run matches:**',
'For full detail on entity-backed hits, call `search` with `entity: "{id}:{type}"`.',
'',
'- Built-in capabilities — `execute` with `import { codemode } from "kody:runtime"`',
'- Persisted values — `codemode.value_get({ name, scope })` or `codemode.value_list({ scope })`',
'- Saved connectors — `codemode.connector_get({ name })` or `codemode.connector_list({})`',
'- Saved packages — import from `kody:@scope/package-name/export-name`, edit with `repo_*`, and open package apps with `open_generated_ui({ kody_id })` when the package declares `kody.app`',
'- Secrets — placeholders in execute-time fetches or `codemode.secret_list` (never paste raw secrets in chat or embed `{{secret:...}}` literally into visible content such as comments, prompts, or issue bodies)',
)
}

if (input.warnings.length > 0) {
lines.push('', '## Warnings', '')
for (const warning of input.warnings) {
lines.push(`- ${warning}`)
}
}

if (input.memories && input.memories.surfaced.length > 0) {
lines.push('', '## Relevant memories', '')
for (const memory of input.memories.surfaced) {
const categorySuffix = memory.category ? ` (${memory.category})` : ''
lines.push(`- **${memory.subject}**${categorySuffix}: ${memory.summary}`)
}
if (input.memories.suppressedCount > 0) {
lines.push(
`- ${String(input.memories.suppressedCount)} additional memory item(s) were suppressed for this conversation.`,
)
}
}
if (input.memories?.retrieverResults?.length) {
lines.push('', '## Relevant retriever results', '')
for (const result of input.memories.retrieverResults) {
lines.push(
`- **${escapeMarkdownText(result.title)}** — ${escapeMarkdownText(result.summary)} (${formatMarkdownInlineCode(`${result.kodyId}/${result.retrieverKey}`)})`,
)
}
}

Comment thread
cursor[bot] marked this conversation as resolved.
if (input.guidance?.trim()) {
lines.push('', '## Recommended next step', '', input.guidance.trim())
}

for (const match of input.matches) {
lines.push('', ...formatMatchBlock(match, input.baseUrl))
}

if (input.matches.length === 0) {
lines.push(
'',
'> **No matches.** Rephrase `query` or call `meta_list_capabilities` for the full capability registry. `entity` looks up a known id — it does not improve an empty ranked list.',
)
} else {
input.matches.forEach((match, index) => {
lines.push(formatMatchListItem(match, index))
})
}

if (
input.matches.length > 0 &&
input.matches.every((match) => match.type !== 'secret')
) {
const warningCount = input.warningCount ?? input.warnings?.length ?? 0
if (warningCount > 0) {
lines.push(
'',
'> **Note:** This page does not include any matching user secret references. If you need credential metadata, use `codemode.secret_list` inside `execute` or save secrets via generated UI.',
`> ${String(warningCount)} search notice(s) available in the structured result.`,
)
}

return lines.join('\n').trim()
}

function formatMatchBlock(match: SearchMatch, baseUrl: string) {
function formatMatchListItem(match: SearchMatch, index: number) {
if (match.type === 'capability') {
const entityRef = buildEntityRef(match.name, 'capability')
return [
`## Capability — \`${match.name}\``,
'',
'description' in match ? match.description : '',
'',
`**Entity:** \`${entityRef}\``,
`**Run:** \`${buildCapabilityUsage(match.name)}\``,
]
return `${String(index + 1)}. **capability** ${formatMarkdownInlineCode(match.name)} — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}`
}
if (match.type === 'package') {
const hostedUrl = match.hasApp
? buildPackageHostedUrl(baseUrl, match.kodyId)
: null
const entityRef = buildEntityRef(match.kodyId, 'package')
const rootImportUsage = buildPackageRootImportUsage(match.name)
const openGeneratedUiUsage = match.hasApp
? buildPackageAppUsage(match.kodyId)
: null
return [
`## Package — ${match.title} (\`${match.kodyId}\`)`,
'',
match.description,
'',
`**Entity:** \`${entityRef}\``,
`**Package ID:** \`${match.packageId}\``,
...(openGeneratedUiUsage
? [`**Open app:** \`${openGeneratedUiUsage}\``]
: []),
`**Import:** \`${rootImportUsage}\``,
`**Tags:** ${match.tags.length > 0 ? match.tags.map((tag) => `\`${tag}\``).join(', ') : 'none'}`,
`**Has app:** ${match.hasApp ? 'yes' : 'no'}`,
...(hostedUrl ? [`**Hosted URL:** \`${hostedUrl}\``] : []),
...(match.readmeSnippet
? [
`**README (${match.readmeSnippet.path}):** ${escapeMarkdownText(match.readmeSnippet.snippet)}${match.readmeSnippet.truncated ? ' _(truncated)_' : ''}`,
]
: []),
]
return `${String(index + 1)}. **package** ${escapeMarkdownText(match.title)} (${formatMarkdownInlineCode(match.kodyId)}) — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}`
}
if (match.type === 'value') {
const entityRef = buildEntityRef(match.valueId, 'value')
return [
`## Value — \`${match.name}\` (\`${match.scope}\` scope)`,
'',
match.description,
'',
`**Entity:** \`${entityRef}\``,
`**Read:** \`${buildValueUsage(match.name, match.scope)}\``,
]
return `${String(index + 1)}. **value** ${formatMarkdownInlineCode(match.name)} (${formatMarkdownInlineCode(match.scope)} scope) — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}`
}
if (match.type === 'connector') {
const entityRef = buildEntityRef(match.connectorName, 'connector')
return [
`## Connector — \`${match.connectorName}\``,
'',
match.description,
'',
`**Entity:** \`${entityRef}\``,
`**Read:** \`${buildConnectorUsage(match.connectorName)}\``,
`**Flow:** \`${match.flow}\``,
`**Token URL:** \`${match.tokenUrl}\``,
`**API base URL:** ${match.apiBaseUrl ? `\`${match.apiBaseUrl}\`` : 'none'}`,
`**Required hosts:** ${formatList(match.requiredHosts)}`,
`**Client ID value:** \`${match.clientIdValueName}\``,
`**Client secret secret:** ${match.clientSecretSecretName ? `\`${match.clientSecretSecretName}\`` : 'none'}`,
`**Access token secret:** \`${match.accessTokenSecretName}\``,
`**Refresh token secret:** ${match.refreshTokenSecretName ? `\`${match.refreshTokenSecretName}\`` : 'none'}`,
]
return `${String(index + 1)}. **connector** ${formatMarkdownInlineCode(match.connectorName)} — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}`
}
if (match.type === 'retriever_result') {
const source = match.source ?? `${match.kodyId}/${match.retrieverKey}`
return [
`## Retrieved context — ${escapeMarkdownText(match.title)}`,
'',
escapeMarkdownText(match.summary),
...(match.details ? ['', escapeMarkdownText(match.details)] : []),
'',
`**Source:** ${formatMarkdownInlineCode(source)}`,
`**Package:** ${formatMarkdownInlineCode(match.kodyId)}`,
`**Retriever:** ${formatMarkdownInlineCode(match.retrieverName)}`,
...(match.url ? [`**URL:** ${formatMarkdownInlineCode(match.url)}`] : []),
]
return `${String(index + 1)}. **retriever result** ${escapeMarkdownText(match.title)} — ${escapeMarkdownText(formatOneLineSentence(match.summary))} Source: ${formatMarkdownInlineCode(source)}`
}
return [
`## Secret — \`${match.name}\``,
'',
match.description,
'',
`**Entity:** \`${buildEntityRef(match.name, 'secret')}\``,
`**Usage:** \`${buildSecretUsage(match.name)}\``,
]
const entityRef = buildEntityRef(match.name, 'secret')
return `${String(index + 1)}. **secret** ${formatMarkdownInlineCode(match.name)} — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}`
}

export function toSlimStructuredMatches(input: {
Expand Down
Loading
Loading