agents.yaml 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.
- package/AGENTS.md +2 -0
- package/README.md +18 -0
- package/dist/index.mjs +54 -20
- package/package.json +3 -1
- package/src/agents-file.ts +185 -170
- package/src/discover.bench.ts +219 -0
- package/src/discover.test.ts +164 -105
- package/src/discover.ts +163 -123
- package/src/index.ts +4 -4
- package/src/paths.ts +13 -8
- package/src/run.ts +296 -198
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { performance } from "node:perf_hooks"
|
|
5
|
+
import { discoverAgentDocuments, type DiscoverOptions } from "./discover.ts"
|
|
6
|
+
|
|
7
|
+
type BenchmarkCase = {
|
|
8
|
+
name: string
|
|
9
|
+
options?: DiscoverOptions
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type BenchmarkResult = {
|
|
13
|
+
name: string
|
|
14
|
+
docs: number
|
|
15
|
+
medianMs: number
|
|
16
|
+
minMs: number
|
|
17
|
+
maxMs: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const hiddenDirectories = readPositiveInteger("AGENTS_BENCH_HIDDEN_DIRS", 250)
|
|
21
|
+
const filesPerHiddenDirectory = readPositiveInteger(
|
|
22
|
+
"AGENTS_BENCH_FILES_PER_HIDDEN_DIR",
|
|
23
|
+
8,
|
|
24
|
+
)
|
|
25
|
+
const visiblePackages = readPositiveInteger("AGENTS_BENCH_VISIBLE_PACKAGES", 25)
|
|
26
|
+
const iterations = readPositiveInteger("AGENTS_BENCH_ITERATIONS", 7)
|
|
27
|
+
const warmups = readPositiveInteger("AGENTS_BENCH_WARMUPS", 1)
|
|
28
|
+
|
|
29
|
+
const cases: BenchmarkCase[] = [
|
|
30
|
+
{ name: "default" },
|
|
31
|
+
{
|
|
32
|
+
name: "include dot directories",
|
|
33
|
+
options: { includeDotDirectories: true },
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-bench-"))
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await createFixture(root)
|
|
41
|
+
const results: BenchmarkResult[] = []
|
|
42
|
+
|
|
43
|
+
for (const benchmarkCase of cases) {
|
|
44
|
+
for (let index = 0; index < warmups; index += 1) {
|
|
45
|
+
await discoverAgentDocuments(root, benchmarkCase.options)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
results.push(await runCase(root, benchmarkCase))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
printResults(results)
|
|
52
|
+
} finally {
|
|
53
|
+
await rm(root, { recursive: true, force: true })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function createFixture(root: string): Promise<void> {
|
|
57
|
+
await writeFile(
|
|
58
|
+
path.join(root, "agents.yaml"),
|
|
59
|
+
"version: 1\n\ndocuments: []\n",
|
|
60
|
+
"utf8",
|
|
61
|
+
)
|
|
62
|
+
await writeFile(path.join(root, "AGENTS.md"), "# Root guidance\n", "utf8")
|
|
63
|
+
|
|
64
|
+
await createDirectDependency(root)
|
|
65
|
+
await createVisibleProjectDocuments(root)
|
|
66
|
+
await createHiddenCache(root)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function createDirectDependency(root: string): Promise<void> {
|
|
70
|
+
const dependencyPath = path.join(root, "node_modules", "direct-lib")
|
|
71
|
+
await mkdir(dependencyPath, { recursive: true })
|
|
72
|
+
await writeFile(
|
|
73
|
+
path.join(dependencyPath, "AGENTS.md"),
|
|
74
|
+
"# Direct dependency guidance\n",
|
|
75
|
+
"utf8",
|
|
76
|
+
)
|
|
77
|
+
await writeFile(
|
|
78
|
+
path.join(dependencyPath, "package.json"),
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
name: "direct-lib",
|
|
81
|
+
description: "Direct fixture dependency.",
|
|
82
|
+
}),
|
|
83
|
+
"utf8",
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function createVisibleProjectDocuments(root: string): Promise<void> {
|
|
88
|
+
for (let index = 0; index < visiblePackages; index += 1) {
|
|
89
|
+
const packagePath = path.join(root, "packages", `visible-${index}`)
|
|
90
|
+
await mkdir(packagePath, { recursive: true })
|
|
91
|
+
await writeFile(
|
|
92
|
+
path.join(packagePath, "AGENTS.md"),
|
|
93
|
+
"# Visible project guidance\n",
|
|
94
|
+
"utf8",
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function createHiddenCache(root: string): Promise<void> {
|
|
100
|
+
for (let index = 0; index < hiddenDirectories; index += 1) {
|
|
101
|
+
const cachePath = path.join(root, ".cache", `entry-${index}`, "nested")
|
|
102
|
+
await mkdir(cachePath, { recursive: true })
|
|
103
|
+
await writeFile(
|
|
104
|
+
path.join(cachePath, "AGENTS.md"),
|
|
105
|
+
"# Hidden cache guidance\n",
|
|
106
|
+
"utf8",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for (
|
|
110
|
+
let fileIndex = 0;
|
|
111
|
+
fileIndex < filesPerHiddenDirectory;
|
|
112
|
+
fileIndex += 1
|
|
113
|
+
) {
|
|
114
|
+
await writeFile(
|
|
115
|
+
path.join(cachePath, `file-${fileIndex}.txt`),
|
|
116
|
+
"x".repeat(100),
|
|
117
|
+
"utf8",
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runCase(
|
|
124
|
+
root: string,
|
|
125
|
+
benchmarkCase: BenchmarkCase,
|
|
126
|
+
): Promise<BenchmarkResult> {
|
|
127
|
+
const durations: number[] = []
|
|
128
|
+
let docs = 0
|
|
129
|
+
|
|
130
|
+
for (let index = 0; index < iterations; index += 1) {
|
|
131
|
+
const start = performance.now()
|
|
132
|
+
const discovered = await discoverAgentDocuments(root, benchmarkCase.options)
|
|
133
|
+
const duration = performance.now() - start
|
|
134
|
+
|
|
135
|
+
docs = discovered.length
|
|
136
|
+
durations.push(duration)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sorted = [...durations].sort((left, right) => left - right)
|
|
140
|
+
const medianMs = sorted[Math.floor(sorted.length / 2)]
|
|
141
|
+
const minMs = sorted[0]
|
|
142
|
+
const maxMs = sorted[sorted.length - 1]
|
|
143
|
+
|
|
144
|
+
if (medianMs === undefined || minMs === undefined || maxMs === undefined) {
|
|
145
|
+
throw new Error("Benchmark did not record any durations")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
name: benchmarkCase.name,
|
|
150
|
+
docs,
|
|
151
|
+
medianMs,
|
|
152
|
+
minMs,
|
|
153
|
+
maxMs,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printResults(results: BenchmarkResult[]): void {
|
|
158
|
+
console.log("agents discover benchmark")
|
|
159
|
+
console.log(
|
|
160
|
+
[
|
|
161
|
+
`fixture: hiddenDirectories=${hiddenDirectories}`,
|
|
162
|
+
`filesPerHiddenDirectory=${filesPerHiddenDirectory}`,
|
|
163
|
+
`visiblePackages=${visiblePackages}`,
|
|
164
|
+
`iterations=${iterations}`,
|
|
165
|
+
`warmups=${warmups}`,
|
|
166
|
+
].join(", "),
|
|
167
|
+
)
|
|
168
|
+
console.log("")
|
|
169
|
+
console.log(
|
|
170
|
+
[
|
|
171
|
+
pad("case", 24),
|
|
172
|
+
pad("docs", 8),
|
|
173
|
+
pad("median", 10),
|
|
174
|
+
pad("min", 10),
|
|
175
|
+
pad("max", 10),
|
|
176
|
+
].join(""),
|
|
177
|
+
)
|
|
178
|
+
console.log("-".repeat(62))
|
|
179
|
+
|
|
180
|
+
for (const result of results) {
|
|
181
|
+
console.log(
|
|
182
|
+
[
|
|
183
|
+
pad(result.name, 24),
|
|
184
|
+
pad(String(result.docs), 8),
|
|
185
|
+
pad(formatMs(result.medianMs), 10),
|
|
186
|
+
pad(formatMs(result.minMs), 10),
|
|
187
|
+
pad(formatMs(result.maxMs), 10),
|
|
188
|
+
].join(""),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const defaultResult = results.find((result) => result.name === "default")
|
|
193
|
+
const includeDotResult = results.find(
|
|
194
|
+
(result) => result.name === "include dot directories",
|
|
195
|
+
)
|
|
196
|
+
if (defaultResult && includeDotResult && defaultResult.medianMs > 0) {
|
|
197
|
+
const ratio = includeDotResult.medianMs / defaultResult.medianMs
|
|
198
|
+
console.log("")
|
|
199
|
+
console.log(`include dot directories median: ${ratio.toFixed(1)}x default`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readPositiveInteger(name: string, fallback: number): number {
|
|
204
|
+
const raw = process.env[name]
|
|
205
|
+
if (!raw) return fallback
|
|
206
|
+
|
|
207
|
+
const value = Number.parseInt(raw, 10)
|
|
208
|
+
if (Number.isInteger(value) && value > 0) return value
|
|
209
|
+
|
|
210
|
+
throw new Error(`${name} must be a positive integer`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatMs(value: number): string {
|
|
214
|
+
return `${value.toFixed(1)}ms`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function pad(value: string, width: number): string {
|
|
218
|
+
return value.padEnd(width, " ")
|
|
219
|
+
}
|
package/src/discover.test.ts
CHANGED
|
@@ -1,114 +1,173 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from
|
|
2
|
-
import { tmpdir } from
|
|
3
|
-
import path from
|
|
4
|
-
import { afterEach, describe, expect, it } from
|
|
5
|
-
import { addDocuments, loadAgentsFile } from
|
|
6
|
-
import { discoverAgentDocuments } from
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
5
|
+
import { addDocuments, loadAgentsFile } from "./agents-file.ts"
|
|
6
|
+
import { discoverAgentDocuments } from "./discover.ts"
|
|
7
7
|
|
|
8
8
|
const tempRoots: string[] = []
|
|
9
9
|
|
|
10
|
-
describe(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
10
|
+
describe("agents.yaml dependency discovery", () => {
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await Promise.all(
|
|
13
|
+
tempRoots
|
|
14
|
+
.splice(0)
|
|
15
|
+
.map((root) => rm(root, { recursive: true, force: true })),
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("discovers a direct dependency with an AGENTS.md and adds it to agents.yaml", async () => {
|
|
20
|
+
const root = await createTempProject()
|
|
21
|
+
const dependencyAgentsPath = path.join(
|
|
22
|
+
root,
|
|
23
|
+
"node_modules",
|
|
24
|
+
"direct-lib",
|
|
25
|
+
"AGENTS.md",
|
|
26
|
+
)
|
|
27
|
+
await mkdir(path.dirname(dependencyAgentsPath), { recursive: true })
|
|
28
|
+
await writeFile(
|
|
29
|
+
dependencyAgentsPath,
|
|
30
|
+
"# Direct dependency guidance\n",
|
|
31
|
+
"utf8",
|
|
32
|
+
)
|
|
33
|
+
await writeFile(
|
|
34
|
+
path.join(root, "node_modules", "direct-lib", "package.json"),
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
name: "direct-lib",
|
|
37
|
+
description: "Direct fixtures for testing agent guidance.",
|
|
38
|
+
}),
|
|
39
|
+
"utf8",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const discovered = await discoverAgentDocuments(root)
|
|
43
|
+
expect(discovered).toEqual([
|
|
44
|
+
{
|
|
45
|
+
path: "./node_modules/direct-lib/AGENTS.md",
|
|
46
|
+
description: "Direct fixtures for testing agent guidance.",
|
|
47
|
+
},
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
await addDocuments(root, [discovered[0]!])
|
|
51
|
+
|
|
52
|
+
await expect(loadAgentsFile(root)).resolves.toEqual({
|
|
53
|
+
version: 1,
|
|
54
|
+
documents: [
|
|
55
|
+
{
|
|
56
|
+
path: "./node_modules/direct-lib/AGENTS.md",
|
|
57
|
+
description: "Direct fixtures for testing agent guidance.",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await addDocuments(root, [{ path: "./node_modules/direct-lib/AGENTS.md" }])
|
|
63
|
+
await expect(loadAgentsFile(root)).resolves.toEqual({
|
|
64
|
+
version: 1,
|
|
65
|
+
documents: [
|
|
66
|
+
{
|
|
67
|
+
path: "./node_modules/direct-lib/AGENTS.md",
|
|
68
|
+
description: "Direct fixtures for testing agent guidance.",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("does not discover an indirect dependency that has an AGENTS.md", async () => {
|
|
75
|
+
const root = await createTempProject()
|
|
76
|
+
const directAgentsPath = path.join(
|
|
77
|
+
root,
|
|
78
|
+
"node_modules",
|
|
79
|
+
"direct-lib",
|
|
80
|
+
"AGENTS.md",
|
|
81
|
+
)
|
|
82
|
+
const indirectAgentsPath = path.join(
|
|
83
|
+
root,
|
|
84
|
+
"node_modules",
|
|
85
|
+
"direct-lib",
|
|
86
|
+
"node_modules",
|
|
87
|
+
"indirect-lib",
|
|
88
|
+
"AGENTS.md",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
await mkdir(path.dirname(directAgentsPath), { recursive: true })
|
|
92
|
+
await mkdir(path.dirname(indirectAgentsPath), { recursive: true })
|
|
93
|
+
await writeFile(directAgentsPath, "# Direct dependency guidance\n", "utf8")
|
|
94
|
+
await writeFile(
|
|
95
|
+
indirectAgentsPath,
|
|
96
|
+
"# Indirect dependency guidance\n",
|
|
97
|
+
"utf8",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
await expect(discoverAgentDocuments(root)).resolves.toEqual([
|
|
101
|
+
{ path: "./node_modules/direct-lib/AGENTS.md" },
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("includes scoped package descriptions when present", async () => {
|
|
106
|
+
const root = await createTempProject()
|
|
107
|
+
const dependencyPath = path.join(
|
|
108
|
+
root,
|
|
109
|
+
"node_modules",
|
|
110
|
+
"@scope",
|
|
111
|
+
"direct-lib",
|
|
112
|
+
)
|
|
113
|
+
await mkdir(dependencyPath, { recursive: true })
|
|
114
|
+
await writeFile(
|
|
115
|
+
path.join(dependencyPath, "AGENTS.md"),
|
|
116
|
+
"# Scoped guidance\n",
|
|
117
|
+
"utf8",
|
|
118
|
+
)
|
|
119
|
+
await writeFile(
|
|
120
|
+
path.join(dependencyPath, "package.json"),
|
|
121
|
+
JSON.stringify({
|
|
122
|
+
name: "@scope/direct-lib",
|
|
123
|
+
description: "Scoped package guidance breadcrumbs.",
|
|
124
|
+
}),
|
|
125
|
+
"utf8",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
await expect(discoverAgentDocuments(root)).resolves.toEqual([
|
|
129
|
+
{
|
|
130
|
+
path: "./node_modules/@scope/direct-lib/AGENTS.md",
|
|
131
|
+
description: "Scoped package guidance breadcrumbs.",
|
|
132
|
+
},
|
|
133
|
+
])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("skips dot-prefixed directories by default", async () => {
|
|
137
|
+
const root = await createTempProject()
|
|
138
|
+
const hiddenAgentsPath = path.join(root, ".cache", "AGENTS.md")
|
|
139
|
+
await mkdir(path.dirname(hiddenAgentsPath), { recursive: true })
|
|
140
|
+
await writeFile(hiddenAgentsPath, "# Hidden cache guidance\n", "utf8")
|
|
141
|
+
|
|
142
|
+
await expect(discoverAgentDocuments(root)).resolves.toEqual([])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("can include dot-prefixed directories when requested", async () => {
|
|
146
|
+
const root = await createTempProject()
|
|
147
|
+
const hiddenAgentsPath = path.join(root, ".cache", "AGENTS.md")
|
|
148
|
+
await mkdir(path.dirname(hiddenAgentsPath), { recursive: true })
|
|
149
|
+
await writeFile(hiddenAgentsPath, "# Hidden cache guidance\n", "utf8")
|
|
150
|
+
|
|
151
|
+
await expect(
|
|
152
|
+
discoverAgentDocuments(root, { includeDotDirectories: true }),
|
|
153
|
+
).resolves.toEqual([{ path: "./.cache/AGENTS.md" }])
|
|
154
|
+
})
|
|
104
155
|
})
|
|
105
156
|
|
|
106
157
|
async function createTempProject(): Promise<string> {
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-"))
|
|
159
|
+
tempRoots.push(root)
|
|
109
160
|
|
|
110
|
-
|
|
111
|
-
|
|
161
|
+
await writeFile(
|
|
162
|
+
path.join(root, "agents.yaml"),
|
|
163
|
+
"version: 1\n\ndocuments: []\n",
|
|
164
|
+
"utf8",
|
|
165
|
+
)
|
|
166
|
+
await writeFile(
|
|
167
|
+
path.join(root, "AGENTS.md"),
|
|
168
|
+
"Consult ./agents.yaml.\n",
|
|
169
|
+
"utf8",
|
|
170
|
+
)
|
|
112
171
|
|
|
113
|
-
|
|
172
|
+
return root
|
|
114
173
|
}
|