Skip to content

Commit 8cff26c

Browse files
authored
Fix agent readiness discovery endpoints (#768)
1 parent 88c9a5e commit 8cff26c

6 files changed

Lines changed: 157 additions & 13 deletions

File tree

.github/workflows/deploy-site.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,10 @@ jobs:
148148
restore-keys: |
149149
playwright-${{ runner.os }}-node${{ env.NODE_VERSION }}-
150150
151-
- name: 🎬 Setup ffmpeg
152-
uses: FedericoCarboni/setup-ffmpeg@v3
151+
- name: 🎬 Install ffmpeg
152+
run: |
153+
sudo apt-get update
154+
sudo apt-get install -y ffmpeg
153155
154156
- name: 🌐 Install Playwright Browsers
155157
if: steps.playwright-cache.outputs.cache-hit != 'true'
Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
11
// @vitest-environment node
2+
import { createHash } from 'node:crypto'
23
import { expect, test } from 'vitest'
4+
import {
5+
getContentSearchSkillDigest,
6+
getContentSearchSkillMarkdown,
7+
} from '#app/utils/agent-skills.ts'
8+
import { loader as skillIndexLoader } from '../agent-skills.index[.]json.ts'
9+
import { loader as skillMarkdownLoader } from '../agent-skills.content-search.SKILL[.]md.ts'
10+
import { type Route as IndexRoute } from '../+types/agent-skills.index[.]json'
311

4-
import { loader } from '../agent-skills.index[.]json.ts'
12+
function createIndexLoaderArgs(request: Request): IndexRoute.LoaderArgs {
13+
return {
14+
context: {},
15+
params: {},
16+
request,
17+
unstable_pattern: '/.well-known/agent-skills/index.json',
18+
}
19+
}
520

6-
test('loader returns the placeholder agent skills discovery index', async () => {
7-
const response = await loader()
21+
test('loader returns a populated agent skills discovery index', async () => {
22+
const request = new Request(
23+
'https://kentcdodds.com/.well-known/agent-skills/index.json',
24+
{
25+
headers: { host: 'kentcdodds.com' },
26+
},
27+
)
28+
const response = await skillIndexLoader(createIndexLoaderArgs(request))
829
const payload = await response.json()
930

1031
expect(response.headers.get('Content-Type')).toContain('application/json')
32+
expect(response.headers.get('Cache-Control')).toBe('public, max-age=3600')
1133
expect(payload).toEqual({
1234
$schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
13-
skills: [],
35+
skills: [
36+
{
37+
name: 'content-search',
38+
type: 'skill-md',
39+
description:
40+
'Find relevant kentcdodds.com content and retrieve it from canonical, agent-friendly endpoints.',
41+
url: 'https://kentcdodds.com/.well-known/agent-skills/content-search/SKILL.md',
42+
digest: getContentSearchSkillDigest(),
43+
},
44+
],
1445
})
1546
})
47+
48+
test('skill markdown route serves the same artifact referenced by the index', async () => {
49+
const response = await skillMarkdownLoader()
50+
const body = await response.text()
51+
const expectedContentDigest = `sha-256=:${createHash('sha256')
52+
.update(getContentSearchSkillMarkdown(), 'utf8')
53+
.digest('base64')}:`
54+
55+
expect(response.headers.get('Content-Type')).toContain('text/markdown')
56+
expect(response.headers.get('Content-Digest')).toBe(expectedContentDigest)
57+
expect(body).toBe(getContentSearchSkillMarkdown())
58+
expect(body).toContain('https://kentcdodds.com/search?query=<terms>')
59+
expect(body).toContain('Accept: text/markdown')
60+
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {
2+
getContentSearchSkillContentDigest,
3+
getContentSearchSkillMarkdown,
4+
} from '#app/utils/agent-skills.ts'
5+
6+
export function loader() {
7+
const markdown = getContentSearchSkillMarkdown()
8+
9+
return new Response(markdown, {
10+
headers: {
11+
'Cache-Control': 'public, max-age=3600',
12+
'Content-Digest': getContentSearchSkillContentDigest(),
13+
'Content-Length': String(Buffer.byteLength(markdown)),
14+
'Content-Type': 'text/markdown; charset=utf-8',
15+
},
16+
})
17+
}

services/site/app/routes/[.]well-known/agent-skills.index[.]json.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
const agentSkillsDiscoveryDocument = {
2-
$schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
3-
skills: [],
4-
} as const
1+
import { getAgentSkillsDiscoveryDocument } from '#app/utils/agent-skills.ts'
2+
import { type Route } from './+types/agent-skills.index[.]json'
53

6-
export function loader() {
7-
const body = JSON.stringify(agentSkillsDiscoveryDocument)
4+
export function loader({ request }: Route.LoaderArgs) {
5+
const body = JSON.stringify(getAgentSkillsDiscoveryDocument(request))
86

97
return new Response(body, {
108
headers: {
9+
'Cache-Control': 'public, max-age=3600',
1110
'Content-Length': String(Buffer.byteLength(body)),
1211
'Content-Type': 'application/json',
1312
},
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { createHash } from 'node:crypto'
2+
import { getDomainUrl } from './misc.ts'
3+
4+
const agentSkillsDiscoverySchemaUrl =
5+
'https://schemas.agentskills.io/discovery/0.2.0/schema.json'
6+
7+
function createSha256Digest(contents: string) {
8+
return `sha256:${createHash('sha256').update(contents, 'utf8').digest('hex')}`
9+
}
10+
11+
function createContentDigestHeader(contents: string) {
12+
return `sha-256=:${createHash('sha256').update(contents, 'utf8').digest('base64')}:`
13+
}
14+
15+
const contentSearchSkillMarkdown = `---
16+
name: content-search
17+
description: Search kentcdodds.com content quickly and prefer canonical, agent-friendly sources.
18+
---
19+
20+
# Search kentcdodds.com content
21+
22+
Use this skill when you need articles, podcast episodes, or other published content from kentcdodds.com.
23+
24+
## Preferred workflow
25+
26+
1. Query \`https://kentcdodds.com/search?query=<terms>\` first and review the highest-signal results.
27+
2. Fetch the canonical page that best matches the request.
28+
3. If the request might involve broader site discovery or you need alternate candidates, fetch \`https://kentcdodds.com/sitemap.xml\`.
29+
4. When reading a page, prefer \`Accept: text/markdown\` so the site can return markdown instead of HTML when available.
30+
31+
## Selection heuristics
32+
33+
- Prefer canonical \`https://kentcdodds.com\` URLs over mirrors, embeds, or syndicated copies.
34+
- Prefer \`/blog/\` articles when the user wants detailed written guidance.
35+
- Use the search results page to shortlist candidates before opening full pages.
36+
- If several results match, prefer the newest relevant post unless the user asks for historical context.
37+
`
38+
39+
const contentSearchSkill = {
40+
name: 'content-search',
41+
type: 'skill-md',
42+
description:
43+
'Find relevant kentcdodds.com content and retrieve it from canonical, agent-friendly endpoints.',
44+
path: '/.well-known/agent-skills/content-search/SKILL.md',
45+
markdown: contentSearchSkillMarkdown,
46+
digest: createSha256Digest(contentSearchSkillMarkdown),
47+
contentDigest: createContentDigestHeader(contentSearchSkillMarkdown),
48+
} as const
49+
50+
const agentSkillDefinitions = [contentSearchSkill] as const
51+
export function getAgentSkillsDiscoveryDocument(request: Request) {
52+
const origin = getDomainUrl(request)
53+
54+
return {
55+
$schema: agentSkillsDiscoverySchemaUrl,
56+
skills: agentSkillDefinitions.map((skill) => ({
57+
name: skill.name,
58+
type: skill.type,
59+
description: skill.description,
60+
url: `${origin}${skill.path}`,
61+
digest: skill.digest,
62+
})),
63+
} as const
64+
}
65+
66+
export function getContentSearchSkillMarkdown() {
67+
return contentSearchSkill.markdown
68+
}
69+
70+
export function getContentSearchSkillDigest() {
71+
return contentSearchSkill.digest
72+
}
73+
74+
export function getContentSearchSkillContentDigest() {
75+
return contentSearchSkill.contentDigest
76+
}

services/site/public/robots.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
# Signals:
2+
# search - build a search index and show results
3+
# ai-input - use content as live input for AI answers
4+
# ai-train - train or fine-tune AI models
5+
16
User-agent: *
2-
Content-Signal: ai-train=yes, search=yes, ai-input=yes
7+
Content-Signal: ai-train=no, search=yes, ai-input=no
38
Allow: /
49

510
Sitemap: https://kentcdodds.com/sitemap.xml

0 commit comments

Comments
 (0)